chore: initial commit
This commit is contained in:
1097
Packages/com.kybernetik.animancer/Editor/GUI/AnimancerGUI.cs
Normal file
1097
Packages/com.kybernetik.animancer/Editor/GUI/AnimancerGUI.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa67bea4f1d70534987fb1358fd71903
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
118
Packages/com.kybernetik.animancer/Editor/GUI/AnimancerIcons.cs
Normal file
118
Packages/com.kybernetik.animancer/Editor/GUI/AnimancerIcons.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Icon textures used throughout Animancer.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerIcons
|
||||
public static class AnimancerIcons
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>A standard icon for information.</summary>
|
||||
public static readonly Texture Info = Load("console.infoicon");
|
||||
|
||||
/// <summary>A standard icon for warnings.</summary>
|
||||
public static readonly Texture Warning = Load("console.warnicon");
|
||||
|
||||
/// <summary>A standard icon for errors.</summary>
|
||||
public static readonly Texture Error = Load("console.erroricon");
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static Texture _ScriptableObject;
|
||||
|
||||
/// <summary>The icon for <see cref="UnityEngine.ScriptableObject"/>.</summary>
|
||||
public static Texture ScriptableObject
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
if (_ScriptableObject == null)
|
||||
{
|
||||
_ScriptableObject = Load("d_ScriptableObject Icon");
|
||||
|
||||
if (_ScriptableObject == null)
|
||||
_ScriptableObject = AssetPreview.GetMiniTypeThumbnail(typeof(StringAsset));
|
||||
}
|
||||
|
||||
return _ScriptableObject;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Loads an icon texture.</summary>
|
||||
public static Texture Load(string name, FilterMode filterMode = FilterMode.Bilinear)
|
||||
{
|
||||
var icon = EditorGUIUtility.Load(name) as Texture;
|
||||
if (icon != null)
|
||||
icon.filterMode = filterMode;
|
||||
return icon;
|
||||
}
|
||||
|
||||
/// <summary>Loads an icon `texture` if it was <c>null</c>.</summary>
|
||||
public static Texture Load(ref Texture texture, string name, FilterMode filterMode = FilterMode.Bilinear)
|
||||
=> texture != null
|
||||
? texture
|
||||
: texture = Load(name, filterMode);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static GUIContent
|
||||
_PlayIcon,
|
||||
_PauseIcon,
|
||||
_StepBackwardIcon,
|
||||
_StepForwardIcon,
|
||||
_AddIcon,
|
||||
_ClearIcon,
|
||||
_CopyIcon;
|
||||
|
||||
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a play button.</summary>
|
||||
public static GUIContent PlayIcon
|
||||
=> IconContent(ref _PlayIcon, "PlayButton");
|
||||
|
||||
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a pause button.</summary>
|
||||
public static GUIContent PauseIcon
|
||||
=> IconContent(ref _PauseIcon, "PauseButton");
|
||||
|
||||
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a step backward button.</summary>
|
||||
public static GUIContent StepBackwardIcon
|
||||
=> IconContent(ref _StepBackwardIcon, "Animation.PrevKey");
|
||||
|
||||
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a step forward button.</summary>
|
||||
public static GUIContent StepForwardIcon
|
||||
=> IconContent(ref _StepForwardIcon, "Animation.NextKey");
|
||||
|
||||
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for an add button.</summary>
|
||||
public static GUIContent AddIcon(string tooltip = "Add")
|
||||
=> IconContent(ref _AddIcon, "Toolbar Plus", tooltip);
|
||||
|
||||
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a clear button.</summary>
|
||||
public static GUIContent ClearIcon(string tooltip = "Clear")
|
||||
=> IconContent(ref _ClearIcon, "Grid.EraserTool", tooltip);
|
||||
|
||||
/// <summary><see cref="IconContent(ref GUIContent, string, string)"/> for a copy button.</summary>
|
||||
public static GUIContent CopyIcon(string tooltip = "Copy to clipboard")
|
||||
=> IconContent(ref _CopyIcon, "UnityEditor.ConsoleWindow", tooltip);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="EditorGUIUtility.IconContent(string)"/> if the `content` was null.</summary>
|
||||
public static GUIContent IconContent(ref GUIContent content, string name, string tooltip = "")
|
||||
{
|
||||
content ??= EditorGUIUtility.IconContent(name);
|
||||
content.tooltip = tooltip;
|
||||
return content;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6481076e108e435459e58460a5d52a74
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,88 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] <see cref="GUIStyle"/>s for a group of connected buttons.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ButtonGroupStyles
|
||||
public struct ButtonGroupStyles
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The style for the button on the far left.</summary>
|
||||
public GUIStyle left;
|
||||
|
||||
/// <summary>The style for any buttons in the middle.</summary>
|
||||
public GUIStyle middle;
|
||||
|
||||
/// <summary>The style for the button on the far right.</summary>
|
||||
public GUIStyle right;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="ButtonGroupStyles"/>.</summary>
|
||||
public ButtonGroupStyles(
|
||||
GUIStyle left,
|
||||
GUIStyle middle,
|
||||
GUIStyle right)
|
||||
{
|
||||
this.left = left;
|
||||
this.middle = middle;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Copies any <c>null</c> values from another group.</summary>
|
||||
public void CopyMissingStyles(ButtonGroupStyles copyFrom)
|
||||
{
|
||||
left ??= copyFrom.left;
|
||||
middle ??= copyFrom.middle;
|
||||
right ??= copyFrom.right;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The default styles for a mini button.</summary>
|
||||
public static ButtonGroupStyles MiniButton => new(
|
||||
EditorStyles.miniButtonLeft,
|
||||
EditorStyles.miniButtonMid,
|
||||
EditorStyles.miniButtonRight);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static ButtonGroupStyles _Button;
|
||||
|
||||
/// <summary>The default styles for a button.</summary>
|
||||
public static ButtonGroupStyles Button
|
||||
{
|
||||
get
|
||||
{
|
||||
_Button.left ??= MiniToRegularButtonStyle(EditorStyles.miniButtonLeft);
|
||||
_Button.middle ??= MiniToRegularButtonStyle(EditorStyles.miniButtonMid);
|
||||
_Button.right ??= MiniToRegularButtonStyle(EditorStyles.miniButtonRight);
|
||||
return _Button;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a copy of the `style` with the size of a regular button.</summary>
|
||||
public static GUIStyle MiniToRegularButtonStyle(GUIStyle style)
|
||||
=> new(style)
|
||||
{
|
||||
fixedHeight = 0,
|
||||
padding = GUI.skin.button.padding,
|
||||
stretchWidth = false,
|
||||
};
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cbe793685698bc24bb5b1087e4c93cd2
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cf051da751d8b14ab331c9a4d43511d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,56 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom GUI for an <see cref="AnimancerEvent.Dispatcher"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerEventDispatcherDrawer
|
||||
[CustomGUI(typeof(AnimancerEvent.Dispatcher))]
|
||||
public class AnimancerEventDispatcherDrawer : CustomGUI<AnimancerEvent.Dispatcher>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
{
|
||||
var state = Value.State;
|
||||
var events = state?.SharedEvents;
|
||||
if (events == null)
|
||||
{
|
||||
EditorGUILayout.LabelField("Event Dispatcher", "Null");
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPath = state != null
|
||||
? state.GetPath()
|
||||
: "Null";
|
||||
|
||||
var eventSequenceDrawer = EventSequenceDrawer.Get(events);
|
||||
var area = AnimancerGUI.LayoutRect(eventSequenceDrawer.CalculateHeight(events));
|
||||
using (var label = PooledGUIContent.Acquire("Event Dispatcher"))
|
||||
using (var summary = PooledGUIContent.Acquire(targetPath))
|
||||
eventSequenceDrawer.DoGUI(ref area, events, label, summary);
|
||||
|
||||
if (eventSequenceDrawer.IsExpanded && state != null)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
var enabled = GUI.enabled;
|
||||
GUI.enabled = false;
|
||||
EditorGUILayout.Toggle("Has Owned Events", state.HasOwnedEvents);
|
||||
GUI.enabled = enabled;
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 342d6bdafab430c48ac47d5f67eced91
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,73 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A <see cref="ICustomGUI"/> for <see cref="float"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/FloatGUI
|
||||
///
|
||||
[CustomGUI(typeof(float))]
|
||||
public class FloatGUI : CustomGUI<float>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
=> Value = EditorGUILayout.FloatField(Label, Value);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>[Editor-Only] A <see cref="ICustomGUI"/> for <see cref="int"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/IntGUI
|
||||
///
|
||||
[CustomGUI(typeof(int))]
|
||||
public class IntGUI : CustomGUI<int>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
=> Value = EditorGUILayout.IntField(Label, Value);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>[Editor-Only] A <see cref="ICustomGUI"/> for <see cref="string"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/StringGUI
|
||||
///
|
||||
[CustomGUI(typeof(string))]
|
||||
public class StringGUI : CustomGUI<string>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
=> Value = EditorGUILayout.TextField(Label, Value);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>[Editor-Only] A <see cref="ICustomGUI"/> for <see cref="Object"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ObjectGUI_1
|
||||
///
|
||||
[CustomGUI(typeof(Object))]
|
||||
public class ObjectGUI<T> : CustomGUI<T>
|
||||
where T : Object
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
=> Value = AnimancerGUI.DoObjectFieldGUI(Label, Value, true);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e4d11fbf9dad8149b1966ed610c9916
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,92 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws a custom GUI for an object.</summary>
|
||||
/// <remarks>
|
||||
/// Every non-abstract type implementing this interface must have at least one <see cref="CustomGUIAttribute"/>.
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ICustomGUI
|
||||
///
|
||||
public interface ICustomGUI
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The optional label to draw in front of the field.</summary>
|
||||
GUIContent Label { get; set; }
|
||||
|
||||
/// <summary>The target object for which this GUI will be drawn.</summary>
|
||||
object Value { get; set; }
|
||||
|
||||
/// <summary>Draws the GUI for the <see cref="Value"/>.</summary>
|
||||
void DoGUI();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>[Editor-Only] Draws a custom GUI for an object.</summary>
|
||||
/// <remarks>
|
||||
/// Every non-abstract type inheriting from this class must have at least one <see cref="CustomGUIAttribute"/>.
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CustomGUI_1
|
||||
///
|
||||
public abstract class CustomGUI<T> : ICustomGUI
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The object for which this GUI will be drawn.</summary>
|
||||
public T Value { get; protected set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
object ICustomGUI.Value
|
||||
{
|
||||
get => Value;
|
||||
set => Value = (T)value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GUIContent Label { get; set; } = GUIContent.none;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract void DoGUI();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>[Editor-Only] Extension methods for <see cref="ICustomGUI"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CustomGUIExtensions
|
||||
///
|
||||
public static class CustomGUIExtensions
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="ICustomGUI.Label"/>.</summary>
|
||||
public static void SetLabel(
|
||||
this ICustomGUI customGUI,
|
||||
string text,
|
||||
string tooltip = null,
|
||||
Texture image = null)
|
||||
{
|
||||
var label = customGUI.Label;
|
||||
if (label == null || label == GUIContent.none)
|
||||
customGUI.Label = label = new(text);
|
||||
|
||||
label.text = text;
|
||||
label.tooltip = tooltip;
|
||||
label.image = image;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5792b81ce4ba30448a3367876e92058
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,35 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// Attribute for classes which implement <see cref="CustomGUI{T}"/> to specify the type of objects they apply to.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CustomGUIAttribute
|
||||
///
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class CustomGUIAttribute : Attribute
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The type of object which the attributed <see cref="CustomGUI{T}"/> class applies to.</summary>
|
||||
public readonly Type TargetType;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="CustomGUIAttribute"/>.</summary>
|
||||
public CustomGUIAttribute(Type targetType)
|
||||
{
|
||||
TargetType = targetType;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea9f8ff21895492479bdc457361a570c
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,152 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
//#define LOG_CUSTOM_GUI_FACTORY
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws a custom GUI for an object.</summary>
|
||||
/// <remarks>
|
||||
/// Every non-abstract type implementing this interface must have at least one <see cref="CustomGUIAttribute"/>.
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CustomGUIFactory
|
||||
///
|
||||
public static class CustomGUIFactory
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly Dictionary<Type, Type>
|
||||
TargetTypeToGUIType = new();
|
||||
|
||||
static CustomGUIFactory()
|
||||
{
|
||||
foreach (var guiType in TypeCache.GetTypesWithAttribute(typeof(CustomGUIAttribute)))
|
||||
{
|
||||
if (guiType.IsAbstract ||
|
||||
guiType.IsInterface)
|
||||
continue;
|
||||
|
||||
if (!typeof(ICustomGUI).IsAssignableFrom(guiType))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"{guiType.FullName} has a {nameof(CustomGUIAttribute)}" +
|
||||
$" but doesn't implement {nameof(ICustomGUI)}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var attribute = guiType.GetCustomAttribute<CustomGUIAttribute>();
|
||||
if (attribute.TargetType != null)
|
||||
{
|
||||
|
||||
TargetTypeToGUIType.Add(attribute.TargetType, guiType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly ConditionalWeakTable<object, ICustomGUI>
|
||||
TargetToGUI = new();
|
||||
|
||||
/// <summary>Returns an existing <see cref="ICustomGUI"/> for the `targetType` or creates one if necessary.</summary>
|
||||
/// <remarks>Returns null if the `targetType` is null or no valid <see cref="ICustomGUI"/> type is found.</remarks>
|
||||
public static ICustomGUI GetOrCreateForType(Type targetType)
|
||||
{
|
||||
if (targetType == null)
|
||||
return null;
|
||||
|
||||
if (TargetToGUI.TryGetValue(targetType, out var gui))
|
||||
return gui;
|
||||
|
||||
gui = Create(targetType);
|
||||
|
||||
TargetToGUI.Add(targetType, gui);
|
||||
|
||||
return gui;
|
||||
}
|
||||
|
||||
/// <summary>Returns an existing <see cref="ICustomGUI"/> for the `value` or creates one if necessary.</summary>
|
||||
/// <remarks>Returns null if the `value` is null or no valid <see cref="ICustomGUI"/> type is found.</remarks>
|
||||
public static ICustomGUI GetOrCreateForObject(object value)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
if (TargetToGUI.TryGetValue(value, out var gui))
|
||||
return gui;
|
||||
|
||||
gui = Create(value.GetType());
|
||||
if (gui != null)
|
||||
gui.Value = value;
|
||||
|
||||
TargetToGUI.Add(value, gui);
|
||||
return gui;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates an <see cref="ICustomGUI"/> for the `targetType`.</summary>
|
||||
/// <remarks>Returns null if the `value` is null or no valid <see cref="ICustomGUI"/> type is found.</remarks>
|
||||
public static ICustomGUI Create(Type targetType)
|
||||
{
|
||||
if (!TryGetGUIType(targetType, out var guiType))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
if (guiType.IsGenericTypeDefinition)
|
||||
guiType = guiType.MakeGenericType(targetType);
|
||||
|
||||
var gui = (ICustomGUI)Activator.CreateInstance(guiType);
|
||||
|
||||
return gui;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Tries to determine the valid <see cref="ICustomGUI"/> type for drawing the `target`.</summary>
|
||||
public static bool TryGetGUIType(Type target, out Type gui)
|
||||
{
|
||||
// Try the target and its base types.
|
||||
|
||||
var type = target;
|
||||
while (type != null && type != typeof(object))
|
||||
{
|
||||
if (TargetTypeToGUIType.TryGetValue(type, out gui))
|
||||
return true;
|
||||
|
||||
type = type.BaseType;
|
||||
}
|
||||
|
||||
// Try any interfaces.
|
||||
|
||||
var interfaces = target.GetInterfaces();
|
||||
for (int i = 0; i < interfaces.Length; i++)
|
||||
if (TargetTypeToGUIType.TryGetValue(interfaces[i], out gui))
|
||||
return true;
|
||||
|
||||
// Try base object.
|
||||
|
||||
return TargetTypeToGUIType.TryGetValue(typeof(object), out gui);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb3162b4afff1f34eaff91a1a123ffde
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,148 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] An <see cref="ICustomGUI"/> for <see cref="MulticastDelegate"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DelegateGUI
|
||||
[CustomGUI(typeof(MulticastDelegate))]
|
||||
public class DelegateGUI : CustomGUI<MulticastDelegate>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly HashSet<MulticastDelegate>
|
||||
ExpandedItems = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calculates the number of vertical pixels required to draw the specified <see cref="MulticastDelegate"/>.</summary>
|
||||
public static float CalculateHeight(MulticastDelegate del)
|
||||
=> AnimancerGUI.CalculateHeight(CalculateLineCount(del));
|
||||
|
||||
/// <summary>Calculates the number of lines required to draw the specified <see cref="MulticastDelegate"/>.</summary>
|
||||
public static int CalculateLineCount(MulticastDelegate del)
|
||||
=> del == null || !ExpandedItems.Contains(del)
|
||||
? 1
|
||||
: 1 + CalculateLineCount(AnimancerReflection.GetInvocationList(del));
|
||||
|
||||
/// <summary>Calculates the number of lines required to draw the specified `invocationList`.</summary>
|
||||
public static int CalculateLineCount(Delegate[] invocationList)
|
||||
=> invocationList == null
|
||||
? 3
|
||||
: invocationList.Length * 3;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
{
|
||||
var area = AnimancerGUI.LayoutRect(CalculateHeight(Value));
|
||||
DoGUI(ref area, Label, Value);
|
||||
}
|
||||
|
||||
/// <summary>Draws the GUI for the given delegate.</summary>
|
||||
public static void DoGUI(
|
||||
ref Rect area,
|
||||
GUIContent label,
|
||||
MulticastDelegate del,
|
||||
GUIContent valueLabel = null)
|
||||
{
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
|
||||
var delegates = AnimancerReflection.GetInvocationList(del);
|
||||
|
||||
var isExpanded = del != null && AnimancerGUI.DoHashedFoldoutGUI(area, ExpandedItems, del);
|
||||
|
||||
if (valueLabel != null)
|
||||
{
|
||||
EditorGUI.LabelField(area, label, valueLabel);
|
||||
}
|
||||
else
|
||||
{
|
||||
var count = delegates == null ? 0 : delegates.Length;
|
||||
using (var countLabel = PooledGUIContent.Acquire(count.ToStringCached()))
|
||||
EditorGUI.LabelField(area, label, countLabel);
|
||||
}
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
|
||||
if (!isExpanded)
|
||||
return;
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
if (delegates == null)
|
||||
{
|
||||
DoSingleGUI(ref area, del);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < delegates.Length; i++)
|
||||
DoSingleGUI(ref area, delegates[i]);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const int TargetFieldCacheCapacity = 128;
|
||||
|
||||
private static readonly Dictionary<object, FastObjectField>
|
||||
TargetFieldCache = new(TargetFieldCacheCapacity);
|
||||
|
||||
/// <summary>Draws the target and name of the specified <see cref="Delegate"/>.</summary>
|
||||
public static void DoSingleGUI(ref Rect area, Delegate del)
|
||||
{
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
|
||||
if (del == null)
|
||||
{
|
||||
EditorGUI.LabelField(area, "Delegate", "Null");
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
return;
|
||||
}
|
||||
|
||||
var method = del.Method;
|
||||
EditorGUI.LabelField(area, "Method", method.ToString());
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
|
||||
EditorGUI.LabelField(area, "Declaring Type", method.DeclaringType.GetNameCS());
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
|
||||
var target = del.Target;
|
||||
|
||||
FastObjectField field;
|
||||
|
||||
if (target is not null)
|
||||
TargetFieldCache.TryGetValue(target, out field);
|
||||
else
|
||||
field = FastObjectField.Null;
|
||||
|
||||
field.Draw(area, "Target", target);
|
||||
|
||||
if (target is not null)
|
||||
{
|
||||
if (TargetFieldCache.Count == TargetFieldCacheCapacity)
|
||||
TargetFieldCache.Clear();
|
||||
|
||||
TargetFieldCache[target] = field;
|
||||
}
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a080cd63060e68249ad553cae3671b62
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,139 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom GUI for <see cref="FadeGroup"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/FadeGroupDrawer
|
||||
[CustomGUI(typeof(FadeGroup))]
|
||||
public class FadeGroupDrawer : CustomGUI<FadeGroup>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _IsExpanded;
|
||||
private AnimationCurve _DisplayCurve;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
{
|
||||
_IsExpanded = EditorGUILayout.Foldout(_IsExpanded, "", true);
|
||||
|
||||
var area = GUILayoutUtility.GetLastRect();
|
||||
|
||||
InitializeDisplayCurve(ref _DisplayCurve);
|
||||
|
||||
_DisplayCurve = EditorGUI.CurveField(area, TargetName, _DisplayCurve);
|
||||
|
||||
if (_IsExpanded)
|
||||
DoDetailsGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The display name of the target.</summary>
|
||||
protected virtual string TargetName
|
||||
{
|
||||
get
|
||||
{
|
||||
var name = Value.GetType().GetNameCS(false);
|
||||
if (!Value.IsValid)
|
||||
name += " (Cancelled)";
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly Keyframe[] DisplayCurveKeyframes = new Keyframe[16];
|
||||
|
||||
/// <summary>Initializes the `curve` to represent the target's fade values over normalized time.</summary>
|
||||
protected virtual void InitializeDisplayCurve(ref AnimationCurve curve)
|
||||
{
|
||||
curve ??= new();
|
||||
|
||||
try
|
||||
{
|
||||
var increment = 1f / (DisplayCurveKeyframes.Length - 1);
|
||||
for (int i = 0; i < DisplayCurveKeyframes.Length; i++)
|
||||
{
|
||||
var progress = increment * i;
|
||||
|
||||
var weight = Value.Easing != null
|
||||
? Value.Easing(progress)
|
||||
: progress;
|
||||
|
||||
DisplayCurveKeyframes[i] = new(progress, weight);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
Array.Clear(DisplayCurveKeyframes, 0, DisplayCurveKeyframes.Length);
|
||||
}
|
||||
|
||||
curve.keys = DisplayCurveKeyframes;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI for the target's fields.</summary>
|
||||
protected virtual void DoDetailsGUI()
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Value.NormalizedTime = EditorGUILayout.Slider("Normalized Time", Value.NormalizedTime, 0, 1);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
Value.NormalizedTime = Mathf.Clamp(Value.NormalizedTime, 0, 0.99f);
|
||||
Value.ApplyWeights();
|
||||
}
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var fadeDuration = EditorGUILayout.FloatField("Fade Duration", Value.FadeDuration);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
Value.FadeDuration = fadeDuration;
|
||||
|
||||
EditorGUILayout.LabelField(
|
||||
Value.TargetWeight > 0 ? "Fade In" : "Fade Out",
|
||||
"To " + Value.TargetWeight);
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
DoNodeWeightGUI(Value.FadeIn);
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
var fadeOutCount = Value.FadeOut.Count;
|
||||
if (fadeOutCount > 0)
|
||||
{
|
||||
EditorGUILayout.LabelField("Fade Out", fadeOutCount.ToStringCached());
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
for (int i = 0; i < fadeOutCount; i++)
|
||||
DoNodeWeightGUI(Value.FadeOut[i]);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI for the given `nodeWeight`.</summary>
|
||||
private void DoNodeWeightGUI(NodeWeight nodeWeight)
|
||||
{
|
||||
EditorGUILayout.LabelField(nodeWeight.Node?.GetPath());
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbc29ff53b377cc498adb3c6cad4ec5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,43 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A default <see cref="ICustomGUI"/> which simply draws the <see cref="object.ToString"/>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/LabelGUI
|
||||
[CustomGUI(typeof(object))]
|
||||
public class LabelGUI : CustomGUI<object>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
{
|
||||
string text;
|
||||
try
|
||||
{
|
||||
text = Value != null
|
||||
? Value.ToString()
|
||||
: "Null";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
text = exception.ToString();
|
||||
}
|
||||
|
||||
using (var value = PooledGUIContent.Acquire(text))
|
||||
EditorGUILayout.LabelField(Label, value);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 998ce5fd65a930b428fda6b0a0d9fd2d
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1649432a594f0b149b2190e2160b59e6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,535 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A custom Inspector for an <see cref="AnimancerLayer"/> which sorts and exposes some of its internal values.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerLayerDrawer
|
||||
///
|
||||
[CustomGUI(typeof(AnimancerLayer))]
|
||||
public 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();
|
||||
|
||||
/// <summary>The states in the target layer which have zero <see cref="AnimancerNode.Weight"/>.</summary>
|
||||
public readonly List<AnimancerState> InactiveStates = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Gathering
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initializes an editor in the list for each layer in the `graph`.</summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
internal static void GatherLayerEditors(
|
||||
AnimancerGraph graph,
|
||||
List<AnimancerLayerDrawer> 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]);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Sets the target `layer` and sorts its states and their keys into the active/inactive lists.
|
||||
/// </summary>
|
||||
private void GatherStates(AnimancerLayer layer)
|
||||
{
|
||||
Value = layer;
|
||||
|
||||
ActiveStates.Clear();
|
||||
InactiveStates.Clear();
|
||||
|
||||
foreach (var state in layer)
|
||||
{
|
||||
if (state.IsActive ||
|
||||
(!AnimancerGraphDrawer.SeparateActiveFromInactiveStates && AnimancerGraphDrawer.ShowInactiveStates))
|
||||
{
|
||||
ActiveStates.Add(state);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (AnimancerGraphDrawer.ShowInactiveStates)
|
||||
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="AnimancerLayer.Play(AnimancerState, float, FadeMode)"/>.
|
||||
/// </summary>
|
||||
private static void SortAndGatherKeys(List<AnimancerState> 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
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the layer's name and weight.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of pixels of indentation required to fit the foldout arrow.</summary>
|
||||
const float FoldoutIndent = 12;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Draws controls for <see cref="AnimancerLayer.IsAdditive"/> and <see cref="AnimancerLayer._Mask"/>.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Are all the target's states fading out to 0?</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws all `states` in the given list.</summary>
|
||||
public void DoStatesGUI(string label, List<AnimancerState> 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--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Cached Inspectors that have already been created for states.</summary>
|
||||
private readonly Dictionary<AnimancerState, ICustomGUI>
|
||||
StateInspectors = new();
|
||||
|
||||
/// <summary>Draws the Inspector for the given `state`.</summary>
|
||||
private void DoStateGUI(AnimancerState state)
|
||||
{
|
||||
if (!StateInspectors.TryGetValue(state, out var inspector))
|
||||
{
|
||||
inspector = CustomGUIFactory.GetOrCreateForObject(state);
|
||||
StateInspectors.Add(state, inspector);
|
||||
}
|
||||
|
||||
inspector?.DoGUI();
|
||||
DoChildStatesGUI(state);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws all child states of the `state`.</summary>
|
||||
private void DoChildStatesGUI(AnimancerState state)
|
||||
{
|
||||
if (!state._IsInspectorExpanded)
|
||||
return;
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
foreach (var child in state)
|
||||
if (child != null)
|
||||
DoStateGUI(child);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
{
|
||||
if (!Value.IsValid())
|
||||
return;
|
||||
|
||||
base.DoGUI();
|
||||
|
||||
var area = GUILayoutUtility.GetLastRect();
|
||||
HandleDragAndDropToPlay(area, Value);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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 HandleDragAndDropToPlay(Rect area, object layerOrGraph)
|
||||
{
|
||||
if (layerOrGraph == null)
|
||||
return;
|
||||
|
||||
_DragAndDropPlayTarget = layerOrGraph;
|
||||
|
||||
_DragAndDropPlayHandler ??= HandleDragAndDropToPlay;
|
||||
_DragAndDropPlayHandler.Handle(area);
|
||||
|
||||
_DragAndDropPlayTarget = null;
|
||||
}
|
||||
|
||||
private static DragAndDropHandler<Object> _DragAndDropPlayHandler;
|
||||
private static object _DragAndDropPlayTarget;
|
||||
|
||||
private static AnimancerLayer DragAndDropPlayTargetLayer
|
||||
=> _DragAndDropPlayTarget as AnimancerLayer
|
||||
?? (_DragAndDropPlayTarget is AnimancerGraph graph ? graph.Layers[0] : null);
|
||||
|
||||
/// <summary>Handles drag and drop events to play animations and transitions.</summary>
|
||||
public static bool HandleDragAndDropToPlay(Object obj, bool isDrop)
|
||||
{
|
||||
if (_DragAndDropPlayTarget == null)
|
||||
return false;
|
||||
|
||||
if (obj is AnimationClip clip)
|
||||
{
|
||||
if (clip.legacy)
|
||||
return false;
|
||||
|
||||
if (isDrop)
|
||||
DragAndDropPlayTargetLayer.Play(clip);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj is ITransition transition)
|
||||
{
|
||||
if (isDrop)
|
||||
DragAndDropPlayTargetLayer.Play(transition);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var transitionAsset = TryCreateTransitionAttribute.TryCreateTransitionAsset(obj);
|
||||
if (transitionAsset != null)
|
||||
{
|
||||
if (isDrop)
|
||||
DragAndDropPlayTargetLayer.Play(transitionAsset);
|
||||
|
||||
if (!EditorUtility.IsPersistent(transitionAsset))
|
||||
Object.DestroyImmediate(transitionAsset);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
using (ListPool<AnimationClip>.Instance.Acquire(out var clips))
|
||||
{
|
||||
clips.GatherFromSource(obj);
|
||||
|
||||
var anyValid = false;
|
||||
|
||||
for (int i = 0; i < clips.Count; i++)
|
||||
{
|
||||
clip = clips[i];
|
||||
if (clip.legacy)
|
||||
continue;
|
||||
|
||||
if (!isDrop)
|
||||
return true;
|
||||
|
||||
anyValid = true;
|
||||
DragAndDropPlayTargetLayer.Play(clip);
|
||||
|
||||
}
|
||||
|
||||
if (anyValid)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Context Menu
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void PopulateContextMenu(GenericMenu menu)
|
||||
{
|
||||
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.CurrentState)}: {Value.CurrentState}"));
|
||||
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.CommandCount)}: {Value.CommandCount}"));
|
||||
|
||||
menu.AddFunction("Stop",
|
||||
HasAnyStates((state) => state.IsPlaying || state.Weight != 0),
|
||||
() => Value.Stop());
|
||||
|
||||
AnimancerEditorUtilities.AddFadeFunction(menu, "Fade In",
|
||||
Value.Index > 0 && Value.Weight != 1, Value,
|
||||
(duration) => Value.StartFade(1, duration));
|
||||
AnimancerEditorUtilities.AddFadeFunction(menu, "Fade Out",
|
||||
Value.Index > 0 && Value.Weight != 0, Value,
|
||||
(duration) => Value.StartFade(0, duration));
|
||||
|
||||
AnimancerNodeBase.AddContextMenuIK(menu, Value);
|
||||
|
||||
menu.AddSeparator("");
|
||||
|
||||
menu.AddFunction("Destroy States",
|
||||
ActiveStates.Count > 0 || InactiveStates.Count > 0,
|
||||
() => Value.DestroyStates());
|
||||
|
||||
AnimancerGraphDrawer.AddRootFunctions(menu, Value.Graph);
|
||||
|
||||
menu.AddSeparator("");
|
||||
|
||||
AnimancerGraphDrawer.AddDisplayOptions(menu);
|
||||
|
||||
AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", Strings.DocsURLs.Layers);
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool HasAnyStates(Func<AnimancerState, bool> condition)
|
||||
{
|
||||
foreach (var state in Value)
|
||||
{
|
||||
if (condition(state))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c7bae7cf59ceb14cbdd08bea1a62717
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,416 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerNode"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerNodeDrawer_1
|
||||
///
|
||||
public abstract class AnimancerNodeDrawer<T> : CustomGUI<T>
|
||||
where T : AnimancerNode
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Extra padding for the left side of the labels.</summary>
|
||||
public const float ExtraLeftPadding = 3;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should the target node's details be expanded in the Inspector?</summary>
|
||||
public ref bool IsExpanded
|
||||
=> ref Value._IsInspectorExpanded;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
{
|
||||
if (!Value.IsValid())
|
||||
return;
|
||||
|
||||
GUILayout.BeginVertical();
|
||||
{
|
||||
DoHeaderGUI();
|
||||
DoDetailsGUI();
|
||||
}
|
||||
GUILayout.EndVertical();
|
||||
|
||||
if (TryUseClickEvent(GUILayoutUtility.GetLastRect(), 1))
|
||||
OpenContextMenu();
|
||||
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the name and other details of the <see cref="CustomGUI{T}.Value"/> in the GUI.</summary>
|
||||
protected virtual void DoHeaderGUI()
|
||||
{
|
||||
var area = LayoutSingleLineRect(SpacingMode.Before);
|
||||
DoLabelGUI(area);
|
||||
DoFoldoutGUI(area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Draws a field for the <see cref="AnimancerState.MainObject"/> if it has one, otherwise just a simple text
|
||||
/// label.
|
||||
/// </summary>
|
||||
protected abstract void DoLabelGUI(Rect area);
|
||||
|
||||
/// <summary>Draws a foldout arrow to expand/collapse the node details.</summary>
|
||||
protected abstract void DoFoldoutGUI(Rect area);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private FastObjectField _DebugNameField;
|
||||
|
||||
/// <summary>Draws the details of the <see cref="CustomGUI{T}.Value"/>.</summary>
|
||||
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();
|
||||
|
||||
/// <summary>
|
||||
/// Draws controls for <see cref="AnimancerState.IsPlaying"/>, <see cref="AnimancerNodeBase.Speed"/>, and
|
||||
/// <see cref="AnimancerNode.Weight"/>.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Indicates whether changing the <see cref="AnimancerNode.Weight"/> should normalize its siblings.</summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Draws the <see cref="AnimancerNode.FadeSpeed"/>
|
||||
/// and <see cref="AnimancerNode.TargetWeight"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Starts a fade or changes the details of an existing one.</summary>
|
||||
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
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// The menu label prefix used for details about the <see cref="CustomGUI{T}.Value"/>.
|
||||
/// </summary>
|
||||
protected const string DetailsPrefix = "Details/";
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current event is a context menu click within the `clickArea` and opens a context menu with various
|
||||
/// functions for the <see cref="CustomGUI{T}.Value"/>.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Adds functions relevant to the <see cref="CustomGUI{T}.Value"/>.</summary>
|
||||
protected abstract void PopulateContextMenu(GenericMenu menu);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abdda148dff930e498ac8f16c742243e
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,716 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using static Animancer.Editor.AnimancerGraphDrawer;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
using static Animancer.Editor.AnimancerStateDrawerColors;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerState"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawer_1
|
||||
[CustomGUI(typeof(AnimancerState))]
|
||||
public class AnimancerStateDrawer<T> : AnimancerNodeDrawer<T>
|
||||
where T : AnimancerState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool AutoNormalizeSiblingWeights
|
||||
=> AutoNormalizeWeights
|
||||
&& typeof(T) != typeof(ManualMixerState);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private FastObjectField _NameField;
|
||||
private FastObjectField _MainObjectField;
|
||||
|
||||
/// <summary>Draws the state's main label with a bar to indicate its current time.</summary>
|
||||
protected override void DoLabelGUI(Rect area)
|
||||
{
|
||||
area = area.Expand(StandardSpacing, 0);
|
||||
|
||||
var wholeArea = area;
|
||||
|
||||
var effectiveWeight = Value.EffectiveWeight;
|
||||
|
||||
var highlightArea = default(Rect);
|
||||
var isRepaint = Event.current.type == EventType.Repaint;
|
||||
if (isRepaint)
|
||||
{
|
||||
EditorGUI.DrawRect(wholeArea, HeaderBackgroundColor);
|
||||
|
||||
highlightArea = DoTimeHighlightBarGUI(wholeArea, effectiveWeight);
|
||||
|
||||
DoEventsGUI(wholeArea);
|
||||
|
||||
ObjectHighlightGUI.Draw(wholeArea, Value);
|
||||
}
|
||||
|
||||
DoWeightLabel(ref area, Value.Weight, effectiveWeight);
|
||||
|
||||
AnimationBindings.DoBindingMatchGUI(ref area, Value);
|
||||
|
||||
HandleLabelClick(wholeArea);
|
||||
|
||||
area = EditorGUI.IndentedRect(area);
|
||||
|
||||
var name = Value.DebugName ?? Value.Key;
|
||||
var mainObject = Value.MainObject;
|
||||
|
||||
if (mainObject == null)
|
||||
{
|
||||
var value = name ?? Value;
|
||||
var drawPing = value != Value;
|
||||
_NameField.Draw(area, value, drawPing);
|
||||
}
|
||||
else if (ReferenceEquals(name, mainObject) ||
|
||||
(name is Object nameObject && nameObject == mainObject) ||
|
||||
(name is ITransition && Current != null && !Current.IsMainObjectUsedMultipleTimes(mainObject)))
|
||||
{
|
||||
_MainObjectField.Draw(area, mainObject, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
var nameArea = StealFromLeft(ref area, EditorGUIUtility.labelWidth - IndentSize);
|
||||
_NameField.Draw(nameArea, name, true);
|
||||
}
|
||||
|
||||
_MainObjectField.Draw(area, mainObject, false);
|
||||
}
|
||||
|
||||
if (isRepaint)
|
||||
DoDetailLinesGUI(wholeArea, highlightArea, effectiveWeight);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a progress bar to show the animation time.</summary>
|
||||
public Rect DoTimeHighlightBarGUI(Rect area, float effectiveWeight)
|
||||
=> DoTimeHighlightBarGUI(
|
||||
area,
|
||||
Value.IsPlaying,
|
||||
effectiveWeight,
|
||||
Value.Time,
|
||||
Value.EffectiveSpeed,
|
||||
Value.Length,
|
||||
Value.IsLooping);
|
||||
|
||||
/// <summary>Draws a progress bar to show the animation time.</summary>
|
||||
public static Rect DoTimeHighlightBarGUI(
|
||||
Rect area,
|
||||
bool isPlaying,
|
||||
float effectiveWeight,
|
||||
float time,
|
||||
float speed,
|
||||
float length,
|
||||
bool isLooping)
|
||||
{
|
||||
if (ScaleTimeBarByWeight)
|
||||
{
|
||||
var height = area.height;
|
||||
area.height *= Mathf.Clamp01(effectiveWeight);
|
||||
area.y += height - area.height;
|
||||
}
|
||||
|
||||
var color = isPlaying ? PlayingBarColor : PausedBarColor;
|
||||
|
||||
var wrappedTime = GetWrappedTime(time, length, isLooping);
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
if (time == 0)
|
||||
return area;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (speed >= 0 || time == 0)
|
||||
{
|
||||
area.width *= Mathf.Clamp01(wrappedTime / length);
|
||||
}
|
||||
else
|
||||
{
|
||||
var xMax = area.xMax;
|
||||
area.x += area.width * Mathf.Clamp01(wrappedTime / length);
|
||||
area.x = Mathf.Floor(area.x);
|
||||
area.xMax = xMax;
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.DrawRect(area, color);
|
||||
|
||||
return area;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws lines for the current weight, time, and fade destination.</summary>
|
||||
public void DoDetailLinesGUI(
|
||||
Rect totalArea,
|
||||
Rect highlightArea,
|
||||
float effectiveWeight)
|
||||
{
|
||||
var length = Value.Length;
|
||||
|
||||
var speed = Value.Speed;
|
||||
var speedSign = speed >= 0 ? 1 : -1;
|
||||
var currentX = speed >= 0 ? highlightArea.xMax : highlightArea.xMin - 1;
|
||||
var forwardEdge = speed >= 0 ? totalArea.xMax : totalArea.xMin - 1;
|
||||
|
||||
var color = FadeLineColor;
|
||||
color.a = color.a * effectiveWeight * 0.75f + 0.25f;
|
||||
|
||||
if (Value.Time != 0 || Value.IsPlaying || Value.Weight != 0)
|
||||
{
|
||||
EditorGUI.DrawRect(
|
||||
new(highlightArea.x, highlightArea.yMin, highlightArea.width, 1),
|
||||
color);
|
||||
|
||||
if (length == 0)
|
||||
return;
|
||||
|
||||
EditorGUI.DrawRect(
|
||||
new(currentX - speedSign, totalArea.y, 1, totalArea.height),
|
||||
color);
|
||||
}
|
||||
else if (length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Value.IsPlaying)
|
||||
return;
|
||||
|
||||
var fade = Value.FadeGroup;
|
||||
if (fade == null || !fade.IsValid)
|
||||
return;
|
||||
|
||||
var currentCorner = new Vector2(currentX, highlightArea.yMin);
|
||||
|
||||
var targetWeight = Value.TargetWeight;
|
||||
var remainingFadeDuration = fade.RemainingFadeDuration;
|
||||
|
||||
var targetCorner = new Vector2(
|
||||
currentCorner.x + speed * remainingFadeDuration / Value.Length * totalArea.width,
|
||||
Mathf.Lerp(totalArea.yMax, totalArea.yMin, targetWeight));
|
||||
|
||||
var intersect = Mathf.InverseLerp(currentCorner.x, targetCorner.x, forwardEdge);
|
||||
var end = Vector2.LerpUnclamped(currentCorner, targetCorner, intersect);
|
||||
|
||||
BeginTriangles(color);
|
||||
|
||||
DrawLineBatched(
|
||||
currentCorner,
|
||||
end,
|
||||
1);
|
||||
|
||||
if (intersect < 1 && Value.IsLooping)
|
||||
{
|
||||
end.x -= speedSign * totalArea.width;
|
||||
targetCorner.x -= speedSign * totalArea.width;
|
||||
|
||||
DrawLineBatched(
|
||||
end,
|
||||
targetCorner,
|
||||
1);
|
||||
}
|
||||
|
||||
EndTriangles();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws marks on the timeline for each event.</summary>
|
||||
private void DoEventsGUI(Rect area)
|
||||
{
|
||||
if (!ShowEvents)
|
||||
return;
|
||||
|
||||
DoAnimancerEventsGUI(area);
|
||||
DoAnimationEventsGUI(area);
|
||||
}
|
||||
|
||||
/// <summary>Draws marks on the timeline for each Animancer Event.</summary>
|
||||
private void DoAnimancerEventsGUI(Rect area)
|
||||
{
|
||||
var events = Value.SharedEvents;
|
||||
if (events == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < events.Count; i++)
|
||||
DoEventTick(area, events[i].normalizedTime);
|
||||
|
||||
if (events.OnEnd != null)
|
||||
DoEventTick(area, events.GetRealNormalizedEndTime(Value.Speed));
|
||||
}
|
||||
|
||||
/// <summary>Draws marks on the timeline for each Animation Event.</summary>
|
||||
private void DoAnimationEventsGUI(Rect area)
|
||||
{
|
||||
var clip = Value.MainObject as AnimationClip;
|
||||
if (clip == null)
|
||||
return;
|
||||
|
||||
var inverseLength = 1f / Value.Length;
|
||||
|
||||
var events = clip.GetCachedEvents();
|
||||
for (int i = 0; i < events.Length; i++)
|
||||
DoEventTick(area, events[i].time * inverseLength);
|
||||
}
|
||||
|
||||
/// <summary>Draws a mark on the timeline for an event.</summary>
|
||||
private static void DoEventTick(Rect area, float normalizedTime)
|
||||
{
|
||||
if (normalizedTime >= 0 && normalizedTime <= 1)
|
||||
{
|
||||
var x = area.x + area.width * normalizedTime;
|
||||
var eventArea = new Rect(x - 1, area.y, 2, area.height * 0.3f);
|
||||
EditorGUI.DrawRect(eventArea, EventTickColor);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Handles clicks on the label area.</summary>
|
||||
private void HandleLabelClick(Rect area)
|
||||
{
|
||||
var currentEvent = Event.current;
|
||||
if (currentEvent.type != EventType.MouseUp ||
|
||||
currentEvent.button != 0 ||
|
||||
!area.Contains(currentEvent.mousePosition))
|
||||
return;
|
||||
|
||||
currentEvent.Use(0);
|
||||
|
||||
if (currentEvent.control)
|
||||
FadeInTarget();
|
||||
else
|
||||
ToggleExpanded(currentEvent.alt);
|
||||
}
|
||||
|
||||
/// <summary>Fades in the target state (or its parent state if not directly attached to a layer).</summary>
|
||||
private void FadeInTarget()
|
||||
{
|
||||
Value.Graph.UnpauseGraph();
|
||||
|
||||
AnimancerState target = Value;
|
||||
while (target != null)
|
||||
{
|
||||
var parent = target.Parent;
|
||||
if (parent is AnimancerLayer layer)
|
||||
{
|
||||
var fadeDuration = target.CalculateEditorFadeDuration(
|
||||
AnimancerGraph.DefaultFadeDuration);
|
||||
layer.Play(target, fadeDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
target = parent as AnimancerState;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Toggles the target's details between expanded and collapsed.</summary>
|
||||
private void ToggleExpanded(bool toggleSiblings)
|
||||
{
|
||||
IsExpanded = !IsExpanded;
|
||||
|
||||
if (toggleSiblings)
|
||||
{
|
||||
var parent = Value.Parent;
|
||||
var childCount = parent.ChildCount;
|
||||
for (int i = 0; i < childCount; i++)
|
||||
parent.GetChildNode(i)._IsInspectorExpanded = IsExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoFoldoutGUI(Rect area)
|
||||
{
|
||||
var hierarchyMode = EditorGUIUtility.hierarchyMode;
|
||||
EditorGUIUtility.hierarchyMode = true;
|
||||
|
||||
IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true);
|
||||
|
||||
EditorGUIUtility.hierarchyMode = hierarchyMode;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="AnimancerState.Time"/>.
|
||||
/// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
|
||||
/// </summary>
|
||||
private float GetWrappedTime(out float length)
|
||||
=> GetWrappedTime(Value.Time, length = Value.Length, Value.IsLooping);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="AnimancerState.Time"/>.
|
||||
/// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
|
||||
/// </summary>
|
||||
private static float GetWrappedTime(float time, float length, bool isLooping)
|
||||
{
|
||||
var wrappedTime = time;
|
||||
|
||||
if (isLooping)
|
||||
{
|
||||
wrappedTime = AnimancerUtilities.Wrap(wrappedTime, length);
|
||||
if (wrappedTime == 0 && time != 0)
|
||||
wrappedTime = length;
|
||||
}
|
||||
|
||||
return wrappedTime;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private FastObjectField _KeyField;
|
||||
private FastObjectField _OwnerField;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The display name of the <see cref="AnimancerState.MainObject"/> field.</summary>
|
||||
public virtual string MainObjectName
|
||||
=> "Main Object";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoDetailsGUI()
|
||||
{
|
||||
base.DoDetailsGUI();
|
||||
|
||||
if (!IsExpanded)
|
||||
return;
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
DoOptionalReferenceGUI(ref _KeyField, "Key", Value.Key);
|
||||
DoOptionalReferenceGUI(ref _OwnerField, "Owner", Value.Owner);
|
||||
|
||||
var mainObject = Value.MainObject;
|
||||
if (mainObject != null)
|
||||
{
|
||||
var mainObjectType = Value.MainObjectType ?? typeof(Object);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
var area = LayoutSingleLineRect(SpacingMode.Before);
|
||||
|
||||
mainObject = EditorGUI.ObjectField(
|
||||
area,
|
||||
MainObjectName,
|
||||
mainObject,
|
||||
mainObjectType,
|
||||
true);
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
Value.MainObject = mainObject;
|
||||
}
|
||||
|
||||
DoTimeSliderGUI();
|
||||
DoNodeDetailsGUI();
|
||||
DoOnEndGUI();
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a `reference` if it isn't <c>null</c>.</summary>
|
||||
private static void DoOptionalReferenceGUI(ref FastObjectField field, string label, object reference)
|
||||
{
|
||||
if (reference != null)
|
||||
field.Draw(LayoutSingleLineRect(SpacingMode.Before), label, reference);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a slider for controlling the current <see cref="AnimancerState.Time"/>.</summary>
|
||||
private void DoTimeSliderGUI()
|
||||
{
|
||||
if (Value.Length <= 0)
|
||||
return;
|
||||
|
||||
var time = GetWrappedTime(out var length);
|
||||
|
||||
if (length == 0)
|
||||
return;
|
||||
|
||||
var area = LayoutSingleLineRect(SpacingMode.Before);
|
||||
|
||||
var normalized = DoNormalizedTimeToggle(ref area);
|
||||
|
||||
string label;
|
||||
float max;
|
||||
if (normalized)
|
||||
{
|
||||
label = "Normalized Time";
|
||||
time /= length;
|
||||
max = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
label = "Time";
|
||||
max = length;
|
||||
}
|
||||
|
||||
DoLoopCounterGUI(ref area, length);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
label = BeginTightLabel(label);
|
||||
time = EditorGUI.Slider(area, label, time, 0, max);
|
||||
EndTightLabel();
|
||||
|
||||
if (TryUseClickEvent(area, 2))
|
||||
time = 0;
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (normalized)
|
||||
Value.NormalizedTime = time;
|
||||
else
|
||||
Value.Time = time;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static bool DoNormalizedTimeToggle(ref Rect area)
|
||||
{
|
||||
using (var label = PooledGUIContent.Acquire("N"))
|
||||
{
|
||||
var style = MiniButtonStyle;
|
||||
|
||||
var width = style.CalculateWidth(label);
|
||||
var toggleArea = StealFromRight(ref area, width);
|
||||
|
||||
UseNormalizedTimeSliders.Value = GUI.Toggle(toggleArea, UseNormalizedTimeSliders, label, style);
|
||||
}
|
||||
|
||||
return UseNormalizedTimeSliders;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static ConversionCache<int, string> _LoopCounterCache;
|
||||
|
||||
private void DoLoopCounterGUI(ref Rect area, float length)
|
||||
{
|
||||
_LoopCounterCache ??= new(x => $"x{x}");
|
||||
|
||||
string label;
|
||||
var normalizedTime = Value.Time / length;
|
||||
if (float.IsNaN(normalizedTime))
|
||||
{
|
||||
label = "NaN";
|
||||
}
|
||||
else
|
||||
{
|
||||
var loops = Mathf.FloorToInt(Value.Time / length);
|
||||
label = _LoopCounterCache.Convert(loops);
|
||||
}
|
||||
|
||||
var width = CalculateLabelWidth(label);
|
||||
|
||||
var labelArea = StealFromRight(ref area, width);
|
||||
|
||||
GUI.Label(labelArea, label);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoOnEndGUI()
|
||||
{
|
||||
var events = Value.SharedEvents;
|
||||
if (events == null)
|
||||
return;
|
||||
|
||||
var drawer = EventSequenceDrawer.Get(events);
|
||||
var area = LayoutRect(drawer.CalculateHeight(events), SpacingMode.Before);
|
||||
|
||||
using (var label = PooledGUIContent.Acquire("Events"))
|
||||
drawer.DoGUI(ref area, events, label);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Context Menu
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void PopulateContextMenu(GenericMenu menu)
|
||||
{
|
||||
AddContextMenuFunctions(menu);
|
||||
|
||||
menu.AddFunction("Play",
|
||||
!Value.IsPlaying || Value.Weight != 1,
|
||||
() =>
|
||||
{
|
||||
AnimancerState.SkipNextExpectFade();
|
||||
Value.Graph.UnpauseGraph();
|
||||
Value.Graph.Layers[0].Play(Value);
|
||||
});
|
||||
|
||||
AnimancerEditorUtilities.AddFadeFunction(menu, "Cross Fade (Ctrl + Click)",
|
||||
Value.Weight != 1,
|
||||
Value,
|
||||
duration =>
|
||||
{
|
||||
AnimancerState.SkipNextExpectFade();
|
||||
Value.Graph.UnpauseGraph();
|
||||
Value.Graph.Layers[0].Play(Value, duration);
|
||||
});
|
||||
|
||||
menu.AddSeparator("");
|
||||
menu.AddItem(new("Destroy State"),
|
||||
false,
|
||||
() => Value.Destroy());
|
||||
|
||||
menu.AddSeparator("");
|
||||
|
||||
AddDisplayOptions(menu);
|
||||
|
||||
AnimancerEditorUtilities.AddDocumentationLink(
|
||||
menu,
|
||||
"State Documentation",
|
||||
Strings.DocsURLs.States);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds the details of this state to the `menu`.</summary>
|
||||
protected virtual void AddContextMenuFunctions(GenericMenu menu)
|
||||
{
|
||||
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.Key)}: {AnimancerUtilities.ToStringOrNull(Value.Key)}"));
|
||||
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.Owner)}: {AnimancerUtilities.ToStringOrNull(Value.Owner)}"));
|
||||
|
||||
var length = Value.Length;
|
||||
if (!float.IsNaN(length))
|
||||
menu.AddDisabledItem(new($"{DetailsPrefix}{nameof(Value.Length)}: {length}"));
|
||||
|
||||
menu.AddDisabledItem(new($"{DetailsPrefix}Playable Path: {Value.GetPath()}"));
|
||||
|
||||
var mainAsset = Value.MainObject;
|
||||
if (mainAsset != null)
|
||||
{
|
||||
var assetPath = AssetDatabase.GetAssetPath(mainAsset);
|
||||
if (assetPath != null)
|
||||
menu.AddDisabledItem(new($"{DetailsPrefix}Asset Path: {assetPath.Replace("/", "->")}"));
|
||||
}
|
||||
|
||||
var events = Value.SharedEvents;
|
||||
if (events != null)
|
||||
{
|
||||
for (int i = 0; i < events.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
var name = events.GetName(i);
|
||||
AddEventFunctions(
|
||||
menu,
|
||||
name.IsNullOrEmpty() ? "Event " + index : name,
|
||||
name,
|
||||
events[index],
|
||||
() => events.SetCallback(index, AnimancerEvent.InvokeBoundCallback),
|
||||
() => events.Remove(index));
|
||||
}
|
||||
|
||||
AddEventFunctions(
|
||||
menu,
|
||||
"End Event",
|
||||
default,
|
||||
events.EndEvent,
|
||||
() => events.EndEvent = new(float.NaN, null), null);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void AddEventFunctions(
|
||||
GenericMenu menu,
|
||||
string displayName,
|
||||
StringReference name,
|
||||
AnimancerEvent animancerEvent,
|
||||
GenericMenu.MenuFunction clearEvent,
|
||||
GenericMenu.MenuFunction removeEvent)
|
||||
{
|
||||
displayName = $"Events/{displayName}/";
|
||||
|
||||
menu.AddDisabledItem(new($"{displayName}{nameof(AnimancerState.NormalizedTime)}: {animancerEvent.normalizedTime}"));
|
||||
|
||||
bool canInvoke;
|
||||
if (animancerEvent.callback == null)
|
||||
{
|
||||
menu.AddDisabledItem(new(displayName + "Callback: null"));
|
||||
canInvoke = false;
|
||||
}
|
||||
else if (animancerEvent.callback == AnimancerEvent.DummyCallback)
|
||||
{
|
||||
menu.AddDisabledItem(new(displayName + "Callback: Dummy"));
|
||||
canInvoke = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var label = displayName +
|
||||
(animancerEvent.callback.Target != null
|
||||
? ("Target: " + animancerEvent.callback.Target)
|
||||
: "Target: null");
|
||||
|
||||
var targetObject = animancerEvent.callback.Target as Object;
|
||||
menu.AddFunction(label,
|
||||
targetObject != null,
|
||||
() => Selection.activeObject = targetObject);
|
||||
|
||||
menu.AddDisabledItem(new(
|
||||
$"{displayName}Declaring Type: {animancerEvent.callback.Method.DeclaringType.GetNameCS()}"));
|
||||
|
||||
menu.AddDisabledItem(new(
|
||||
$"{displayName}Method: {animancerEvent.callback.Method}"));
|
||||
|
||||
canInvoke = true;
|
||||
}
|
||||
|
||||
if (clearEvent != null)
|
||||
menu.AddFunction(displayName + "Clear", canInvoke || !float.IsNaN(animancerEvent.normalizedTime), clearEvent);
|
||||
|
||||
if (removeEvent != null)
|
||||
menu.AddFunction(displayName + "Remove", true, removeEvent);
|
||||
|
||||
menu.AddFunction(displayName + "Invoke", canInvoke, () => animancerEvent.DelayInvoke(name, Value));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>[Editor-Only] Colors used by <see cref="AnimancerStateDrawer{T}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawerColors
|
||||
public static class AnimancerStateDrawerColors
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Colors used by this system.</summary>
|
||||
public static readonly Color
|
||||
HeaderBackgroundColor = Grey(0.35f, 0.35f),
|
||||
PlayingBarColor = new(0.15f, 0.7f, 0.15f, 0.4f),// Green = Playing.
|
||||
PausedBarColor = new(0.7f, 0.7f, 0.15f, 0.4f),// Yelow = Paused.
|
||||
FadeLineColor = new(0.3f, 1, 0.3f, 1);
|
||||
|
||||
/// <summary>Colors used by this system.</summary>
|
||||
public static Color EventTickColor
|
||||
=> Grey(EditorGUIUtility.isProSkin ? 0.8f : 0.2f, 0.8f);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be16b611f2570484dbfd1a4369ef2fa1
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,36 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ClipStateDrawer
|
||||
[CustomGUI(typeof(ClipState))]
|
||||
public class ClipStateDrawer : AnimancerStateDrawer<ClipState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string MainObjectName
|
||||
=> "Clip";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddContextMenuFunctions(UnityEditor.GenericMenu menu)
|
||||
{
|
||||
menu.AddDisabledItem(new(
|
||||
$"{DetailsPrefix}Animation Type: {AnimationBindings.GetAnimationType(Value.Clip)}"));
|
||||
|
||||
base.AddContextMenuFunctions(menu);
|
||||
|
||||
AnimancerNodeBase.AddContextMenuIK(menu, Value);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34f989a643e362047ad721eee5385571
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ControllerStateDrawer
|
||||
[CustomGUI(typeof(ControllerState))]
|
||||
public class ControllerStateDrawer : ParametizedAnimancerStateDrawer<ControllerState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string MainObjectName
|
||||
=> "Animator Controller";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfb75952401f7664aac294342b0a33e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/LinearMixerStateDrawer
|
||||
[CustomGUI(typeof(LinearMixerState))]
|
||||
public class LinearMixerStateDrawer : ParametizedAnimancerStateDrawer<LinearMixerState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddContextMenuFunctions(GenericMenu menu)
|
||||
{
|
||||
base.AddContextMenuFunctions(menu);
|
||||
|
||||
menu.AddItem(new("Extrapolate Speed"), Value.ExtrapolateSpeed, () =>
|
||||
{
|
||||
Value.ExtrapolateSpeed = !Value.ExtrapolateSpeed;
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f416ad99b4587c543a7988a4aba85fec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,203 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerState"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ParametizedAnimancerStateDrawer_1
|
||||
[CustomGUI(typeof(ManualMixerState))]
|
||||
public class ParametizedAnimancerStateDrawer<T> : AnimancerStateDrawer<T>
|
||||
where T : AnimancerState, IParametizedState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoDetailsGUI()
|
||||
{
|
||||
base.DoDetailsGUI();
|
||||
|
||||
if (!IsExpanded)
|
||||
return;
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
var parameters = ListPool.Acquire<StateParameterDetails>();
|
||||
Value.GetParameters(parameters);
|
||||
DoParametersGUI(parameters);
|
||||
ListPool.Release(parameters);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws fields for all `parameters`.</summary>
|
||||
private void DoParametersGUI(List<StateParameterDetails> parameters)
|
||||
{
|
||||
if (parameters.Count == 0)
|
||||
return;
|
||||
|
||||
var labelWidth = EditorGUIUtility.labelWidth;
|
||||
EditorGUIUtility.labelWidth -= AnimancerGUI.IndentSize;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
for (int i = 0; i < parameters.Count; i++)
|
||||
parameters[i] = DoParameterGUI(i, parameters[i]);
|
||||
|
||||
EditorGUIUtility.labelWidth = labelWidth;
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
Value.SetParameters(parameters);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws fields for the `parameter`.</summary>
|
||||
private StateParameterDetails DoParameterGUI(int index, StateParameterDetails parameter)
|
||||
{
|
||||
var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.Before);
|
||||
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
var labelWidth = EditorGUIUtility.labelWidth;
|
||||
|
||||
var label = parameter.label;
|
||||
|
||||
if (parameter.SupportsBinding && Value.Graph.HasParameters)
|
||||
{
|
||||
area = EditorGUI.IndentedRect(area);
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
parameter = DoBindingGUI(ref area, index, parameter, ref label);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUIUtility.labelWidth += AnimancerGUI.IndentSize;
|
||||
}
|
||||
|
||||
switch (parameter.type)
|
||||
{
|
||||
case AnimatorControllerParameterType.Float:
|
||||
parameter.value = EditorGUI.FloatField(area, label, (float)parameter.value);
|
||||
break;
|
||||
|
||||
case AnimatorControllerParameterType.Int:
|
||||
parameter.value = EditorGUI.IntField(area, label, (int)parameter.value);
|
||||
break;
|
||||
|
||||
case AnimatorControllerParameterType.Bool:
|
||||
parameter.value = EditorGUI.Toggle(area, label, (bool)parameter.value);
|
||||
break;
|
||||
|
||||
case AnimatorControllerParameterType.Trigger:
|
||||
parameter.value = EditorGUI.Toggle(area, label, (bool)parameter.value, EditorStyles.radioButton);
|
||||
break;
|
||||
|
||||
default:
|
||||
EditorGUI.LabelField(area, label, "Unsupported Type: " + parameter.type);
|
||||
break;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
EditorGUIUtility.labelWidth = labelWidth;
|
||||
|
||||
return parameter;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a dropdown for the `parameter`'s name binding.</summary>
|
||||
private StateParameterDetails DoBindingGUI(
|
||||
ref Rect area,
|
||||
int index,
|
||||
StateParameterDetails parameter,
|
||||
ref string fieldLabel)
|
||||
{
|
||||
if (!parameter.SupportsBinding)
|
||||
return parameter;
|
||||
|
||||
var spacing = AnimancerGUI.StandardSpacing;
|
||||
|
||||
float width;
|
||||
if (string.IsNullOrEmpty(parameter.name))
|
||||
{
|
||||
width = area.height + spacing;
|
||||
EditorGUIUtility.labelWidth -= width + AnimancerGUI.IndentSize + spacing;
|
||||
}
|
||||
else
|
||||
{
|
||||
width = EditorGUIUtility.labelWidth - AnimancerGUI.IndentSize;
|
||||
fieldLabel = "";
|
||||
}
|
||||
|
||||
var labelArea = AnimancerGUI.StealFromLeft(
|
||||
ref area,
|
||||
width,
|
||||
spacing);
|
||||
|
||||
using (var label = PooledGUIContent.Acquire(parameter.name))
|
||||
{
|
||||
if (EditorGUI.DropdownButton(labelArea, label, FocusType.Passive))
|
||||
ShowBindingSelectionMenu(labelArea, index, parameter.name);
|
||||
}
|
||||
|
||||
return parameter;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Shows a context menu for selecting the parameter binding.</summary>
|
||||
private void ShowBindingSelectionMenu(Rect area, int index, string currentName)
|
||||
{
|
||||
var menu = new GenericMenu();
|
||||
|
||||
menu.AddItem(
|
||||
new("None"),
|
||||
string.IsNullOrEmpty(currentName),
|
||||
() => SetParameterName(index, null));
|
||||
|
||||
menu.AddSeparator("");
|
||||
|
||||
foreach (var parameter in Value.Graph.Parameters)
|
||||
{
|
||||
if (parameter.ValueType != typeof(float))
|
||||
continue;
|
||||
|
||||
var name = parameter.Key;
|
||||
|
||||
menu.AddItem(
|
||||
new(name),
|
||||
name == currentName,
|
||||
() => SetParameterName(index, name));
|
||||
}
|
||||
|
||||
menu.DropDown(area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the name binding of the specified parameter.</summary>
|
||||
private void SetParameterName(int index, string name)
|
||||
{
|
||||
var parameters = ListPool.Acquire<StateParameterDetails>();
|
||||
Value.GetParameters(parameters);
|
||||
|
||||
var modify = parameters[index];
|
||||
modify.name = name;
|
||||
parameters[index] = modify;
|
||||
|
||||
Value.SetParameters(parameters);
|
||||
ListPool.Release(parameters);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c4b9498f1412de40a107ac24fb55910
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/PlayableAssetStateDrawer
|
||||
[CustomGUI(typeof(PlayableAssetState))]
|
||||
public class PlayableAssetStateDrawer : AnimancerStateDrawer<PlayableAssetState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string MainObjectName
|
||||
=> "Playable Asset";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f5aa603bfca9f446ad64081df30c975
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,139 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom GUI for <see cref="IParameter"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ParameterGUI_1
|
||||
///
|
||||
[CustomGUI(typeof(IParameter))]
|
||||
public class ParameterGUI<TParameter> : CustomGUI<TParameter>
|
||||
where TParameter : IParameter
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly HashSet<string>
|
||||
ExpandedNames = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static ICustomGUI _ValueGUI;
|
||||
private static ICustomGUI _DelegateGUI;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI()
|
||||
{
|
||||
ParameterDictionary.IsDrawingInspector = true;
|
||||
|
||||
var isExpanded = DoFoldoutGUI(out var startArea);
|
||||
|
||||
DoValueGUI();
|
||||
|
||||
if (isExpanded)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
DoDetailsGUI();
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
ParameterDictionary.IsDrawingInspector = false;
|
||||
|
||||
var endArea = GUILayoutUtility.GetLastRect();
|
||||
|
||||
var totalArea = startArea;
|
||||
totalArea.yMax = endArea.yMax;
|
||||
|
||||
if (AnimancerGUI.TryUseClickEvent(totalArea, 1))
|
||||
ShowContextMenu();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the <see cref="CustomGUI{T}.Value"/> and returns the area used.</summary>
|
||||
private Rect DoValueGUI()
|
||||
{
|
||||
_ValueGUI ??= CustomGUIFactory.GetOrCreateForType(Value.ValueType);
|
||||
|
||||
_ValueGUI.Label = Label;
|
||||
_ValueGUI.Value = Value.Value;
|
||||
_ValueGUI.DoGUI();
|
||||
Value.Value = _ValueGUI.Value;
|
||||
|
||||
return GUILayoutUtility.GetLastRect();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a foldout for the parameter details and returns true if expanded.</summary>
|
||||
private bool DoFoldoutGUI(out Rect totalArea)
|
||||
{
|
||||
var area = AnimancerGUI.LayoutSingleLineRect();
|
||||
totalArea = area;
|
||||
|
||||
GUILayout.Space(-AnimancerGUI.LineHeight - AnimancerGUI.StandardSpacing);
|
||||
|
||||
var indented = EditorGUI.IndentedRect(area);
|
||||
area.xMax = indented.xMin;
|
||||
|
||||
EditorGUIUtility.AddCursorRect(area, MouseCursor.Arrow);
|
||||
|
||||
return AnimancerGUI.DoHashedFoldoutGUI(area, ExpandedNames, Label.text);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the details of the parameter.</summary>
|
||||
private void DoDetailsGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("Type", Value.ValueType.GetNameCS(false));
|
||||
|
||||
_DelegateGUI ??= CustomGUIFactory.GetOrCreateForType(typeof(MulticastDelegate));
|
||||
|
||||
_DelegateGUI.SetLabel("On Value Changed");
|
||||
_DelegateGUI.Value = Value.GetOnValueChanged();
|
||||
_DelegateGUI.DoGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Shows a context menu for the parameter.</summary>
|
||||
private void ShowContextMenu()
|
||||
{
|
||||
var menu = new GenericMenu();
|
||||
|
||||
menu.AddItem(new("Log Interactions"), Value.LogContext != null, () =>
|
||||
{
|
||||
Value.LogContext = Value.LogContext is null
|
||||
? ""
|
||||
: null;
|
||||
});
|
||||
|
||||
menu.AddItem(new("Inspector Control Only"), Value.InspectorControlOnly, () =>
|
||||
{
|
||||
Value.InspectorControlOnly = !Value.InspectorControlOnly;
|
||||
});
|
||||
|
||||
AnimancerEditorUtilities.AddDocumentationLink(
|
||||
menu,
|
||||
"Parameters Documentation",
|
||||
Strings.DocsURLs.Parameters);
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51417698361edad48a657ecfe747334d
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,127 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// Delegate for validating and responding to <see cref="DragAndDrop"/> operations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
///
|
||||
/// <strong>Example:</strong>
|
||||
/// <code>
|
||||
/// private DragAndDropHandler<AnimationClip> _AnimationDropHandler;
|
||||
///
|
||||
/// void OnGUI(Rect area)
|
||||
/// {
|
||||
/// _AnimationDropHandler ??= (clip, isDrop) =>
|
||||
/// {
|
||||
/// if (clip.legacy)// Reject legacy animations
|
||||
/// return false;
|
||||
///
|
||||
/// if (isDrop)// Only act when dropping.
|
||||
/// Debug.Log(clip + " was dropped");
|
||||
///
|
||||
/// return true;// Drag or drop is accepted.
|
||||
/// };
|
||||
///
|
||||
/// _AnimationDropHandler.Handle(area);
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DragAndDropHandler_1
|
||||
public delegate bool DragAndDropHandler<T>(
|
||||
T dragging,
|
||||
bool isDrop)
|
||||
where T : class;
|
||||
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGUI
|
||||
public static partial class AnimancerGUI
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Handles the current event.</summary>
|
||||
/// <remarks>See <see cref="DragAndDropHandler{T}"/> for a usage example.</remarks>
|
||||
public static bool Handle<T>(
|
||||
this DragAndDropHandler<T> handler,
|
||||
Rect area,
|
||||
DragAndDropVisualMode mode = DragAndDropVisualMode.Link)
|
||||
where T : class
|
||||
{
|
||||
var currentEvent = Event.current;
|
||||
|
||||
bool isDrop;
|
||||
switch (currentEvent.type)
|
||||
{
|
||||
case EventType.DragUpdated:
|
||||
isDrop = false;
|
||||
break;
|
||||
|
||||
case EventType.DragPerform:
|
||||
isDrop = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!area.Contains(currentEvent.mousePosition))
|
||||
return false;
|
||||
|
||||
return handler.Handle(DragAndDrop.objectReferences, isDrop, mode);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Handles the current event.</summary>
|
||||
/// <remarks>See <see cref="DragAndDropHandler{T}"/> for a usage example.</remarks>
|
||||
public static bool Handle<T>(
|
||||
this DragAndDropHandler<T> handler,
|
||||
IEnumerable dragging,
|
||||
bool isDrop,
|
||||
DragAndDropVisualMode mode = DragAndDropVisualMode.Link)
|
||||
where T : class
|
||||
{
|
||||
if (dragging == null)
|
||||
return false;
|
||||
|
||||
var droppedAny = false;
|
||||
|
||||
foreach (var obj in dragging)
|
||||
{
|
||||
if (obj is not T t ||
|
||||
!handler(t, isDrop))
|
||||
continue;
|
||||
|
||||
Deselect();
|
||||
Event.current.Use();
|
||||
|
||||
if (isDrop)
|
||||
{
|
||||
droppedAny = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DragAndDrop.visualMode = mode;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!droppedAny)
|
||||
return false;
|
||||
|
||||
DragAndDrop.AcceptDrag();
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2df9fbbc3b50be848980b6f0bd64b818
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7b86b4f7872d7944b4379dd41e5ac2a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,172 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using Animancer.Units;
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws manual controls for the <see cref="AnimancerGraph.PlayableGraph"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGraphControls
|
||||
[Serializable, InternalSerializableType]
|
||||
public class AnimancerGraphControls : AnimancerSettingsGroup
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws manual controls for the <see cref="AnimancerGraph.PlayableGraph"/>.</summary>
|
||||
public static void DoGraphGUI(AnimancerGraph graph, out Rect area)
|
||||
{
|
||||
GUILayout.BeginVertical();
|
||||
|
||||
DoSpeedSliderGUI(graph);
|
||||
|
||||
DoAddAnimationGUI(graph);
|
||||
|
||||
GUILayout.EndVertical();
|
||||
|
||||
area = GUILayoutUtility.GetLastRect();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the <see cref="AnimancerGraphSpeedSlider"/>.</summary>
|
||||
private static void DoSpeedSliderGUI(AnimancerGraph graph)
|
||||
{
|
||||
if (!AnimancerGraphSpeedSlider.Instance.IsOn)
|
||||
return;
|
||||
|
||||
var area = LayoutSingleLineRect();
|
||||
area = area.Expand(StandardSpacing, 0);
|
||||
|
||||
AnimancerGraphSpeedSlider.Instance.Graph = graph;
|
||||
AnimancerGraphSpeedSlider.Instance.DoSpeedSlider(ref area, null);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a toggle to play and pause the graph.</summary>
|
||||
public static void DoPlayPauseToggle(Rect area, AnimancerGraph graph, GUIStyle style = null)
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
var isGraphPlaying = AnimancerGUI.DoPlayPauseToggle(
|
||||
area,
|
||||
graph.IsGraphPlaying,
|
||||
style);
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
graph.IsGraphPlaying = isGraphPlaying;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a button to step time forward.</summary>
|
||||
public static void DoFrameStepButton(Rect area, AnimancerGraph graph, GUIStyle style)
|
||||
{
|
||||
if (GUI.Button(area, AnimancerIcons.StepForwardIcon, style))
|
||||
graph.Evaluate(FrameStep);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Add Animation
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void DoAddAnimationGUI(AnimancerGraph graph)
|
||||
{
|
||||
if (!AnimancerGraphDrawer.ShowAddAnimation)
|
||||
return;
|
||||
|
||||
var label = "Add Animation";
|
||||
|
||||
var area = LayoutSingleLineRect(SpacingMode.Before);
|
||||
|
||||
var labelArea = StealFromLeft(ref area, EditorStyles.label.CalculateWidth(label), StandardSpacing);
|
||||
var objectArea = StealFromRight(ref area, area.width * 0.35f, StandardSpacing);
|
||||
var clipArea = area;
|
||||
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
GUI.Label(labelArea, label);
|
||||
|
||||
var sourceClip = EditorGUI.ObjectField(clipArea, null, typeof(AnimationClip), false);
|
||||
var source = EditorGUI.ObjectField(objectArea, null, typeof(Object), false);
|
||||
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
|
||||
if (sourceClip is AnimationClip sourceClipTyped)
|
||||
{
|
||||
graph.Layers[0].Play(sourceClipTyped);
|
||||
return;
|
||||
}
|
||||
|
||||
if (source == null)
|
||||
return;
|
||||
|
||||
if (source is ITransition transition)
|
||||
{
|
||||
graph.Layers[0].Play(transition);
|
||||
return;
|
||||
}
|
||||
|
||||
var transitionAsset = TryCreateTransitionAttribute.TryCreateTransitionAsset(source);
|
||||
if (transitionAsset != null)
|
||||
{
|
||||
var state = graph.Layers[0].Play(transitionAsset);
|
||||
|
||||
if (!EditorUtility.IsPersistent(transitionAsset))
|
||||
{
|
||||
state.SetDebugName(source);
|
||||
Object.DestroyImmediate(transitionAsset);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
using (SetPool<AnimationClip>.Instance.Acquire(out var clips))
|
||||
{
|
||||
clips.GatherFromSource(source);
|
||||
foreach (var clip in clips)
|
||||
graph.Layers[0].Play(clip);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Settings
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName
|
||||
=> "Graph Controls";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Index
|
||||
=> 2;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Seconds(Rule = Validate.Value.IsNotNegative)]
|
||||
[DefaultValue(0.02f)]
|
||||
[Tooltip("The amount of time that will be added by a single frame step")]
|
||||
private float _FrameStep = 0.02f;
|
||||
|
||||
/// <summary>The amount of time that will be added by a single frame step (in seconds).</summary>
|
||||
public static float FrameStep
|
||||
=> AnimancerSettingsGroup<AnimancerGraphControls>.Instance._FrameStep;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfca60fe8eef9c1498bd69e3cdf77e04
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,602 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="IAnimancerComponent.Graph"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGraphDrawer
|
||||
///
|
||||
public class AnimancerGraphDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The currently drawing instance.</summary>
|
||||
public static AnimancerGraphDrawer Current { get; private set; }
|
||||
|
||||
/// <summary>A lazy list of information about the layers currently being displayed.</summary>
|
||||
private readonly List<AnimancerLayerDrawer>
|
||||
LayerDrawers = new();
|
||||
|
||||
/// <summary>The number of elements in <see cref="LayerDrawers"/> that are currently being used.</summary>
|
||||
private int _LayerCount;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI of the <see cref="IAnimancerComponent.Graph"/> if there is only one target.</summary>
|
||||
public void DoGUI(IAnimancerComponent[] targets)
|
||||
{
|
||||
if (targets.Length != 1)
|
||||
return;
|
||||
|
||||
DoGUI(targets[0]);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI of the <see cref="IAnimancerComponent.Graph"/>.</summary>
|
||||
public void DoGUI(IAnimancerComponent target)
|
||||
{
|
||||
Current = this;
|
||||
|
||||
DoNativeAnimatorControllerGUI(target);
|
||||
|
||||
if (!target.IsGraphInitialized)
|
||||
{
|
||||
DoGraphNotInitializedGUI(target);
|
||||
return;
|
||||
}
|
||||
|
||||
GUILayout.BeginVertical();
|
||||
|
||||
var hierarchyMode = EditorGUIUtility.hierarchyMode;
|
||||
EditorGUIUtility.hierarchyMode = true;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
var graph = target.Graph;
|
||||
|
||||
// Gather the during the layout event and use the same ones during subsequent events to avoid GUI errors
|
||||
// in case they change (they shouldn't, but this is also more efficient).
|
||||
if (Event.current.type == EventType.Layout)
|
||||
{
|
||||
AnimancerLayerDrawer.GatherLayerEditors(graph, LayerDrawers, out _LayerCount);
|
||||
GatherMainObjectUsage(graph);
|
||||
}
|
||||
|
||||
AnimancerGraphControls.DoGraphGUI(graph, out var area);
|
||||
CheckContextMenu(area, graph);
|
||||
|
||||
for (int i = 0; i < _LayerCount; i++)
|
||||
LayerDrawers[i].DoGUI();
|
||||
|
||||
DoOrphanStatesGUI(graph);
|
||||
|
||||
GUILayout.Space(StandardSpacing);
|
||||
|
||||
DoLayerWeightWarningGUI(target);
|
||||
|
||||
ParameterDictionaryDrawer.DoParametersGUI(graph);
|
||||
NamedEventDictionaryDrawer.DoEventsGUI(graph);
|
||||
|
||||
if (ShowInternalDetails)
|
||||
DoInternalDetailsGUI(graph);
|
||||
|
||||
if (EditorGUI.EndChangeCheck() && !graph.IsGraphPlaying)
|
||||
graph.Evaluate();
|
||||
|
||||
DoMultipleAnimationSystemWarningGUI(target);
|
||||
|
||||
EditorGUIUtility.hierarchyMode = hierarchyMode;
|
||||
|
||||
GUILayout.EndVertical();
|
||||
|
||||
AnimancerLayerDrawer.HandleDragAndDropToPlay(GUILayoutUtility.GetLastRect(), graph);
|
||||
|
||||
Current = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a GUI for the <see cref="Animator.runtimeAnimatorController"/> if there is one.</summary>
|
||||
private void DoNativeAnimatorControllerGUI(IAnimancerComponent target)
|
||||
{
|
||||
if (!EditorApplication.isPlaying &&
|
||||
!target.IsGraphInitialized)
|
||||
return;
|
||||
|
||||
var animator = target.Animator;
|
||||
if (animator == null)
|
||||
return;
|
||||
|
||||
var controller = animator.runtimeAnimatorController;
|
||||
if (controller == null)
|
||||
return;
|
||||
|
||||
BeginVerticalBox(GUI.skin.box);
|
||||
|
||||
var label = "Native Animator Controller";
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
controller = DoObjectFieldGUI(label, controller, false);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
animator.runtimeAnimatorController = controller;
|
||||
|
||||
if (controller is AnimatorController editorController)
|
||||
{
|
||||
var layers = editorController.layers;
|
||||
for (int i = 0; i < layers.Length; i++)
|
||||
{
|
||||
var layer = layers[i];
|
||||
|
||||
var runtimeState = animator.IsInTransition(i) ?
|
||||
animator.GetNextAnimatorStateInfo(i) :
|
||||
animator.GetCurrentAnimatorStateInfo(i);
|
||||
|
||||
var states = layer.stateMachine.states;
|
||||
var editorState = GetState(states, runtimeState.shortNameHash);
|
||||
|
||||
var area = LayoutSingleLineRect(SpacingMode.Before);
|
||||
|
||||
var weight = i == 0 ? 1 : animator.GetLayerWeight(i);
|
||||
|
||||
string stateName;
|
||||
if (editorState != null)
|
||||
{
|
||||
stateName = editorState.GetCachedName();
|
||||
|
||||
var isLooping = editorState.motion != null && editorState.motion.isLooping;
|
||||
AnimancerStateDrawer<ClipState>.DoTimeHighlightBarGUI(
|
||||
area,
|
||||
true,
|
||||
weight,
|
||||
runtimeState.normalizedTime * runtimeState.length,
|
||||
runtimeState.speed,
|
||||
runtimeState.length,
|
||||
isLooping);
|
||||
}
|
||||
else
|
||||
{
|
||||
stateName = "State Not Found";
|
||||
}
|
||||
|
||||
DoWeightLabel(ref area, weight, weight);
|
||||
|
||||
EditorGUI.LabelField(area, layer.name, stateName);
|
||||
}
|
||||
}
|
||||
|
||||
EndVerticalBox(GUI.skin.box);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the state with the specified <see cref="AnimatorState.nameHash"/>.</summary>
|
||||
private static AnimatorState GetState(ChildAnimatorState[] states, int nameHash)
|
||||
{
|
||||
for (int i = 0; i < states.Length; i++)
|
||||
{
|
||||
var state = states[i].state;
|
||||
if (state.nameHash == nameHash)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoGraphNotInitializedGUI(IAnimancerComponent target)
|
||||
{
|
||||
if (!EditorApplication.isPlaying ||
|
||||
target.Animator == null ||
|
||||
EditorUtility.IsPersistent(target.Animator))
|
||||
return;
|
||||
|
||||
EditorGUILayout.HelpBox("Animancer is not initialized." +
|
||||
" It will be initialized automatically when something uses it, such as playing an animation.",
|
||||
MessageType.Info);
|
||||
|
||||
if (TryUseClickEventInLastRect(1))
|
||||
{
|
||||
var menu = new GenericMenu();
|
||||
|
||||
menu.AddItem(new("Initialize"), false, () => target.Graph.Evaluate());
|
||||
|
||||
AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", Strings.DocsURLs.Layers);
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly AnimancerLayerDrawer OrphanStatesDrawer = new();
|
||||
|
||||
private void DoOrphanStatesGUI(AnimancerGraph graph)
|
||||
{
|
||||
var states = OrphanStatesDrawer.ActiveStates;
|
||||
states.Clear();
|
||||
foreach (var state in graph.States)
|
||||
if (state.Parent == null)
|
||||
states.Add(state);
|
||||
|
||||
if (states.Count > 0)
|
||||
{
|
||||
ApplySortStatesByName(states);
|
||||
|
||||
OrphanStatesDrawer.DoStatesGUI("Orphans", states);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoLayerWeightWarningGUI(IAnimancerComponent target)
|
||||
{
|
||||
if (_LayerCount == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"No layers have been created, which likely means no animations have been played yet.",
|
||||
MessageType.Warning);
|
||||
|
||||
if (GUILayout.Button("Create Base Layer"))
|
||||
target.Graph.Layers.Count = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.gameObject.activeInHierarchy ||
|
||||
!target.enabled ||
|
||||
(target.Animator != null && target.Animator.runtimeAnimatorController != null))
|
||||
return;
|
||||
|
||||
if (_LayerCount == 1)
|
||||
{
|
||||
var layer = LayerDrawers[0].Value;
|
||||
if (layer.Weight == 0)
|
||||
EditorGUILayout.HelpBox(
|
||||
layer + " is at 0 weight, which likely means no animations have been played yet.",
|
||||
MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _LayerCount; i++)
|
||||
{
|
||||
var layer = LayerDrawers[i].Value;
|
||||
if (layer.Weight == 1 &&
|
||||
!layer.IsAdditive &&
|
||||
layer._Mask == null &&
|
||||
Mathf.Approximately(layer.GetTotalChildWeight(), 1))
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"There are no Override layers at weight 1, which will likely give undesirable results." +
|
||||
" Click here for more information.",
|
||||
MessageType.Warning);
|
||||
|
||||
if (TryUseClickEventInLastRect())
|
||||
EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.Layers + "#blending");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoMultipleAnimationSystemWarningGUI(IAnimancerComponent target)
|
||||
{
|
||||
const string OnlyOneSystemWarning =
|
||||
"This is not supported. Each object can only be controlled by one system at a time.";
|
||||
|
||||
using (ListPool<IAnimancerComponent>.Instance.Acquire(out var animancers))
|
||||
{
|
||||
target.gameObject.GetComponents(animancers);
|
||||
if (animancers.Count > 1)
|
||||
{
|
||||
for (int i = 0; i < animancers.Count; i++)
|
||||
{
|
||||
var other = animancers[i];
|
||||
if (other != target && other.Animator == target.Animator)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"There are multiple {nameof(IAnimancerComponent)}s trying to control the target" +
|
||||
$" {nameof(Animator)}. {OnlyOneSystemWarning}",
|
||||
MessageType.Warning);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target.Animator.TryGetComponent<Animation>(out _))
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
$"There is a Legacy {nameof(Animation)} component on the same object as the target" +
|
||||
$" {nameof(Animator)}. {OnlyOneSystemWarning}",
|
||||
MessageType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly BoolPref
|
||||
ArePreUpdatablesExpanded = new(KeyPrefix + nameof(ArePreUpdatablesExpanded), false),
|
||||
ArePostUpdatablesExpanded = new(KeyPrefix + nameof(ArePostUpdatablesExpanded), false),
|
||||
AreDisposablesExpanded = new(KeyPrefix + nameof(AreDisposablesExpanded), false);
|
||||
|
||||
/// <summary>Draws a box describing the internal details of the `graph`.</summary>
|
||||
private void DoInternalDetailsGUI(AnimancerGraph graph)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
DoGroupDetailsGUI(graph.PreUpdatables, "Pre-Updatables", ArePreUpdatablesExpanded);
|
||||
DoGroupDetailsGUI(graph.PostUpdatables, "Post-Updatables", ArePostUpdatablesExpanded);
|
||||
DoGroupDetailsGUI(graph.Disposables, "Disposables", AreDisposablesExpanded);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/// <summary>Draws the `items`.</summary>
|
||||
private static void DoGroupDetailsGUI<T>(IReadOnlyList<T> items, string groupName, BoolPref isExpanded)
|
||||
{
|
||||
var count = items.Count;
|
||||
|
||||
isExpanded.Value = DoLabelFoldoutFieldGUI(groupName, count.ToStringCached(), isExpanded);
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
if (isExpanded)
|
||||
for (int i = 0; i < count; i++)
|
||||
DoDetailsGUI(items[i]);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/// <summary>Draws the details of the `item`.</summary>
|
||||
private static void DoDetailsGUI(object item)
|
||||
{
|
||||
if (item is AnimancerNode node)
|
||||
{
|
||||
var area = LayoutSingleLineRect(SpacingMode.Before);
|
||||
area = EditorGUI.IndentedRect(area);
|
||||
|
||||
var field = new FastObjectField();
|
||||
field.Set(node, node.GetPath(), FastObjectField.GetIcon(node));
|
||||
field.Draw(area);
|
||||
return;
|
||||
}
|
||||
|
||||
var gui = CustomGUIFactory.GetOrCreateForObject(item);
|
||||
if (gui != null)
|
||||
{
|
||||
gui.DoGUI();
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField(item.ToString());
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Main Object Lookup
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly Dictionary<Object, bool>
|
||||
MainObjectDuplicateUsage = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is the given `mainObject` used as the <see cref="AnimancerState.MainObject"/> of multiple states?</summary>
|
||||
public bool IsMainObjectUsedMultipleTimes(Object mainObject)
|
||||
=> MainObjectDuplicateUsage.TryGetValue(mainObject, out var duplicate)
|
||||
&& duplicate;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void GatherMainObjectUsage(AnimancerGraph graph)
|
||||
{
|
||||
MainObjectDuplicateUsage.Clear();
|
||||
|
||||
var layers = graph.Layers;
|
||||
var layerCount = layers.Count;
|
||||
|
||||
for (int iLayer = 0; iLayer < layerCount; iLayer++)
|
||||
{
|
||||
var layer = layers[iLayer];
|
||||
var childCount = layer.ChildCount;
|
||||
for (int iState = 0; iState < childCount; iState++)
|
||||
{
|
||||
var state = layer.GetChild(iState);
|
||||
var mainObject = state.MainObject;
|
||||
if (mainObject == null)
|
||||
continue;
|
||||
|
||||
if (MainObjectDuplicateUsage.TryGetValue(mainObject, out var duplicate))
|
||||
{
|
||||
if (!duplicate)
|
||||
MainObjectDuplicateUsage[mainObject] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
MainObjectDuplicateUsage.Add(mainObject, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Context Menu
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current event is a context menu click within the `clickArea`
|
||||
/// and opens a context menu with various functions for the `graph`.
|
||||
/// </summary>
|
||||
private void CheckContextMenu(Rect clickArea, AnimancerGraph graph)
|
||||
{
|
||||
if (!TryUseClickEvent(clickArea, 1))
|
||||
return;
|
||||
|
||||
var menu = new GenericMenu();
|
||||
|
||||
menu.AddDisabledItem(new(graph._PlayableGraph.GetEditorName() ?? "Unnamed Graph"), false);
|
||||
menu.AddDisabledItem(new("Frame ID: " + graph.FrameID), false);
|
||||
AddDisposablesFunctions(menu, graph.Disposables);
|
||||
|
||||
AddUpdateModeFunctions(menu, graph);
|
||||
AnimancerNodeBase.AddContextMenuIK(menu, graph);
|
||||
|
||||
AddRootFunctions(menu, graph);
|
||||
|
||||
menu.AddSeparator("");
|
||||
|
||||
AddDisplayOptions(menu);
|
||||
|
||||
menu.AddItem(new("Log Details Of Everything"), false,
|
||||
() => Debug.Log(graph.GetDescription(), graph.Component as Object));
|
||||
AddPlayableGraphVisualizerFunction(menu, "", graph._PlayableGraph);
|
||||
|
||||
AnimancerEditorUtilities.AddDocumentationLink(menu, "Inspector Documentation", Strings.DocsURLs.Inspector);
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds functions for controlling the `graph`.</summary>
|
||||
public static void AddRootFunctions(GenericMenu menu, AnimancerGraph graph)
|
||||
{
|
||||
menu.AddFunction("Add Layer",
|
||||
graph.Layers.Count < AnimancerLayerList.DefaultCapacity,
|
||||
() => graph.Layers.Count++);
|
||||
menu.AddFunction("Remove Layer",
|
||||
graph.Layers.Count > 0,
|
||||
() => graph.Layers.Count--);
|
||||
|
||||
menu.AddItem(new("Keep Children Connected ?"),
|
||||
graph.KeepChildrenConnected,
|
||||
() => graph.SetKeepChildrenConnected(!graph.KeepChildrenConnected));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds menu functions to set the <see cref="DirectorUpdateMode"/>.</summary>
|
||||
private void AddUpdateModeFunctions(GenericMenu menu, AnimancerGraph graph)
|
||||
{
|
||||
var modes = Enum.GetValues(typeof(DirectorUpdateMode));
|
||||
for (int i = 0; i < modes.Length; i++)
|
||||
{
|
||||
var mode = (DirectorUpdateMode)modes.GetValue(i);
|
||||
menu.AddItem(new("Update Mode/" + mode), graph.UpdateMode == mode,
|
||||
() => graph.UpdateMode = mode);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds disabled items for each disposable.</summary>
|
||||
private void AddDisposablesFunctions(GenericMenu menu, List<IDisposable> disposables)
|
||||
{
|
||||
var prefix = $"{nameof(AnimancerGraph.Disposables)}: {disposables.Count}";
|
||||
if (disposables.Count == 0)
|
||||
{
|
||||
menu.AddDisabledItem(new(prefix), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
prefix += "/";
|
||||
for (int i = 0; i < disposables.Count; i++)
|
||||
{
|
||||
menu.AddDisabledItem(new(prefix + disposables[i]), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds a menu function to open the Playable Graph Visualiser if it exists in the project.</summary>
|
||||
public static void AddPlayableGraphVisualizerFunction(GenericMenu menu, string prefix, PlayableGraph graph)
|
||||
{
|
||||
var type = Type.GetType(
|
||||
"GraphVisualizer.PlayableGraphVisualizerWindow, Unity.PlayableGraphVisualizer.Editor");
|
||||
|
||||
menu.AddFunction(prefix + "Playable Graph Visualizer", type != null, () =>
|
||||
{
|
||||
var window = EditorWindow.GetWindow(type);
|
||||
|
||||
var field = type.GetField("m_CurrentGraph", AnimancerReflection.AnyAccessBindings);
|
||||
|
||||
field?.SetValue(window, graph);
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Prefs
|
||||
/************************************************************************************************************************/
|
||||
|
||||
internal const string
|
||||
KeyPrefix = "Inspector/",
|
||||
MenuPrefix = "Display Options/";
|
||||
|
||||
internal static readonly BoolPref
|
||||
SortStatesByName = new(KeyPrefix, MenuPrefix + "Sort States By Name", true),
|
||||
SeparateActiveFromInactiveStates = new(KeyPrefix, MenuPrefix + "Separate Active From Inactive States", false),
|
||||
ShowInactiveStates = new(KeyPrefix, MenuPrefix + "Show Inactive States", true),
|
||||
ShowSingleLayerHeader = new(KeyPrefix, MenuPrefix + "Show Single Layer Header", false),
|
||||
ShowEvents = new(KeyPrefix, MenuPrefix + "Show Events", true),
|
||||
ShowInternalDetails = new(KeyPrefix, MenuPrefix + "Show Internal Details", false),
|
||||
ShowAddAnimation = new(KeyPrefix, MenuPrefix + "Show 'Add Animation' Field", false),
|
||||
RepaintConstantly = new(KeyPrefix, MenuPrefix + "Repaint Constantly", true),
|
||||
ScaleTimeBarByWeight = new(KeyPrefix, MenuPrefix + "Scale Time Bar by Weight", true),
|
||||
VerifyAnimationBindings = new(KeyPrefix, MenuPrefix + "Verify Animation Bindings", true),
|
||||
AutoNormalizeWeights = new(KeyPrefix, MenuPrefix + "Auto Normalize Weights", true),
|
||||
UseNormalizedTimeSliders = new("Inspector", nameof(UseNormalizedTimeSliders), false);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds functions to the `menu` for each of the Display Options.</summary>
|
||||
public static void AddDisplayOptions(GenericMenu menu)
|
||||
{
|
||||
RepaintConstantly.AddToggleFunction(menu);
|
||||
SortStatesByName.AddToggleFunction(menu);
|
||||
SeparateActiveFromInactiveStates.AddToggleFunction(menu);
|
||||
ShowInactiveStates.AddToggleFunction(menu);
|
||||
ShowSingleLayerHeader.AddToggleFunction(menu);
|
||||
ShowEvents.AddToggleFunction(menu);
|
||||
ShowInternalDetails.AddToggleFunction(menu);
|
||||
ShowAddAnimation.AddToggleFunction(menu);
|
||||
ScaleTimeBarByWeight.AddToggleFunction(menu);
|
||||
VerifyAnimationBindings.AddToggleFunction(menu);
|
||||
AutoNormalizeWeights.AddToggleFunction(menu);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sorts the `states` if <see cref="SortStatesByName"/> is enabled.</summary>
|
||||
public static void ApplySortStatesByName(List<AnimancerState> states)
|
||||
{
|
||||
if (SortStatesByName)
|
||||
states.Sort((x, y) => x.ToString().CompareTo(y.ToString()));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ec6dbbd785ee314fad604b370743fe2
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// <see cref="ToggledSpeedSlider"/> for <see cref="AnimancerGraph"/>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGraphSpeedSlider
|
||||
public class AnimancerGraphSpeedSlider : ToggledSpeedSlider
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Singleton.</summary>
|
||||
public static readonly AnimancerGraphSpeedSlider
|
||||
Instance = new();
|
||||
|
||||
/// <summary>The target graph.</summary>
|
||||
public AnimancerGraph Graph { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="AnimancerGraphSpeedSlider"/>.</summary>
|
||||
public AnimancerGraphSpeedSlider()
|
||||
: base(nameof(AnimancerGraphSpeedSlider) + ".Show")
|
||||
{
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnSetSpeed(float speed)
|
||||
{
|
||||
if (Graph != null)
|
||||
Graph.Speed = speed;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool DoToggleGUI(Rect area, GUIStyle style)
|
||||
{
|
||||
if (Graph != null)
|
||||
Speed = Graph.Speed;
|
||||
|
||||
return base.DoToggleGUI(area, style);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a44a4bf07f2b1064ba2f15d08a482ce8
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,108 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] [Internal]
|
||||
/// A utility for drawing empty [<see cref="SerializeReference"/>] <see cref="IInvokable"/> fields.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used to draw empty slots in <see cref="AnimancerEvent.Sequence.Serializable.Callbacks"/>
|
||||
/// which don't actually have a <see cref="SerializedProperty"/> of their own because
|
||||
/// the array is compacted to trim any <c>null</c> items from the end.
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DummyInvokableDrawer
|
||||
internal class DummyInvokableDrawer : ScriptableObject
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeReference, Polymorphic]
|
||||
private IInvokable[] _Invokable;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static SerializedProperty _InvokableProperty;
|
||||
|
||||
/// <summary>[Editor-Only] A static dummy <see cref="IInvokable"/>.</summary>
|
||||
private static SerializedProperty InvokableProperty
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_InvokableProperty == null)
|
||||
{
|
||||
var instance = CreateInstance<DummyInvokableDrawer>();
|
||||
|
||||
instance.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSave;
|
||||
var serializedObject = new SerializedObject(instance);
|
||||
_InvokableProperty = serializedObject.FindProperty(nameof(_Invokable));
|
||||
|
||||
AssemblyReloadEvents.beforeAssemblyReload += () =>
|
||||
{
|
||||
serializedObject.Dispose();
|
||||
DestroyImmediate(instance);
|
||||
};
|
||||
}
|
||||
|
||||
return _InvokableProperty;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] The GUI height required by <see cref="DoGUI"/>.</summary>
|
||||
public static float Height
|
||||
=> AnimancerGUI.LineHeight;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static int _LastControlID;
|
||||
private static int _PropertyIndex;
|
||||
|
||||
/// <summary>[Editor-Only] Draws the <see cref="InvokableProperty"/> GUI.</summary>
|
||||
public static bool DoGUI(
|
||||
ref Rect area,
|
||||
GUIContent label,
|
||||
SerializedProperty property,
|
||||
out object invokable)
|
||||
{
|
||||
var controlID = GUIUtility.GetControlID(FocusType.Passive);
|
||||
|
||||
if (_LastControlID >= controlID)
|
||||
_PropertyIndex = 0;
|
||||
|
||||
_LastControlID = controlID;
|
||||
|
||||
var invokablesProperty = InvokableProperty;
|
||||
if (invokablesProperty.arraySize <= _PropertyIndex)
|
||||
invokablesProperty.arraySize = _PropertyIndex + 1;
|
||||
|
||||
var invokableProperty = invokablesProperty.GetArrayElementAtIndex(_PropertyIndex);
|
||||
invokableProperty.prefabOverride = property.prefabOverride;
|
||||
|
||||
_PropertyIndex++;
|
||||
|
||||
label = EditorGUI.BeginProperty(area, label, property);
|
||||
|
||||
EditorGUI.PropertyField(area, invokableProperty, label, false);
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
|
||||
invokable = invokableProperty.managedReferenceValue;
|
||||
if (invokable == null)
|
||||
return false;
|
||||
|
||||
invokableProperty.managedReferenceValue = null;
|
||||
invokableProperty.serializedObject.ApplyModifiedProperties();
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3cb5d53924d6c94db149f251762c306
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,166 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Sequence = Animancer.AnimancerEvent.Sequence;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="Sequence"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/EventSequenceDrawer
|
||||
public class EventSequenceDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly ConditionalWeakTable<Sequence, EventSequenceDrawer>
|
||||
SequenceToDrawer = new();
|
||||
|
||||
/// <summary>Returns a cached <see cref="EventSequenceDrawer"/> for the `events`.</summary>
|
||||
/// <remarks>
|
||||
/// The cache uses a <see cref="ConditionalWeakTable{TKey, TValue}"/> so it doesn't prevent the `events`
|
||||
/// from being garbage collected.
|
||||
/// </remarks>
|
||||
public static EventSequenceDrawer Get(Sequence events)
|
||||
{
|
||||
if (events == null)
|
||||
return null;
|
||||
|
||||
if (!SequenceToDrawer.TryGetValue(events, out var drawer))
|
||||
SequenceToDrawer.Add(events, drawer = new());
|
||||
|
||||
return drawer;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calculates the number of vertical pixels required to draw the contents of the `events`.</summary>
|
||||
public float CalculateHeight(Sequence events)
|
||||
=> AnimancerGUI.CalculateHeight(CalculateLineCount(events));
|
||||
|
||||
/// <summary>Calculates the number of lines required to draw the contents of the `events`.</summary>
|
||||
public int CalculateLineCount(Sequence events)
|
||||
{
|
||||
if (events == null)
|
||||
return 0;
|
||||
|
||||
if (!IsExpanded)
|
||||
return 1;
|
||||
|
||||
var count = 1;
|
||||
|
||||
for (int i = 0; i < events.Count; i++)
|
||||
count += DelegateGUI.CalculateLineCount(events[i].callback);
|
||||
|
||||
count += DelegateGUI.CalculateLineCount(events.EndEvent.callback);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should the target's default be shown?</summary>
|
||||
public bool IsExpanded { get; set; }
|
||||
|
||||
private static ConversionCache<int, string> _EventNumberCache;
|
||||
private static float _LogButtonWidth = float.NaN;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI for the `events`.</summary>
|
||||
public void DoGUI(ref Rect area, Sequence events, GUIContent label)
|
||||
{
|
||||
using (var content = PooledGUIContent.Acquire(GetSummary(events)))
|
||||
DoGUI(ref area, events, label, content);
|
||||
}
|
||||
|
||||
/// <summary>Draws the GUI for the `events`.</summary>
|
||||
public void DoGUI(ref Rect area, Sequence events, GUIContent label, GUIContent summary)
|
||||
{
|
||||
if (events == null)
|
||||
return;
|
||||
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
|
||||
var headerArea = area;
|
||||
|
||||
const string LogLabel = "Log";
|
||||
if (float.IsNaN(_LogButtonWidth))
|
||||
_LogButtonWidth = EditorStyles.miniButton.CalculateWidth(LogLabel);
|
||||
var logArea = AnimancerGUI.StealFromRight(ref headerArea, _LogButtonWidth);
|
||||
if (GUI.Button(logArea, LogLabel, EditorStyles.miniButton))
|
||||
Debug.Log(events.DeepToString());
|
||||
|
||||
IsExpanded = EditorGUI.Foldout(headerArea, IsExpanded, GUIContent.none, true);
|
||||
EditorGUI.LabelField(headerArea, label, summary);
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
|
||||
if (!IsExpanded)
|
||||
return;
|
||||
|
||||
var enabled = GUI.enabled;
|
||||
GUI.enabled = false;
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
for (int i = 0; i < events.Count; i++)
|
||||
{
|
||||
var name = events.GetName(i);
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
_EventNumberCache ??= new(index => $"Event {index}");
|
||||
|
||||
name = _EventNumberCache.Convert(i);
|
||||
}
|
||||
|
||||
Draw(ref area, name, events[i]);
|
||||
}
|
||||
|
||||
Draw(ref area, "End Event", events.EndEvent);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
GUI.enabled = enabled;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly ConversionCache<int, string>
|
||||
SummaryCache = new((count) => $"[{count}]"),
|
||||
EndSummaryCache = new((count) => $"[{count}] + End");
|
||||
|
||||
/// <summary>Returns a summary of the `events`.</summary>
|
||||
public static string GetSummary(Sequence events)
|
||||
{
|
||||
var cache = float.IsNaN(events.NormalizedEndTime) && AnimancerEvent.IsNullOrDummy(events.OnEnd)
|
||||
? SummaryCache
|
||||
: EndSummaryCache;
|
||||
return cache.Convert(events.Count);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static ConversionCache<float, string> _EventTimeCache;
|
||||
|
||||
/// <summary>Draws the GUI for the `animancerEvent`.</summary>
|
||||
public static void Draw(ref Rect area, string name, AnimancerEvent animancerEvent)
|
||||
{
|
||||
_EventTimeCache ??= new((time)
|
||||
=> float.IsNaN(time) ? "Time = Auto" : $"Time = {time.ToStringCached()}x");
|
||||
|
||||
var timeText = _EventTimeCache.Convert(animancerEvent.normalizedTime);
|
||||
|
||||
using (var label = PooledGUIContent.Acquire(name))
|
||||
using (var value = PooledGUIContent.Acquire(timeText))
|
||||
DelegateGUI.DoGUI(ref area, label, animancerEvent.callback, value);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 093331cbc1ddef74ebf6f1b8056f1c00
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,195 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A GUI wrapper for drawing any object as a label with an icon.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/FastObjectField
|
||||
///
|
||||
public struct FastObjectField
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>A <see cref="FastObjectField"/> representing <c>null</c>.</summary>
|
||||
public static FastObjectField Null => new()
|
||||
{
|
||||
Text = "Null"
|
||||
};
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The object passed into the last <c>Draw</c> call.</summary>
|
||||
public object Value { get; private set; }
|
||||
|
||||
/// <summary>The text used for the label in the last <c>Draw</c> call.</summary>
|
||||
public string Text { get; private set; }
|
||||
|
||||
/// <summary>The icon drawn in the last <c>Draw</c> call.</summary>
|
||||
public Texture Icon { get; private set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the current details directly.</summary>
|
||||
public void Set(object value, string text, Texture icon)
|
||||
{
|
||||
Value = value;
|
||||
Text = text;
|
||||
Icon = icon;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a field for the `value`.</summary>
|
||||
public Rect Draw(Rect area, string label, object value, bool drawPing = true)
|
||||
{
|
||||
if (drawPing)
|
||||
ObjectHighlightGUI.Draw(area, value);
|
||||
|
||||
if (!string.IsNullOrEmpty(label))
|
||||
{
|
||||
var labelWidth = EditorGUIUtility.labelWidth - area.x + AnimancerGUI.StandardSpacing + 1;
|
||||
var labelArea = AnimancerGUI.StealFromLeft(ref area, labelWidth);
|
||||
EditorGUI.LabelField(labelArea, label);
|
||||
}
|
||||
|
||||
Draw(area, value, false);
|
||||
return area;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a field for the `value`.</summary>
|
||||
public void Draw(Rect area, object value, bool drawPing = true)
|
||||
{
|
||||
if (Value != value && Event.current.type == EventType.Layout)
|
||||
SetValue(value);
|
||||
|
||||
Draw(area, drawPing);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a field for the <see cref="Value"/>.</summary>
|
||||
public readonly void Draw(Rect area, bool drawPing = true)
|
||||
{
|
||||
HandleClick(area, drawPing);
|
||||
|
||||
if (Icon != null)
|
||||
{
|
||||
var iconArea = AnimancerGUI.StealFromLeft(ref area, area.height);
|
||||
GUI.DrawTexture(iconArea, Icon);
|
||||
}
|
||||
|
||||
GUI.Label(area, Text);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly void HandleClick(Rect area, bool drawPing)
|
||||
{
|
||||
var currentEvent = Event.current;
|
||||
|
||||
switch (currentEvent.rawType)
|
||||
{
|
||||
case EventType.MouseUp:
|
||||
if (currentEvent.button == 0 &&
|
||||
area.Contains(currentEvent.mousePosition))
|
||||
{
|
||||
ObjectHighlightGUI.Highlight(Value);
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType.Repaint:
|
||||
if (drawPing)
|
||||
ObjectHighlightGUI.Draw(area, Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the cached details.</summary>
|
||||
public void SetValue(object value, string text, Texture icon = null)
|
||||
{
|
||||
Value = value;
|
||||
Text = text;
|
||||
Icon = icon;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the cached details based on the `value`.</summary>
|
||||
public void SetValue(object value)
|
||||
{
|
||||
Value = value;
|
||||
Text = GetText();
|
||||
Icon = GetIcon();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string based on the <see cref="Value"/>.</summary>
|
||||
private readonly string GetText()
|
||||
{
|
||||
if (Value == null)
|
||||
return "Null";
|
||||
|
||||
if (Value is Object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return $"Null({obj.GetType().Name})";
|
||||
|
||||
return obj.GetCachedName();
|
||||
}
|
||||
|
||||
if (Value is string str)
|
||||
return $"\"{str}\"";
|
||||
|
||||
return Value.ToString();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an icon based on the type of the <see cref="Value"/>.</summary>
|
||||
private readonly Texture GetIcon()
|
||||
=> GetIcon(Value);
|
||||
|
||||
/// <summary>Returns an icon based on the type of the `value`.</summary>
|
||||
public static Texture GetIcon(object value)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
if (value is Object obj)
|
||||
return AssetPreview.GetMiniThumbnail(obj);
|
||||
|
||||
var type = value is AnimancerState
|
||||
? typeof(AnimatorState)
|
||||
: value.GetType();
|
||||
|
||||
return AssetPreview.GetMiniTypeThumbnail(type);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Clears all cached details.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Value = null;
|
||||
Text = null;
|
||||
Icon = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 119892efdb3185441a33090cfe8aaa57
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,81 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="NamedEventDictionary"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/NamedEventDictionaryDrawer
|
||||
///
|
||||
public static class NamedEventDictionaryDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string
|
||||
KeyPrefix = AnimancerGraphDrawer.KeyPrefix;
|
||||
|
||||
private static readonly BoolPref
|
||||
AreEventsExpanded = new(KeyPrefix + nameof(AreEventsExpanded), false);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the <see cref="AnimancerGraph.Events"/>.</summary>
|
||||
public static void DoEventsGUI(AnimancerGraph graph)
|
||||
{
|
||||
if (!graph.HasEvents)
|
||||
return;
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
var events = graph.Events;
|
||||
|
||||
AreEventsExpanded.Value = AnimancerGUI.DoLabelFoldoutFieldGUI(
|
||||
"Events",
|
||||
events.Count.ToStringCached(),
|
||||
AreEventsExpanded);
|
||||
|
||||
if (AreEventsExpanded)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
var sortedEvents = ListPool.Acquire<StringReference>();
|
||||
sortedEvents.AddRange(events.Keys);
|
||||
sortedEvents.Sort();
|
||||
|
||||
foreach (var item in sortedEvents)
|
||||
DoEventGUI(item, events[item]);
|
||||
|
||||
ListPool.Release(sortedEvents);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws an event.</summary>
|
||||
public static void DoEventGUI(string name, Action action)
|
||||
{
|
||||
var gui = CustomGUIFactory.GetOrCreateForObject(action);
|
||||
if (gui == null)
|
||||
{
|
||||
EditorGUILayout.LabelField(name, action.ToStringDetailed());
|
||||
return;
|
||||
}
|
||||
|
||||
gui.SetLabel(name);
|
||||
gui.DoGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6635752e1a2ee1f4b9ab200f95c5200b
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,79 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="ParameterDictionary"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ParameterDictionaryDrawer
|
||||
///
|
||||
public static class ParameterDictionaryDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string
|
||||
KeyPrefix = AnimancerGraphDrawer.KeyPrefix;
|
||||
|
||||
private static readonly BoolPref
|
||||
AreParametersExpanded = new(KeyPrefix + nameof(AreParametersExpanded), false);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the <see cref="AnimancerGraph.Parameters"/>.</summary>
|
||||
public static void DoParametersGUI(AnimancerGraph graph)
|
||||
{
|
||||
if (!graph.HasParameters)
|
||||
return;
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
var parameters = graph.Parameters;
|
||||
|
||||
AreParametersExpanded.Value = AnimancerGUI.DoLabelFoldoutFieldGUI(
|
||||
"Parameters",
|
||||
parameters.Count.ToStringCached(),
|
||||
AreParametersExpanded);
|
||||
|
||||
if (AreParametersExpanded)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
var sortedParameters = ListPool.Acquire<IParameter>();
|
||||
sortedParameters.AddRange(parameters);
|
||||
sortedParameters.Sort();
|
||||
|
||||
foreach (var item in sortedParameters)
|
||||
DoParameterGUI(item);
|
||||
ListPool.Release(sortedParameters);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the `parameter`.</summary>
|
||||
private static void DoParameterGUI(IParameter parameter)
|
||||
{
|
||||
var gui = CustomGUIFactory.GetOrCreateForObject(parameter);
|
||||
if (gui == null)
|
||||
{
|
||||
EditorGUILayout.LabelField(parameter.Key, parameter.Value.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
gui.SetLabel(parameter.Key);
|
||||
gui.DoGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e51b2491d1979ef46a1ab47b81f5a6aa
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cecb0f396a3194458efd24438a77b03
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,274 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="StringAsset"/> fields.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/StringAssetDrawer
|
||||
[CustomPropertyDrawer(typeof(StringAsset), true)]
|
||||
public class StringAssetDrawer : PropertyDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
=> LineHeight;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
label = EditorGUI.BeginProperty(area, label, property);
|
||||
|
||||
property.objectReferenceValue = DrawGUI(
|
||||
area,
|
||||
label,
|
||||
property,
|
||||
out var exitGUI);
|
||||
|
||||
if (exitGUI)
|
||||
{
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
GUIUtility.ExitGUI();
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly Func<Object[]> GetCurrentPropertyValues =
|
||||
() => _CurrentProperty != null ? Serialization.GetValues<Object>(_CurrentProperty) : null;
|
||||
|
||||
private static SerializedProperty _CurrentProperty;
|
||||
|
||||
/// <summary>Draws the GUI for a <see cref="StringAsset"/>.</summary>
|
||||
public static Object DrawGUI(
|
||||
Rect area,
|
||||
GUIContent label,
|
||||
SerializedProperty property,
|
||||
out bool exitGUI)
|
||||
{
|
||||
var showMixedValue = EditorGUI.showMixedValue;
|
||||
if (property != null && property.hasMultipleDifferentValues)
|
||||
EditorGUI.showMixedValue = true;
|
||||
|
||||
_CurrentProperty = property;
|
||||
var value = DrawGUI(
|
||||
area,
|
||||
label,
|
||||
property?.objectReferenceValue,
|
||||
property?.serializedObject?.targetObject,
|
||||
out exitGUI,
|
||||
GetCurrentPropertyValues);
|
||||
_CurrentProperty = null;
|
||||
|
||||
EditorGUI.showMixedValue = showMixedValue;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly int ButtonHash = "Button".GetHashCode();
|
||||
private static readonly int ObjectFieldHash = "s_ObjectFieldHash".GetHashCode();
|
||||
|
||||
/// <summary>Draws the GUI for a <see cref="StringAsset"/>.</summary>
|
||||
public static Object DrawGUI(
|
||||
Rect area,
|
||||
GUIContent label,
|
||||
Object value,
|
||||
Object context,
|
||||
out bool exitGUI,
|
||||
Func<Object[]> getAllValues = null)
|
||||
{
|
||||
var currentEvent = Event.current;
|
||||
if (currentEvent.type == EventType.Repaint &&
|
||||
!area.Contains(currentEvent.mousePosition) &&
|
||||
!IsDraggingStringAsset())
|
||||
{
|
||||
GUIUtility.GetControlID(ButtonHash, FocusType.Passive);// Button.
|
||||
GUIUtility.GetControlID(ButtonHash, FocusType.Passive);// Button.
|
||||
GUIUtility.GetControlID(DragHint, FocusType.Passive, area);// DragAndDrop.
|
||||
|
||||
var iconSize = EditorGUIUtility.GetIconSize();
|
||||
var newIconSize = LineHeight * 2 / 3;
|
||||
EditorGUIUtility.SetIconSize(new(newIconSize, newIconSize));
|
||||
|
||||
var controlID = GUIUtility.GetControlID(ObjectFieldHash, FocusType.Keyboard, area);// Object.
|
||||
|
||||
var valueArea = EditorGUI.PrefixLabel(area, controlID, label);
|
||||
|
||||
using (var content = PooledGUIContent.Acquire())
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
content.text = value.name;
|
||||
content.image = AnimancerIcons.ScriptableObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
content.text = "";
|
||||
content.image = AssetPreview.GetMiniTypeThumbnail(typeof(StringAsset));
|
||||
}
|
||||
|
||||
EditorStyles.objectField.Draw(valueArea, content, false, false, false, false);
|
||||
}
|
||||
|
||||
EditorGUIUtility.SetIconSize(iconSize);
|
||||
|
||||
exitGUI = false;
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
var buttonArea = StealFromRight(ref area, area.height, StandardSpacing);
|
||||
|
||||
var content = AnimancerIcons.AddIcon("Create and save a new String Asset");
|
||||
if (GUI.Button(buttonArea, content, NoPaddingButtonStyle))
|
||||
{
|
||||
exitGUI = true;
|
||||
return CreateNewInstance(label.text, context);
|
||||
}
|
||||
|
||||
GUIUtility.GetControlID(ButtonHash, FocusType.Passive);
|
||||
}
|
||||
else
|
||||
{
|
||||
var clearArea = StealFromRight(ref area, area.height, StandardSpacing);
|
||||
var copyArea = StealFromRight(ref area, area.height, StandardSpacing);
|
||||
|
||||
var content = AnimancerIcons.CopyIcon("Copy string to clipboard");
|
||||
if (GUI.Button(copyArea, content, NoPaddingButtonStyle))
|
||||
GUIUtility.systemCopyBuffer = value.name;
|
||||
|
||||
content = AnimancerIcons.ClearIcon("Clear reference");
|
||||
if (GUI.Button(clearArea, content, NoPaddingButtonStyle))
|
||||
value = null;
|
||||
}
|
||||
|
||||
HandleDragAndDrop(area, currentEvent, value, getAllValues);
|
||||
|
||||
exitGUI = false;
|
||||
return EditorGUI.ObjectField(area, label, value, typeof(StringAsset), false);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly int DragHint = "Drag".GetHashCode();
|
||||
|
||||
private static void HandleDragAndDrop(
|
||||
Rect area,
|
||||
Event currentEvent,
|
||||
Object value,
|
||||
Func<Object[]> getAllValues)
|
||||
{
|
||||
var id = GUIUtility.GetControlID(DragHint, FocusType.Passive, area);
|
||||
|
||||
switch (currentEvent.type)
|
||||
{
|
||||
// Drag out of object field.
|
||||
case EventType.MouseDrag:
|
||||
if (GUIUtility.keyboardControl == id + 1 &&
|
||||
currentEvent.button == 0 &&
|
||||
area.Contains(currentEvent.mousePosition) &&
|
||||
value != null)
|
||||
{
|
||||
var values = getAllValues?.Invoke() ?? new Object[] { value };
|
||||
DragAndDrop.PrepareStartDrag();
|
||||
DragAndDrop.objectReferences = values;
|
||||
DragAndDrop.StartDrag("Objects");
|
||||
currentEvent.Use();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Is a <see cref="StringAsset"/> currently being dragged?</summary>
|
||||
private static bool IsDraggingStringAsset()
|
||||
{
|
||||
var dragging = DragAndDrop.objectReferences;
|
||||
if (dragging.IsNullOrEmpty())
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < dragging.Length; i++)
|
||||
if (dragging[i] is not StringAsset)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string FolderPathKey = nameof(StringAsset) + ".FolderPath";
|
||||
|
||||
/// <summary>Asks where to save a new <see cref="StringAsset"/>.</summary>
|
||||
private static Object CreateNewInstance(string name, Object targetObject)
|
||||
{
|
||||
var folderPath = GetSaveFolder(targetObject);
|
||||
|
||||
var path = EditorUtility.SaveFilePanelInProject(
|
||||
"Create String Asset",
|
||||
name,
|
||||
"asset",
|
||||
"Where yould you like to save the new String Asset?",
|
||||
folderPath);
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return null;
|
||||
|
||||
EditorPrefs.SetString(FolderPathKey, Path.GetDirectoryName(path));
|
||||
|
||||
var instance = ScriptableObject.CreateInstance<StringAsset>();
|
||||
|
||||
AssetDatabase.CreateAsset(instance, path);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static string GetSaveFolder(Object targetObject)
|
||||
{
|
||||
var getActiveFolderPath = typeof(ProjectWindowUtil).GetMethod(
|
||||
"GetActiveFolderPath",
|
||||
AnimancerReflection.StaticBindings,
|
||||
null,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
if (getActiveFolderPath != null &&
|
||||
getActiveFolderPath.ReturnType == typeof(string))
|
||||
{
|
||||
var activeFolderPath = getActiveFolderPath.Invoke(null, Array.Empty<object>())?.ToString();
|
||||
if (!string.IsNullOrEmpty(activeFolderPath))
|
||||
return activeFolderPath;
|
||||
}
|
||||
|
||||
var folderPath = AssetDatabase.GetAssetPath(targetObject);
|
||||
if (!string.IsNullOrEmpty(folderPath))
|
||||
folderPath = Path.GetDirectoryName(folderPath);
|
||||
|
||||
if (string.IsNullOrEmpty(folderPath))
|
||||
{
|
||||
folderPath = EditorPrefs.GetString(FolderPathKey);
|
||||
if (folderPath == null || !folderPath.StartsWith("Assets/"))
|
||||
folderPath = "Assets/";
|
||||
}
|
||||
|
||||
return folderPath;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6917a44e40a2ec74c86baf5d286c1160
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,833 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using Animancer.Editor.Previews;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws a GUI box denoting a period of time.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TimelineGUI
|
||||
///
|
||||
public class TimelineGUI : IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly ConversionCache<float, string>
|
||||
G2Cache = new(value =>
|
||||
{
|
||||
if (Math.Abs(value) <= 99)
|
||||
return value.ToString("G2");
|
||||
else
|
||||
return ((int)value).ToString();
|
||||
});
|
||||
|
||||
private static Texture _EventIcon;
|
||||
|
||||
/// <summary>The icon used for events.</summary>
|
||||
public static Texture EventIcon
|
||||
=> AnimancerIcons.Load(ref _EventIcon, "Animation.EventMarker");
|
||||
|
||||
private static readonly Color
|
||||
FadeHighlightColor = new(0.35f, 0.5f, 1, 0.5f),
|
||||
SelectedEventColor = new(0.3f, 0.55f, 0.95f),
|
||||
UnselectedEventColor = AnimancerGUI.Grey(0.9f),
|
||||
PreviewTimeColor = AnimancerStateDrawerColors.FadeLineColor,
|
||||
BaseTimeColor = AnimancerGUI.Grey(0.5f, 0.75f);
|
||||
|
||||
private Rect _Area;
|
||||
|
||||
/// <summary>The pixel area in which this <see cref="TimelineGUI"/> is drawing.</summary>
|
||||
public Rect Area => _Area;
|
||||
|
||||
private float _Speed, _Duration, _MinTime, _MaxTime, _StartTime, _EndTime, _FadeInEnd, _FadeOutEnd, _SecondsToPixels;
|
||||
private bool _HasEndTime;
|
||||
|
||||
private readonly List<float>
|
||||
EventTimes = new();
|
||||
|
||||
/// <summary>The height of the time ticks.</summary>
|
||||
public float TickHeight { get; private set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Conversions
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Converts a number of seconds to a horizontal pixel position along the ruler.</summary>
|
||||
/// <remarks>The value is rounded to the nearest integer.</remarks>
|
||||
public float SecondsToPixels(float seconds) => AnimancerUtilities.Round((seconds - _MinTime) * _SecondsToPixels);
|
||||
|
||||
/// <summary>Converts a horizontal pixel position along the ruler to a number of seconds.</summary>
|
||||
public float PixelsToSeconds(float pixels) => (pixels / _SecondsToPixels) + _MinTime;
|
||||
|
||||
/// <summary>Converts a number of seconds to a normalized time value.</summary>
|
||||
public float SecondsToNormalized(float seconds) => seconds / _Duration;
|
||||
|
||||
/// <summary>Converts a normalized time value to a number of seconds.</summary>
|
||||
public float NormalizedToSeconds(float normalized) => normalized * _Duration;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private TimelineGUI() { }
|
||||
private static readonly TimelineGUI Instance = new();
|
||||
|
||||
/// <summary>The currently drawing <see cref="TimelineGUI"/> (or null if none is drawing).</summary>
|
||||
public static TimelineGUI Current { get; private set; }
|
||||
|
||||
/// <summary>Ends the area started by <see cref="BeginGUI"/>.</summary>
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
Current = null;
|
||||
GUI.EndClip();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Sets the `area` in which the ruler will be drawn and draws a <see cref="GUI.Box(Rect, string)"/> there.
|
||||
/// The returned object must have <see cref="IDisposable.Dispose"/> called on it afterwards.
|
||||
/// </summary>
|
||||
private static IDisposable BeginGUI(Rect area)
|
||||
{
|
||||
if (Current != null)
|
||||
throw new InvalidOperationException($"{nameof(TimelineGUI)} can't be used recursively.");
|
||||
|
||||
if (!EditorGUIUtility.hierarchyMode)
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
area = EditorGUI.IndentedRect(area);
|
||||
|
||||
if (!EditorGUIUtility.hierarchyMode)
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
GUI.Box(area, "");
|
||||
|
||||
GUI.BeginClip(area);
|
||||
|
||||
area.x = area.y = 0;
|
||||
Instance._Area = area;
|
||||
|
||||
Instance.TickHeight = Mathf.Ceil(area.height * 0.3f);
|
||||
|
||||
return Current = Instance;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the ruler GUI and handles input events for the specified `context`.</summary>
|
||||
public static void DoGUI(Rect area, SerializableEventSequenceDrawer.Context context, out float addEventNormalizedTime)
|
||||
{
|
||||
using (BeginGUI(area))
|
||||
Current.DoGUI(context, out addEventNormalizedTime);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the ruler GUI and handles input events for the specified `context`.</summary>
|
||||
private void DoGUI(SerializableEventSequenceDrawer.Context context, out float addEventNormalizedTime)
|
||||
{
|
||||
if (context.Property.hasMultipleDifferentValues)
|
||||
{
|
||||
GUI.Label(_Area, "Multi-editing events is not supported");
|
||||
addEventNormalizedTime = float.NaN;
|
||||
return;
|
||||
}
|
||||
|
||||
var transition = context.TransitionContext.Transition;
|
||||
|
||||
_Speed = transition.Speed;
|
||||
if (float.IsNaN(_Speed))
|
||||
_Speed = 1;
|
||||
|
||||
_Duration = context.TransitionContext.MaximumLength;
|
||||
if (_Duration <= 0)
|
||||
_Duration = 1;
|
||||
|
||||
GatherEventTimes(context);
|
||||
|
||||
_StartTime = GetStartTime(transition.NormalizedStartTime, _Speed, _Duration);
|
||||
|
||||
_FadeInEnd = _StartTime + transition.FadeDuration * _Speed;
|
||||
|
||||
_FadeOutEnd = GetFadeOutEnd(_Speed, _EndTime, _Duration);
|
||||
|
||||
_MinTime = Mathf.Min(0, _StartTime);
|
||||
_MinTime = Mathf.Min(_MinTime, _FadeOutEnd);
|
||||
_MinTime = Mathf.Min(_MinTime, EventTimes[0]);
|
||||
|
||||
_MaxTime = Mathf.Max(_StartTime, _FadeOutEnd);
|
||||
if (EventTimes.Count >= 2)
|
||||
_MaxTime = Mathf.Max(_MaxTime, EventTimes[^2]);
|
||||
|
||||
if (_MaxTime < _Duration)
|
||||
_MaxTime = _Duration;
|
||||
|
||||
_SecondsToPixels = _Area.width / (_MaxTime - _MinTime);
|
||||
|
||||
DoFadeHighlightGUI();
|
||||
|
||||
if (AnimancerUtilities.TryGetWrappedObject(transition, out ITransitionGUI gui))
|
||||
gui.OnTimelineBackgroundGUI();
|
||||
|
||||
DoEventsGUI(context, out addEventNormalizedTime);
|
||||
DoRulerGUI();
|
||||
|
||||
if (_Speed > 0)
|
||||
{
|
||||
if (_StartTime >= _EndTime)
|
||||
GUI.Label(_Area, "Start Time is not before End Time");
|
||||
}
|
||||
else if (_Speed < 0)
|
||||
{
|
||||
if (_StartTime <= _EndTime)
|
||||
GUI.Label(_Area, "Start Time is not after End Time");
|
||||
}
|
||||
|
||||
gui?.OnTimelineForegroundGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calculates the start time of the transition (in seconds).</summary>
|
||||
public static float GetStartTime(float normalizedStartTime, float speed, float duration)
|
||||
{
|
||||
if (float.IsNaN(normalizedStartTime))
|
||||
{
|
||||
return speed < 0 ? duration : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return normalizedStartTime * duration;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Calculates the end time of the fade out (in seconds).</summary>
|
||||
public static float GetFadeOutEnd(float speed, float endTime, float duration)
|
||||
{
|
||||
if (speed < 0)
|
||||
return endTime > 0 ? 0 : (endTime - AnimancerGraph.DefaultFadeDuration) * -speed;
|
||||
else
|
||||
return endTime < duration ? duration : endTime + AnimancerGraph.DefaultFadeDuration * speed;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly Vector3[] QuadVertices = new Vector3[4];
|
||||
|
||||
/// <summary>Draws a polygon describing the start, end, and fade details.</summary>
|
||||
private void DoFadeHighlightGUI()
|
||||
{
|
||||
if (Event.current.type != EventType.Repaint)
|
||||
return;
|
||||
|
||||
var color = Handles.color;
|
||||
Handles.color = FadeHighlightColor;
|
||||
QuadVertices[0] = new(SecondsToPixels(_StartTime), _Area.y);
|
||||
QuadVertices[1] = new(SecondsToPixels(_FadeInEnd), _Area.yMax + 1);
|
||||
QuadVertices[2] = new(SecondsToPixels(_FadeOutEnd), _Area.yMax + 1);
|
||||
QuadVertices[3] = new(SecondsToPixels(_EndTime), _Area.y);
|
||||
Handles.DrawAAConvexPolygon(QuadVertices);
|
||||
Handles.color = color;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Events
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void GatherEventTimes(SerializableEventSequenceDrawer.Context context)
|
||||
{
|
||||
EventTimes.Clear();
|
||||
|
||||
if (context.Times.Count > 0)
|
||||
{
|
||||
var depth = context.Times.Property.depth;
|
||||
var time = context.Times.GetElement(0);
|
||||
|
||||
while (time.depth > depth)
|
||||
{
|
||||
EventTimes.Add(time.floatValue * _Duration);
|
||||
time.Next(false);
|
||||
}
|
||||
|
||||
_EndTime = EventTimes[^1];
|
||||
if (!float.IsNaN(_EndTime))
|
||||
{
|
||||
_HasEndTime = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_EndTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(_Speed) * _Duration;
|
||||
_HasEndTime = false;
|
||||
if (EventTimes.Count == 0)
|
||||
EventTimes.Add(_EndTime);
|
||||
else
|
||||
EventTimes[^1] = _EndTime;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly int EventHash = "Event".GetHashCode();
|
||||
private static readonly List<int> EventControlIDs = new();
|
||||
|
||||
/// <summary>Draws the details of the <see cref="SerializableEventSequenceDrawer.Context.Callbacks"/>.</summary>
|
||||
private void DoEventsGUI(SerializableEventSequenceDrawer.Context context, out float addEventNormalizedTime)
|
||||
{
|
||||
addEventNormalizedTime = float.NaN;
|
||||
var currentEvent = Event.current;
|
||||
|
||||
EventControlIDs.Clear();
|
||||
var selectedEventControlID = -1;
|
||||
|
||||
var baseControlID = GUIUtility.GetControlID(EventHash - 1, FocusType.Passive);
|
||||
|
||||
for (int i = 0; i < EventTimes.Count; i++)
|
||||
{
|
||||
var controlID = GUIUtility.GetControlID(EventHash + i, FocusType.Keyboard);
|
||||
EventControlIDs.Add(controlID);
|
||||
if (context.SelectedEvent == i)
|
||||
selectedEventControlID = controlID;
|
||||
}
|
||||
|
||||
EventControlIDs.Add(baseControlID);
|
||||
|
||||
switch (currentEvent.type)
|
||||
{
|
||||
case EventType.Repaint:
|
||||
RepaintEventsGUI(context);
|
||||
break;
|
||||
|
||||
case EventType.MouseDown:
|
||||
OnMouseDown(currentEvent, context, ref addEventNormalizedTime);
|
||||
break;
|
||||
|
||||
case EventType.MouseUp:
|
||||
OnMouseUp(currentEvent, context);
|
||||
break;
|
||||
|
||||
case EventType.MouseDrag:
|
||||
if (_Duration <= 0)
|
||||
break;
|
||||
|
||||
var hotControl = GUIUtility.hotControl;
|
||||
if (hotControl == baseControlID)
|
||||
{
|
||||
SetPreviewTime(context, currentEvent);
|
||||
GUIUtility.ExitGUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < EventTimes.Count; i++)
|
||||
{
|
||||
if (hotControl == EventControlIDs[i])
|
||||
{
|
||||
if (context.Times.Count < 1)
|
||||
context.Times.Count = 1;
|
||||
|
||||
var seconds = PixelsToSeconds(currentEvent.mousePosition.x);
|
||||
|
||||
if (currentEvent.control)
|
||||
SnapToFrameRate(context, ref seconds);
|
||||
|
||||
var timeProperty = context.Times.GetElement(i);
|
||||
var normalizedTime = seconds / _Duration;
|
||||
timeProperty.floatValue = normalizedTime;
|
||||
SerializableEventSequenceDrawer.SyncEventTimeChange(context, i, normalizedTime);
|
||||
timeProperty.serializedObject.ApplyModifiedProperties();
|
||||
timeProperty.serializedObject.Update();
|
||||
|
||||
GUIUtility.hotControl = EventControlIDs[context.SelectedEvent];
|
||||
GUI.changed = true;
|
||||
|
||||
SetPreviewTime(context, currentEvent);
|
||||
GUIUtility.ExitGUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType.KeyUp:
|
||||
if (GUIUtility.keyboardControl != selectedEventControlID)
|
||||
break;
|
||||
|
||||
var exitGUI = false;
|
||||
|
||||
switch (currentEvent.keyCode)
|
||||
{
|
||||
case KeyCode.Delete:
|
||||
case KeyCode.Backspace:
|
||||
SerializableEventSequenceDrawer.RemoveEvent(context, context.SelectedEvent);
|
||||
exitGUI = true;
|
||||
break;
|
||||
|
||||
case KeyCode.LeftArrow:
|
||||
NudgeEventTime(context, Event.current.shift ? -10 : -1);
|
||||
break;
|
||||
|
||||
case KeyCode.RightArrow:
|
||||
NudgeEventTime(context, Event.current.shift ? 10 : 1);
|
||||
break;
|
||||
|
||||
case KeyCode.Space:
|
||||
RoundEventTime(context);
|
||||
break;
|
||||
|
||||
default: return;// Don't call Use.
|
||||
}
|
||||
|
||||
GUI.changed = true;
|
||||
currentEvent.Use();
|
||||
|
||||
if (exitGUI)
|
||||
GUIUtility.ExitGUI();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Snaps the `seconds` value to the nearest multiple of the <see cref="AnimationClip.frameRate"/>.</summary>
|
||||
public void SnapToFrameRate(SerializableEventSequenceDrawer.Context context, ref float seconds)
|
||||
{
|
||||
if (AnimancerUtilities.TryGetFrameRate(context.TransitionContext.Transition, out var frameRate))
|
||||
{
|
||||
seconds = AnimancerUtilities.Round(seconds, 1f / frameRate);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void RepaintEventsGUI(SerializableEventSequenceDrawer.Context context)
|
||||
{
|
||||
var color = GUI.color;
|
||||
|
||||
for (int i = 0; i < EventTimes.Count; i++)
|
||||
{
|
||||
var currentColor = color;
|
||||
// Read Only: currentColor *= new(0.9f, 0.9f, 0.9f, 0.5f * alpha);
|
||||
if (context.SelectedEvent == i)
|
||||
{
|
||||
currentColor *= SelectedEventColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentColor *= UnselectedEventColor;
|
||||
}
|
||||
|
||||
if (i == EventTimes.Count - 1 && !_HasEndTime)
|
||||
currentColor.a *= 0.65f;
|
||||
|
||||
GUI.color = currentColor;
|
||||
|
||||
var area = GetEventIconArea(i);
|
||||
GUI.DrawTexture(area, EventIcon);
|
||||
}
|
||||
|
||||
GUI.color = color;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void OnMouseDown(Event currentEvent, SerializableEventSequenceDrawer.Context context, ref float addEventNormalizedTime)
|
||||
{
|
||||
if (!_Area.Contains(currentEvent.mousePosition))
|
||||
return;
|
||||
|
||||
var selectedEventControlID = 0;
|
||||
var selectedEvent = -1;
|
||||
|
||||
for (int i = 0; i < EventControlIDs.Count; i++)
|
||||
{
|
||||
var area = i < EventTimes.Count ? GetEventIconArea(i) : _Area;
|
||||
|
||||
if (area.Contains(currentEvent.mousePosition))
|
||||
{
|
||||
selectedEventControlID = EventControlIDs[i];
|
||||
selectedEvent = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedEvent < 0 || selectedEvent >= EventTimes.Count)
|
||||
{
|
||||
SetPreviewTime(context, currentEvent);
|
||||
selectedEvent = -1;
|
||||
}
|
||||
|
||||
if (currentEvent.type == EventType.MouseDown &&
|
||||
currentEvent.clickCount == 2)
|
||||
{
|
||||
addEventNormalizedTime = PixelsToSeconds(currentEvent.mousePosition.x);
|
||||
addEventNormalizedTime = SecondsToNormalized(addEventNormalizedTime);
|
||||
}
|
||||
|
||||
context.SelectedEvent = selectedEvent;
|
||||
|
||||
GUIUtility.keyboardControl = selectedEventControlID;
|
||||
currentEvent.Use(selectedEventControlID);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void OnMouseUp(Event currentEvent, SerializableEventSequenceDrawer.Context context)
|
||||
{
|
||||
if (currentEvent.button == 1 &&
|
||||
_Area.Contains(currentEvent.mousePosition))
|
||||
{
|
||||
currentEvent.Use(0);
|
||||
|
||||
ShowContextMenu(currentEvent, context);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void ShowContextMenu(Event currentEvent, SerializableEventSequenceDrawer.Context context)
|
||||
{
|
||||
context = context.Copy();
|
||||
var time = SecondsToNormalized(PixelsToSeconds(currentEvent.mousePosition.x));
|
||||
var hasSelectedEvent = context.SelectedEvent >= 0;
|
||||
|
||||
var menu = new GenericMenu();
|
||||
|
||||
AddContextFunction(menu, context, "Add Event (Double Click)", true,
|
||||
() => SerializableEventSequenceDrawer.AddEvent(context, time, null));
|
||||
|
||||
AddContextFunction(menu, context, "Remove Event (Delete)", hasSelectedEvent,
|
||||
() => SerializableEventSequenceDrawer.RemoveEvent(context, context.SelectedEvent));
|
||||
|
||||
AddCopyEventsFromAnimationClipsFunction(menu, context);
|
||||
|
||||
const string NudgePrefix = "Nudge Event Time/";
|
||||
AddContextFunction(menu, context, NudgePrefix + "Left 1 Pixel (Left Arrow)", hasSelectedEvent,
|
||||
() => NudgeEventTime(context, -1));
|
||||
AddContextFunction(menu, context, NudgePrefix + "Left 10 Pixels (Shift + Left Arrow)", hasSelectedEvent,
|
||||
() => NudgeEventTime(context, -10));
|
||||
AddContextFunction(menu, context, NudgePrefix + "Right 1 Pixel (Right Arrow)", hasSelectedEvent,
|
||||
() => NudgeEventTime(context, 1));
|
||||
AddContextFunction(menu, context, NudgePrefix + "Right 10 Pixels (Shift + Right Arrow)", hasSelectedEvent,
|
||||
() => NudgeEventTime(context, 10));
|
||||
|
||||
var canRoundTime = hasSelectedEvent;
|
||||
if (canRoundTime)
|
||||
{
|
||||
time = context.Times.GetElement(context.SelectedEvent).floatValue;
|
||||
canRoundTime = TryRoundValue(ref time);
|
||||
}
|
||||
|
||||
AddContextFunction(menu, context, $"Round Event Time to {time}x (Space)", canRoundTime,
|
||||
() => RoundEventTime(context));
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void AddContextFunction(
|
||||
GenericMenu menu,
|
||||
SerializableEventSequenceDrawer.Context context,
|
||||
string label,
|
||||
bool enabled,
|
||||
Action function)
|
||||
{
|
||||
menu.AddFunction(label, enabled, () =>
|
||||
{
|
||||
using (context.SetAsCurrent())
|
||||
{
|
||||
function();
|
||||
GUI.changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void AddCopyEventsFromAnimationClipsFunction(
|
||||
GenericMenu menu,
|
||||
SerializableEventSequenceDrawer.Context context)
|
||||
{
|
||||
|
||||
var transition = context.TransitionContext.Transition;
|
||||
|
||||
var clips = ListPool<AnimationClip>.Instance.Acquire();
|
||||
|
||||
clips.GatherFromSource(transition);
|
||||
var enabled = clips.Count > 0;
|
||||
|
||||
AddContextFunction(menu, context, "Copy Events from Animation Clip", enabled,
|
||||
() =>
|
||||
{
|
||||
var names = ListPool<StringAsset>.Instance.Acquire();
|
||||
var normalizedTimes = ListPool<float>.Instance.Acquire();
|
||||
|
||||
string createDirectory = null;
|
||||
|
||||
foreach (var clip in clips)
|
||||
{
|
||||
foreach (var animancerEvent in clip.events)
|
||||
{
|
||||
var name = StringReference.Get(animancerEvent.functionName);
|
||||
var asset = StringAsset.Find(name, out _);
|
||||
|
||||
if (asset == null)
|
||||
{
|
||||
asset = StringAsset.Create(name, ref createDirectory, out _);
|
||||
|
||||
// If no directory is picked, cancel the rest of this function.
|
||||
if (asset == null)
|
||||
return;
|
||||
}
|
||||
|
||||
names.Add(asset);
|
||||
normalizedTimes.Add(animancerEvent.time / clip.length);
|
||||
}
|
||||
}
|
||||
|
||||
ListPool<AnimationClip>.Instance.Release(clips);
|
||||
|
||||
for (int i = 0; i < normalizedTimes.Count; i++)
|
||||
SerializableEventSequenceDrawer.AddEvent(context, normalizedTimes[i], names[i]);
|
||||
|
||||
ListPool<StringAsset>.Instance.Release(names);
|
||||
ListPool<float>.Instance.Release(normalizedTimes);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void SetPreviewTime(SerializableEventSequenceDrawer.Context context, Event currentEvent)
|
||||
{
|
||||
if (_Duration > 0)
|
||||
{
|
||||
var seconds = PixelsToSeconds(currentEvent.mousePosition.x);
|
||||
|
||||
if (currentEvent.control)
|
||||
SnapToFrameRate(context, ref seconds);
|
||||
|
||||
TransitionPreviewWindow.PreviewNormalizedTime = seconds / _Duration;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private Rect GetEventIconArea(int index)
|
||||
{
|
||||
var width = EventIcon.width;
|
||||
|
||||
var x = SecondsToPixels(EventTimes[index]) - width * 0.5f;
|
||||
x = Mathf.Clamp(x, 0, _Area.width - width);
|
||||
|
||||
return new(x, _Area.y, width, EventIcon.height);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void NudgeEventTime(SerializableEventSequenceDrawer.Context context, float offsetPixels)
|
||||
{
|
||||
var index = context.SelectedEvent;
|
||||
var time = context.Times.GetElement(index);
|
||||
|
||||
var value = time.floatValue;
|
||||
value = NormalizedToSeconds(value);
|
||||
value = SecondsToPixels(value);
|
||||
|
||||
value += offsetPixels;
|
||||
|
||||
value = PixelsToSeconds(value);
|
||||
value = SecondsToNormalized(value);
|
||||
time.floatValue = value;
|
||||
|
||||
SerializableEventSequenceDrawer.SyncEventTimeChange(context, index, value);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void RoundEventTime(SerializableEventSequenceDrawer.Context context)
|
||||
{
|
||||
var index = context.SelectedEvent;
|
||||
var time = context.Times.GetElement(index);
|
||||
var value = time.floatValue;
|
||||
|
||||
if (TryRoundValue(ref value))
|
||||
{
|
||||
time.floatValue = value;
|
||||
SerializableEventSequenceDrawer.SyncEventTimeChange(context, index, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryRoundValue(ref float value)
|
||||
{
|
||||
var format = System.Globalization.NumberFormatInfo.InvariantInfo;
|
||||
var text = value.ToString(format);
|
||||
var dot = text.IndexOf('.');
|
||||
if (dot < 0)
|
||||
return false;
|
||||
|
||||
Round:
|
||||
var newValue = (float)Math.Round(value, text.Length - dot - 2, MidpointRounding.AwayFromZero);
|
||||
if (newValue == value)
|
||||
{
|
||||
dot--;
|
||||
if (dot > 0)
|
||||
goto Round;
|
||||
}
|
||||
|
||||
if (value != newValue)
|
||||
{
|
||||
value = newValue;
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Ticks
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly List<float> TickTimes = new();
|
||||
|
||||
/// <summary>Draws ticks and labels for important times throughout the area.</summary>
|
||||
private void DoRulerGUI()
|
||||
{
|
||||
if (Event.current.type != EventType.Repaint)
|
||||
return;
|
||||
|
||||
var area = new Rect(SecondsToPixels(0), _Area.yMax - TickHeight, 0, TickHeight)
|
||||
{
|
||||
xMax = SecondsToPixels(_Duration)
|
||||
};
|
||||
|
||||
EditorGUI.DrawRect(area, BaseTimeColor);
|
||||
|
||||
TickTimes.Clear();
|
||||
TickTimes.Add(0);
|
||||
TickTimes.Add(_StartTime);
|
||||
TickTimes.Add(_FadeInEnd);
|
||||
TickTimes.Add(_Duration);
|
||||
TickTimes.AddRange(EventTimes);
|
||||
TickTimes.Sort();
|
||||
|
||||
var previousTime = float.NaN;
|
||||
area.x = float.NegativeInfinity;
|
||||
|
||||
for (int i = 0; i < TickTimes.Count; i++)
|
||||
{
|
||||
var time = TickTimes[i];
|
||||
if (previousTime != time)
|
||||
{
|
||||
previousTime = time;
|
||||
DoRulerLabelGUI(ref area, time);
|
||||
}
|
||||
}
|
||||
|
||||
DrawPreviewTime();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DrawPreviewTime()
|
||||
{
|
||||
var state = TransitionPreviewWindow.GetCurrentState();
|
||||
if (state == null)
|
||||
return;
|
||||
|
||||
var normalizedTime = TransitionPreviewWindow.PreviewNormalizedTime;
|
||||
DrawPreviewTime(normalizedTime, alpha: 1);
|
||||
|
||||
// Looping states show faded indicators at every other multiple of the loop.
|
||||
if (!state.IsLooping)
|
||||
return;
|
||||
|
||||
// Make sure the area is actually wide enough for it to not just be a solid bar.
|
||||
if ((int)SecondsToPixels(0) > (int)SecondsToPixels(_Duration) - 4)
|
||||
return;
|
||||
|
||||
// Go back to the first visible increment.
|
||||
while (normalizedTime * _Duration >= _MinTime + _Duration)
|
||||
normalizedTime -= 1;
|
||||
|
||||
// Draw every visible increment from there on.
|
||||
while (normalizedTime * _Duration <= _MaxTime)
|
||||
{
|
||||
DrawPreviewTime(normalizedTime, alpha: 0.2f);
|
||||
normalizedTime += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPreviewTime(float normalizedTime, float alpha)
|
||||
{
|
||||
var time = NormalizedToSeconds(normalizedTime);
|
||||
var x = SecondsToPixels(time);
|
||||
if (x >= 0 && x <= _Area.width)
|
||||
{
|
||||
var color = PreviewTimeColor;
|
||||
color.a = alpha;
|
||||
EditorGUI.DrawRect(new(x - 1, _Area.y, 2, _Area.height), color);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static GUIStyle _RulerLabelStyle;
|
||||
private static ConversionCache<string, float> _TimeLabelWidthCache;
|
||||
|
||||
private void DoRulerLabelGUI(ref Rect previousArea, float time)
|
||||
{
|
||||
_RulerLabelStyle ??= new(GUI.skin.label)
|
||||
{
|
||||
padding = new(),
|
||||
contentOffset = new(0, -2),
|
||||
alignment = TextAnchor.UpperLeft,
|
||||
fontSize = Mathf.CeilToInt(AnimancerGUI.LineHeight * 0.6f),
|
||||
};
|
||||
|
||||
var text = G2Cache.Convert(time);
|
||||
|
||||
_TimeLabelWidthCache ??= ConversionCache.CreateWidthCache(_RulerLabelStyle);
|
||||
|
||||
var area = new Rect(
|
||||
SecondsToPixels(time),
|
||||
_Area.y,
|
||||
_TimeLabelWidthCache.Convert(text),
|
||||
_Area.height);
|
||||
|
||||
if (area.x > _Area.x)
|
||||
{
|
||||
var tickY = _Area.yMax - TickHeight;
|
||||
EditorGUI.DrawRect(new(area.x, tickY, 1, TickHeight), AnimancerGUI.TextColor);
|
||||
}
|
||||
|
||||
if (area.xMax > _Area.xMax)
|
||||
area.x = _Area.xMax - area.width;
|
||||
if (area.x < 0)
|
||||
area.x = 0;
|
||||
|
||||
if (area.x > previousArea.xMax + 2)
|
||||
{
|
||||
GUI.Label(area, text, _RulerLabelStyle);
|
||||
|
||||
previousArea = area;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9c54a0309811974c89768241690ab3e
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,126 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Utility for a toggle which can show and hide a speed slider.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ToggledSpeedSlider
|
||||
public class ToggledSpeedSlider
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The content displayed on the toggle.</summary>
|
||||
/// <remarks>The <see cref="GUIContent.text"/> is set by the <see cref="Speed"/>.</remarks>
|
||||
public readonly GUIContent GUIContent = new();
|
||||
|
||||
/// <summary>Is the toggle currently on?</summary>
|
||||
public readonly BoolPref IsOn;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private float _Speed = float.NaN;
|
||||
|
||||
/// <summary>The current speed value.</summary>
|
||||
public float Speed
|
||||
{
|
||||
get => _Speed;
|
||||
set
|
||||
{
|
||||
if (_Speed == value)
|
||||
return;
|
||||
|
||||
_Speed = value;
|
||||
GUIContent.text = _Speed.ToString("0.0x");
|
||||
OnSetSpeed(_Speed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when the <see cref="Speed"/> is changed.</summary>
|
||||
protected virtual void OnSetSpeed(float speed) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="ToggledSpeedSlider"/>.</summary>
|
||||
public ToggledSpeedSlider(string prefKey)
|
||||
{
|
||||
IsOn = new(prefKey);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a toggle to show or hide the speed slider.</summary>
|
||||
public virtual bool DoToggleGUI(Rect area, GUIStyle style)
|
||||
{
|
||||
HandleResetClick(area);
|
||||
|
||||
style ??= GUI.skin.toggle;
|
||||
|
||||
IsOn.Value = GUI.Toggle(area, IsOn, GUIContent, style);
|
||||
|
||||
return IsOn;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static float _SpeedLabelWidth;
|
||||
|
||||
/// <summary>Draws a slider to control the <see cref="Speed"/>.</summary>
|
||||
public float DoSpeedSlider(ref Rect area, GUIStyle backgroundStyle = null)
|
||||
{
|
||||
if (!IsOn)
|
||||
return Speed;
|
||||
|
||||
var sliderArea = StealLineFromTop(ref area);
|
||||
sliderArea = sliderArea.Expand(-StandardSpacing, 0);
|
||||
|
||||
if (backgroundStyle != null)
|
||||
GUI.Label(sliderArea, GUIContent.none, backgroundStyle);
|
||||
|
||||
HandleResetClick(sliderArea);
|
||||
|
||||
var label = "Speed";
|
||||
|
||||
if (_SpeedLabelWidth == 0)
|
||||
_SpeedLabelWidth = CalculateLabelWidth(label);
|
||||
|
||||
var labelWidth = EditorGUIUtility.labelWidth;
|
||||
EditorGUIUtility.labelWidth = _SpeedLabelWidth;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
var speed = EditorGUI.Slider(sliderArea, label, Speed, 0.1f, 2);
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
Speed = speed;
|
||||
|
||||
EditorGUIUtility.labelWidth = labelWidth;
|
||||
|
||||
return Speed;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Handles Right or Middle Clicking in the `area` to reset the <see cref="Speed"/>.</summary>
|
||||
public void HandleResetClick(Rect area)
|
||||
{
|
||||
if (!TryUseClickEvent(area, 1) && !TryUseClickEvent(area, 2))
|
||||
return;
|
||||
|
||||
if (Speed != 1)
|
||||
Speed = 1;
|
||||
else
|
||||
Speed = 0.5f;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57ef553ee87d8b9468e5ea1c26bc92e6
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,56 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using Animancer.TransitionLibraries;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
|
||||
namespace Animancer.Editor.TransitionLibraries
|
||||
{
|
||||
/// <summary>[Editor-Only] A custom Inspector for <see cref="TransitionLibraryAsset"/> fields.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryDrawer
|
||||
[CustomPropertyDrawer(typeof(TransitionLibraryAsset), true)]
|
||||
public class TransitionLibraryDrawer : PropertyDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string EditLabel = "Edit";
|
||||
|
||||
private static float _EditButtonWidth;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
=> LineHeight;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var library = property.objectReferenceValue as TransitionLibraryAsset;
|
||||
if (library != null)
|
||||
{
|
||||
var style = EditorStyles.miniButton;
|
||||
|
||||
if (_EditButtonWidth <= 0)
|
||||
_EditButtonWidth = style.CalculateWidth(EditLabel);
|
||||
|
||||
var editArea = StealFromRight(ref area, _EditButtonWidth, StandardSpacing);
|
||||
|
||||
if (GUI.Button(editArea, EditLabel, style))
|
||||
TransitionLibraryWindow.Open(library);
|
||||
}
|
||||
|
||||
EditorGUI.PropertyField(area, property, label);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48d63f40c5113fd4baa957806b5d6ddc
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a788bddb70bff9d41bac4c3a69207a4a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ClipTransitionDrawer
|
||||
[CustomPropertyDrawer(typeof(ClipTransition), true)]
|
||||
public class ClipTransitionDrawer : TransitionDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="ClipTransitionDrawer"/>.</summary>
|
||||
public ClipTransitionDrawer()
|
||||
: base(ClipTransition.ClipFieldName)
|
||||
{ }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb3f0552aa807e340b39f53ea42a9a2c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,201 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ControllerTransitionDrawer
|
||||
[CustomPropertyDrawer(typeof(ControllerTransition<>), true)]
|
||||
[CustomPropertyDrawer(typeof(ControllerTransition), true)]
|
||||
public class ControllerTransitionDrawer : TransitionDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly string[] Parameters;
|
||||
private readonly string[] ParameterPropertySuffixes;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="ControllerTransitionDrawer"/> without any parameters.</summary>
|
||||
public ControllerTransitionDrawer()
|
||||
: base(ControllerTransition.ControllerFieldName)
|
||||
{ }
|
||||
|
||||
/// <summary>Creates a new <see cref="ControllerTransitionDrawer"/> and sets the <see cref="Parameters"/>.</summary>
|
||||
public ControllerTransitionDrawer(params string[] parameters)
|
||||
: base(ControllerTransition.ControllerFieldName)
|
||||
{
|
||||
Parameters = parameters;
|
||||
if (parameters == null)
|
||||
return;
|
||||
|
||||
ParameterPropertySuffixes = new string[parameters.Length];
|
||||
|
||||
for (int i = 0; i < ParameterPropertySuffixes.Length; i++)
|
||||
{
|
||||
ParameterPropertySuffixes[i] = "." + parameters[i];
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoChildPropertyGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty property,
|
||||
GUIContent label)
|
||||
{
|
||||
var path = property.propertyPath;
|
||||
|
||||
if (ParameterPropertySuffixes != null)
|
||||
{
|
||||
var controllerProperty = rootProperty.FindPropertyRelative(MainPropertyName);
|
||||
if (controllerProperty.objectReferenceValue is AnimatorController controller)
|
||||
{
|
||||
for (int i = 0; i < ParameterPropertySuffixes.Length; i++)
|
||||
{
|
||||
if (path.EndsWith(ParameterPropertySuffixes[i]))
|
||||
{
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
DoParameterGUI(area, controller, property);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
|
||||
|
||||
// When the controller changes, validate all parameters.
|
||||
if (EditorGUI.EndChangeCheck() &&
|
||||
Parameters != null &&
|
||||
path.EndsWith(MainPropertyPathSuffix))
|
||||
{
|
||||
if (property.objectReferenceValue is AnimatorController controller)
|
||||
{
|
||||
for (int i = 0; i < Parameters.Length; i++)
|
||||
{
|
||||
property = rootProperty.FindPropertyRelative(Parameters[i]);
|
||||
var parameterName = property.stringValue;
|
||||
|
||||
// If a parameter is missing, assign it to the first float parameter.
|
||||
if (!HasFloatParameter(controller, parameterName))
|
||||
{
|
||||
parameterName = GetFirstFloatParameterName(controller);
|
||||
if (!string.IsNullOrEmpty(parameterName))
|
||||
property.stringValue = parameterName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a dropdown menu to select the name of a parameter in the `controller`.</summary>
|
||||
protected void DoParameterGUI(Rect area, AnimatorController controller, SerializedProperty property)
|
||||
{
|
||||
var parameterName = property.stringValue;
|
||||
var parameters = controller.parameters;
|
||||
|
||||
using (var label = PooledGUIContent.Acquire(property))
|
||||
{
|
||||
var propertyLabel = EditorGUI.BeginProperty(area, label, property);
|
||||
|
||||
var xMax = area.xMax;
|
||||
area.width = EditorGUIUtility.labelWidth;
|
||||
EditorGUI.PrefixLabel(area, propertyLabel);
|
||||
|
||||
area.x += area.width;
|
||||
area.xMax = xMax;
|
||||
}
|
||||
|
||||
var color = GUI.color;
|
||||
if (!HasFloatParameter(controller, parameterName))
|
||||
GUI.color = AnimancerGUI.ErrorFieldColor;
|
||||
|
||||
using (var label = PooledGUIContent.Acquire(parameterName))
|
||||
{
|
||||
if (EditorGUI.DropdownButton(area, label, FocusType.Passive))
|
||||
{
|
||||
property = property.Copy();
|
||||
|
||||
var menu = new GenericMenu();
|
||||
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var parameter = parameters[i];
|
||||
Serialization.AddPropertyModifierFunction(menu, property, parameter.name,
|
||||
parameter.type == AnimatorControllerParameterType.Float,
|
||||
(targetProperty) =>
|
||||
{
|
||||
targetProperty.stringValue = parameter.name;
|
||||
});
|
||||
}
|
||||
|
||||
if (menu.GetItemCount() == 0)
|
||||
menu.AddDisabledItem(new("No Parameters"));
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
}
|
||||
|
||||
GUI.color = color;
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static bool HasFloatParameter(AnimatorController controller, string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return false;
|
||||
|
||||
var parameters = controller.parameters;
|
||||
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var parameter = parameters[i];
|
||||
if (parameter.type == AnimatorControllerParameterType.Float &&
|
||||
parameter.name == name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static string GetFirstFloatParameterName(AnimatorController controller)
|
||||
{
|
||||
var parameters = controller.parameters;
|
||||
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var parameter = parameters[i];
|
||||
if (parameter.type == AnimatorControllerParameterType.Float)
|
||||
{
|
||||
return parameter.name;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: faa04e0e6ae08764dbf279c99225920e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,98 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DirectionalClipTransitionDrawer
|
||||
[CustomPropertyDrawer(typeof(DirectionalClipTransition), true)]
|
||||
public class DirectionalClipTransitionDrawer : TransitionDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="DirectionalClipTransitionDrawer"/>.</summary>
|
||||
public DirectionalClipTransitionDrawer()
|
||||
: base(DirectionalClipTransition.AnimationSetField)
|
||||
{ }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoChildPropertyGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty property,
|
||||
GUIContent label)
|
||||
{
|
||||
var width = area.width;
|
||||
|
||||
var path = property.propertyPath;
|
||||
if (path.EndsWith($".{ClipTransition.ClipFieldName}"))
|
||||
{
|
||||
if (property.objectReferenceValue != null)
|
||||
{
|
||||
var removeArea = AnimancerGUI.StealFromRight(
|
||||
ref area, AnimancerGUI.LineHeight, AnimancerGUI.StandardSpacing);
|
||||
|
||||
var removeContent = AnimancerIcons.ClearIcon(
|
||||
$"A {nameof(DirectionalClipTransition)}" +
|
||||
$" will get its Clip from the Animation Set at runtime" +
|
||||
$" so the Clip might as well be null until then.");
|
||||
|
||||
if (GUI.Button(removeArea, removeContent, AnimancerGUI.NoPaddingButtonStyle))
|
||||
property.objectReferenceValue = null;
|
||||
}
|
||||
|
||||
if (Context.Transition is DirectionalClipTransition directionalClipTransition &&
|
||||
directionalClipTransition.AnimationSet != null)
|
||||
{
|
||||
var dropdownArea = AnimancerGUI.StealFromRight(
|
||||
ref area, area.height, AnimancerGUI.StandardSpacing);
|
||||
|
||||
if (GUI.Button(dropdownArea, GUIContent.none, EditorStyles.popup))
|
||||
PickAnimation(property, directionalClipTransition);
|
||||
}
|
||||
}
|
||||
|
||||
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
|
||||
|
||||
area.width = width;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Shows a context menu to choose an <see cref="AnimationClip"/> from the `source`.</summary>
|
||||
private void PickAnimation(SerializedProperty property, object source)
|
||||
{
|
||||
var menu = new GenericMenu();
|
||||
|
||||
using (SetPool<AnimationClip>.Instance.Acquire(out var clips))
|
||||
{
|
||||
clips.GatherFromSource(source);
|
||||
if (clips.Count == 0)
|
||||
return;
|
||||
|
||||
property = property.Copy();
|
||||
|
||||
foreach (var clip in clips)
|
||||
{
|
||||
menu.AddPropertyModifierFunction(property, clip.name, true, modify =>
|
||||
{
|
||||
modify.objectReferenceValue = clip;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5f34d72d5484544da63e422dca5d0a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,156 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/LinearMixerTransitionDrawer
|
||||
[CustomPropertyDrawer(typeof(LinearMixerTransition), true)]
|
||||
public class LinearMixerTransitionDrawer : MixerTransitionDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static GUIContent _SortingErrorContent;
|
||||
private static GUIStyle _SortingErrorStyle;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoThresholdGUI(Rect area, int index)
|
||||
{
|
||||
var color = GUI.color;
|
||||
|
||||
var iconArea = default(Rect);
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
var previousThreshold = CurrentThresholds.GetArrayElementAtIndex(index - 1);
|
||||
var currentThreshold = CurrentThresholds.GetArrayElementAtIndex(index);
|
||||
if (previousThreshold.floatValue >= currentThreshold.floatValue)
|
||||
{
|
||||
iconArea = AnimancerGUI.StealFromRight(
|
||||
ref area,
|
||||
area.height,
|
||||
AnimancerGUI.StandardSpacing);
|
||||
|
||||
GUI.color = AnimancerGUI.ErrorFieldColor;
|
||||
}
|
||||
}
|
||||
|
||||
base.DoThresholdGUI(area, index);
|
||||
|
||||
if (iconArea != default)
|
||||
{
|
||||
_SortingErrorContent ??= new(AnimancerIcons.Error)
|
||||
{
|
||||
tooltip =
|
||||
"Linear Mixer Thresholds must always be unique" +
|
||||
" and sorted in ascending order (click to sort)"
|
||||
};
|
||||
|
||||
_SortingErrorStyle ??= new(GUI.skin.label)
|
||||
{
|
||||
padding = new(),
|
||||
};
|
||||
|
||||
if (GUI.Button(iconArea, _SortingErrorContent, _SortingErrorStyle))
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
Serialization.RecordUndo(Context.Property);
|
||||
((LinearMixerTransition)Context.Transition).SortByThresholds();
|
||||
}
|
||||
}
|
||||
|
||||
GUI.color = color;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddThresholdFunctionsToMenu(GenericMenu menu)
|
||||
{
|
||||
const string EvenlySpaced = "Evenly Spaced";
|
||||
|
||||
var count = CurrentThresholds.arraySize;
|
||||
if (count <= 1)
|
||||
{
|
||||
menu.AddDisabledItem(new(EvenlySpaced));
|
||||
}
|
||||
else
|
||||
{
|
||||
var first = CurrentThresholds.GetArrayElementAtIndex(0).floatValue;
|
||||
var last = CurrentThresholds.GetArrayElementAtIndex(count - 1).floatValue;
|
||||
|
||||
if (last == first)
|
||||
last++;
|
||||
|
||||
AddPropertyModifierFunction(menu, $"{EvenlySpaced} ({first} to {last})", _ =>
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
CurrentThresholds.GetArrayElementAtIndex(i).floatValue =
|
||||
Mathf.Lerp(first, last, i / (float)(count - 1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
AddCalculateThresholdsFunction(menu, "From Speed",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
|
||||
? velocity.magnitude
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunction(menu, "From Velocity X",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
|
||||
? velocity.x
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunction(menu, "From Velocity Y",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
|
||||
? velocity.y
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunction(menu, "From Velocity Z",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
|
||||
? velocity.z
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunction(menu, "From Angular Speed (Rad)",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageAngularSpeed(state, out var speed)
|
||||
? speed
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunction(menu, "From Angular Speed (Deg)",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageAngularSpeed(state, out var speed)
|
||||
? speed * Mathf.Rad2Deg
|
||||
: float.NaN);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void AddCalculateThresholdsFunction(
|
||||
GenericMenu menu,
|
||||
string label,
|
||||
Func<Object, float, float> calculateThreshold)
|
||||
{
|
||||
AddPropertyModifierFunction(menu, label, (property) =>
|
||||
{
|
||||
var count = CurrentAnimations.arraySize;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
|
||||
if (state == null)
|
||||
continue;
|
||||
|
||||
var threshold = CurrentThresholds.GetArrayElementAtIndex(i);
|
||||
var value = calculateThreshold(state, threshold.floatValue);
|
||||
if (!float.IsNaN(value))
|
||||
threshold.floatValue = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b407e7bedc3c63946b3dc476d4540873
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,931 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using Animancer.Units.Editor;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ManualMixerTransitionDrawer
|
||||
[CustomPropertyDrawer(typeof(ManualMixerTransition), true)]
|
||||
public class ManualMixerTransitionDrawer : TransitionDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should two lines be used to draw each child?</summary>
|
||||
public static readonly BoolPref
|
||||
TwoLineMode = new(
|
||||
nameof(ManualMixerTransitionDrawer) + "." + nameof(TwoLineMode),
|
||||
"Two Line Mode",
|
||||
true);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The property currently being drawn.</summary>
|
||||
/// <remarks>
|
||||
/// Normally each property has its own drawer,
|
||||
/// but arrays share a single drawer for all elements.
|
||||
/// </remarks>
|
||||
public static SerializedProperty CurrentProperty { get; private set; }
|
||||
|
||||
/// <summary>The <see cref="ManualMixerTransition{TState}.Animations"/> field.</summary>
|
||||
public static SerializedProperty CurrentAnimations { get; private set; }
|
||||
|
||||
/// <summary>The <see cref="ManualMixerTransition{TState}.Speeds"/> field.</summary>
|
||||
public static SerializedProperty CurrentSpeeds { get; private set; }
|
||||
|
||||
/// <summary>The <see cref="ManualMixerTransition{TState}.SynchronizeChildren"/> field.</summary>
|
||||
public static SerializedProperty CurrentSynchronizeChildren { get; private set; }
|
||||
|
||||
private readonly Dictionary<string, ReorderableList>
|
||||
PropertyPathToStates = new();
|
||||
|
||||
private ReorderableList _MultiSelectDummyList;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gather the details of the `property`.</summary>
|
||||
/// <remarks>
|
||||
/// This method gets called by every <see cref="GetPropertyHeight"/> and <see cref="OnGUI"/> call since
|
||||
/// Unity uses the same <see cref="PropertyDrawer"/> instance for each element in a collection, so it
|
||||
/// needs to gather the details associated with the current property.
|
||||
/// </remarks>
|
||||
protected virtual ReorderableList GatherDetails(SerializedProperty property)
|
||||
{
|
||||
InitializeMode(property);
|
||||
GatherSubProperties(property);
|
||||
|
||||
if (property.hasMultipleDifferentValues)
|
||||
{
|
||||
return _MultiSelectDummyList ??= new(new List<Object>(), typeof(Object))
|
||||
{
|
||||
elementHeight = LineHeight,
|
||||
displayAdd = false,
|
||||
displayRemove = false,
|
||||
footerHeight = 0,
|
||||
drawHeaderCallback = DoAnimationHeaderGUI,
|
||||
drawNoneElementCallback = area
|
||||
=> EditorGUI.LabelField(area, "Multi-editing animations is not supported"),
|
||||
};
|
||||
}
|
||||
|
||||
if (CurrentAnimations == null)
|
||||
return null;
|
||||
|
||||
var path = property.propertyPath;
|
||||
|
||||
if (!PropertyPathToStates.TryGetValue(path, out var states))
|
||||
{
|
||||
states = new(CurrentAnimations.serializedObject, CurrentAnimations)
|
||||
{
|
||||
drawHeaderCallback = DoChildListHeaderGUI,
|
||||
elementHeightCallback = GetElementHeight,
|
||||
drawElementCallback = DoElementGUI,
|
||||
onAddCallback = OnAddElement,
|
||||
onRemoveCallback = OnRemoveElement,
|
||||
onReorderCallbackWithDetails = OnReorderList,
|
||||
drawFooterCallback = DoChildListFooterGUI,
|
||||
};
|
||||
|
||||
PropertyPathToStates.Add(path, states);
|
||||
}
|
||||
|
||||
states.serializedProperty = CurrentAnimations;
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Called every time a `property` is drawn to find the relevant child properties and store them to be
|
||||
/// used in <see cref="GetPropertyHeight"/> and <see cref="OnGUI"/>.
|
||||
/// </summary>
|
||||
protected virtual void GatherSubProperties(SerializedProperty property)
|
||||
{
|
||||
CurrentProperty = property;
|
||||
CurrentAnimations = property.FindPropertyRelative(ManualMixerTransition.AnimationsField);
|
||||
CurrentSpeeds = property.FindPropertyRelative(ManualMixerTransition.SpeedsField);
|
||||
CurrentSynchronizeChildren = property.FindPropertyRelative(ManualMixerTransition.SynchronizeChildrenField);
|
||||
|
||||
if (!property.hasMultipleDifferentValues &&
|
||||
CurrentAnimations != null &&
|
||||
CurrentSpeeds != null &&
|
||||
CurrentSpeeds.arraySize != 0)
|
||||
CurrentSpeeds.arraySize = CurrentAnimations.arraySize;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Adds a menu item that will call <see cref="GatherSubProperties"/> then run the specified
|
||||
/// `function`.
|
||||
/// </summary>
|
||||
protected void AddPropertyModifierFunction(GenericMenu menu, string label,
|
||||
MenuFunctionState state, Action<SerializedProperty> function)
|
||||
{
|
||||
Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, state, (property) =>
|
||||
{
|
||||
GatherSubProperties(property);
|
||||
function(property);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a menu item that will call <see cref="GatherSubProperties"/> then run the specified
|
||||
/// `function`.
|
||||
/// </summary>
|
||||
protected void AddPropertyModifierFunction(GenericMenu menu, string label,
|
||||
Action<SerializedProperty> function)
|
||||
{
|
||||
Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, (property) =>
|
||||
{
|
||||
GatherSubProperties(property);
|
||||
function(property);
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var height = EditorGUI.GetPropertyHeight(property, label);
|
||||
|
||||
if (property.isExpanded)
|
||||
{
|
||||
var states = GatherDetails(property);
|
||||
if (states != null)
|
||||
height += StandardSpacing +
|
||||
states.GetHeight();
|
||||
|
||||
if (CurrentAnimations != null)
|
||||
height -= StandardSpacing +
|
||||
EditorGUI.GetPropertyHeight(CurrentAnimations, label);
|
||||
|
||||
if (CurrentSpeeds != null)
|
||||
height -= StandardSpacing +
|
||||
EditorGUI.GetPropertyHeight(CurrentSpeeds, label);
|
||||
|
||||
if (CurrentSynchronizeChildren != null)
|
||||
height -= StandardSpacing +
|
||||
EditorGUI.GetPropertyHeight(CurrentSynchronizeChildren, label);
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private SerializedProperty _RootProperty;
|
||||
private ReorderableList _CurrentChildList;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
_RootProperty = null;
|
||||
|
||||
base.OnGUI(area, property, label);
|
||||
|
||||
if (_RootProperty == null ||
|
||||
!_RootProperty.isExpanded)
|
||||
return;
|
||||
|
||||
using (new DrawerContext(_RootProperty))
|
||||
{
|
||||
if (Context.Transition == null)
|
||||
return;
|
||||
|
||||
_CurrentChildList = GatherDetails(_RootProperty);
|
||||
if (_CurrentChildList == null)
|
||||
return;
|
||||
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
|
||||
area.yMin = area.yMax - _CurrentChildList.GetHeight();
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
area = EditorGUI.IndentedRect(area);
|
||||
|
||||
EditorGUI.indentLevel = 0;
|
||||
_CurrentChildList.DoList(area);
|
||||
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
|
||||
TryCollapseArrays();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoChildPropertyGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty property,
|
||||
GUIContent label)
|
||||
{
|
||||
if (Context.Transition != null)
|
||||
{
|
||||
area.height = 0;
|
||||
|
||||
// If we find the Animations property, hide it to draw it last.
|
||||
|
||||
var path = property.propertyPath;
|
||||
if (path.EndsWith("." + ManualMixerTransition.AnimationsField))
|
||||
{
|
||||
_RootProperty = rootProperty;
|
||||
return;
|
||||
}
|
||||
else if (_RootProperty != null)
|
||||
{
|
||||
// If we already found the Animations property, also hide Speeds and Synchronize Children.
|
||||
if (path.EndsWith("." + ManualMixerTransition.SpeedsField) ||
|
||||
path.EndsWith("." + ManualMixerTransition.SynchronizeChildrenField))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static float _SpeedLabelWidth;
|
||||
private static float _SyncLabelWidth;
|
||||
|
||||
/// <summary>Splits the specified `area` into separate sections.</summary>
|
||||
protected static void SplitListRect(Rect area, bool isHeader,
|
||||
out Rect animation, out Rect speed, out Rect sync)
|
||||
{
|
||||
if (_SpeedLabelWidth == 0)
|
||||
_SpeedLabelWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Speed");
|
||||
|
||||
if (_SyncLabelWidth == 0)
|
||||
_SyncLabelWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Sync");
|
||||
|
||||
var spacing = StandardSpacing;
|
||||
|
||||
var syncWidth = isHeader ?
|
||||
_SyncLabelWidth :
|
||||
ToggleWidth - spacing;
|
||||
|
||||
var speedWidth = _SpeedLabelWidth + _SyncLabelWidth - syncWidth;
|
||||
if (!isHeader)
|
||||
{
|
||||
// Don't use Clamp because the max might be smaller than the min.
|
||||
var max = Math.Max(area.height, area.width * 0.25f - 30);
|
||||
speedWidth = Math.Min(speedWidth, max);
|
||||
}
|
||||
|
||||
area.width += spacing;
|
||||
if (TwoLineMode && !isHeader)
|
||||
{
|
||||
animation = area;
|
||||
area.y += area.height;
|
||||
sync = StealFromRight(ref area, syncWidth, spacing);
|
||||
speed = area;
|
||||
}
|
||||
else
|
||||
{
|
||||
sync = StealFromRight(ref area, syncWidth, spacing);
|
||||
speed = StealFromRight(ref area, speedWidth, spacing);
|
||||
animation = area;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Headers
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the headdings of the child list.</summary>
|
||||
protected virtual void DoChildListHeaderGUI(Rect area)
|
||||
{
|
||||
SplitListRect(area, true, out var animationArea, out var speedArea, out var syncArea);
|
||||
|
||||
DoAnimationHeaderGUI(animationArea);
|
||||
DoSpeedHeaderGUI(speedArea);
|
||||
DoSyncHeaderGUI(syncArea);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws an "Animation" header.</summary>
|
||||
protected void DoAnimationHeaderGUI(Rect area)
|
||||
{
|
||||
using (var label = PooledGUIContent.Acquire("Animation",
|
||||
$"The animations that will be used for each child state" +
|
||||
$"\n\nCtrl + Click to allow picking Transition Assets" +
|
||||
$" (or anything that implements {nameof(ITransition)})"))
|
||||
{
|
||||
DoHeaderDropdownGUI(area, CurrentAnimations, label, menu =>
|
||||
{
|
||||
menu.AddItem(new(TwoLineMode.MenuItem), TwoLineMode.Value, () =>
|
||||
{
|
||||
TwoLineMode.Value = !TwoLineMode.Value;
|
||||
ReSelectCurrentObjects();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Speeds
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a "Speed" header.</summary>
|
||||
protected void DoSpeedHeaderGUI(Rect area)
|
||||
{
|
||||
using (var label = PooledGUIContent.Acquire("Speed", Strings.Tooltips.Speed))
|
||||
{
|
||||
DoHeaderDropdownGUI(area, CurrentSpeeds, label, menu =>
|
||||
{
|
||||
AddPropertyModifierFunction(menu, "Reset All to 1",
|
||||
CurrentSpeeds.arraySize == 0 ? MenuFunctionState.Selected : MenuFunctionState.Normal,
|
||||
(_) => CurrentSpeeds.arraySize = 0);
|
||||
|
||||
AddPropertyModifierFunction(menu, "Normalize Durations", MenuFunctionState.Normal, NormalizeDurations);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Recalculates the <see cref="CurrentSpeeds"/> depending on the <see cref="AnimationClip.length"/> of
|
||||
/// their animations so that they all take the same amount of time to play fully.
|
||||
/// </summary>
|
||||
private static void NormalizeDurations(SerializedProperty property)
|
||||
{
|
||||
var speedCount = CurrentSpeeds.arraySize;
|
||||
|
||||
var lengths = new float[CurrentAnimations.arraySize];
|
||||
if (lengths.Length <= 1)
|
||||
return;
|
||||
|
||||
int nonZeroLengths = 0;
|
||||
float totalLength = 0;
|
||||
float totalSpeed = 0;
|
||||
for (int i = 0; i < lengths.Length; i++)
|
||||
{
|
||||
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
|
||||
if (AnimancerUtilities.TryGetLength(state, out var length) &&
|
||||
length > 0)
|
||||
{
|
||||
nonZeroLengths++;
|
||||
totalLength += length;
|
||||
lengths[i] = length;
|
||||
|
||||
if (speedCount > 0)
|
||||
totalSpeed += CurrentSpeeds.GetArrayElementAtIndex(i).floatValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (nonZeroLengths == 0)
|
||||
return;
|
||||
|
||||
var averageLength = totalLength / nonZeroLengths;
|
||||
var averageSpeed = speedCount > 0 ? totalSpeed / nonZeroLengths : 1;
|
||||
|
||||
CurrentSpeeds.arraySize = lengths.Length;
|
||||
InitializeSpeeds(speedCount);
|
||||
|
||||
for (int i = 0; i < lengths.Length; i++)
|
||||
{
|
||||
if (lengths[i] == 0)
|
||||
continue;
|
||||
|
||||
CurrentSpeeds.GetArrayElementAtIndex(i).floatValue = averageSpeed * lengths[i] / averageLength;
|
||||
}
|
||||
|
||||
TryCollapseArrays();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Initializes every element in the <see cref="CurrentSpeeds"/> array
|
||||
/// from the `start` to the end of the array to contain a value of 1.
|
||||
/// </summary>
|
||||
public static void InitializeSpeeds(int start)
|
||||
{
|
||||
var count = CurrentSpeeds.arraySize;
|
||||
while (start < count)
|
||||
CurrentSpeeds.GetArrayElementAtIndex(start++).floatValue = 1;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Sync
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a "Sync" header.</summary>
|
||||
protected void DoSyncHeaderGUI(Rect area)
|
||||
{
|
||||
using (var label = PooledGUIContent.Acquire("Sync",
|
||||
"Determines which child states have their normalized times constantly synchronized"))
|
||||
{
|
||||
DoHeaderDropdownGUI(area, CurrentSpeeds, label, menu =>
|
||||
{
|
||||
var syncCount = CurrentSynchronizeChildren.arraySize;
|
||||
|
||||
var allState = syncCount == 0 ? MenuFunctionState.Selected : MenuFunctionState.Normal;
|
||||
AddPropertyModifierFunction(menu, "All", allState,
|
||||
(_) => CurrentSynchronizeChildren.arraySize = 0);
|
||||
|
||||
var syncNone = syncCount == CurrentAnimations.arraySize;
|
||||
if (syncNone)
|
||||
{
|
||||
for (int i = 0; i < syncCount; i++)
|
||||
{
|
||||
if (CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue)
|
||||
{
|
||||
syncNone = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var noneState = syncNone ? MenuFunctionState.Selected : MenuFunctionState.Normal;
|
||||
AddPropertyModifierFunction(menu, "None", noneState, (_) =>
|
||||
{
|
||||
var count = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
|
||||
for (int i = 0; i < count; i++)
|
||||
CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
|
||||
});
|
||||
|
||||
AddPropertyModifierFunction(menu, "Invert", MenuFunctionState.Normal, (_) =>
|
||||
{
|
||||
var count = CurrentSynchronizeChildren.arraySize;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var property = CurrentSynchronizeChildren.GetArrayElementAtIndex(i);
|
||||
property.boolValue = !property.boolValue;
|
||||
}
|
||||
|
||||
var newCount = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
|
||||
for (int i = count; i < newCount; i++)
|
||||
CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
|
||||
});
|
||||
|
||||
AddPropertyModifierFunction(menu, "Non-Stationary", MenuFunctionState.Normal, (_) =>
|
||||
{
|
||||
var count = CurrentAnimations.arraySize;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
|
||||
if (state == null)
|
||||
continue;
|
||||
|
||||
if (i >= syncCount)
|
||||
{
|
||||
CurrentSynchronizeChildren.arraySize = i + 1;
|
||||
for (int j = syncCount; j < i; j++)
|
||||
CurrentSynchronizeChildren.GetArrayElementAtIndex(j).boolValue = true;
|
||||
syncCount = i + 1;
|
||||
}
|
||||
|
||||
CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue =
|
||||
AnimancerUtilities.TryGetAverageVelocity(state, out var velocity) &&
|
||||
velocity != default;
|
||||
}
|
||||
|
||||
TryCollapseSync();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void SyncNone()
|
||||
{
|
||||
var count = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
|
||||
for (int i = 0; i < count; i++)
|
||||
CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI for a header dropdown button.</summary>
|
||||
public static void DoHeaderDropdownGUI(
|
||||
Rect area,
|
||||
SerializedProperty property,
|
||||
GUIContent content,
|
||||
Action<GenericMenu> populateMenu)
|
||||
{
|
||||
if (property != null)
|
||||
EditorGUI.BeginProperty(area, GUIContent.none, property);
|
||||
|
||||
if (populateMenu != null)
|
||||
{
|
||||
if (EditorGUI.DropdownButton(area, content, FocusType.Passive))
|
||||
{
|
||||
var menu = new GenericMenu();
|
||||
populateMenu(menu);
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.Label(area, content);
|
||||
}
|
||||
|
||||
if (property != null)
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the footer of the child list.</summary>
|
||||
protected virtual void DoChildListFooterGUI(Rect area)
|
||||
{
|
||||
ReorderableList.defaultBehaviours.DrawFooter(area, _CurrentChildList);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
area.xMax = EditorGUIUtility.labelWidth + IndentSize;
|
||||
|
||||
area.y++;
|
||||
area.height = LineHeight;
|
||||
|
||||
using (var label = PooledGUIContent.Acquire("Count"))
|
||||
{
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
var labelWidth = EditorGUIUtility.labelWidth;
|
||||
EditorGUIUtility.labelWidth = CalculateLabelWidth(label.text);
|
||||
|
||||
var count = EditorGUI.DelayedIntField(area, label, _CurrentChildList.count);
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
ResizeList(count);
|
||||
|
||||
EditorGUIUtility.labelWidth = labelWidth;
|
||||
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calculates the height of the state at the specified `index`.</summary>
|
||||
protected virtual float GetElementHeight(int index)
|
||||
=> TwoLineMode
|
||||
? LineHeight * 2
|
||||
: LineHeight;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI of the state at the specified `index`.</summary>
|
||||
private void DoElementGUI(Rect area, int index, bool isActive, bool isFocused)
|
||||
{
|
||||
if (index < 0 || index > CurrentAnimations.arraySize)
|
||||
return;
|
||||
|
||||
area.height = LineHeight;
|
||||
|
||||
var state = CurrentAnimations.GetArrayElementAtIndex(index);
|
||||
var speed = CurrentSpeeds.arraySize > 0
|
||||
? CurrentSpeeds.GetArrayElementAtIndex(index)
|
||||
: null;
|
||||
DoElementGUI(area, index, state, speed);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI of the animation at the specified `index`.</summary>
|
||||
protected virtual void DoElementGUI(Rect area, int index,
|
||||
SerializedProperty animation, SerializedProperty speed)
|
||||
{
|
||||
SplitListRect(area, false, out var animationArea, out var speedArea, out var syncArea);
|
||||
|
||||
DoAnimationField(animationArea, animation);
|
||||
DoSpeedFieldGUI(speedArea, speed, index);
|
||||
DoSyncToggleGUI(syncArea, index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Draws an <see cref="EditorGUI.ObjectField(Rect, GUIContent, Object, Type, bool)"/> that accepts
|
||||
/// <see cref="AnimationClip"/>s and <see cref="ITransition"/>s
|
||||
/// </summary>
|
||||
public static void DoAnimationField(Rect area, SerializedProperty property)
|
||||
{
|
||||
EditorGUI.BeginProperty(area, GUIContent.none, property);
|
||||
|
||||
var targetObject = property.serializedObject.targetObject;
|
||||
var oldReference = property.objectReferenceValue;
|
||||
|
||||
var currentEvent = Event.current;
|
||||
var isDrag =
|
||||
currentEvent.type == EventType.DragUpdated ||
|
||||
currentEvent.type == EventType.DragPerform;
|
||||
var type =
|
||||
isDrag ||
|
||||
currentEvent.control ||
|
||||
currentEvent.commandName == "ObjectSelectorUpdated"
|
||||
? typeof(Object)
|
||||
: typeof(AnimationClip);
|
||||
|
||||
var allowSceneObjects = targetObject != null && !EditorUtility.IsPersistent(targetObject);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
var newReference = EditorGUI.ObjectField(
|
||||
area,
|
||||
GUIContent.none,
|
||||
oldReference,
|
||||
type,
|
||||
allowSceneObjects);
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (newReference == null ||
|
||||
(IsClipOrTransition(newReference) && newReference != targetObject))
|
||||
property.objectReferenceValue = newReference;
|
||||
}
|
||||
|
||||
if (isDrag && area.Contains(currentEvent.mousePosition))
|
||||
{
|
||||
var objects = DragAndDrop.objectReferences;
|
||||
if (objects.Length != 1 ||
|
||||
!IsClipOrTransition(objects[0]) ||
|
||||
objects[0] == targetObject)
|
||||
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
/// <summary>Is the `clipOrTransition` an <see cref="AnimationClip"/> or <see cref="ITransition"/>?</summary>
|
||||
public static bool IsClipOrTransition(object clipOrTransition)
|
||||
=> clipOrTransition is AnimationClip || clipOrTransition is ITransition;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static CompactUnitConversionCache _XSuffixCache;
|
||||
|
||||
/// <summary>
|
||||
/// Draws a toggle to enable or disable <see cref="ManualMixerState.SynchronizedChildren"/> for the child
|
||||
/// at the specified `index`.
|
||||
/// </summary>
|
||||
protected void DoSpeedFieldGUI(Rect area, SerializedProperty speed, int index)
|
||||
{
|
||||
if (speed != null)
|
||||
{
|
||||
EditorGUI.PropertyField(area, speed, GUIContent.none);
|
||||
}
|
||||
else// If this element doesn't have its own speed property, just show 1.
|
||||
{
|
||||
EditorGUI.BeginProperty(area, GUIContent.none, CurrentSpeeds);
|
||||
|
||||
_XSuffixCache ??= new("x");
|
||||
|
||||
var value = UnitsAttributeDrawer.DoSpecialFloatField(
|
||||
area,
|
||||
null,
|
||||
1,
|
||||
_XSuffixCache);
|
||||
|
||||
// Middle Click toggles from 1 to -1.
|
||||
if (TryUseClickEvent(area, 2))
|
||||
value = -1;
|
||||
|
||||
if (value != 1)
|
||||
{
|
||||
CurrentSpeeds.InsertArrayElementAtIndex(0);
|
||||
CurrentSpeeds.GetArrayElementAtIndex(0).floatValue = 1;
|
||||
CurrentSpeeds.arraySize = CurrentAnimations.arraySize;
|
||||
CurrentSpeeds.GetArrayElementAtIndex(index).floatValue = value;
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Draws a toggle to enable or disable <see cref="ManualMixerState.SynchronizedChildren"/>
|
||||
/// for the child at the specified `index`.
|
||||
/// </summary>
|
||||
protected void DoSyncToggleGUI(Rect area, int index)
|
||||
{
|
||||
var syncProperty = CurrentSynchronizeChildren;
|
||||
var syncFlagCount = syncProperty.arraySize;
|
||||
|
||||
var enabled = true;
|
||||
|
||||
if (index < syncFlagCount)
|
||||
{
|
||||
syncProperty = syncProperty.GetArrayElementAtIndex(index);
|
||||
enabled = syncProperty.boolValue;
|
||||
}
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUI.BeginProperty(area, GUIContent.none, syncProperty);
|
||||
|
||||
enabled = GUI.Toggle(area, enabled, GUIContent.none);
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (index < syncFlagCount)
|
||||
{
|
||||
syncProperty.boolValue = enabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
syncProperty.arraySize = index + 1;
|
||||
|
||||
for (int i = syncFlagCount; i < index; i++)
|
||||
{
|
||||
syncProperty.GetArrayElementAtIndex(i).boolValue = true;
|
||||
}
|
||||
|
||||
syncProperty.GetArrayElementAtIndex(index).boolValue = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Called when adding a new state to the list to ensure that
|
||||
/// any other relevant arrays have new elements added as well.
|
||||
/// </summary>
|
||||
private void OnAddElement(ReorderableList list)
|
||||
{
|
||||
var index = list.index;
|
||||
if (index < 0 || Event.current.button == 1)// Right Click to add at the end.
|
||||
{
|
||||
index = CurrentAnimations.arraySize - 1;
|
||||
if (index < 0)
|
||||
index = 0;
|
||||
}
|
||||
|
||||
OnAddElement(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when adding a new state to the list to ensure that
|
||||
/// any other relevant arrays have new elements added as well.
|
||||
/// </summary>
|
||||
protected virtual void OnAddElement(int index)
|
||||
{
|
||||
CurrentAnimations.InsertArrayElementAtIndex(index);
|
||||
|
||||
if (CurrentSpeeds.arraySize > 0)
|
||||
CurrentSpeeds.InsertArrayElementAtIndex(index);
|
||||
|
||||
if (CurrentSynchronizeChildren.arraySize > index)
|
||||
CurrentSynchronizeChildren.InsertArrayElementAtIndex(index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Called when removing a state from the list to ensure that
|
||||
/// any other relevant arrays have elements removed as well.
|
||||
/// </summary>
|
||||
protected virtual void OnRemoveElement(ReorderableList list)
|
||||
{
|
||||
var index = list.index;
|
||||
|
||||
Serialization.RemoveArrayElement(CurrentAnimations, index);
|
||||
|
||||
if (CurrentSpeeds.arraySize > index)
|
||||
Serialization.RemoveArrayElement(CurrentSpeeds, index);
|
||||
|
||||
if (CurrentSynchronizeChildren.arraySize > index)
|
||||
Serialization.RemoveArrayElement(CurrentSynchronizeChildren, index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the number of items in the child list.</summary>
|
||||
protected virtual void ResizeList(int size)
|
||||
{
|
||||
CurrentAnimations.arraySize = size;
|
||||
|
||||
if (CurrentSpeeds.arraySize > size)
|
||||
CurrentSpeeds.arraySize = size;
|
||||
|
||||
if (CurrentSynchronizeChildren.arraySize > size)
|
||||
CurrentSynchronizeChildren.arraySize = size;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Called when reordering states in the list to ensure that
|
||||
/// any other relevant arrays have their corresponding elements reordered as well.
|
||||
/// </summary>
|
||||
protected virtual void OnReorderList(ReorderableList list, int oldIndex, int newIndex)
|
||||
{
|
||||
CurrentSpeeds.MoveArrayElement(oldIndex, newIndex);
|
||||
|
||||
var syncCount = CurrentSynchronizeChildren.arraySize;
|
||||
if (Math.Max(oldIndex, newIndex) >= syncCount)
|
||||
{
|
||||
CurrentSynchronizeChildren.arraySize++;
|
||||
CurrentSynchronizeChildren.GetArrayElementAtIndex(syncCount).boolValue = true;
|
||||
CurrentSynchronizeChildren.arraySize = newIndex + 1;
|
||||
}
|
||||
|
||||
CurrentSynchronizeChildren.MoveArrayElement(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="TryCollapseSpeeds"/> and <see cref="TryCollapseSync"/>.
|
||||
/// </summary>
|
||||
public static void TryCollapseArrays()
|
||||
{
|
||||
if (CurrentProperty == null ||
|
||||
CurrentProperty.hasMultipleDifferentValues)
|
||||
return;
|
||||
|
||||
TryCollapseSpeeds();
|
||||
TryCollapseSync();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If every element in the <see cref="CurrentSpeeds"/> array is 1,
|
||||
/// this method sets the array size to 0.
|
||||
/// </summary>
|
||||
public static void TryCollapseSpeeds()
|
||||
{
|
||||
var property = CurrentSpeeds;
|
||||
if (property == null)
|
||||
return;
|
||||
|
||||
var speedCount = property.arraySize;
|
||||
if (speedCount <= 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < speedCount; i++)
|
||||
{
|
||||
if (property.GetArrayElementAtIndex(i).floatValue != 1)
|
||||
return;
|
||||
}
|
||||
|
||||
property.arraySize = 0;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Removes any true elements from the end of the <see cref="CurrentSynchronizeChildren"/> array.
|
||||
/// </summary>
|
||||
public static void TryCollapseSync()
|
||||
{
|
||||
var property = CurrentSynchronizeChildren;
|
||||
if (property == null)
|
||||
return;
|
||||
|
||||
var count = property.arraySize;
|
||||
var changed = false;
|
||||
|
||||
for (int i = count - 1; i >= 0; i--)
|
||||
{
|
||||
if (property.GetArrayElementAtIndex(i).boolValue)
|
||||
{
|
||||
count = i;
|
||||
changed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
property.arraySize = count;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86e53fc4d232c3c48b32aa883a16005f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,223 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/MixerTransition2DDrawer
|
||||
[CustomPropertyDrawer(typeof(MixerTransition2D), true)]
|
||||
public class MixerTransition2DDrawer : MixerTransitionDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MixerTransition2DDrawer"/> using a wider
|
||||
/// `thresholdWidth` than usual to accomodate both the X and Y values.
|
||||
/// </summary>
|
||||
public MixerTransition2DDrawer()
|
||||
: base(StandardThresholdWidth * 2 + 20)
|
||||
{ }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddThresholdFunctionsToMenu(GenericMenu menu)
|
||||
{
|
||||
AddCalculateThresholdsFunction(menu, "From Velocity/XY", (state, threshold) =>
|
||||
{
|
||||
if (AnimancerUtilities.TryGetAverageVelocity(state, out var velocity))
|
||||
return new(velocity.x, velocity.y);
|
||||
else
|
||||
return new(float.NaN, float.NaN);
|
||||
});
|
||||
|
||||
AddCalculateThresholdsFunction(menu, "From Velocity/XZ", (state, threshold) =>
|
||||
{
|
||||
if (AnimancerUtilities.TryGetAverageVelocity(state, out var velocity))
|
||||
return new(velocity.x, velocity.z);
|
||||
else
|
||||
return new(float.NaN, float.NaN);
|
||||
});
|
||||
|
||||
AddCalculateThresholdsFunctionPerAxis(menu, "From Speed",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
|
||||
? velocity.magnitude
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunctionPerAxis(menu, "From Velocity X",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
|
||||
? velocity.x
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunctionPerAxis(menu, "From Velocity Y",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
|
||||
? velocity.y
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunctionPerAxis(menu, "From Velocity Z",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageVelocity(state, out var velocity)
|
||||
? velocity.z
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunctionPerAxis(menu, "From Angular Speed (Rad)",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageAngularSpeed(state, out var speed)
|
||||
? speed
|
||||
: float.NaN);
|
||||
AddCalculateThresholdsFunctionPerAxis(menu, "From Angular Speed (Deg)",
|
||||
(state, threshold) => AnimancerUtilities.TryGetAverageAngularSpeed(state, out var speed)
|
||||
? speed * Mathf.Rad2Deg
|
||||
: float.NaN);
|
||||
|
||||
AddPropertyModifierFunction(menu, "Initialize 4 Directions", Initialize4Directions);
|
||||
AddPropertyModifierFunction(menu, "Initialize 8 Directions", Initialize8Directions);
|
||||
AddPropertyModifierFunction(menu, "Normalize", NormalizeThresholds);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void Initialize4Directions(SerializedProperty property)
|
||||
{
|
||||
var oldSpeedCount = CurrentSpeeds.arraySize;
|
||||
|
||||
CurrentAnimations.arraySize = CurrentThresholds.arraySize = CurrentSpeeds.arraySize = 5;
|
||||
CurrentThresholds.GetArrayElementAtIndex(0).vector2Value = default;
|
||||
CurrentThresholds.GetArrayElementAtIndex(1).vector2Value = Vector2.up;
|
||||
CurrentThresholds.GetArrayElementAtIndex(2).vector2Value = Vector2.right;
|
||||
CurrentThresholds.GetArrayElementAtIndex(3).vector2Value = Vector2.down;
|
||||
CurrentThresholds.GetArrayElementAtIndex(4).vector2Value = Vector2.left;
|
||||
|
||||
InitializeSpeeds(oldSpeedCount);
|
||||
|
||||
var type = property.FindPropertyRelative(MixerTransition2D.TypeField);
|
||||
type.enumValueIndex = (int)MixerTransition2D.MixerType.Directional;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void Initialize8Directions(SerializedProperty property)
|
||||
{
|
||||
var oldSpeedCount = CurrentSpeeds.arraySize;
|
||||
|
||||
var diagonal = 1 / Mathf.Sqrt(2);
|
||||
|
||||
CurrentAnimations.arraySize = CurrentThresholds.arraySize = CurrentSpeeds.arraySize = 9;
|
||||
CurrentThresholds.GetArrayElementAtIndex(0).vector2Value = default;
|
||||
CurrentThresholds.GetArrayElementAtIndex(1).vector2Value = Vector2.up;
|
||||
CurrentThresholds.GetArrayElementAtIndex(2).vector2Value = new(diagonal, diagonal);
|
||||
CurrentThresholds.GetArrayElementAtIndex(3).vector2Value = Vector2.right;
|
||||
CurrentThresholds.GetArrayElementAtIndex(4).vector2Value = new(diagonal, -diagonal);
|
||||
CurrentThresholds.GetArrayElementAtIndex(5).vector2Value = Vector2.down;
|
||||
CurrentThresholds.GetArrayElementAtIndex(6).vector2Value = new(-diagonal, -diagonal);
|
||||
CurrentThresholds.GetArrayElementAtIndex(7).vector2Value = Vector2.left;
|
||||
CurrentThresholds.GetArrayElementAtIndex(8).vector2Value = new(-diagonal, diagonal);
|
||||
|
||||
InitializeSpeeds(oldSpeedCount);
|
||||
|
||||
var type = property.FindPropertyRelative(MixerTransition2D.TypeField);
|
||||
type.enumValueIndex = (int)MixerTransition2D.MixerType.Directional;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void NormalizeThresholds(SerializedProperty property)
|
||||
{
|
||||
var thresholdCount = CurrentThresholds.arraySize;
|
||||
if (thresholdCount == 0)
|
||||
return;
|
||||
|
||||
var largestSquaredMagnitude = 0f;
|
||||
|
||||
for (int i = 0; i < thresholdCount; i++)
|
||||
{
|
||||
var threshold = CurrentThresholds.GetArrayElementAtIndex(i).vector2Value;
|
||||
var squaredMagnitude = threshold.sqrMagnitude;
|
||||
if (largestSquaredMagnitude < squaredMagnitude)
|
||||
largestSquaredMagnitude = squaredMagnitude;
|
||||
}
|
||||
|
||||
if (largestSquaredMagnitude == 0f)
|
||||
return;
|
||||
|
||||
var inverseSquaredMagnitude = 1 / Mathf.Sqrt(largestSquaredMagnitude);
|
||||
|
||||
for (int i = 0; i < thresholdCount; i++)
|
||||
{
|
||||
var threshold = CurrentThresholds.GetArrayElementAtIndex(i);
|
||||
threshold.vector2Value *= inverseSquaredMagnitude;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void AddCalculateThresholdsFunction(
|
||||
GenericMenu menu,
|
||||
string label,
|
||||
Func<Object, Vector2, Vector2> calculateThreshold)
|
||||
{
|
||||
var functionState = CurrentAnimations == null || CurrentThresholds == null
|
||||
? MenuFunctionState.Disabled
|
||||
: MenuFunctionState.Normal;
|
||||
|
||||
AddPropertyModifierFunction(menu, label, functionState, property =>
|
||||
{
|
||||
GatherSubProperties(property);
|
||||
|
||||
if (CurrentAnimations == null ||
|
||||
CurrentThresholds == null)
|
||||
return;
|
||||
|
||||
var count = CurrentAnimations.arraySize;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
|
||||
if (state == null)
|
||||
continue;
|
||||
|
||||
var threshold = CurrentThresholds.GetArrayElementAtIndex(i);
|
||||
var value = calculateThreshold(state, threshold.vector2Value);
|
||||
if (!AnimancerEditorUtilities.IsNaN(value))
|
||||
threshold.vector2Value = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void AddCalculateThresholdsFunctionPerAxis(GenericMenu menu, string label,
|
||||
Func<Object, float, float> calculateThreshold)
|
||||
{
|
||||
AddCalculateThresholdsFunction(menu, "X/" + label, 0, calculateThreshold);
|
||||
AddCalculateThresholdsFunction(menu, "Y/" + label, 1, calculateThreshold);
|
||||
}
|
||||
|
||||
private void AddCalculateThresholdsFunction(GenericMenu menu, string label, int axis,
|
||||
Func<Object, float, float> calculateThreshold)
|
||||
{
|
||||
AddPropertyModifierFunction(menu, label, (property) =>
|
||||
{
|
||||
var count = CurrentAnimations.arraySize;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
|
||||
if (state == null)
|
||||
continue;
|
||||
|
||||
var threshold = CurrentThresholds.GetArrayElementAtIndex(i);
|
||||
|
||||
var value = threshold.vector2Value;
|
||||
var newValue = calculateThreshold(state, value[axis]);
|
||||
if (!float.IsNaN(newValue))
|
||||
value[axis] = newValue;
|
||||
threshold.vector2Value = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f0aef2af7c620749ad340f2545e0832
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,262 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="MixerTransition{TMixer, TParameter}"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions">
|
||||
/// Transitions</see> and
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/mixers">
|
||||
/// Mixers</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/MixerTransitionDrawer
|
||||
public class MixerTransitionDrawer : ManualMixerTransitionDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of horizontal pixels the "Threshold" label occupies.</summary>
|
||||
private readonly float ThresholdWidth;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static float _StandardThresholdWidth;
|
||||
|
||||
/// <summary>
|
||||
/// The number of horizontal pixels the word "Threshold" occupies when drawn with the
|
||||
/// <see cref="EditorStyles.popup"/> style.
|
||||
/// </summary>
|
||||
protected static float StandardThresholdWidth
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_StandardThresholdWidth == 0)
|
||||
_StandardThresholdWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Threshold");
|
||||
return _StandardThresholdWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MixerTransitionDrawer"/> using the default <see cref="StandardThresholdWidth"/>.
|
||||
/// </summary>
|
||||
public MixerTransitionDrawer()
|
||||
: this(StandardThresholdWidth)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MixerTransitionDrawer"/> using a custom width for its threshold labels.
|
||||
/// </summary>
|
||||
protected MixerTransitionDrawer(float thresholdWidth)
|
||||
=> ThresholdWidth = thresholdWidth;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// The serialized <see cref="MixerTransition{TMixer, TParameter}.Thresholds"/> of the
|
||||
/// <see cref="ManualMixerTransition.ManualMixerTransitionDrawer.CurrentProperty"/>.
|
||||
/// </summary>
|
||||
protected static SerializedProperty CurrentThresholds { get; private set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void GatherSubProperties(SerializedProperty property)
|
||||
{
|
||||
base.GatherSubProperties(property);
|
||||
|
||||
CurrentThresholds = property.FindPropertyRelative(MixerTransition2D.ThresholdsField);
|
||||
|
||||
if (CurrentAnimations == null ||
|
||||
CurrentThresholds == null ||
|
||||
property.hasMultipleDifferentValues)
|
||||
return;
|
||||
|
||||
var count = Math.Max(CurrentAnimations.arraySize, CurrentThresholds.arraySize);
|
||||
CurrentAnimations.arraySize = count;
|
||||
CurrentThresholds.arraySize = count;
|
||||
if (CurrentSpeeds != null &&
|
||||
CurrentSpeeds.arraySize != 0)
|
||||
CurrentSpeeds.arraySize = count;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var height = base.GetPropertyHeight(property, label);
|
||||
|
||||
if (property.isExpanded)
|
||||
{
|
||||
if (CurrentThresholds != null)
|
||||
{
|
||||
height -= StandardSpacing +
|
||||
EditorGUI.GetPropertyHeight(CurrentThresholds, label);
|
||||
}
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoChildPropertyGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty property,
|
||||
GUIContent label)
|
||||
{
|
||||
if (property.propertyPath.EndsWith($".{MixerTransition2D.ThresholdsField}"))
|
||||
return;
|
||||
|
||||
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Splits the specified `area` into separate sections.</summary>
|
||||
protected void SplitListRect(
|
||||
Rect area,
|
||||
bool isHeader,
|
||||
out Rect animation,
|
||||
out Rect threshold,
|
||||
out Rect speed,
|
||||
out Rect sync)
|
||||
{
|
||||
SplitListRect(area, isHeader, out animation, out speed, out sync);
|
||||
|
||||
if (TwoLineMode && !isHeader)
|
||||
{
|
||||
threshold = StealFromLeft(ref speed, ThresholdWidth, StandardSpacing);
|
||||
}
|
||||
else
|
||||
{
|
||||
threshold = animation;
|
||||
|
||||
var xMin = threshold.xMin = EditorGUIUtility.labelWidth + IndentSize;
|
||||
|
||||
animation.xMax = xMin - StandardSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoChildListHeaderGUI(Rect area)
|
||||
{
|
||||
SplitListRect(
|
||||
area,
|
||||
true,
|
||||
out var animationArea,
|
||||
out var thresholdArea,
|
||||
out var speedArea,
|
||||
out var syncArea);
|
||||
|
||||
DoAnimationHeaderGUI(animationArea);
|
||||
|
||||
var attribute = AttributeCache<ThresholdLabelAttribute>.FindAttribute(CurrentThresholds);
|
||||
var text = attribute != null
|
||||
? attribute.Label
|
||||
: "Threshold";
|
||||
|
||||
using (var label = PooledGUIContent.Acquire(text,
|
||||
"The parameter values at which each child state will be fully active"))
|
||||
DoHeaderDropdownGUI(thresholdArea, CurrentThresholds, label, AddThresholdFunctionsToMenu);
|
||||
|
||||
DoSpeedHeaderGUI(speedArea);
|
||||
|
||||
DoSyncHeaderGUI(syncArea);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoElementGUI(
|
||||
Rect area,
|
||||
int index,
|
||||
SerializedProperty animation,
|
||||
SerializedProperty speed)
|
||||
{
|
||||
SplitListRect(
|
||||
area,
|
||||
false,
|
||||
out var animationArea,
|
||||
out var thresholdArea,
|
||||
out var speedArea,
|
||||
out var syncArea);
|
||||
|
||||
DoAnimationField(animationArea, animation);
|
||||
DoThresholdGUI(thresholdArea, index);
|
||||
DoSpeedFieldGUI(speedArea, speed, index);
|
||||
DoSyncToggleGUI(syncArea, index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI of the threshold at the specified `index`.</summary>
|
||||
protected virtual void DoThresholdGUI(Rect area, int index)
|
||||
{
|
||||
var threshold = CurrentThresholds.GetArrayElementAtIndex(index);
|
||||
EditorGUI.PropertyField(area, threshold, GUIContent.none);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAddElement(int index)
|
||||
{
|
||||
base.OnAddElement(index);
|
||||
|
||||
if (CurrentThresholds.arraySize > 0)
|
||||
CurrentThresholds.InsertArrayElementAtIndex(index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnRemoveElement(ReorderableList list)
|
||||
{
|
||||
base.OnRemoveElement(list);
|
||||
Serialization.RemoveArrayElement(CurrentThresholds, list.index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void ResizeList(int size)
|
||||
{
|
||||
base.ResizeList(size);
|
||||
CurrentThresholds.arraySize = size;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnReorderList(ReorderableList list, int oldIndex, int newIndex)
|
||||
{
|
||||
base.OnReorderList(list, oldIndex, newIndex);
|
||||
CurrentThresholds.MoveArrayElement(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds functions to the `menu` relating to the thresholds.</summary>
|
||||
protected virtual void AddThresholdFunctionsToMenu(GenericMenu menu) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e5c41022aa077243bec1efac972f890
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,319 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/PlayableAssetTransitionDrawer
|
||||
[CustomPropertyDrawer(typeof(PlayableAssetTransition), true)]
|
||||
public class PlayableAssetTransitionDrawer : TransitionDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="PlayableAssetTransitionDrawer"/>.</summary>
|
||||
public PlayableAssetTransitionDrawer()
|
||||
: base(PlayableAssetTransition.AssetField)
|
||||
{ }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
_CurrentAsset = null;
|
||||
|
||||
var height = base.GetPropertyHeight(property, label);
|
||||
|
||||
if (property.isExpanded)
|
||||
{
|
||||
var bindings = property.FindPropertyRelative(PlayableAssetTransition.BindingsField);
|
||||
if (bindings != null)
|
||||
{
|
||||
bindings.isExpanded = true;
|
||||
height -= StandardSpacing + LineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private PlayableAsset _CurrentAsset;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoMainPropertyGUI(
|
||||
Rect area,
|
||||
out Rect labelArea,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty mainProperty)
|
||||
{
|
||||
_CurrentAsset = mainProperty.objectReferenceValue as PlayableAsset;
|
||||
base.DoMainPropertyGUI(area, out labelArea, rootProperty, mainProperty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
base.OnGUI(area, property, label);
|
||||
_CurrentAsset = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void DoChildPropertyGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty property,
|
||||
GUIContent label)
|
||||
{
|
||||
var path = property.propertyPath;
|
||||
if (path.EndsWith($".{PlayableAssetTransition.BindingsField}"))
|
||||
{
|
||||
DoBindingsGUI(ref area, property, label);
|
||||
return;
|
||||
}
|
||||
|
||||
base.DoChildPropertyGUI(ref area, rootProperty, property, label);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoBindingsGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty property,
|
||||
GUIContent label)
|
||||
{
|
||||
var outputCount = GetOutputCount(out var outputEnumerator, out var firstBindingIsAnimation);
|
||||
|
||||
// Bindings.
|
||||
property.Next(true);
|
||||
// Array.
|
||||
property.Next(true);
|
||||
// Array Size.
|
||||
DoBindingsCountGUI(area, property, label, outputCount, firstBindingIsAnimation, out var bindingCount);
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
for (int i = 0; i < bindingCount; i++)
|
||||
{
|
||||
NextVerticalArea(ref area);
|
||||
|
||||
if (!property.Next(false))
|
||||
{
|
||||
EditorGUI.LabelField(area, "Binding Count Mismatch");
|
||||
break;
|
||||
}
|
||||
// First Array Item.
|
||||
|
||||
if (outputEnumerator != null && outputEnumerator.MoveNext())
|
||||
{
|
||||
DoBindingGUI(area, property, label, outputEnumerator, i);
|
||||
}
|
||||
else
|
||||
{
|
||||
var color = GUI.color;
|
||||
GUI.color = WarningFieldColor;
|
||||
|
||||
EditorGUI.PropertyField(area, property, false);
|
||||
|
||||
GUI.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private int GetOutputCount(
|
||||
out IEnumerator<PlayableBinding> outputEnumerator,
|
||||
out bool firstBindingIsAnimation)
|
||||
{
|
||||
var outputCount = 0;
|
||||
|
||||
firstBindingIsAnimation = false;
|
||||
if (_CurrentAsset != null)
|
||||
{
|
||||
var outputs = _CurrentAsset.outputs;
|
||||
_CurrentAsset = null;
|
||||
outputEnumerator = outputs.GetEnumerator();
|
||||
|
||||
while (outputEnumerator.MoveNext())
|
||||
{
|
||||
PlayableAssetState.GetBindingDetails(
|
||||
outputEnumerator.Current, out var _, out var _, out var isMarkers);
|
||||
if (isMarkers)
|
||||
continue;
|
||||
|
||||
if (outputCount == 0 && outputEnumerator.Current.outputTargetType == typeof(Animator))
|
||||
firstBindingIsAnimation = true;
|
||||
|
||||
outputCount++;
|
||||
}
|
||||
|
||||
outputEnumerator = outputs.GetEnumerator();
|
||||
}
|
||||
else outputEnumerator = null;
|
||||
|
||||
return outputCount;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoBindingsCountGUI(
|
||||
Rect area,
|
||||
SerializedProperty property,
|
||||
GUIContent label,
|
||||
int outputCount,
|
||||
bool firstBindingIsAnimation,
|
||||
out int bindingCount)
|
||||
{
|
||||
var color = GUI.color;
|
||||
|
||||
var sizeArea = area;
|
||||
bindingCount = property.intValue;
|
||||
|
||||
// Button to fix the number of bindings in the array.
|
||||
if (bindingCount != outputCount && !(bindingCount == 0 && outputCount == 1 && firstBindingIsAnimation))
|
||||
{
|
||||
GUI.color = WarningFieldColor;
|
||||
|
||||
var labelText = label.text;
|
||||
var style = MiniButtonStyle;
|
||||
|
||||
var countLabel = outputCount.ToStringCached();
|
||||
var fixSizeWidth = style.CalculateWidth(countLabel);
|
||||
var fixSizeArea = StealFromRight(
|
||||
ref sizeArea, fixSizeWidth, StandardSpacing);
|
||||
if (GUI.Button(fixSizeArea, countLabel, style))
|
||||
property.intValue = bindingCount = outputCount;
|
||||
|
||||
label.text = labelText;
|
||||
}
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
EditorGUI.PropertyField(sizeArea, property, label, false);
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
bindingCount = property.intValue;
|
||||
|
||||
GUI.color = color;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoBindingGUI(
|
||||
Rect area,
|
||||
SerializedProperty property,
|
||||
GUIContent label,
|
||||
IEnumerator<PlayableBinding> outputEnumerator,
|
||||
int trackIndex)
|
||||
{
|
||||
CheckIfSkip:
|
||||
PlayableAssetState.GetBindingDetails(
|
||||
outputEnumerator.Current,
|
||||
out var name,
|
||||
out var bindingType,
|
||||
out var isMarkers);
|
||||
|
||||
if (isMarkers)
|
||||
{
|
||||
outputEnumerator.MoveNext();
|
||||
goto CheckIfSkip;
|
||||
}
|
||||
|
||||
label.text = name;
|
||||
|
||||
var targetObject = property.serializedObject.targetObject;
|
||||
var allowSceneObjects =
|
||||
targetObject != null &&
|
||||
!EditorUtility.IsPersistent(targetObject);
|
||||
|
||||
label = EditorGUI.BeginProperty(area, label, property);
|
||||
var fieldArea = area;
|
||||
var obj = property.objectReferenceValue;
|
||||
var objExists = obj != null;
|
||||
|
||||
if (objExists)
|
||||
DoRemoveButtonIfNecessary(ref fieldArea, property, trackIndex, ref bindingType, ref obj);
|
||||
|
||||
if (bindingType != null || objExists)
|
||||
{
|
||||
property.objectReferenceValue =
|
||||
EditorGUI.ObjectField(fieldArea, label, obj, bindingType, allowSceneObjects);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.LabelField(fieldArea, label);
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void DoRemoveButtonIfNecessary(
|
||||
ref Rect area,
|
||||
SerializedProperty property,
|
||||
int trackIndex,
|
||||
ref Type bindingType,
|
||||
ref Object obj)
|
||||
{
|
||||
if (trackIndex == 0 && bindingType == typeof(Animator))
|
||||
{
|
||||
DoRemoveButton(ref area, property, ref obj,
|
||||
"This Animation Track is the first Track" +
|
||||
" so it will automatically control the Animancer output" +
|
||||
" and likely doesn't need a binding.");
|
||||
}
|
||||
else if (bindingType == null)
|
||||
{
|
||||
DoRemoveButton(ref area, property, ref obj,
|
||||
"This Track doesn't need a binding.");
|
||||
bindingType = typeof(Object);
|
||||
}
|
||||
else if (!bindingType.IsAssignableFrom(obj.GetType()))
|
||||
{
|
||||
DoRemoveButton(ref area, property, ref obj,
|
||||
"This binding has the wrong type for this Track.");
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void DoRemoveButton(
|
||||
ref Rect area,
|
||||
SerializedProperty property,
|
||||
ref Object obj,
|
||||
string tooltip)
|
||||
{
|
||||
GUI.color = WarningFieldColor;
|
||||
|
||||
var removeArea = StealFromRight(
|
||||
ref area,
|
||||
area.height,
|
||||
StandardSpacing);
|
||||
|
||||
if (GUI.Button(
|
||||
removeArea,
|
||||
AnimancerIcons.ClearIcon(tooltip),
|
||||
NoPaddingButtonStyle))
|
||||
property.objectReferenceValue = obj = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbdbd2675cb58604a8b719a36eb1adad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,276 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary><see cref="PropertyDrawer"/> for <see cref="ControllerState.SerializableParameterBindings"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializableParameterBindingsDrawer
|
||||
[CustomPropertyDrawer(typeof(ControllerState.SerializableParameterBindings), true)]
|
||||
public class SerializableParameterBindingsDrawer : PropertyDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
if (!property.isExpanded)
|
||||
return LineHeight;
|
||||
|
||||
GetFields(property, out var mode, out var bindings);
|
||||
|
||||
var count = bindings.arraySize;
|
||||
if (count > 0 && mode.boolValue)
|
||||
count = 1 + Mathf.CeilToInt(count * 0.5f);
|
||||
|
||||
return CalculateHeight(count + 3);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
area.height = LineHeight;
|
||||
|
||||
var isExpanded = EditorGUI.PropertyField(area, property, label, false);
|
||||
if (!isExpanded)
|
||||
return;
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
NextVerticalArea(ref area);
|
||||
|
||||
GetFields(property, out var mode, out var bindings);
|
||||
|
||||
var parameterList = GetContextParameterList(property);
|
||||
|
||||
var bindingCount = bindings.arraySize;
|
||||
|
||||
DoModeGUI(ref area, mode, bindingCount, parameterList);
|
||||
|
||||
var modeValue = mode.boolValue;
|
||||
|
||||
DoBindingCountGUI(ref area, bindings, modeValue, ref bindingCount, parameterList);
|
||||
|
||||
DoBindingsGUI(area, bindings, modeValue, bindingCount, parameterList);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoModeGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty mode,
|
||||
int bindingCount,
|
||||
string parameterList)
|
||||
{
|
||||
using var label = PooledGUIContent.Acquire();
|
||||
|
||||
if (bindingCount == 0)
|
||||
{
|
||||
label.text = "Bind All Parameters";
|
||||
label.tooltip =
|
||||
"If enabled, all parameters in the Animator Controller will be bound" +
|
||||
" to Animancer parameters with the same name and the Bindings array can be left empty." +
|
||||
parameterList;
|
||||
}
|
||||
else
|
||||
{
|
||||
label.text = "Rebind Names";
|
||||
label.tooltip =
|
||||
"If enabled, the Bindings array will be taken in pairs so that each" +
|
||||
" Animator Controller parameter can be bound to an Animancer Parameter with different name." +
|
||||
parameterList;
|
||||
}
|
||||
|
||||
EditorGUI.PropertyField(area, mode, label, false);
|
||||
|
||||
NextVerticalArea(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoBindingCountGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty bindings,
|
||||
bool mode,
|
||||
ref int bindingCount,
|
||||
string parameterList)
|
||||
{
|
||||
using var label = PooledGUIContent.Acquire(
|
||||
"Bindings",
|
||||
"The names of parameters in the Animator Controller to bind to Animancer parameters." +
|
||||
"\n<> Leave this array empty and enable the toggle if you want to bind all parameters." +
|
||||
parameterList);
|
||||
|
||||
var newCount = bindingCount;
|
||||
|
||||
if (mode && bindingCount > 0)
|
||||
newCount /= 2;
|
||||
|
||||
newCount = EditorGUI.DelayedIntField(area, label, newCount);
|
||||
|
||||
if (newCount < 0)
|
||||
newCount = 0;
|
||||
else if (mode && newCount > 0)
|
||||
newCount *= 2;
|
||||
|
||||
if (bindingCount != newCount)
|
||||
{
|
||||
bindingCount = newCount;
|
||||
bindings.arraySize = newCount;
|
||||
}
|
||||
|
||||
NextVerticalArea(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoBindingsGUI(
|
||||
Rect area,
|
||||
SerializedProperty bindings,
|
||||
bool mode,
|
||||
int bindingCount,
|
||||
string parameterList)
|
||||
{
|
||||
if (bindingCount <= 0)
|
||||
return;
|
||||
|
||||
using var label = PooledGUIContent.Acquire();
|
||||
|
||||
if (mode)
|
||||
{
|
||||
var controllerArea = EditorGUI.IndentedRect(area);
|
||||
controllerArea.xMin -= 1;// Not sure why.
|
||||
var animancerArea = StealFromRight(ref controllerArea, controllerArea.width * 0.5f, StandardSpacing);
|
||||
|
||||
label.text = "Controller";
|
||||
label.tooltip = "The name of the Animator Controller parameter" + parameterList;
|
||||
GUI.Label(controllerArea, label);
|
||||
|
||||
label.text = "Animancer";
|
||||
label.tooltip = "The name of the Animancer parameter";
|
||||
GUI.Label(animancerArea, label);
|
||||
|
||||
NextVerticalArea(ref controllerArea);
|
||||
NextVerticalArea(ref animancerArea);
|
||||
|
||||
for (int i = 0; i < bindingCount; i++)
|
||||
{
|
||||
DoBindingGUI(ref controllerArea, bindings, i, GUIContent.none);
|
||||
i++;
|
||||
DoBindingGUI(ref animancerArea, bindings, i, GUIContent.none);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
label.tooltip = "";
|
||||
|
||||
for (int i = 0; i < bindingCount; i++)
|
||||
{
|
||||
label.text = "Binding " + i;
|
||||
DoBindingGUI(ref area, bindings, i, label);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void DoBindingGUI(ref Rect area, SerializedProperty bindings, int index, GUIContent label)
|
||||
{
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
if (string.IsNullOrEmpty(label.text))
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
var binding = bindings.GetArrayElementAtIndex(index);
|
||||
|
||||
EditorGUI.PropertyField(area, binding, label);
|
||||
|
||||
NextVerticalArea(ref area);
|
||||
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void GetFields(
|
||||
SerializedProperty root,
|
||||
out SerializedProperty mode,
|
||||
out SerializedProperty bindings)
|
||||
{
|
||||
mode = root.FindPropertyRelative(ControllerState.SerializableParameterBindings.ModeFieldName);
|
||||
bindings = root.FindPropertyRelative(ControllerState.SerializableParameterBindings.BindingsFieldName);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private string GetContextParameterList(SerializedProperty property)
|
||||
{
|
||||
var path = property.propertyPath;
|
||||
var lastDot = path.LastIndexOf('.');
|
||||
if (lastDot < 0)
|
||||
return null;
|
||||
|
||||
path = path[..(lastDot + 1)] + ControllerTransition.ControllerFieldName;
|
||||
property = property.serializedObject.FindProperty(path);
|
||||
if (property == null ||
|
||||
property.objectReferenceValue is not AnimatorController animatorController)
|
||||
return null;
|
||||
|
||||
return GetParameterList(animatorController);
|
||||
}
|
||||
|
||||
private readonly Dictionary<AnimatorController, string>
|
||||
ControllerToParameterList = new();
|
||||
|
||||
private string GetParameterList(AnimatorController animatorController)
|
||||
{
|
||||
if (animatorController == null)
|
||||
return null;
|
||||
|
||||
if (ControllerToParameterList.TryGetValue(animatorController, out var parameterList))
|
||||
return parameterList;
|
||||
|
||||
var text = StringBuilderPool.Instance.Acquire();
|
||||
|
||||
var parameters = animatorController.parameters;
|
||||
if (parameters.Length > 0)
|
||||
{
|
||||
text.Append("\n\nParameters in ")
|
||||
.Append(animatorController.name)
|
||||
.Append(':');
|
||||
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var parameter = parameters[i];
|
||||
|
||||
text.Append("\n<> ")
|
||||
.Append(parameter.type)
|
||||
.Append(' ')
|
||||
.Append(parameter.name);
|
||||
}
|
||||
}
|
||||
|
||||
parameterList = text.ReleaseToString();
|
||||
ControllerToParameterList.Add(animatorController, parameterList);
|
||||
return parameterList;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 198f2234f919be64992de958b4dbef90
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TransitionAssetReferenceDrawer
|
||||
[CustomPropertyDrawer(typeof(TransitionAssetReference), true)]
|
||||
public class TransitionAssetReferenceDrawer : TransitionDrawer
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 303b872471b0bca46b23add07d8a9e6f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,646 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using Animancer.Editor.Previews;
|
||||
using Animancer.Units.Editor;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="ITransition"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TransitionDrawer
|
||||
[CustomPropertyDrawer(typeof(ITransition), true)]
|
||||
[CustomPropertyDrawer(typeof(TransitionAssetBase), true)]
|
||||
public class TransitionDrawer : PropertyDrawer,
|
||||
IPolymorphic
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The visual state of a drawer.</summary>
|
||||
private enum Mode
|
||||
{
|
||||
Uninitialized,
|
||||
Normal,
|
||||
AlwaysExpanded,
|
||||
}
|
||||
|
||||
/// <summary>The current state of this drawer.</summary>
|
||||
private Mode _Mode;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If set, the field with this name will be drawn on the header line
|
||||
/// with the foldout arrow instead of in its regular place.
|
||||
/// </summary>
|
||||
protected readonly string MainPropertyName;
|
||||
|
||||
/// <summary>"." + <see cref="MainPropertyName"/> (to avoid creating garbage repeatedly).</summary>
|
||||
protected readonly string MainPropertyPathSuffix;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="TransitionDrawer"/>.</summary>
|
||||
public TransitionDrawer() { }
|
||||
|
||||
/// <summary>Creates a new <see cref="TransitionDrawer"/> and sets the <see cref="MainPropertyName"/>.</summary>
|
||||
public TransitionDrawer(string mainPropertyName)
|
||||
{
|
||||
MainPropertyName = mainPropertyName;
|
||||
MainPropertyPathSuffix = "." + mainPropertyName;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the property specified by the <see cref="MainPropertyName"/>.</summary>
|
||||
private SerializedProperty GetMainProperty(SerializedProperty rootProperty)
|
||||
=> MainPropertyName == null
|
||||
? null
|
||||
: rootProperty.FindPropertyRelative(MainPropertyName);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the number of vertical pixels the `property` will occupy when it is drawn.</summary>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
using (new DrawerContext(property))
|
||||
{
|
||||
InitializeMode(property);
|
||||
|
||||
var height = EditorGUI.GetPropertyHeight(property, label, true);
|
||||
|
||||
if (property.isExpanded)
|
||||
{
|
||||
if (property.propertyType != SerializedPropertyType.ManagedReference)
|
||||
{
|
||||
var mainProperty = GetMainProperty(property);
|
||||
if (mainProperty != null)
|
||||
height -= EditorGUI.GetPropertyHeight(mainProperty) + AnimancerGUI.StandardSpacing;
|
||||
}
|
||||
|
||||
// The End Time from the Event Sequence is drawn out in the main transition so we need to add it.
|
||||
// But rather than figuring out which array element actually holds the end time, we just use the
|
||||
// Start Time field since it will have the same height.
|
||||
var startTime = property.FindPropertyRelative(NormalizedStartTimeFieldName);
|
||||
if (startTime != null)
|
||||
height += EditorGUI.GetPropertyHeight(startTime) + AnimancerGUI.StandardSpacing;
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the root `property` GUI and calls <see cref="DoChildPropertyGUI"/> for each of its children.</summary>
|
||||
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
InitializeMode(property);
|
||||
|
||||
// Highlight the whole area if this transition is currently being previewed.
|
||||
var isPreviewing = TransitionPreviewWindow.IsPreviewing(property);
|
||||
if (isPreviewing)
|
||||
{
|
||||
var highlightArea = area;
|
||||
highlightArea.xMin -= AnimancerGUI.IndentSize;
|
||||
EditorGUI.DrawRect(highlightArea, new(0.35f, 0.5f, 1, 0.2f));
|
||||
}
|
||||
|
||||
if (property.propertyType == SerializedPropertyType.ObjectReference)
|
||||
{
|
||||
DoObjectReferenceGUI(area, property, label);
|
||||
return;
|
||||
}
|
||||
|
||||
var headerArea = area;
|
||||
|
||||
if (property.propertyType == SerializedPropertyType.ManagedReference)
|
||||
DoPreviewButtonGUI(ref headerArea, property, isPreviewing);
|
||||
|
||||
using (new TypeSelectionButton(headerArea, property, true))
|
||||
{
|
||||
DoPropertyGUI(area, property, label, isPreviewing);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly CachedEditor NestedEditor = new();
|
||||
|
||||
private static GUIStyle _NestAreaStyle;
|
||||
|
||||
private void DoObjectReferenceGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
EditorGUI.PropertyField(area, property, label, property.isExpanded);
|
||||
|
||||
if (property.hasMultipleDifferentValues)
|
||||
return;
|
||||
|
||||
var value = property.objectReferenceValue;
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
property.isExpanded = EditorGUI.Foldout(area, property.isExpanded, GUIContent.none, true);
|
||||
if (!property.isExpanded)
|
||||
return;
|
||||
|
||||
const float NegativePadding = 4;
|
||||
EditorGUIUtility.labelWidth -= NegativePadding;
|
||||
|
||||
if (_NestAreaStyle == null)
|
||||
{
|
||||
_NestAreaStyle = new GUIStyle(GUI.skin.box);
|
||||
var rect = _NestAreaStyle.margin;
|
||||
rect.bottom = rect.top = 0;
|
||||
_NestAreaStyle.margin = rect;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
GUILayout.BeginVertical(_NestAreaStyle);
|
||||
|
||||
try
|
||||
{
|
||||
NestedEditor.GetEditor(value).OnInspectorGUI();
|
||||
}
|
||||
catch (ExitGUIException)
|
||||
{
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
}
|
||||
|
||||
GUILayout.EndVertical();
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
EditorGUIUtility.labelWidth += NegativePadding;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoPropertyGUI(Rect area, SerializedProperty property, GUIContent label, bool isPreviewing)
|
||||
{
|
||||
using (new DrawerContext(property))
|
||||
{
|
||||
var indent = !string.IsNullOrEmpty(label.text);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
var mainProperty = GetMainProperty(property);
|
||||
DoHeaderGUI(ref area, property, mainProperty, label, isPreviewing);
|
||||
DoChildPropertiesGUI(area, property, mainProperty, indent);
|
||||
|
||||
if (EditorGUI.EndChangeCheck() && isPreviewing)
|
||||
TransitionPreviewWindow.PreviewNormalizedTime = TransitionPreviewWindow.PreviewNormalizedTime;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="_Mode"/> is <see cref="Mode.Uninitialized"/>, this method determines how it should start
|
||||
/// based on the number of properties in the `serializedObject`. If the only serialized field is an
|
||||
/// <see cref="ITransition"/> then it should start expanded.
|
||||
/// </summary>
|
||||
protected void InitializeMode(SerializedProperty property)
|
||||
{
|
||||
if (_Mode == Mode.Uninitialized)
|
||||
{
|
||||
if (property.depth > 0)
|
||||
{
|
||||
_Mode = Mode.Normal;
|
||||
return;
|
||||
}
|
||||
|
||||
_Mode = Mode.AlwaysExpanded;
|
||||
|
||||
var iterator = property.serializedObject.GetIterator();
|
||||
iterator.Next(true);
|
||||
|
||||
var count = 0;
|
||||
do
|
||||
{
|
||||
switch (iterator.propertyPath)
|
||||
{
|
||||
// Ignore MonoBehaviour inherited fields.
|
||||
case "m_ObjectHideFlags":
|
||||
case "m_Script":
|
||||
break;
|
||||
|
||||
default:
|
||||
count++;
|
||||
if (count > 1)
|
||||
{
|
||||
_Mode = Mode.Normal;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (iterator.NextVisible(false));
|
||||
}
|
||||
|
||||
if (_Mode == Mode.AlwaysExpanded)
|
||||
property.isExpanded = true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the root property of a transition with an optional main property on the same line.</summary>
|
||||
protected virtual void DoHeaderGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty mainProperty,
|
||||
GUIContent label,
|
||||
bool isPreviewing)
|
||||
{
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
var labelArea = area;
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
|
||||
if (rootProperty.propertyType != SerializedPropertyType.ManagedReference)
|
||||
DoPreviewButtonGUI(ref labelArea, rootProperty, isPreviewing);
|
||||
|
||||
// Draw the Root Property after the Main Property to give better spacing between the label and field.
|
||||
|
||||
// Drawing the main property might assign its details to the label so we keep our own copy.
|
||||
using (var rootLabel = PooledGUIContent.Acquire(label.text, label.tooltip))
|
||||
{
|
||||
// Main Property.
|
||||
|
||||
DoMainPropertyGUI(labelArea, out labelArea, rootProperty, mainProperty);
|
||||
|
||||
// Root Property.
|
||||
|
||||
var propertyLabel = EditorGUI.BeginProperty(labelArea, rootLabel, rootProperty);
|
||||
EditorGUI.LabelField(labelArea, propertyLabel);
|
||||
EditorGUI.EndProperty();
|
||||
|
||||
if (_Mode != Mode.AlwaysExpanded)
|
||||
{
|
||||
var hierarchyMode = EditorGUIUtility.hierarchyMode;
|
||||
EditorGUIUtility.hierarchyMode = true;
|
||||
|
||||
rootProperty.isExpanded =
|
||||
EditorGUI.Foldout(labelArea, rootProperty.isExpanded, GUIContent.none, true);
|
||||
|
||||
EditorGUIUtility.hierarchyMode = hierarchyMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI the the target transition's main property.</summary>
|
||||
protected virtual void DoMainPropertyGUI(
|
||||
Rect area,
|
||||
out Rect labelArea,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty mainProperty)
|
||||
{
|
||||
labelArea = area;
|
||||
if (mainProperty == null)
|
||||
return;
|
||||
|
||||
var fullArea = area;
|
||||
|
||||
labelArea = AnimancerGUI.StealFromLeft(
|
||||
ref area,
|
||||
EditorGUIUtility.labelWidth,
|
||||
AnimancerGUI.StandardSpacing);
|
||||
|
||||
var mainPropertyReferenceIsMissing =
|
||||
mainProperty.propertyType == SerializedPropertyType.ObjectReference &&
|
||||
mainProperty.objectReferenceValue == null;
|
||||
|
||||
var hierarchyMode = EditorGUIUtility.hierarchyMode;
|
||||
EditorGUIUtility.hierarchyMode = true;
|
||||
|
||||
if (rootProperty.propertyType == SerializedPropertyType.ManagedReference)
|
||||
{
|
||||
if (rootProperty.isExpanded ||
|
||||
_Mode == Mode.AlwaysExpanded)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref fullArea);
|
||||
using (var label = PooledGUIContent.Acquire(mainProperty))
|
||||
EditorGUI.PropertyField(fullArea, mainProperty, label, true);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
EditorGUI.PropertyField(area, mainProperty, GUIContent.none, true);
|
||||
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
}
|
||||
|
||||
EditorGUIUtility.hierarchyMode = hierarchyMode;
|
||||
|
||||
// If the main Object reference was just assigned and all fields were at their type default,
|
||||
// reset the value to run its default constructor and field initializers then reassign the reference.
|
||||
var reference = mainProperty.objectReferenceValue;
|
||||
if (mainPropertyReferenceIsMissing && reference != null)
|
||||
{
|
||||
mainProperty.objectReferenceValue = null;
|
||||
if (Serialization.IsDefaultValueByType(rootProperty))
|
||||
rootProperty.GetAccessor().ResetValue(rootProperty);
|
||||
mainProperty.objectReferenceValue = reference;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a small button using the <see cref="TransitionPreviewWindow.Icon"/>.</summary>
|
||||
private static void DoPreviewButtonGUI(ref Rect area, SerializedProperty property, bool isPreviewing)
|
||||
{
|
||||
if (property.serializedObject.targetObjects.Length != 1 ||
|
||||
!TransitionPreviewWindow.CanBePreviewed(property))
|
||||
return;
|
||||
|
||||
var enabled = GUI.enabled;
|
||||
var currentEvent = Event.current;
|
||||
if (currentEvent.button == 1)// Ignore Right Clicks on the Preview Button.
|
||||
{
|
||||
switch (currentEvent.type)
|
||||
{
|
||||
case EventType.MouseDown:
|
||||
case EventType.MouseUp:
|
||||
case EventType.ContextClick:
|
||||
GUI.enabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var tooltip = isPreviewing ? TransitionPreviewWindow.Inspector.CloseTooltip : "Preview this transition";
|
||||
|
||||
if (DoPreviewButtonGUI(ref area, isPreviewing, tooltip))
|
||||
TransitionPreviewWindow.OpenOrClose(property);
|
||||
|
||||
GUI.enabled = enabled;
|
||||
}
|
||||
|
||||
/// <summary>Draws a small button using the <see cref="TransitionPreviewWindow.Icon"/>.</summary>
|
||||
public static bool DoPreviewButtonGUI(ref Rect area, bool selected, string tooltip)
|
||||
{
|
||||
var width = AnimancerGUI.LineHeight + AnimancerGUI.StandardSpacing * 2;
|
||||
var buttonArea = AnimancerGUI.StealFromRight(ref area, width, AnimancerGUI.StandardSpacing);
|
||||
buttonArea.height = AnimancerGUI.LineHeight;
|
||||
|
||||
using (var content = PooledGUIContent.Acquire("", tooltip))
|
||||
{
|
||||
content.image = TransitionPreviewWindow.Icon;
|
||||
|
||||
return GUI.Toggle(buttonArea, selected, content, PreviewButtonStyle) != selected;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static GUIStyle _PreviewButtonStyle;
|
||||
|
||||
/// <summary>The style used for the button that opens the <see cref="TransitionPreviewWindow"/>.</summary>
|
||||
public static GUIStyle PreviewButtonStyle
|
||||
=> _PreviewButtonStyle ??= new(AnimancerGUI.MiniButtonStyle)
|
||||
{
|
||||
padding = new(0, 0, 0, 1),
|
||||
fixedWidth = 0,
|
||||
fixedHeight = 0,
|
||||
};
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoChildPropertiesGUI(Rect area, SerializedProperty rootProperty, SerializedProperty mainProperty, bool indent)
|
||||
{
|
||||
if (!rootProperty.isExpanded && _Mode != Mode.AlwaysExpanded)
|
||||
return;
|
||||
|
||||
// Skip over the main property if it was already drawn by the header.
|
||||
if (rootProperty.propertyType == SerializedPropertyType.ManagedReference &&
|
||||
mainProperty != null)
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
|
||||
if (indent)
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
var property = rootProperty.Copy();
|
||||
|
||||
SerializedProperty eventsProperty = null;
|
||||
|
||||
var depth = property.depth;
|
||||
if (property.NextVisible(true))
|
||||
{
|
||||
while (property.depth > depth)
|
||||
{
|
||||
// Grab the Events property and draw it last.
|
||||
var path = property.propertyPath;
|
||||
if (eventsProperty == null && path.EndsWith("._Events"))
|
||||
{
|
||||
eventsProperty = property.Copy();
|
||||
}
|
||||
// Don't draw the main property again.
|
||||
else if (mainProperty != null && path.EndsWith(MainPropertyPathSuffix))
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
if (eventsProperty != null)
|
||||
{
|
||||
var type = Context.Transition.GetType();
|
||||
var accessor = property.GetAccessor();
|
||||
var field = Serialization.GetField(type, accessor.Name);
|
||||
if (field != null && field.IsDefined(typeof(DrawAfterEventsAttribute), false))
|
||||
{
|
||||
using (var eventsLabel = PooledGUIContent.Acquire(eventsProperty))
|
||||
DoChildPropertyGUI(ref area, rootProperty, eventsProperty, eventsLabel);
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
eventsProperty = null;
|
||||
}
|
||||
}
|
||||
|
||||
using (var label = PooledGUIContent.Acquire(property))
|
||||
DoChildPropertyGUI(ref area, rootProperty, property, label);
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
}
|
||||
|
||||
if (!property.NextVisible(false))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (eventsProperty != null)
|
||||
{
|
||||
using (var label = PooledGUIContent.Acquire(eventsProperty))
|
||||
DoChildPropertyGUI(ref area, rootProperty, eventsProperty, label);
|
||||
}
|
||||
|
||||
if (indent)
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Draws the `property` GUI in relation to the `rootProperty` which was passed into <see cref="OnGUI"/>.
|
||||
/// </summary>
|
||||
protected virtual void DoChildPropertyGUI(
|
||||
ref Rect area,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty property,
|
||||
GUIContent label)
|
||||
{
|
||||
// If we keep using the GUIContent that was passed into OnGUI then GetPropertyHeight will change it to
|
||||
// match the 'property' which we don't want.
|
||||
|
||||
using (var content = PooledGUIContent.Acquire(label.text, label.tooltip))
|
||||
{
|
||||
area.height = EditorGUI.GetPropertyHeight(property, content, true);
|
||||
|
||||
if (TryDoStartTimeField(ref area, rootProperty, property, content))
|
||||
return;
|
||||
|
||||
EditorGUI.PropertyField(area, property, content, true);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The name of the backing field of <c>ClipTransition.NormalizedStartTime</c>.</summary>
|
||||
public const string NormalizedStartTimeFieldName = "_NormalizedStartTime";
|
||||
|
||||
/// <summary>
|
||||
/// If the `property` is a "Start Time" field, this method draws it as well as the "End Time" below it and
|
||||
/// returns true.
|
||||
/// </summary>
|
||||
public static bool TryDoStartTimeField(
|
||||
ref Rect area,
|
||||
SerializedProperty rootProperty,
|
||||
SerializedProperty property,
|
||||
GUIContent label)
|
||||
{
|
||||
if (!property.propertyPath.EndsWith("." + NormalizedStartTimeFieldName))
|
||||
return false;
|
||||
|
||||
// Start Time.
|
||||
label.text = "Start Time";
|
||||
AnimationTimeAttributeDrawer.SetNextDefaultValue(
|
||||
AnimancerEvent.Sequence.GetDefaultNormalizedStartTime(Context.Transition.Speed));
|
||||
EditorGUI.PropertyField(area, property, label, false);
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
|
||||
// End Time.
|
||||
var events = rootProperty.FindPropertyRelative("_Events");
|
||||
using (var context = SerializableEventSequenceDrawer.Context.Get(events))
|
||||
{
|
||||
var areaCopy = area;
|
||||
var index = Mathf.Max(0, context.Times.Count - 1);
|
||||
SerializableEventSequenceDrawer.DoTimeGUI(ref areaCopy, context, index, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Context
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The current <see cref="DrawerContext"/>.</summary>
|
||||
public static DrawerContext Context { get; private set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Details used to draw an <see cref="ITransition"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/DrawerContext
|
||||
public readonly struct DrawerContext : IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The stack of active contexts.</summary>
|
||||
public static readonly List<DrawerContext> Stack = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The main property representing the <see cref="ITransition"/> field.</summary>
|
||||
public readonly SerializedProperty Property;
|
||||
|
||||
/// <summary>The actual transition object rerieved from the <see cref="Property"/>.</summary>
|
||||
public readonly ITransition Transition;
|
||||
|
||||
/// <summary>The cached value of <see cref="ITransition.MaximumLength"/>.</summary>
|
||||
public readonly float MaximumLength;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="DrawerContext"/>.</summary>
|
||||
/// <remarks>Be sure to <see cref="Dispose"/> it when done.</remarks>
|
||||
public DrawerContext(
|
||||
SerializedProperty transitionProperty)
|
||||
: this(transitionProperty, transitionProperty.GetValue<ITransition>())
|
||||
{ }
|
||||
|
||||
/// <summary>Creates a new <see cref="DrawerContext"/>.</summary>
|
||||
/// <remarks>Be sure to <see cref="Dispose"/> it when done.</remarks>
|
||||
public DrawerContext(
|
||||
ITransition transition)
|
||||
: this(null, transition)
|
||||
{ }
|
||||
|
||||
/// <summary>Creates a new <see cref="DrawerContext"/>.</summary>
|
||||
/// <remarks>Be sure to <see cref="Dispose"/> it when done.</remarks>
|
||||
public DrawerContext(
|
||||
SerializedProperty transitionProperty,
|
||||
ITransition transition)
|
||||
{
|
||||
Property = transitionProperty;
|
||||
Transition = transition;
|
||||
AnimancerUtilities.TryGetLength(Transition, out MaximumLength);
|
||||
|
||||
if (Property != null)
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
Stack.Add(this);
|
||||
Context = this;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Applies any modified properties and decrements the stack.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Debug.Assert(
|
||||
Transition == Context.Transition,
|
||||
$"{nameof(DrawerContext)}.{nameof(Dispose)}" +
|
||||
$" must be called in the reverse order in which instances were created." +
|
||||
$" Recommended: using (new DrawerContext(property)) to ensure correct disposal.");
|
||||
|
||||
if (Property != null && EditorGUI.EndChangeCheck())
|
||||
Property.serializedObject.ApplyModifiedProperties();
|
||||
|
||||
Stack.RemoveAt(Stack.Count - 1);
|
||||
|
||||
Context = Stack.Count > 0
|
||||
? Stack[^1]
|
||||
: default;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e77e914e65226dc439316086147dc410
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,21 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && ULT_EVENTS
|
||||
|
||||
using Animancer.Editor;
|
||||
using UnityEditor;
|
||||
|
||||
[assembly: PolymorphicDrawerDetails(typeof(UltEvents.UltEventBase), SeparateHeader = true)]
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] <see cref="PropertyDrawer"/> for <see cref="UltEvent"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/UltEventDrawer
|
||||
[CustomPropertyDrawer(typeof(UltEvent), true)]
|
||||
public class UltEventDrawer : UltEvents.Editor.UltEventDrawer,
|
||||
PropertyDrawers.IDiscardOnSelectionChange
|
||||
{ }
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87a84c3eff39c224a8192d2d1cd40cd7
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b7d1c1b70200d5498e65acbbb6199e0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Units.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A <see cref="PropertyDrawer"/> for fields with an <see cref="AnimationSpeedAttributeDrawer"/>
|
||||
/// which displays them using an 'x' suffix.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Units.Editor/AnimationSpeedAttributeDrawer
|
||||
[CustomPropertyDrawer(typeof(AnimationSpeedAttribute), true)]
|
||||
public class AnimationSpeedAttributeDrawer : UnitsAttributeDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override int GetLineCount(SerializedProperty property, GUIContent label)
|
||||
=> 1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52b7804967cafb14c9bfc15e59c2338e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,281 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using Animancer.Editor;
|
||||
using Animancer.Editor.Previews;
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Units.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A <see cref="PropertyDrawer"/> for <see cref="float"/> fields with a <see cref="UnitsAttribute"/>
|
||||
/// which displays them using 3 fields: Normalized, Seconds, and Frames.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions#time-fields">
|
||||
/// Time Fields</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Units.Editor/AnimationTimeAttributeDrawer
|
||||
[CustomPropertyDrawer(typeof(AnimationTimeAttribute), true)]
|
||||
public class AnimationTimeAttributeDrawer : UnitsAttributeDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Should the <see cref="NextDefaultValue"/> and <see cref="NextValueIsOptional"/>
|
||||
/// be used for the next field to be drawn?
|
||||
/// </summary>
|
||||
public static bool HasNextDefaultValue { get; private set; }
|
||||
|
||||
/// <summary>The default value to be used for the next field drawn by this attribute.</summary>
|
||||
public static float NextDefaultValue { get; private set; } = float.NaN;
|
||||
|
||||
/// <summary>The default value to be used for the next field drawn by this attribute.</summary>
|
||||
public static bool NextValueIsOptional { get; private set; }
|
||||
|
||||
/// <summary>Sets the <see cref="NextDefaultValue"/> and <see cref="NextValueIsOptional"/>.</summary>
|
||||
public static void SetNextDefaultValue(float defaultValue, bool isOptional)
|
||||
{
|
||||
HasNextDefaultValue = true;
|
||||
NextDefaultValue = defaultValue;
|
||||
NextValueIsOptional = isOptional;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="NextDefaultValue"/>
|
||||
/// and the <see cref="NextValueIsOptional"/> is true if the value is not <see cref="float.NaN"/>.
|
||||
/// </summary>
|
||||
public static void SetNextDefaultValue(float defaultValue)
|
||||
{
|
||||
HasNextDefaultValue = true;
|
||||
NextDefaultValue = defaultValue;
|
||||
NextValueIsOptional = !float.IsNaN(defaultValue);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override int GetLineCount(SerializedProperty property, GUIContent label)
|
||||
=> EditorGUIUtility.wideMode || TransitionDrawer.Context.Property == null
|
||||
? 1
|
||||
: 2;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
var nextDefaultValue = NextDefaultValue;
|
||||
|
||||
BeginProperty(area, property, ref label, out var value);
|
||||
OnGUI(area, label, ref value);
|
||||
EndProperty(area, property, ref value);
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
var index = (int)AnimationTimeAttribute.Units.Normalized;
|
||||
TransitionPreviewWindow.PreviewNormalizedTime =
|
||||
GetDisplayValue(value, nextDefaultValue) * Attribute.Multipliers[index];
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI for this attribute.</summary>
|
||||
public void OnGUI(Rect area, GUIContent label, ref float value)
|
||||
{
|
||||
try
|
||||
{
|
||||
Initialize();
|
||||
|
||||
var isOptional = Attribute.IsOptional;
|
||||
var defaultValue = Attribute.DefaultValue;
|
||||
|
||||
try
|
||||
{
|
||||
if (HasNextDefaultValue)
|
||||
{
|
||||
Attribute.IsOptional = NextValueIsOptional;
|
||||
Attribute.DefaultValue = NextDefaultValue;
|
||||
}
|
||||
|
||||
var context = TransitionDrawer.Context;
|
||||
if (context.Transition == null)
|
||||
{
|
||||
value = DoSpecialFloatField(area, label, value, DisplayConverters[Attribute.UnitIndex]);
|
||||
return;
|
||||
}
|
||||
|
||||
var length = context.MaximumLength;
|
||||
if (length <= 0)
|
||||
length = float.NaN;
|
||||
|
||||
AnimancerUtilities.TryGetFrameRate(context.Transition, out var frameRate);
|
||||
|
||||
var multipliers = CalculateMultipliers(length, frameRate);
|
||||
if (multipliers == null)
|
||||
{
|
||||
EditorGUI.LabelField(area, label.text, $"Invalid {nameof(Validate)}.{nameof(Validate.Value)}");
|
||||
return;
|
||||
}
|
||||
|
||||
DoPreviewTimeButton(ref area, ref value, multipliers);
|
||||
|
||||
DoFieldGUI(area, label, ref value);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Attribute.IsOptional = isOptional;
|
||||
Attribute.DefaultValue = defaultValue;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
HasNextDefaultValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private float[] CalculateMultipliers(float length, float frameRate)
|
||||
{
|
||||
switch ((AnimationTimeAttribute.Units)Attribute.UnitIndex)
|
||||
{
|
||||
case AnimationTimeAttribute.Units.Normalized:
|
||||
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Normalized] = 1;
|
||||
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Seconds] = length;
|
||||
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Frames] = length * frameRate;
|
||||
break;
|
||||
|
||||
case AnimationTimeAttribute.Units.Seconds:
|
||||
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Normalized] = 1f / length;
|
||||
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Seconds] = 1;
|
||||
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Frames] = frameRate;
|
||||
break;
|
||||
|
||||
case AnimationTimeAttribute.Units.Frames:
|
||||
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Normalized] = 1f / length / frameRate;
|
||||
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Seconds] = 1f / frameRate;
|
||||
Attribute.Multipliers[(int)AnimationTimeAttribute.Units.Frames] = 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
var settings = AnimancerSettingsGroup<AnimationTimeAttributeSettings>.Instance;
|
||||
ApplyVisibilitySetting(settings.showNormalized, AnimationTimeAttribute.Units.Normalized);
|
||||
ApplyVisibilitySetting(settings.showSeconds, AnimationTimeAttribute.Units.Seconds);
|
||||
ApplyVisibilitySetting(settings.showFrames, AnimationTimeAttribute.Units.Frames);
|
||||
|
||||
void ApplyVisibilitySetting(bool show, AnimationTimeAttribute.Units setting)
|
||||
{
|
||||
if (show)
|
||||
return;
|
||||
|
||||
var index = (int)setting;
|
||||
if (Attribute.UnitIndex != index)
|
||||
Attribute.Multipliers[index] = float.NaN;
|
||||
}
|
||||
|
||||
return Attribute.Multipliers;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DoPreviewTimeButton(
|
||||
ref Rect area,
|
||||
ref float value,
|
||||
float[] multipliers)
|
||||
{
|
||||
if (!TransitionPreviewWindow.IsPreviewingCurrentProperty())
|
||||
return;
|
||||
|
||||
var previewTime = TransitionPreviewWindow.PreviewNormalizedTime;
|
||||
|
||||
const string Tooltip =
|
||||
"<22> Left Click = preview the current value of this field." +
|
||||
"\n<> Right Click = set this field to use the current preview time.";
|
||||
|
||||
var displayValue = GetDisplayValue(value, NextDefaultValue);
|
||||
|
||||
var multiplier = multipliers[(int)AnimationTimeAttribute.Units.Normalized];
|
||||
displayValue *= multiplier;
|
||||
|
||||
var isCurrent = Mathf.Approximately(displayValue, previewTime);
|
||||
|
||||
var buttonArea = area;
|
||||
if (TransitionDrawer.DoPreviewButtonGUI(ref buttonArea, isCurrent, Tooltip))
|
||||
{
|
||||
if (Event.current.button != 1)
|
||||
TransitionPreviewWindow.PreviewNormalizedTime = displayValue;
|
||||
else
|
||||
value = previewTime / multiplier;
|
||||
}
|
||||
|
||||
// Only steal the button area for single line fields.
|
||||
if (area.height <= AnimancerGUI.LineHeight)
|
||||
area = buttonArea;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Settings
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] Options to determine how <see cref="AnimationTimeAttribute"/> displays.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Units.Editor/AnimationTimeAttributeSettings
|
||||
[Serializable, InternalSerializableType]
|
||||
public class AnimationTimeAttributeSettings : AnimancerSettingsGroup
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName
|
||||
=> "Time Fields";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Index
|
||||
=> 5;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should time fields show approximations if the value is too long for the GUI?</summary>
|
||||
/// <remarks>This setting is used by <see cref="CompactUnitConversionCache"/>.</remarks>
|
||||
[Tooltip("Should time fields show approximations if the value is too long for the GUI?" +
|
||||
" For example, '1.111111' could instead show '1.111~'.")]
|
||||
public bool showApproximations = true;
|
||||
|
||||
/// <summary>Should the <see cref="AnimationTimeAttribute.Units.Normalized"/> field be shown?</summary>
|
||||
/// <remarks>This setting is ignored for fields which directly store the normalized value.</remarks>
|
||||
[Tooltip("Should the " + nameof(AnimationTimeAttribute.Units.Normalized) + " field be shown?")]
|
||||
public bool showNormalized = true;
|
||||
|
||||
/// <summary>Should the <see cref="AnimationTimeAttribute.Units.Seconds"/> field be shown?</summary>
|
||||
/// <remarks>This setting is ignored for fields which directly store the seconds value.</remarks>
|
||||
[Tooltip("Should the " + nameof(AnimationTimeAttribute.Units.Seconds) + " field be shown?")]
|
||||
public bool showSeconds = true;
|
||||
|
||||
/// <summary>Should the <see cref="AnimationTimeAttribute.Units.Frames"/> field be shown?</summary>
|
||||
/// <remarks>This setting is ignored for fields which directly store the frame value.</remarks>
|
||||
[Tooltip("Should the " + nameof(AnimationTimeAttribute.Units.Frames) + " field be shown?")]
|
||||
public bool showFrames = true;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user