This commit is contained in:
2025-08-18 09:22:24 +08:00
commit cef5623ab0
1333 changed files with 305844 additions and 0 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8e1eb37f0147d7d4a8ddf11a7d7b04ee
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,231 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using static Animancer.Editor.TransitionLibraries.TransitionLibrarySelection;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// A <see cref="TransitionLibraryWindowPage"/> for editing transition aliases.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryAliasesPage
[Serializable]
public class TransitionLibraryAliasesPage : TransitionLibraryWindowPage
{
/************************************************************************************************************************/
[SerializeField]
private Vector2 _ScrollPosition;
[NonSerialized]
private bool _HasSorted;
/************************************************************************************************************************/
/// <inheritdoc/>
public override string DisplayName
=> "Transition Aliases";
/// <inheritdoc/>
public override string HelpTooltip
=> "Aliases are custom names which can be used to refer to transitions instead of direct references.";
/// <inheritdoc/>
public override int Index
=> 1;
/************************************************************************************************************************/
private static readonly List<Rect>
TransitionAreas = new();
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area)
{
var definition = Window.Data;
if (!_HasSorted)
{
_HasSorted = true;
definition.SortAliases();
}
var currentEvent = Event.current;
var isRepaint = currentEvent.type == EventType.Repaint;
if (isRepaint)
TransitionAreas.Clear();
area.yMin += StandardSpacing;
area.xMin += StandardSpacing;
area.xMax -= StandardSpacing;
var transitions = definition.Transitions;
var aliases = definition.Aliases;
var viewArea = new Rect(
0,
0,
area.width,
CalculateHeight(1 + transitions.Length + aliases.Length) + StandardSpacing);
if (viewArea.height > area.height)
viewArea.width -= GUI.skin.verticalScrollbar.fixedWidth;
_ScrollPosition = GUI.BeginScrollView(area, _ScrollPosition, viewArea);
viewArea.height = LineHeight;
DoAliasAllGUI(viewArea);
NextVerticalArea(ref viewArea);
var aliasIndex = 0;
for (int i = 0; i < transitions.Length; i++)
{
var totalTransitionArea = viewArea;
if (isRepaint)
TransitionAreas.Add(viewArea);
DoTransitionGUI(viewArea, transitions[i], i);
NextVerticalArea(ref viewArea);
while (aliasIndex < aliases.Length)
{
var alias = aliases[aliasIndex];
if (alias.Index != i)
{
if (alias.Index < i && currentEvent.type != EventType.Used)
{
Debug.LogError("Aliases aren't properly sorted.");
definition.SortAliases();
GUIUtility.ExitGUI();
}
break;
}
DoAliasGUI(viewArea, alias, aliasIndex++);
NextVerticalArea(ref viewArea);
}
// Highlights.
totalTransitionArea.yMax = viewArea.yMin - StandardSpacing;
var selected = Window.Selection.FromIndex == i || Window.Selection.ToIndex == i;
var hover = totalTransitionArea.Contains(currentEvent.mousePosition);
Window.Highlighter.DrawHighlightGUI(totalTransitionArea, selected, hover);
}
GUI.EndScrollView();
}
/************************************************************************************************************************/
/// <summary>Draws <see cref="TransitionLibraryDefinition.AliasAllTransitions"/>.</summary>
private void DoAliasAllGUI(Rect area)
{
var definition = Window.Data;
using (var label = PooledGUIContent.Acquire(
"Alias All Transitions",
TransitionLibraryDefinition.AliasAllTransitionsTooltip))
definition.AliasAllTransitions = EditorGUI.Toggle(area, label, definition.AliasAllTransitions);
if (TryUseClickEvent(area, 0))
definition.AliasAllTransitions = !definition.AliasAllTransitions;
}
/************************************************************************************************************************/
/// <summary>Draws a `transition`.</summary>
private void DoTransitionGUI(Rect area, TransitionAssetBase transition, int index)
{
var addArea = StealFromLeft(ref area, LineHeight * 5, StandardSpacing);
TransitionModifierTableGUI.HandleTransitionLabelInput(
ref area,
Window,
transition,
index,
SelectionType.ToTransition,
CalculateTargetTransitionIndex);
var typeArea = StealFromRight(ref area, area.width * 0.5f, StandardSpacing);
var label = transition.GetCachedName();
GUI.Label(area, label);
var wrappedTransition = transition.GetTransition();
var type = wrappedTransition != null
? wrappedTransition.GetType().GetNameCS(false)
: "Null";
GUI.Label(typeArea, type);
if (GUI.Button(addArea, "Add"))
{
var alias = new NamedIndex(null, index);
Window.RecordUndo().AddAlias(alias);
}
}
/************************************************************************************************************************/
/// <summary>Calculates the transition index for a drag and drop operation.</summary>
private static int CalculateTargetTransitionIndex(
Rect area,
int index,
Event currentEvent)
{
var y = currentEvent.mousePosition.y;
for (int i = 0; i < TransitionAreas.Count; i++)
if (y <= TransitionAreas[i].yMax)
return i;
return TransitionAreas.Count;
}
/************************************************************************************************************************/
/// <summary>Draws an `alias`.</summary>
private void DoAliasGUI(Rect area, NamedIndex alias, int index)
{
var removeArea = StealFromLeft(ref area, LineHeight * 5, StandardSpacing);
EditorGUI.BeginChangeCheck();
var name = StringAssetDrawer.DrawGUI(area, GUIContent.none, alias.Name, Window.SourceObject, out _);
if (EditorGUI.EndChangeCheck())
{
Window.RecordUndo().Aliases[index] = alias.With(name as StringAsset);
}
if (GUI.Button(removeArea, "Remove"))
{
Window.RecordUndo().RemoveAlias(index);
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 0d5be687cb9b8da4e9c61bfb49ab861d
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,71 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// A <see cref="TransitionLibraryWindowPage"/> for editing transition modifiers.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryModifiersPage
[Serializable]
public class TransitionLibraryModifiersPage : TransitionLibraryWindowPage
{
/************************************************************************************************************************/
[SerializeField]
private TransitionModifierTableGUI _TableGUI;
/************************************************************************************************************************/
/// <inheritdoc/>
public override string DisplayName
=> "Transition Modifiers";
/// <inheritdoc/>
public override string HelpTooltip
=> "Modifiers allow you to replace the usual fade duration for specific combinations of transitions.";
/// <inheritdoc/>
public override int Index
=> 0;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area)
{
_TableGUI ??= new();
if (Window.Data.Transitions.Length == 0)
{
area = new Rect(
area.x + AnimancerGUI.StandardSpacing,
area.y + AnimancerGUI.StandardSpacing,
area.width - AnimancerGUI.StandardSpacing * 2,
AnimancerGUI.LineHeight);
GUI.Label(area, "Library contains no Transitions");
AnimancerGUI.NextVerticalArea(ref area);
if (GUI.Button(area, "Create Transition"))
TransitionLibraryOperations.CreateTransition(Window);
}
else
{
_TableGUI.DoGUI(area, Window);
}
TransitionLibraryOperations.HandleBackgroundInput(area, Window);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: de81cf7262eb1514894f722e6050eead
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// Manages the selection of pages in the <see cref="TransitionLibraryWindow"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryWindowPage
[Serializable]
public abstract class TransitionLibraryWindowPage : IComparable<TransitionLibraryWindowPage>
{
/************************************************************************************************************************/
/// <summary>The window containing this page.</summary>
public TransitionLibraryWindow Window { get; set; }
/************************************************************************************************************************/
/// <summary>The name of this page.</summary>
public abstract string DisplayName { get; }
/// <summary>The text to use for the tooltip on the help button while this page is visible.</summary>
public abstract string HelpTooltip { get; }
/************************************************************************************************************************/
/// <summary>The sorting index of this page.</summary>
public abstract int Index { get; }
/// <summary>Compares the <see cref="Index"/>.</summary>
public int CompareTo(TransitionLibraryWindowPage other)
=> Index.CompareTo(other.Index);
/************************************************************************************************************************/
/// <summary>Draws the GUI of this page.</summary>
public abstract void OnGUI(Rect area);
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 61210733873388e4e815c6628988b306
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,428 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using static Animancer.Editor.TransitionLibraries.TransitionLibrarySelection;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// A <see cref="TableGUI"/> for editing
/// <see cref="Animancer.TransitionLibraries.TransitionLibraryDefinition.Modifiers"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionModifierTableGUI
[Serializable]
public class TransitionModifierTableGUI : TableGUI
{
/************************************************************************************************************************/
[NonSerialized] private TransitionLibraryWindow _Window;
[NonSerialized] private Vector2Int _SelectedCell;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionModifierTableGUI"/>.</summary>
public TransitionModifierTableGUI()
{
base.DoCellGUI = DoCellGUI;
CalculateWidestLabel = CalculateWidestTransitionLabel;
MinCellSize = new(LineHeight * 2, LineHeight);
MaxCellSize = new(LineHeight * 4, LineHeight);
}
/************************************************************************************************************************/
/// <summary>Draws the table GUI.</summary>
public void DoGUI(
Rect area,
TransitionLibraryWindow window)
{
_Window = window;
_SelectedCell = RecalculateSelectedCell(window.Selection);
var transitions = window.Data.Transitions;
DoTableGUI(area, transitions.Length, transitions.Length);
}
/************************************************************************************************************************/
/// <summary>Calculates the table coordinates of the `selection`.</summary>
private Vector2Int RecalculateSelectedCell(TransitionLibrarySelection selection)
{
if (selection.Validate())
{
switch (selection.Type)
{
case SelectionType.FromTransition:
case SelectionType.ToTransition:
case SelectionType.Modifier:
var cell = new Vector2Int(selection.ToIndex, selection.FromIndex);
if (cell.x < 0)
cell.x = int.MinValue;
if (cell.y < 0)
cell.y = int.MinValue;
return cell;
}
}
return new(int.MinValue, int.MinValue);
}
/************************************************************************************************************************/
/// <summary>Draws a table cell.</summary>
private new void DoCellGUI(Rect area, int column, int row)
{
var invertHover = false;
if (column < 0)
{
if (row < 0)
DoCornerGUI(area);
else
DoLabelGUI(
area,
row,
RightLabelStyle,
SelectionType.FromTransition);
}
else if (row < 0)
{
DoLabelGUI(
area,
column,
EditorStyles.label,
SelectionType.ToTransition);
invertHover = true;
}
else
{
DoFadeDurationGUI(area, _Window, row, column, "");
}
DrawHighlightGUI(area, column, row, invertHover);
}
/************************************************************************************************************************/
/// <summary>Draws the header corner.</summary>
private void DoCornerGUI(Rect area)
{
area.xMin += StandardSpacing;
var fromArea = area;
fromArea.y += area.height - LineHeight;
fromArea.height = LineHeight;
var toArea = fromArea;
toArea.y -= toArea.height - Padding;
var removeArea = toArea;
removeArea.y -= removeArea.height - Padding;
var createArea = removeArea;
createArea.y -= createArea.height - Padding;
fromArea.width -= VerticalScrollBar.fixedWidth + Padding;
var style = RightLabelStyle;
var fontStyle = style.fontStyle;
style.fontStyle = FontStyle.Bold;
GUI.Label(fromArea, "From", style);
GUI.Label(toArea, "To", style);
style.fontStyle = fontStyle;
DoCreateButtonGUI(createArea);
DoDeleteButtonGUI(removeArea);
}
/************************************************************************************************************************/
/// <summary>Draws a button to create a new transition.</summary>
private void DoCreateButtonGUI(Rect area)
{
if (GUI.Button(area, "Create Transition"))
TransitionLibraryOperations.CreateTransition(_Window);
}
/************************************************************************************************************************/
/// <summary>Draws a button to remove the selected transition.</summary>
private void DoDeleteButtonGUI(Rect area)
{
TransitionAssetBase transition = null;
int index = -1;
var selection = _Window.Selection;
switch (selection.Type)
{
case SelectionType.FromTransition:
transition = selection.FromTransition;
index = selection.FromIndex;
break;
case SelectionType.ToTransition:
transition = selection.ToTransition;
index = selection.ToIndex;
break;
}
using (new EditorGUI.DisabledScope(index < 0 || index >= _Window.Data.Transitions.Length))
if (GUI.Button(area, "Remove Transition"))
TransitionLibraryOperations.AskHowToDeleteTransition(transition, index, _Window);
}
/************************************************************************************************************************/
/// <summary>Draws a row or column label.</summary>
private void DoLabelGUI(
Rect area,
int index,
GUIStyle style,
SelectionType selectionType)
{
if (!_Window.Data.Transitions.TryGet(index, out var transition))
return;
HandleTransitionLabelInput(
ref area,
_Window,
transition,
index,
selectionType,
CalculateTargetTransitionIndex);
GUI.Label(area, GetTransitionName(transition), style);
}
/************************************************************************************************************************/
/// <summary>Returns the name of the `transition` with a special message for <c>null</c>.</summary>
public static string GetTransitionName(TransitionAssetBase transition)
=> transition != null
? transition.GetCachedName()
: "<Missing Transition>";
/************************************************************************************************************************/
private static readonly int LabelHint = "Label".GetHashCode();
[NonSerialized] private static bool _IsLabelDrag;
/// <summary>Handles input events on transition labels.</summary>
public static void HandleTransitionLabelInput(
ref Rect area,
TransitionLibraryWindow window,
TransitionAssetBase transition,
int index,
SelectionType selectionType,
Func<Rect, int, Event, int> calculateTargetTransitionIndex)
{
var control = new GUIControl(area, LabelHint);
switch (control.EventType)
{
case EventType.MouseDown:
if (control.Event.button == 0 &&
control.TryUseMouseDown())
{
if (control.Event.clickCount == 2)
EditorGUIUtility.PingObject(transition);
else
window.Selection.Select(window, transition, index, selectionType);
_IsLabelDrag = false;
}
break;
case EventType.MouseUp:
if (control.TryUseMouseUp() && _IsLabelDrag)
{
var target = calculateTargetTransitionIndex(area, index, control.Event);
TransitionLibrarySort.MoveTransition(window, index, target);
window.Selection.Select(window, transition, index, selectionType);
}
break;
case EventType.MouseDrag:
if (control.TryUseHotControl())
_IsLabelDrag = true;
break;
}
if (GUIUtility.hotControl == control.ID && _IsLabelDrag)
{
RepaintEverything();
area.y = control.Event.mousePosition.y - area.height * 0.5f;
}
}
/************************************************************************************************************************/
/// <summary>Calculates the transition index for a drag and drop operation.</summary>
private static int CalculateTargetTransitionIndex(
Rect area,
int index,
Event currentEvent)
{
var distance = currentEvent.mousePosition.y - area.y;
var offset = Mathf.FloorToInt(distance / area.height);
return index + offset;
}
/************************************************************************************************************************/
private static GUIStyle _FadeDurationStyle;
/// <summary>Draws the fade duration for a particular transition combination.</summary>
public static void DoFadeDurationGUI(
Rect area,
TransitionLibraryWindow window,
int from,
int to,
string label)
{
_FadeDurationStyle ??= new(EditorStyles.numberField)
{
alignment = TextAnchor.MiddleLeft,
};
var previousHotControl = GUIUtility.hotControl;
var hasModifier = window.Data.TryGetModifier(from, to, out var modifier);
var labelStyle = EditorStyles.label.fontStyle;
try
{
if (hasModifier)
{
EditorStyles.label.fontStyle = FontStyle.Bold;
_FadeDurationStyle.fontStyle = FontStyle.Bold;
_FadeDurationStyle.fontSize = EditorStyles.numberField.fontSize;
}
else
{
_FadeDurationStyle.fontStyle = FontStyle.Normal;
_FadeDurationStyle.fontSize = EditorStyles.numberField.fontSize * 4 / 5;
}
EditorGUI.BeginChangeCheck();
// This is basically a float field,
// but anything that fails to parse will clear the field instead of setting it to 0.
var text = modifier.FadeDuration.ToStringCached();
text = EditorGUI.TextField(area, label, text, _FadeDurationStyle);
if (EditorGUI.EndChangeCheck())
{
if (!float.TryParse(text, out var fadeDuration))
fadeDuration = float.NaN;
window.RecordUndo()
.SetModifier(modifier.WithFadeDuration(fadeDuration));
hasModifier = true;
RepaintEverything();
}
}
finally
{
EditorStyles.label.fontStyle = labelStyle;
}
if (previousHotControl != GUIUtility.hotControl)
{
window.Selection.Select(
window,
modifier,
modifier.FromIndex,
SelectionType.Modifier);
}
}
/************************************************************************************************************************/
/// <summary>Draws the selection and hover highlights for a particular cell.</summary>
private void DrawHighlightGUI(Rect area, int column, int row, bool invertHover)
{
if (_Window.Highlighter.EventType != EventType.Repaint)
return;
var selected =
_SelectedCell.x == column ||
_SelectedCell.y == row;
var hover = false;
if (_Window.Highlighter.IsMouseOver)
{
if (invertHover)
(row, column) = (column, row);
var mousePosition = Event.current.mousePosition;
if ((column >= 0 && IsInlineWithX(area, mousePosition.x)) ||
(row >= 0 && IsInlineWithY(area, mousePosition.y)))
{
hover = true;
}
}
_Window.Highlighter.DrawHighlightGUI(area, selected, hover);
}
/************************************************************************************************************************/
/// <summary>Is `x` inside the `area`.</summary>
private static bool IsInlineWithX(Rect area, float x)
=> area.xMin <= x
&& area.xMax > x;
/// <summary>Is `y` inside the `area`.</summary>
private static bool IsInlineWithY(Rect area, float y)
=> area.yMin <= y
&& area.yMax > y;
/************************************************************************************************************************/
/// <summary>Calculates the largest width of all transition labels.</summary>
private float CalculateWidestTransitionLabel()
{
var widest = LineHeight * 2;
var transitions = _Window.Data.Transitions;
for (int i = 0; i < transitions.Length; i++)
{
var transition = transitions[i];
if (transition == null)
continue;
var label = transition.GetCachedName();
var width = CalculateLabelWidth(label);
if (widest < width)
widest = width;
}
return widest;
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 78f4b212f3ad9cd4285498110975c389
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 61919166e1e7d634f95c77e80360ab63
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,290 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// A dummy object for tracking the selection within the <see cref="TransitionLibraryWindow"/>
/// and showing its details in the Inspector.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibrarySelection
[AnimancerHelpUrl(typeof(TransitionLibrarySelection))]
public class TransitionLibrarySelection : ScriptableObject
{
/************************************************************************************************************************/
/// <summary>[Editor-Only] Types of objects can be selected.</summary>
public enum SelectionType
{
/// <summary>Nothing selected.</summary>
None,
/// <summary>The main library.</summary>
Library,
/// <summary>A from-transition.</summary>
FromTransition,
/// <summary>A to-transition.</summary>
ToTransition,
/// <summary>A fade duration modifier.</summary>
Modifier,
}
/************************************************************************************************************************/
[SerializeField] private TransitionLibraryWindow _Window;
[SerializeField] private SelectionType _Type;
[SerializeField] private int _FromIndex = -1;
[SerializeField] private int _ToIndex = -1;
[SerializeField] private int _Version;
/// <summary>The window this selection is associated with.</summary>
public TransitionLibraryWindow Window
=> _Window;
/// <summary>The type of selected object.</summary>
public SelectionType Type
=> _Type;
/// <summary>The index of the <see cref="FromTransition"/>.</summary>
public int FromIndex
=> _FromIndex;
/// <summary>The index of the <see cref="ToTransition"/>.</summary>
public int ToIndex
=> _ToIndex;
/// <summary>The number of times this selection has been changed.</summary>
public int Version
=> _Version;
/************************************************************************************************************************/
/// <summary>The transition the current selection is coming from.</summary>
public TransitionAssetBase FromTransition { get; private set; }
/// <summary>The transition the current selection is going to.</summary>
public TransitionAssetBase ToTransition { get; private set; }
/// <summary>The <see cref="ITransition.FadeDuration"/> of the current selection.</summary>
public float FadeDuration { get; private set; }
/// <summary>Does the current selection have a modified <see cref="FadeDuration"/>?</summary>
public bool HasModifier { get; private set; }
/************************************************************************************************************************/
[NonSerialized] private object _Selected;
/// <summary>The currently selected object.</summary>
public object Selected
{
get
{
Validate();
return _Selected;
}
}
/************************************************************************************************************************/
/// <summary>Deselects the current object if it isn't valid.</summary>
public bool Validate()
{
if (IsValid())
return true;
Deselect();
return false;
}
/// <summary>Is the current selection valid?</summary>
public bool IsValid()
{
if (this == null ||
_Window == null ||
Selection.activeObject != this)
return false;
var library = _Window.SourceObject;
if (library == null)
return false;
FromTransition = null;
ToTransition = null;
FadeDuration = float.NaN;
HasModifier = false;
switch (_Type)
{
case SelectionType.Library:
name = "Transition Library";
_Selected = library;
return library != null;
case SelectionType.FromTransition:
name = "From Transition";
if (!_Window.Data.Transitions.TryGet(_FromIndex, out var transition))
return false;
FromTransition = transition;
FadeDuration = transition.TryGetFadeDuration();
_Selected = transition;
return true;
case SelectionType.ToTransition:
name = "To Transition";
if (!_Window.Data.Transitions.TryGet(_ToIndex, out transition))
return false;
ToTransition = transition;
FadeDuration = transition.TryGetFadeDuration();
_Selected = transition;
return true;
case SelectionType.Modifier:
name = "Transition Modifier";
var hasTransitions = _Window.Data.TryGetTransition(_FromIndex, out transition);
FromTransition = transition;
hasTransitions |= _Window.Data.TryGetTransition(_ToIndex, out transition);
ToTransition = transition;
if (_Window.Data.TryGetModifier(_FromIndex, _ToIndex, out var modifier))
{
HasModifier = true;
}
else if (hasTransitions)
{
modifier = modifier.WithFadeDuration(transition.TryGetFadeDuration());
}
else
{
return false;
}
FadeDuration = modifier.FadeDuration;
_Selected = modifier;
return true;
default:
return false;
};
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="Selected"/> object.</summary>
/// <remarks>
/// We can't simply set the <see cref="Selection.activeObject"/>
/// because it might not be a <see cref="UnityEngine.Object"/>
/// and if it is then we don't want the Project window to move to it.
/// <para></para>
/// So instead, we select this dummy object and <see cref="TransitionLibrarySelectionEditor"/>
/// draws a custom Inspector for the target object.
/// </remarks>
public void Select(
TransitionLibraryWindow window,
object select,
int index,
SelectionType type)
{
switch (type)
{
case SelectionType.Library:
_FromIndex = -1;
_ToIndex = -1;
break;
case SelectionType.FromTransition:
_FromIndex = index;
_ToIndex = -1;
break;
case SelectionType.ToTransition:
_FromIndex = -1;
_ToIndex = index;
break;
case SelectionType.Modifier:
if (select is TransitionModifierDefinition modifier)
{
_FromIndex = modifier.FromIndex;
_ToIndex = modifier.ToIndex;
break;
}
else
{
Deselect();
return;
}
default:
Deselect();
throw new ArgumentException($"Unhandled {nameof(SelectionType)}", nameof(type));
}
_Window = window;
_Type = type;
_Selected = select;
_Version++;
Selection.activeObject = this;
Validate();
}
/************************************************************************************************************************/
/// <summary>Clears the <see cref="Selected"/> object.</summary>
public void Deselect()
{
_Window = null;
_Type = default;
_FromIndex = -1;
_ToIndex = -1;
_Selected = null;
_Version++;
if (Selection.activeObject == this)
Selection.activeObject = null;
}
/************************************************************************************************************************/
/// <summary>Handles selection changes.</summary>
public void OnSelectionChange()
{
if (Selection.activeObject == this)
return;
Deselect();
if (_Window != null)
_Window.Repaint();
}
/************************************************************************************************************************/
/// <summary>Selects this object if it contains a valid selection.</summary>
protected virtual void OnEnable()
{
if (Selected != null)
Selection.activeObject = this;
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 7d5cccea07064a74a91890855fd2aaa4
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,216 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// A custom Inspector for <see cref="TransitionLibrarySelection"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibrarySelectionEditor
[CustomEditor(typeof(TransitionLibrarySelection), true)]
public class TransitionLibrarySelectionEditor : UnityEditor.Editor
{
/************************************************************************************************************************/
/// <summary>Casts the <see cref="UnityEditor.Editor.target"/>.</summary>
public TransitionLibrarySelection Target
=> target as TransitionLibrarySelection;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnInspectorGUI()
{
var target = Target;
if (target == null || !target.Validate())
return;
EditorGUI.BeginChangeCheck();
switch (target.Type)
{
case TransitionLibrarySelection.SelectionType.Library:
DoNestedEditorGUI(target.Selected as TransitionLibraryAsset, "Transition Library");
break;
case TransitionLibrarySelection.SelectionType.FromTransition:
case TransitionLibrarySelection.SelectionType.ToTransition:
DoTransitionGUI(target.Selected as TransitionAssetBase);
break;
case TransitionLibrarySelection.SelectionType.Modifier:
DoModifierGUI(target, (TransitionModifierDefinition)target.Selected);
break;
default:
target.Deselect();
break;
}
if (EditorGUI.EndChangeCheck())
target.Window.Repaint();
}
/************************************************************************************************************************/
#region Nested Editor
/************************************************************************************************************************/
[NonSerialized] private readonly CachedEditor NestedEditor = new();
[NonSerialized] private readonly CachedEditor NestedEditor2 = new();
/************************************************************************************************************************/
/// <summary>Draws the <see cref="UnityEditor.Editor"/> for the `target`.</summary>
private void DoNestedEditorGUI<T>(T target, string referenceLabel)
where T : Object
{
using (new EditorGUI.DisabledScope(true))
AnimancerGUI.DoObjectFieldGUI(referenceLabel, target, false);
var editor = NestedEditor.GetEditor(target);
if (editor != null)
editor.OnInspectorGUI();
}
/************************************************************************************************************************/
/// <summary>Cleans up any nested editors.</summary>
protected virtual void OnDestroy()
{
NestedEditor.Dispose();
NestedEditor2.Dispose();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Transitions
/************************************************************************************************************************/
/// <summary>Draws the GUI for the `transition`.</summary>
private void DoTransitionGUI(
TransitionAssetBase transition)
{
DoTransitionNameGUI(transition);
DoNestedEditorGUI(transition, "Transition Asset");
}
/************************************************************************************************************************/
/// <summary>Draws a field for editing the name of the `transition`.</summary>
private void DoTransitionNameGUI(
TransitionAssetBase transition)
{
var isSubAsset = AssetDatabase.IsSubAsset(transition);
var isMainAsset = !isSubAsset && AssetDatabase.IsMainAsset(transition);
var label = isSubAsset
? "Sub-Asset Name"
: isMainAsset
? "File Name"
: "Name";
EditorGUI.BeginChangeCheck();
var name = TransitionModifierTableGUI.GetTransitionName(transition);
name = EditorGUILayout.DelayedTextField(label, name);
if (EditorGUI.EndChangeCheck() && transition != null)
{
transition.SetName(name);
if (isSubAsset)
{
AssetDatabase.SaveAssets();
}
else if (isMainAsset)
{
AssetDatabase.RenameAsset(
AssetDatabase.GetAssetPath(transition),
name);
}
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Modifiers
/************************************************************************************************************************/
private static readonly BoolPref
IsFromExpanded = new($"{nameof(TransitionLibrarySelectionEditor)}.{nameof(IsFromExpanded)}"),
IsToExpanded = new($"{nameof(TransitionLibrarySelectionEditor)}.{nameof(IsToExpanded)}");
/************************************************************************************************************************/
/// <summary>Draws the GUI for the `modifier`.</summary>
private void DoModifierGUI(
TransitionLibrarySelection selection,
TransitionModifierDefinition modifier)
{
var library = selection.Window.Data;
DoTransitionField(library, NestedEditor, IsFromExpanded, modifier.FromIndex, "From");
DoTransitionField(library, NestedEditor2, IsToExpanded, modifier.ToIndex, "To");
var area = AnimancerGUI.LayoutSingleLineRect();
TransitionModifierTableGUI.DoFadeDurationGUI(
area,
selection.Window,
modifier.FromIndex,
modifier.ToIndex,
"Fade Duration");
}
/************************************************************************************************************************/
/// <summary>Draws the GUI for a transition.</summary>
private TransitionAssetBase DoTransitionField(
TransitionLibraryDefinition library,
CachedEditor cachedEditor,
BoolPref isExpanded,
int transitionIndex,
string label)
{
library.TryGetTransition(transitionIndex, out var transition);
var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.After);
var labelArea = area;
labelArea.width = EditorGUIUtility.labelWidth;
isExpanded.Value = EditorGUI.Foldout(labelArea, isExpanded, GUIContent.none, true);
var enabled = GUI.enabled;
GUI.enabled = false;
AnimancerGUI.DoObjectFieldGUI(area, label, transition, false);
GUI.enabled = enabled;
if (isExpanded)
{
GUILayout.BeginVertical(GUI.skin.box);
var editor = cachedEditor.GetEditor(transition);
editor.OnInspectorGUI();
GUILayout.EndVertical();
}
return transition;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 0cfcfbc7bdddf4d4fabcf7aa15635df6
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,523 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using Animancer.Editor.Previews;
using Animancer.TransitionLibraries;
using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only] Custom preview for <see cref="TransitionLibrarySelection"/>.</summary>
/// <remarks>Parts of this class are based on Unity's <see cref="MeshPreview"/>.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibrarySelectionPreview
[CustomPreview(typeof(TransitionLibrarySelection))]
public class TransitionLibrarySelectionPreview : ObjectPreview
{
/************************************************************************************************************************/
[SerializeField] private AnimancerPreviewRenderer _PreviewRenderer;
[SerializeField] private TransitionPreviewPlayer _PreviewPlayer;
[NonSerialized] private TransitionLibrarySelection _Target;
[NonSerialized] private int _TargetVersion = -1;
[NonSerialized] private readonly TransitionLibrarySelectionPreviewSpeed Speed = new();
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Initialize(Object[] targets)
{
_PreviewRenderer ??= new();
_PreviewPlayer ??= new();
if (targets.Length == 1)
{
_Target = targets[0] as TransitionLibrarySelection;
if (_Target != null)
{
_TargetVersion = _Target.Version - 1;
if (_Target.Window != null)
_PreviewRenderer.PreviewObject.TrySelectBestModel(_Target.Window.Data);
CheckTarget();
}
}
base.Initialize(targets);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Cleanup()
{
base.Cleanup();
_PreviewPlayer?.Dispose();
_PreviewPlayer = null;
_PreviewRenderer?.Dispose();
_PreviewRenderer = null;
}
/************************************************************************************************************************/
/// <summary>Handles changes to the target object.</summary>
private void CheckTarget()
{
if (_TargetVersion == _Target.Version)
return;
_TargetVersion = _Target.Version;
_PreviewPlayer.IsPlaying = false;
switch (_Target.Type)
{
case TransitionLibrarySelection.SelectionType.FromTransition:
_PreviewPlayer.FromTransition = _Target.FromTransition;
_PreviewPlayer.ToTransition = null;
break;
case TransitionLibrarySelection.SelectionType.ToTransition:
_PreviewPlayer.FromTransition = null;
_PreviewPlayer.ToTransition = _Target.ToTransition;
break;
case TransitionLibrarySelection.SelectionType.Modifier:
_PreviewPlayer.FromTransition = _Target.FromTransition;
_PreviewPlayer.ToTransition = _Target.ToTransition;
break;
}
}
/************************************************************************************************************************/
/// <summary>Updates the settings of the <see cref="TransitionPreviewPlayer"/>.</summary>
private void UpdatePlayerSettings()
{
_PreviewPlayer.Graph = _PreviewRenderer.PreviewObject.Graph;
_PreviewPlayer.FadeDuration = _Target.FadeDuration;
_PreviewPlayer.Speed = Speed.Speed;
_PreviewPlayer.RecalculateTimeBounds();
}
/************************************************************************************************************************/
private static readonly GUIContent
Title = new("Preview");
/// <inheritdoc/>
public override GUIContent GetPreviewTitle()
=> Title;
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool HasPreviewGUI()
=> _Target != null
&& _Target.Type switch
{
TransitionLibrarySelection.SelectionType.FromTransition or
TransitionLibrarySelection.SelectionType.ToTransition or
TransitionLibrarySelection.SelectionType.Modifier
=> true,
_ => false,
};
/************************************************************************************************************************/
#region Header Settings
/************************************************************************************************************************/
private static GUIStyle _ToolbarButtonStyle;
/// <inheritdoc/>
public override void OnPreviewSettings()
{
CheckTarget();
_ToolbarButtonStyle ??= new(EditorStyles.toolbarButton)
{
padding = new(),
};
var area = GUILayoutUtility.GetRect(LineHeight * 1.5f, LineHeight);
DoPlayPauseToggle(area, _ToolbarButtonStyle);
area = GUILayoutUtility.GetRect(LineHeight * 2f, LineHeight);
Speed.DoToggleGUI(area, _ToolbarButtonStyle);
}
/************************************************************************************************************************/
/// <summary>Draws a toggle to play and pause the preview.</summary>
private void DoPlayPauseToggle(Rect area, GUIStyle style)
{
if (TryUseClickEvent(area, 1) || TryUseClickEvent(area, 2))
_PreviewPlayer.CurrentTime = _PreviewPlayer.MinTime;
_PreviewPlayer.IsPlaying = AnimancerGUI.DoPlayPauseToggle(
area,
_PreviewPlayer.IsPlaying,
style,
"Left Click = Play/Pause\nRight Click = Reset Time");
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnInteractivePreviewGUI(Rect area, GUIStyle background)
{
if (_Target == null)
return;
CheckTarget();
UpdatePlayerSettings();
DoSettingsGUI(ref area);
DoTimelineGUI(ref area);
_PreviewRenderer.DoGUI(area, background);
AnimancerPreviewObjectGUI.HandleDragAndDrop(area, _PreviewRenderer.PreviewObject);
}
/************************************************************************************************************************/
/// <summary>Draws settings for modifying the preview.</summary>
private void DoSettingsGUI(ref Rect area)
{
if (!Speed.IsOn)
return;
area.yMin += StandardSpacing;
Speed.DoSpeedSlider(ref area, EditorStyles.toolbar);
var preview = _PreviewRenderer.PreviewObject;
var height = AnimancerPreviewObjectGUI.CalculateHeight(preview);
var settingsArea = StealFromTop(ref area, height, StandardSpacing);
settingsArea = settingsArea.Expand(-StandardSpacing, 0);
GUI.Label(settingsArea, GUIContent.none, EditorStyles.toolbar);
AnimancerPreviewObjectGUI.DoModelGUI(settingsArea, preview);
}
/************************************************************************************************************************/
#region Timeline
/************************************************************************************************************************/
/// <summary>Draws the preview timeline.</summary>
private void DoTimelineGUI(ref Rect area)
{
var timelineArea = StealFromTop(ref area, EditorStyles.toolbar.fixedHeight, StandardSpacing);
EditorGUI.DrawRect(timelineArea, Grey(0.25f, 0.3f));
EditorGUI.DrawRect(new(timelineArea.x, timelineArea.yMax - 1, timelineArea.width, 1), Grey(0, 0.5f));
DoFadeDurationSliderGUI(timelineArea);
DoTimeSliderGUI(timelineArea);
}
/************************************************************************************************************************/
private static readonly int SliderHash = "Slider".GetHashCode();
/************************************************************************************************************************/
/// <summary>Draws the fade duration slider.</summary>
private void DoFadeDurationSliderGUI(Rect area)
{
if (!CalculateFadeBounds(area, out var startFadeX, out var endFadeX))
return;
switch (_Target.Type)
{
default:
return;
case TransitionLibrarySelection.SelectionType.FromTransition:
case TransitionLibrarySelection.SelectionType.ToTransition:
case TransitionLibrarySelection.SelectionType.Modifier:
break;
}
var sliderArea = area;
sliderArea.width = LineHeight * 0.5f;
sliderArea.x = endFadeX - sliderArea.width * 0.5f;
var control = new GUIControl(sliderArea, SliderHash);
switch (control.EventType)
{
case EventType.MouseDown:
if (control.TryUseMouseDown())
_PreviewPlayer.IsPlaying = false;
break;
case EventType.MouseUp:
control.TryUseMouseUp();
break;
case EventType.MouseDrag:
if (control.TryUseHotControl())
{
var x = Math.Max(startFadeX, control.Event.mousePosition.x);
var normalizedTime = area.InverseLerpUnclampedX(x);
var normalizedStartFade = area.InverseLerpUnclampedX(startFadeX);
_PreviewPlayer.NormalizedTime = normalizedTime;
var fadeDuration =
_PreviewPlayer.LerpTimeUnclamped(normalizedTime) -
_PreviewPlayer.LerpTimeUnclamped(normalizedStartFade);
var selected = _Target.Selected;
if (selected is TransitionModifierDefinition modifier)
{
_Target.Window.RecordUndo()
.SetModifier(modifier.WithFadeDuration(fadeDuration));
}
else if (selected is TransitionAssetBase transitionAsset)
{
if (fadeDuration < 0)
fadeDuration = 0;
using var serializedObject = new SerializedObject(transitionAsset);
var property = serializedObject.FindProperty(TransitionAssetBase.TransitionField);
property = property.FindPropertyRelative("_" + nameof(ITransition.FadeDuration));
property.floatValue = fadeDuration;
serializedObject.ApplyModifiedProperties();
}
_Target.Window.Repaint();
}
break;
case EventType.Repaint:
var color = AnimancerStateDrawerColors.FadeLineColor;
var showCursor = GUIUtility.hotControl == 0 || GUIUtility.hotControl == control.ID;
if (showCursor)
EditorGUIUtility.AddCursorRect(sliderArea, MouseCursor.ResizeHorizontal);
if (!showCursor || !sliderArea.Contains(control.Event.mousePosition))
color.a *= 0.5f;
EditorGUI.DrawRect(
new(endFadeX, sliderArea.y, 1, sliderArea.height - 1),
color);
break;
}
}
/************************************************************************************************************************/
/// <summary>Draws the preview time slider.</summary>
private void DoTimeSliderGUI(Rect area)
{
var control = new GUIControl(area, SliderHash);
switch (control.EventType)
{
case EventType.MouseDown:
if (control.TryUseMouseDown())
{
_ForceClampTime = true;
_DidWrapTime = false;
HandleDragTime(area, control.Event);
_ForceClampTime = control.Event.control;
if (!_ForceClampTime)
EditorGUIUtility.SetWantsMouseJumping(1);
_PreviewPlayer.IsPlaying = control.Event.clickCount > 1;
}
break;
case EventType.MouseUp:
if (control.TryUseMouseUp())
EditorGUIUtility.SetWantsMouseJumping(0);
break;
case EventType.MouseDrag:
if (control.TryUseHotControl())
HandleDragTime(area, control.Event);
break;
case EventType.Repaint:
BeginTriangles(AnimancerStateDrawerColors.FadeLineColor);
if (CalculateFadeBounds(area, out var startFadeX, out var endFadeX))
{
// Fade.
DrawLineBatched(
new(startFadeX, area.yMin + 1),
new(endFadeX, area.yMax - 1),
1);
// To.
if (endFadeX < area.xMax)
DrawLineBatched(
new(endFadeX, area.yMax - 1),
new(area.xMax, area.yMax - 1),
1);
}
// From.
if (area.xMin < startFadeX)
DrawLineBatched(
new(area.xMin, area.yMin + 1),
new(startFadeX, area.yMin + 1),
1);
var color = _PreviewPlayer.IsPlaying
? AnimancerStateDrawerColors.PlayingBarColor
: AnimancerStateDrawerColors.PausedBarColor;
color.a = 1;
var timeX = area.LerpUnclampedX(_PreviewPlayer.NormalizedTime);
GL.Color(color);
DrawLineBatched(new(timeX, area.yMin), new(timeX, area.yMax), 2);
EndTriangles();
DoTransitionLabels(area);
break;
}
}
/************************************************************************************************************************/
private bool _ForceClampTime;
private bool _DidWrapTime;
/// <summary>Draws handles drag events to control the preview time.</summary>
private void HandleDragTime(Rect area, Event currentEvent)
{
if (_ForceClampTime)
{
_PreviewPlayer.NormalizedTime = area.InverseLerpUnclampedX(currentEvent.mousePosition.x);
return;
}
var delta = currentEvent.delta.x;
var normalizedTime = _PreviewPlayer.NormalizedTime;
if (normalizedTime == 0 && !_DidWrapTime && delta > 0)
{
var x = currentEvent.mousePosition.x;
if (area.xMin > x || area.xMax < x)
return;
}
normalizedTime += delta / area.width;
if (normalizedTime >= 0 || _DidWrapTime)
{
if (normalizedTime > 1)
_DidWrapTime = true;
normalizedTime = AnimancerUtilities.Wrap01(normalizedTime);
}
else
{
normalizedTime = 0;
}
_PreviewPlayer.NormalizedTime = normalizedTime;
}
/************************************************************************************************************************/
/// <summary>Calculates the start and end pixels of the fade.</summary>
private bool CalculateFadeBounds(
Rect area,
out float startFadeX,
out float endFadeX)
{
var fadeDuration = _Target.FadeDuration;
if (!float.IsNaN(fadeDuration))
{
startFadeX = area.LerpUnclampedX(_PreviewPlayer.InverseLerpTimeUnclamped(0));
endFadeX = area.LerpUnclampedX(_PreviewPlayer.InverseLerpTimeUnclamped(fadeDuration));
if (_Target.FromTransition.IsValid())
{
if (!_Target.ToTransition.IsValid())
{
endFadeX -= startFadeX;
startFadeX = area.xMin;
}
return true;
}
else
{
if (_Target.ToTransition.IsValid())
{
return true;
}
}
}
startFadeX = area.LerpUnclampedX(_PreviewPlayer.InverseLerpTimeUnclamped(0));
endFadeX = startFadeX;
return false;
}
/************************************************************************************************************************/
/// <summary>Draws labels for the selected transitions.</summary>
private void DoTransitionLabels(Rect area)
{
area.xMin += 1;
area.xMax -= 2;
var mid = area.width * 0.5f;
var leftArea = area;
var rightArea = area;
var fromTransition = _Target.FromTransition;
var toTransition = _Target.ToTransition;
var hasFrom = fromTransition.IsValid();
var hasTo = toTransition.IsValid();
if (hasFrom && hasTo)
{
leftArea.width = mid - StandardSpacing * 0.5f;
rightArea.x = area.xMax - leftArea.width;
rightArea.width = leftArea.width;
}
if (hasFrom)
GUI.Label(leftArea, _Target.FromTransition.GetCachedName());
if (hasTo)
GUI.Label(rightArea, _Target.ToTransition.GetCachedName(), RightLabelStyle);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 7de29fe26fe25b540987aef2f28c1ee0
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,53 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// <see cref="ToggledSpeedSlider"/> for <see cref="TransitionLibrarySelectionPreview"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibrarySelectionPreviewSpeed
public class TransitionLibrarySelectionPreviewSpeed : ToggledSpeedSlider
{
/************************************************************************************************************************/
private const string
SpeedPrefKey = nameof(TransitionLibrarySelectionPreviewSpeed) + "." + nameof(Speed);
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionLibrarySelectionPreviewSpeed"/>.</summary>
public TransitionLibrarySelectionPreviewSpeed()
: base(nameof(TransitionLibrarySelectionPreviewSpeed) + ".Show")
{
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnSetSpeed(float speed)
{
EditorPrefs.SetFloat(SpeedPrefKey, speed);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool DoToggleGUI(Rect area, GUIStyle style)
{
if (float.IsNaN(Speed))
Speed = EditorPrefs.GetFloat(SpeedPrefKey, 1);
return base.DoToggleGUI(area, style);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 29d5d1e026917374788c9f8258fc1fca
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,179 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// A custom Inspector for <see cref="TransitionLibraryAsset"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryAssetEditor
[CustomEditor(typeof(TransitionLibraryAsset), true)]
public class TransitionLibraryAssetEditor : UnityEditor.Editor
{
/************************************************************************************************************************/
private static GUIStyle _HeaderStyle;
/// <summary>Style for section headers.</summary>
public static GUIStyle HeaderStyle
=> _HeaderStyle ??= new(EditorStyles.label)
{
fontSize = EditorStyles.label.fontSize * 2,
};
/************************************************************************************************************************/
[NonSerialized]
private SerializedProperty _AliasAllTransitions;
/************************************************************************************************************************/
/// <summary>Called when a <see cref="TransitionLibraryAsset"/> is selected.</summary>
protected virtual void OnEnable()
{
_AliasAllTransitions = serializedObject.FindProperty(
TransitionLibraryAsset.DefinitionField + "." + TransitionLibraryDefinition.AliasAllTransitionsField);
}
/************************************************************************************************************************/
/// <summary>Called when a <see cref="TransitionLibraryAsset"/> is deselected.</summary>
protected virtual void OnDestroy()
{
NestedEditor.Dispose();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnInspectorGUI()
{
var library = target as TransitionLibraryAsset;
if (library == null)
return;
DoMainButtonsGUI(library);
DoSettingsGUI(library);
DoEditorDataGUI(library);
DoSubAssetWarningGUI(library);
}
/************************************************************************************************************************/
/// <summary>Draws several buttons with utility functions.</summary>
private void DoMainButtonsGUI(TransitionLibraryAsset library)
{
var editLabel = TransitionLibraryWindow.IsShowing(library)
? "Currently Editing"
: "Edit";
if (GUILayout.Button(editLabel))
TransitionLibraryWindow.Open(library);
using (var label = PooledGUIContent.Acquire("Documentation", Strings.DocsURLs.TransitionLibraries))
if (GUILayout.Button(label))
Application.OpenURL(Strings.DocsURLs.TransitionLibraries);
}
/************************************************************************************************************************/
/// <summary>Draws the `library`'s main settings.</summary>
private void DoSettingsGUI(TransitionLibraryAsset library)
{
GUILayout.Space(AnimancerGUI.LineHeight);
GUILayout.Label("Settings", HeaderStyle);
EditorGUILayout.PropertyField(_AliasAllTransitions);
}
/************************************************************************************************************************/
[NonSerialized] private readonly CachedEditor NestedEditor = new();
/// <summary>Draws the `library`'s <see cref="TransitionLibraryEditorData"/>.</summary>
private void DoEditorDataGUI(TransitionLibraryAsset library)
{
GUILayout.Space(AnimancerGUI.LineHeight);
GUILayout.Label("Editor-Only Settings", HeaderStyle);
var data = library.GetOrCreateEditorData();
var editor = NestedEditor.GetEditor(data);
editor.OnInspectorGUI();
}
/************************************************************************************************************************/
/// <summary>Draws warnings about any sub-assets which aren't actually referenced by the `library`.</summary>
private void DoSubAssetWarningGUI(TransitionLibraryAsset library)
{
var assetPath = AssetDatabase.GetAssetPath(library);
if (string.IsNullOrEmpty(assetPath))
return;
var subAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
for (int i = 0; i < subAssets.Length; i++)
DoSubAssetWarningGUI(library, assetPath, subAssets[i]);
}
/// <summary>Draws a warning about the `subAsset` if it isn't actually referenced by the `library`.</summary>
private void DoSubAssetWarningGUI(
TransitionLibraryAsset library,
string assetPath,
Object subAsset)
{
switch (subAsset)
{
case TransitionAssetBase transition:
if (Array.IndexOf(library.Definition.Transitions, transition) < 0)
break;
return;
case StringAsset alias:
var aliases = library.Definition.Aliases;
for (int i = 0; i < aliases.Length; i++)
if (aliases[i].Name == alias)
return;
break;
default:
return;
}
EditorGUILayout.HelpBox(
$"Sub-Asset '{subAsset.name}' isn't referenced by this Transition Library." +
$" Click to ping. Shift + Click to delete.",
MessageType.Warning);
if (AnimancerGUI.TryUseClickEventInLastRect(0))
{
if (Event.current.shift)
{
if (EditorUtility.DisplayDialog("Delete Sub-Asset",
$"Are you sure you want to delete '{subAsset.name}'" +
$" inside {assetPath}?" +
$"\n\nThis operation cannot be undone.",
"Delete",
"Cancel"))
AnimancerEditorUtilities.DeleteSubAsset(subAsset);
}
else
{
EditorGUIUtility.PingObject(subAsset);
}
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 7e85015084cb6e947947199df796b532
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,170 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// Additional data for a <see cref="TransitionLibraryAsset"/> which is excluded from Runtime Builds.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryEditorData
[AnimancerHelpUrl(typeof(TransitionLibraryEditorData))]
public partial class TransitionLibraryEditorData : ScriptableObject
{
/************************************************************************************************************************/
private static readonly Dictionary<TransitionLibraryAsset, TransitionLibraryEditorData>
LibraryToEditorData = new();
/************************************************************************************************************************/
[SerializeField, HideInInspector]
private TransitionLibraryAsset _Library;
/// <summary>The library this data is associated with.</summary>
public TransitionLibraryAsset Library
{
get => _Library;
private set
{
if (_Library == value)
return;
if (_Library != null)
LibraryToEditorData.Remove(_Library);
_Library = value;
EditorUtility.SetDirty(this);
if (_Library != null)
LibraryToEditorData.Add(_Library, this);
}
}
/************************************************************************************************************************/
/// <summary>Registers this data for the <see cref="Library"/>.</summary>
protected virtual void OnEnable()
{
if (_Library != null)
LibraryToEditorData[_Library] = this;
}
/// <summary>Un-registers this data for the <see cref="Library"/>.</summary>
protected virtual void OnDisable()
{
if (_Library != null)
LibraryToEditorData.Remove(_Library);
}
/************************************************************************************************************************/
/// <summary>Tries to get the `data` associated with the `library`.</summary>
private static bool TryGet(
TransitionLibraryAsset library,
out TransitionLibraryEditorData data)
{
if (!LibraryToEditorData.TryGetValue(library, out data))
return false;
if (data != null)
{
data.Library = library;
return true;
}
LibraryToEditorData.Remove(library);
return false;
}
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="TransitionLibraryEditorData"/> sub-asset of the `library` if one exists.
/// </summary>
public static TransitionLibraryEditorData GetEditorData(TransitionLibraryAsset library)
{
if (TryGet(library, out var data))
return data;
var assetPath = AssetDatabase.GetAssetPath(library);
if (string.IsNullOrEmpty(assetPath))
return null;
var subAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
for (int i = 0; i < subAssets.Length; i++)
{
if (subAssets[i] is TransitionLibraryEditorData editorData)
{
editorData.Library = library;
return editorData;
}
}
return null;
}
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="TransitionLibraryEditorData"/> sub-asset of the `library` if one exists.
/// Otherwise, creates and saves a new one.
/// </summary>
public static TransitionLibraryEditorData GetOrCreateEditorData(TransitionLibraryAsset library)
{
var data = library.GetEditorData();
if (data != null)
return data;
data = CreateInstance<TransitionLibraryEditorData>();
data.name = "Editor Data";
data.hideFlags = HideFlags.DontSaveInBuild | HideFlags.HideInHierarchy;
data.Library = library;
EditorApplication.CallbackFunction addSubAsset = null;
addSubAsset = () =>
{
if (AssetDatabase.Contains(library))
{
EditorApplication.update -= addSubAsset;
AssetDatabase.AddObjectToAsset(data, library);
AssetDatabase.SaveAssets();
}
};
EditorApplication.update += addSubAsset;
return data;
}
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] Extension methods for <see cref="TransitionLibraryEditorData"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryEditorDataExtensions
public static class TransitionLibraryEditorDataExtensions
{
/************************************************************************************************************************/
/// <summary><see cref="TransitionLibraryEditorData.GetEditorData"/></summary>
public static TransitionLibraryEditorData GetEditorData(this TransitionLibraryAsset library)
=> TransitionLibraryEditorData.GetEditorData(library);
/// <summary><see cref="TransitionLibraryEditorData.GetOrCreateEditorData"/></summary>
public static TransitionLibraryEditorData GetOrCreateEditorData(this TransitionLibraryAsset library)
=> TransitionLibraryEditorData.GetOrCreateEditorData(library);
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f04d4a67566152e40aaa01b8b745ff6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only] Custom Inspector for <see cref="TransitionLibraryEditorData"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryEditorDataEditor
[CustomEditor(typeof(TransitionLibraryEditorData), true)]
public class TransitionLibraryEditorDataEditor : UnityEditor.Editor
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnInspectorGUI()
{
var target = this.target as TransitionLibraryEditorData;
if (target == null)
return;
var transitionSortMode = target.TransitionSortMode;
base.OnInspectorGUI();
if (transitionSortMode != target.TransitionSortMode &&
target.Library != null)
TransitionLibrarySort.Sort(target.Library);
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 5e70b9fb99ed77049978e82246db2d3d
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,257 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using static Animancer.Editor.TransitionLibraries.TransitionLibrarySelection;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// Operations for modifying a <see cref="TransitionLibraryAsset"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryOperations
public static class TransitionLibraryOperations
{
/************************************************************************************************************************/
/// <summary>Handles input events for the background of the `window`.</summary>
public static void HandleBackgroundInput(
Rect area,
TransitionLibraryWindow window)
{
// Click to select the library.
if (TryUseClickEvent(area, 0))
window.Selection.Select(window, window.SourceObject, -1, SelectionType.Library);
var currentEvent = Event.current;
switch (currentEvent.type)
{
// Drag and drop to add transition.
case EventType.DragUpdated:
case EventType.DragPerform:
HandleDragAndDrop(currentEvent, window);
break;
case EventType.ValidateCommand:
case EventType.ExecuteCommand:
switch (currentEvent.commandName)
{
// Delete to remove the selection.
case Commands.Delete:
case Commands.SoftDelete:
HandleDelete(currentEvent, window);
break;
}
break;
}
}
/************************************************************************************************************************/
/// <summary>Handles drag and drop events to add transitions to the `window`.</summary>
private static void HandleDragAndDrop(
Event currentEvent,
TransitionLibraryWindow window)
{
var dragging = DragAndDrop.objectReferences;
TransitionAssetBase dropped = null;
int index = -1;
for (int i = dragging.Length - 1; i >= 0; i--)
{
var transition = TryCreateTransitionAttribute.TryCreateTransitionAsset(dragging[i]);
if (transition != null &&
Array.IndexOf(window.Data.Transitions, transition) >= 0)
continue;
switch (currentEvent.type)
{
case EventType.DragUpdated:
DragAndDrop.visualMode = DragAndDropVisualMode.Link;
currentEvent.Use();
return;
case EventType.DragPerform:
dropped = transition;
index = window.Data.Transitions.Length;
window.RecordUndo().AddTransition(transition);
break;
}
}
if (dropped != null)
{
window.Selection.Select(
window,
dropped,
index,
SelectionType.ToTransition);
DragAndDrop.AcceptDrag();
currentEvent.Use();
}
}
/************************************************************************************************************************/
/// <summary>Creates a new transition as a sub-asset of the `window`'s library.</summary>
public static TransitionAssetBase CreateTransition(
TransitionLibraryWindow window)
{
var createInstance = TransitionAssetBase.CreateInstance;
if (createInstance == null)
{
Debug.LogError(
$"{nameof(CreateTransition)} failed because " +
$"{nameof(TransitionAssetBase)}.{nameof(TransitionAssetBase.CreateInstance)}" +
$" hasn't been assigned." +
$" It should be automatically initialized by TransitionAsset.");
return null;
}
var definition = window.RecordUndo();
var transition = createInstance(null);
transition.name = "Transition " + (definition.Transitions.Length + 1);
AnimancerReflection.TryInvoke(transition, "Reset");
definition.AddTransition(transition);
var index = definition.Transitions.Length;
window.Selection.Select(window, transition, index, SelectionType.ToTransition);
return transition;
}
/************************************************************************************************************************/
/// <summary>Handles a delete event.</summary>
private static void HandleDelete(
Event currentEvent,
TransitionLibraryWindow window)
{
if (!window.Selection.Validate())
return;
switch (window.Selection.Type)
{
case SelectionType.FromTransition:
if (currentEvent.type == EventType.ExecuteCommand &&
window.Selection.Selected is TransitionAssetBase fromTransition)
AskHowToDeleteTransition(
fromTransition,
window.Selection.FromIndex,
window);
break;
case SelectionType.ToTransition:
if (currentEvent.type == EventType.ExecuteCommand &&
window.Selection.Selected is TransitionAssetBase toTransition)
AskHowToDeleteTransition(
toTransition,
window.Selection.ToIndex,
window);
break;
case SelectionType.Modifier:
if (currentEvent.type == EventType.ExecuteCommand &&
window.Selection.Selected is TransitionModifierDefinition modifier)
window.RecordUndo().RemoveModifier(modifier);
break;
default:
return;
}
currentEvent.Use();
}
/************************************************************************************************************************/
/// <summary>Asks if the user wants to delete a transition asset or just remove it from the library.</summary>
public static void AskHowToDeleteTransition(
TransitionAssetBase transition,
int index,
TransitionLibraryWindow window)
{
var assetPath = AssetDatabase.GetAssetPath(transition);
if (string.IsNullOrEmpty(assetPath))
{
if (transition != null)
Undo.DestroyObjectImmediate(transition);
window.RecordUndo().RemoveTransition(index);
return;
}
var isMainAsset = AssetDatabase.IsMainAsset(transition);
var assetType = isMainAsset ? "Asset" : "Sub-Asset";
var isSubAssetOfLibrary =
!isMainAsset &&
assetPath == AssetDatabase.GetAssetPath(window.SourceObject);
var message = assetPath;
if (!isSubAssetOfLibrary)
message += "\n\nRemove Transition: removes it from this Transition Library.";
message += $"\n\nDelete {assetType}: deletes the Transition {assetType} from your project (cannot be undone).";
int choice;
if (isSubAssetOfLibrary)
{
if (EditorUtility.DisplayDialog(
"Delete transition?",
message,
"Delete " + assetType,
"Cancel"))
choice = 2;
else
return;
}
else
{
choice = EditorUtility.DisplayDialogComplex(
"Remove or Delete transition?",
message,
"Remove Transition",
"Cancel",
"Delete " + assetType);
}
switch (choice)
{
case 0:// Remove.
window.RecordUndo().RemoveTransition(index);
break;
case 2:// Delete.
if (isMainAsset)
{
AssetDatabase.DeleteAsset(assetPath);
}
else
{
AnimancerEditorUtilities.DeleteSubAsset(transition);
}
window.Data.RemoveTransition(index);
Undo.ClearUndo(window);
break;
default:
return;
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 981e396a9df2f7741b95b02098bb3838
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,408 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only] Utility for sorting a <see cref="TransitionLibraryAsset"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibrarySort
public class TransitionLibrarySort : AssetModificationProcessor
{
/************************************************************************************************************************/
#region Automation
/************************************************************************************************************************/
/// <summary>Ensures that a <see cref="TransitionLibraryAsset"/> is sorted before being saved.</summary>
private static string[] OnWillSaveAssets(string[] paths)
{
foreach (var path in paths)
{
if (!path.EndsWith(".asset", StringComparison.Ordinal))
continue;
var library = AssetDatabase.LoadAssetAtPath<TransitionLibraryAsset>(path);
if (library == null)
continue;
Sort(library);
}
return paths;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Sort Modes
/************************************************************************************************************************/
/// <summary>Applies the <see cref="TransitionLibraryEditorData.TransitionSortMode"/>.</summary>
public static void Sort(TransitionLibraryAsset library)
{
// Can't have editor data if not an asset, so the sort mode will be custom anyway.
if (!AssetDatabase.Contains(library))
return;
var data = library.GetOrCreateEditorData();
if (data.TransitionSortMode == TransitionSortMode.Custom)
return;
NameCache.Clear();
switch (data.TransitionSortMode)
{
case TransitionSortMode.Name:
Sort(library.Definition, Static<CompareName>.Instance);
break;
case TransitionSortMode.Path:
Sort(library.Definition, Static<ComparePath>.Instance);
break;
case TransitionSortMode.TypeThenName:
Sort(library.Definition, Static<CompareTypeThenName>.Instance);
break;
case TransitionSortMode.TypeThenPath:
Sort(library.Definition, Static<CompareTypeThenPath>.Instance);
break;
}
}
/************************************************************************************************************************/
/// <summary>Compares the asset names then GUIDs.</summary>
private class CompareName : IComparer<TransitionAssetBase>
{
public int Compare(TransitionAssetBase a, TransitionAssetBase b)
{
var result = CompareNulls(a, b);
if (result != 0)
return result;
result = CompareCachedNames(a, b);
if (result != 0)
return result;
return CompareGUIDs(a, b);
}
}
/************************************************************************************************************************/
/// <summary>Compares the asset paths then GUIDs.</summary>
private class ComparePath : IComparer<TransitionAssetBase>
{
public int Compare(TransitionAssetBase a, TransitionAssetBase b)
{
var result = CompareNulls(a, b);
if (result != 0)
return result;
result = ComparePaths(a, b);
if (result != 0)
return result;
result = CompareCachedNames(a, b);
if (result != 0)
return result;
return CompareGUIDs(a, b);
}
}
/************************************************************************************************************************/
/// <summary>Compares the transition types then asset names then GUIDs.</summary>
private class CompareTypeThenName : IComparer<TransitionAssetBase>
{
public int Compare(TransitionAssetBase a, TransitionAssetBase b)
{
var result = CompareNulls(a, b);
if (result != 0)
return result;
result = CompareTypes(a, b);
if (result != 0)
return result;
result = CompareCachedNames(a, b);
if (result != 0)
return result;
return CompareGUIDs(a, b);
}
}
/************************************************************************************************************************/
/// <summary>Compares the transition types then asset paths then GUIDs.</summary>
private class CompareTypeThenPath : IComparer<TransitionAssetBase>
{
public int Compare(TransitionAssetBase a, TransitionAssetBase b)
{
var result = CompareNulls(a, b);
if (result != 0)
return result;
result = CompareTypes(a, b);
if (result != 0)
return result;
result = ComparePaths(a, b);
if (result != 0)
return result;
result = CompareCachedNames(a, b);
if (result != 0)
return result;
return CompareGUIDs(a, b);
}
}
/************************************************************************************************************************/
/// <summary>Compares objects to put null or destroyed ones at the end.</summary>
private static int CompareNulls(TransitionAssetBase a, TransitionAssetBase b)
=> (a == null).CompareTo(b == null);
/// <summary>Compares the asset GUIDs.</summary>
private static int CompareGUIDs(TransitionAssetBase a, TransitionAssetBase b)
{
var gotA = AssetDatabase.TryGetGUIDAndLocalFileIdentifier(a, out var aGUID, out long aLocalID);
var gotB = AssetDatabase.TryGetGUIDAndLocalFileIdentifier(b, out var bGUID, out long bLocalID);
var result = gotA.CompareTo(gotB);
if (result != 0)
return result;
result = aGUID.CompareTo(bGUID);
if (result != 0)
return result;
return aLocalID.CompareTo(bLocalID);
}
/// <summary>Compares the asset names.</summary>
private static int CompareCachedNames(TransitionAssetBase a, TransitionAssetBase b)
=> a.GetCachedName().CompareTo(b.GetCachedName());
/// <summary>Compares the asset paths.</summary>
private static int ComparePaths(TransitionAssetBase a, TransitionAssetBase b)
=> AssetDatabase.GetAssetPath(a).CompareTo(AssetDatabase.GetAssetPath(b));
/// <summary>Compares the transition types.</summary>
private static int CompareTypes(TransitionAssetBase a, TransitionAssetBase b)
{
if (AnimancerUtilities.TryGetWrappedObject<ITransition>(a, out var transitionA) &&
AnimancerUtilities.TryGetWrappedObject<ITransition>(b, out var transitionB))
{
var result = transitionA.GetType().GetNameCS().CompareTo(transitionB.GetType().GetNameCS());
if (result != 0)
return result;
}
return a.GetType().GetNameCS().CompareTo(b.GetType().GetNameCS());
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Sorting
/************************************************************************************************************************/
private static TransitionAssetBase[]
_SortingTransitions = Array.Empty<TransitionAssetBase>();
private static int[] _OldIndexToNew;
/************************************************************************************************************************/
/// <summary>Sorts the <see cref="TransitionLibraryDefinition.Transitions"/>.</summary>
public static void Sort(
TransitionLibraryDefinition library,
Comparison<TransitionAssetBase> comparison)
=> Sort(library, new Comparison<TransitionAssetBase>(comparison));
/// <summary>Sorts the <see cref="TransitionLibraryDefinition.Transitions"/>.</summary>
public static void Sort(
TransitionLibraryDefinition library,
IComparer<TransitionAssetBase> comparer)
{
var transitions = library.Transitions;
var count = transitions.Length;
if (_SortingTransitions.Length < count)
{
var length = Mathf.NextPowerOfTwo(count);
_SortingTransitions = new TransitionAssetBase[length];
_OldIndexToNew = new int[length];
}
Array.Copy(transitions, _SortingTransitions, count);
// Indices 0 -> Count.
var newIndexToOld = GetTempSequentialIndices(count);
Array.Sort(_SortingTransitions, newIndexToOld, 0, count, comparer);
// Remove nulls which should have been sorted to the end.
for (int i = count - 1; i >= 0; i--)
if (_SortingTransitions[i] == null)
count--;
else
break;
// _NewIndexToOld[x] is now the index that Transitions[x] was at previously.
// We need to invert that so _OldIndexToNew[x] is the new index of whatever was previously at Transitions[x].
// That allows the library to update any index references using a simple x = _OldIndexToNew[x];
for (int i = 0; i < count; i++)
_OldIndexToNew[newIndexToOld[i]] = i;
SetTransitions(library, _SortingTransitions, _OldIndexToNew, count);
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="TransitionLibraryDefinition.Transitions"/>
/// using `oldIndexToNew` to remap any references to the old order.
/// </summary>
public static void SetTransitions(
TransitionLibraryDefinition library,
TransitionAssetBase[] transitions,
int[] oldIndexToNew,
int count)
{
var libraryTransitions = library.Transitions;
if (libraryTransitions != transitions)
{
AnimancerUtilities.SetLength(ref libraryTransitions, count);
Array.Copy(transitions, libraryTransitions, count);
library.Transitions = libraryTransitions;
}
var modifiers = library.Modifiers;
for (int i = modifiers.Length - 1; i >= 0; i--)
{
var modifier = modifiers[i];
var isValid = true;
var fromIndex = ConvertIndex(modifier.FromIndex, oldIndexToNew, count, ref isValid);
var toIndex = ConvertIndex(modifier.ToIndex, oldIndexToNew, count, ref isValid);
if (isValid)
modifiers[i] = modifier.WithIndices(fromIndex, toIndex);
else
AnimancerUtilities.RemoveAt(ref modifiers, i);
}
var aliases = library.Aliases;
for (int i = aliases.Length - 1; i >= 0; i--)
{
var alias = aliases[i];
var isValid = true;
var index = ConvertIndex(alias.Index, oldIndexToNew, count, ref isValid);
if (isValid)
aliases[i] = alias.With(index);
else
AnimancerUtilities.RemoveAt(ref aliases, i);
}
library.SortAliases();
}
/************************************************************************************************************************/
/// <summary>Converts an old index to a new one.</summary>
private static int ConvertIndex(int index, int[] oldIndexToNew, int count, ref bool isValid)
{
if ((uint)index >= (uint)count)
{
isValid = false;
return -1;
}
index = oldIndexToNew[index];
if ((uint)index >= (uint)count)
{
isValid = false;
return -1;
}
return index;
}
/************************************************************************************************************************/
private static int[] _SequentialIndices = Array.Empty<int>();
/// <summary>Returns a cached array containing sequential indices, i.e. <c>array[i] = i</c>.</summary>
public static int[] GetTempSequentialIndices(int count)
{
if (_SequentialIndices.Length < count)
_SequentialIndices = new int[Mathf.NextPowerOfTwo(count)];
for (int i = 0; i < _SequentialIndices.Length; i++)
_SequentialIndices[i] = i;
return _SequentialIndices;
}
/************************************************************************************************************************/
/// <summary>Changes the index of a transition.</summary>
public static void MoveTransition(TransitionLibraryWindow window, int from, int to)
{
var transitions = window.Data.Transitions;
to = Mathf.Clamp(to, 0, transitions.Length - 1);
if (from == to)
return;
var editorData = window.SourceObject.GetOrCreateEditorData();
var definition = window.RecordUndo();
editorData.TransitionSortMode = TransitionSortMode.Custom;
var moving = transitions[from];
var indices = GetTempSequentialIndices(transitions.Length);
if (to > from)// Moving forwards.
{
Array.Copy(transitions, from + 1, transitions, from, to - from);
Array.Copy(indices, from, indices, from + 1, to - from);
}
else// Moving backwards.
{
Array.Copy(transitions, to, transitions, to + 1, from - to);
Array.Copy(indices, to + 1, indices, to, from - to);
}
transitions[to] = moving;
indices[from] = to;
SetTransitions(
definition,
transitions,
indices,
transitions.Length);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 1193a923ec8c6f847a7e7b6dfca91c19
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,357 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// An <see cref="EditorWindow"/> for configuring <see cref="TransitionLibraryAsset"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryWindow
public class TransitionLibraryWindow :
SerializedDataEditorWindow<TransitionLibraryAsset, TransitionLibraryDefinition>
{
/************************************************************************************************************************/
/// <summary>Opens a window for the `library`.</summary>
public static TransitionLibraryWindow Open(TransitionLibraryAsset library)
=> Open<TransitionLibraryWindow>(library, true, typeof(SceneView));
/************************************************************************************************************************/
/// <summary>
/// Double clicking a <see cref="TransitionLibraryAsset"/>
/// opens it in the <see cref="TransitionLibraryWindow"/>.
/// </summary>
[OnOpenAsset]
private static bool OnOpenAsset(int instanceID, int line)
{
var library = EditorUtility.InstanceIDToObject(instanceID) as TransitionLibraryAsset;
if (library == null)
return false;
Open(library);
return true;
}
/************************************************************************************************************************/
/// <summary>The current window instance.</summary>
public static TransitionLibraryWindow Instance { get; private set; }
/// <summary>Is a window currently showing the `library`.</summary>
public static bool IsShowing(Object library)
=> Instance != null
&& Instance.SourceObject == library;
/************************************************************************************************************************/
/// <inheritdoc/>
public override TransitionLibraryDefinition SourceData
{
get => SourceObject.Definition;
set => SourceObject.Definition = value;
}
/************************************************************************************************************************/
[SerializeField]
private TransitionLibrarySelection _Selection;
/// <summary>Manages the objects which can be selected within a library.</summary>
public TransitionLibrarySelection Selection
=> AnimancerEditorUtilities.FindOrCreate(ref _Selection);
/************************************************************************************************************************/
[SerializeReference]
private List<TransitionLibraryWindowPage> _Pages;
[SerializeField]
private int _CurrentPage;
/// <summary>The currently selected page.</summary>
public TransitionLibraryWindowPage CurrentPage
{
get
{
_CurrentPage = Mathf.Clamp(_CurrentPage, 0, _Pages.Count - 1);
return _Pages[_CurrentPage];
}
}
/************************************************************************************************************************/
/// <summary>Object highlight manager.</summary>
public readonly TransitionLibraryWindowHighlighter
Highlighter = new();
/************************************************************************************************************************/
/// <summary>Called when an object is selected.</summary>
private void OnSelectionChange()
{
if (_Selection != null)
_Selection.OnSelectionChange();
var library = UnityEditor.Selection.activeObject as TransitionLibraryAsset;
if (library != null && library != SourceObject)
SetAndCaptureSource(library);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnEnable()
{
base.OnEnable();
Instance = this;
wantsMouseMove = true;
// MainStageView, CanvasGroup Icon, GridLayoutGroup Icon.
titleContent = EditorGUIUtility.IconContent("CanvasGroup Icon");
titleContent.text = "Transition Library";
AnimancerEditorUtilities.InstantiateDerivedTypes(ref _Pages);
for (int i = 0; i < _Pages.Count; i++)
_Pages[i].Window = this;
OnSelectionChange();
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnDisable()
{
base.OnDisable();
if (Instance == this)
Instance = null;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnDestroy()
{
base.OnDestroy();
DestroyImmediate(_Selection);
}
/************************************************************************************************************************/
/// <summary>Draws the GUI of this window.</summary>
protected virtual void OnGUI()
{
if (SourceObject == null)
{
GUILayout.Label("No Transition Library has been selected");
return;
}
DoHeaderGUI();
DoBodyGUI();
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void CaptureData()
{
base.CaptureData();
Data.SortAliases();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Apply()
{
base.Apply();
for (int i = 0; i < Data.Transitions.Length; i++)
{
var transition = Data.Transitions[i];
if (EditorUtility.IsPersistent(transition))
continue;
AssetDatabase.AddObjectToAsset(transition, SourceObject);
}
}
/************************************************************************************************************************/
private static ButtonGroupStyles _ApplyRevertStyles;
/// <summary>Draws the header GUI.</summary>
private void DoHeaderGUI()
{
if (_ApplyRevertStyles.left == null)
_ApplyRevertStyles = new(
EditorStyles.toolbarButton,
EditorStyles.toolbarButton,
EditorStyles.toolbarButton);
GUILayout.BeginHorizontal();
var style = EditorStyles.toolbar;
var applyRevertWidth = CalculateApplyRevertWidth(_ApplyRevertStyles) - StandardSpacing - 1;
var area = GUILayoutUtility.GetRect(position.width, style.fixedHeight);
var currentEvent = Event.current;
if (currentEvent.type == EventType.Repaint)
style.Draw(area, false, false, false, false);
var pageArea = StealFromLeft(ref area, PageSelectionWidth);
var applyRevertArea = StealFromRight(ref area, applyRevertWidth);
var pathArea = area;
DoPageSelectionDropdown(pageArea);
DoAssetPathButton(pathArea, currentEvent);
DoApplyRevertGUI(applyRevertArea, _ApplyRevertStyles);
GUILayout.EndHorizontal();
}
/************************************************************************************************************************/
[NonSerialized]
private float _PageSelectionWidth;
private float PageSelectionWidth
{
get
{
if (_PageSelectionWidth == 0)
{
for (int i = 0; i < _Pages.Count; i++)
{
_PageSelectionWidth = Math.Max(
_PageSelectionWidth,
EditorStyles.toolbarDropDown.CalculateWidth(_Pages[i].DisplayName));
}
}
return _PageSelectionWidth;
}
}
/************************************************************************************************************************/
/// <summary>Draws a dropdown button for selecting the <see cref="CurrentPage"/>.</summary>
private void DoPageSelectionDropdown(Rect area)
{
using (var label = PooledGUIContent.Acquire(CurrentPage.DisplayName, CurrentPage.HelpTooltip))
if (!EditorGUI.DropdownButton(area, label, FocusType.Passive, EditorStyles.toolbarDropDown))
return;
var menu = new GenericMenu();
for (int i = 0; i < _Pages.Count; i++)
{
var index = i;
var page = _Pages[index];
menu.AddItem(
new(page.DisplayName),
_CurrentPage == index,
() => _CurrentPage = index);
}
menu.AddSeparator("");
menu.AddItem(
new("Documentation"),
false,
() => Application.OpenURL(Strings.DocsURLs.TransitionLibraries));
menu.ShowAsContext();
}
/************************************************************************************************************************/
private static GUIStyle _AssetPathStyle;
private readonly GUIContent AssetPath = new();
/// <summary>Draws the asset path of the target library and selects it if clicked.</summary>
private void DoAssetPathButton(Rect area, Event currentEvent)
{
_AssetPathStyle ??= new(EditorStyles.toolbarButton)
{
richText = true,
alignment = TextAnchor.MiddleRight,
fontStyle = FontStyle.Italic,
fontSize = (int)(EditorStyles.toolbarButton.fontSize * 0.8f),
};
if (currentEvent.type == EventType.Repaint)
{
var assetPath = AssetDatabase.GetAssetPath(SourceObject);
if (string.IsNullOrEmpty(assetPath))
{
AssetPath.text = "The target Transition Library isn't saved as an asset.";
AssetPath.tooltip = null;
}
else if (AssetPath.tooltip != assetPath)
{
AssetPath.tooltip = assetPath;
var directory = Path.GetDirectoryName(assetPath).Replace('\\', '/');
var file = Path.GetFileNameWithoutExtension(assetPath);
assetPath = $"{directory}/<b>{file}</b>";
AssetPath.text = assetPath;
}
}
if (GUI.Button(area, AssetPath, _AssetPathStyle))
{
if (Selection.Selected != (object)SourceObject)
Selection.Select(this, SourceObject, -1, TransitionLibrarySelection.SelectionType.Library);
else
EditorGUIUtility.PingObject(SourceObject);
}
}
/************************************************************************************************************************/
/// <summary>Draws the <see cref="CurrentPage"/>.</summary>
private void DoBodyGUI()
{
GUILayout.FlexibleSpace();
var area = GUILayoutUtility.GetLastRect();
area.width = position.width;
EditorGUI.DrawRect(area, Grey(0.2f, 0.5f));
if (_Pages.Count > 0)
{
Highlighter.BeginGUI(area);
CurrentPage?.OnGUI(area);
Highlighter.EndGUI(this);
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 5ce82e4a867719941a5e8f1fba513d28
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,83 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// An <see cref="EditorWindow"/> for configuring <see cref="TransitionLibraryAsset"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryWindowHighlighter
public class TransitionLibraryWindowHighlighter
{
/************************************************************************************************************************/
private static readonly Color
SelectionHighlightColor = new(0.5f, 0.5f, 1, 0.1f),
HoverHighlightColor = new(0.5f, 1, 0.5f, 0.1f);
/************************************************************************************************************************/
/// <summary>The current <see cref="Event.type"/>.</summary>
public EventType EventType { get; private set; }
/// <summary>The the mouse currently over the highlighter area.</summary>
public bool IsMouseOver { get; private set; }
/// <summary>The the hover highlight currently visible.</summary>
public bool DidHoverHighlight { get; private set; }
/************************************************************************************************************************/
/// <summary>Gathers the details of the <see cref="Event.current"/>.</summary>
public void BeginGUI(Rect area)
{
var currentEvent = Event.current;
EventType = currentEvent.type;
IsMouseOver = area.Contains(currentEvent.mousePosition);
DidHoverHighlight = false;
}
/************************************************************************************************************************/
/// <summary>Repaints the `window` if necessary.</summary>
public void EndGUI(TransitionLibraryWindow window)
{
if (DidHoverHighlight && window != EditorWindow.mouseOverWindow)
{
DidHoverHighlight = false;
window.Repaint();
}
else if (EventType == EventType.MouseMove)
{
window.Repaint();
}
}
/************************************************************************************************************************/
/// <summary>Draws highlights for the `area`.</summary>
public void DrawHighlightGUI(Rect area, bool selected, bool hover)
{
if (selected)
{
EditorGUI.DrawRect(area, SelectionHighlightColor);
}
if (hover)
{
DidHoverHighlight = true;
EditorGUI.DrawRect(area, HoverHighlightColor);
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: edd6e6ad3fd0dd14cadf6ffeeefacf73
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,64 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// Sorting algorithms for <see cref="Animancer.TransitionLibraries.TransitionLibraryDefinition.Transitions"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionSortMode
public enum TransitionSortMode
{
/************************************************************************************************************************/
/// <summary>Manual sorting.</summary>
Custom,
/// <summary>Based on the transition file names.</summary>
Name,
/// <summary>Based on the transition file paths.</summary>
Path,
/// <summary>Based on the transition types then file names.</summary>
TypeThenName,
/// <summary>Based on the transition types then file paths.</summary>
TypeThenPath,
/************************************************************************************************************************/
}
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryEditorData
public partial class TransitionLibraryEditorData
{
/************************************************************************************************************************/
[SerializeField]
private TransitionSortMode _TransitionSortMode;
/// <summary>[<see cref="SerializeField"/>] The algorithm to use for sorting transitions.</summary>
public TransitionSortMode TransitionSortMode
{
get => _TransitionSortMode;
set
{
if (_TransitionSortMode == value)
return;
_TransitionSortMode = value;
if (Library != null)
TransitionLibrarySort.Sort(Library);
}
}
/************************************************************************************************************************/
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9387a41aa99644146aed014d11ba8bc5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: