chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8e1eb37f0147d7d4a8ddf11a7d7b04ee
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,284 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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
=> 2;
/************************************************************************************************************************/
private static readonly List<Rect>
TransitionAreas = new();
private static float ButtonWidth
=> LineHeight * 4;
/************************************************************************************************************************/
/// <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 items = Window.Items;
var aliases = definition.Aliases;
var viewArea = new Rect(
0,
0,
area.width,
CalculateHeight(1 + items.Count + 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);
for (int i = 0; i < items.Count; i++)
{
if (isRepaint)
TransitionAreas.Add(viewArea);
DoItemGUI(ref viewArea, i, currentEvent);
}
GUI.EndScrollView();
}
/************************************************************************************************************************/
private void DoItemGUI(
ref Rect area,
int itemIndex,
Event currentEvent)
{
var totalTransitionArea = area;
var items = Window.Items;
var item = items.GetItem(itemIndex);
if (item is TransitionAssetBase transition)
{
var hasGroup = items.GetGroup(itemIndex) != null;
if (hasGroup)
area.xMin += IndentSize;
var transitions = Window.Data.Transitions;
var transitionIndex = Array.IndexOf(transitions, transition);
DoTransitionGUI(area, transition, transitionIndex);
NextVerticalArea(ref area);
DoAliasGUI(ref area, transitionIndex);
if (hasGroup)
area.xMin -= IndentSize;
}
else if (item is TransitionGroup group)
{
var groupArea = area;
NextVerticalArea(ref area);
var foldoutArea = StealFromLeft(ref groupArea, LineHeight, StandardSpacing);
TransitionModifierTableGUI.HandleTransitionLabelInput(
ref groupArea,
Window,
group,
SelectionType.Group,
CalculateTarget);
GUI.Label(groupArea, group.Name);
EditorGUI.BeginChangeCheck();
group.IsExpanded = EditorGUI.Foldout(foldoutArea, group.IsExpanded, GUIContent.none);
if (EditorGUI.EndChangeCheck())
Window.Selection.Select(Window, group, group.Index, SelectionType.Group);
}
// Highlights.
totalTransitionArea.yMax = area.yMin - StandardSpacing;
var selected = Window.Selection.Selected == item;
var hover = totalTransitionArea.Contains(currentEvent.mousePosition);
Window.Highlighter.DrawHighlightGUI(totalTransitionArea, selected, hover);
}
/************************************************************************************************************************/
/// <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, ButtonWidth, StandardSpacing);
TransitionModifierTableGUI.HandleTransitionLabelInput(
ref area,
Window,
transition,
SelectionType.ToTransition,
CalculateTarget);
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 target index for a drag and drop operation.</summary>
private static ListTargetCalculation CalculateTarget(
Rect area,
int index,
Event currentEvent)
{
var y = currentEvent.mousePosition.y;
for (int i = 0; i < TransitionAreas.Count; i++)
{
area = TransitionAreas[i];
var yMax = area.yMax;
if (y > yMax)
continue;
return new(
i,
Mathf.InverseLerp(area.y, yMax, y));
}
return new(TransitionAreas.Count, 1);
}
/************************************************************************************************************************/
/// <summary>Draws all aliases for the specified `transitionIndex`.</summary>
private void DoAliasGUI(ref Rect area, int transitionIndex)
{
var aliases = Window.Data.Aliases;
for (int i = 0; i < aliases.Length; i++)
{
var alias = aliases[i];
if (alias.Index != transitionIndex)
continue;
DoAliasGUI(area, alias, i);
NextVerticalArea(ref area);
}
}
/// <summary>Draws an `alias`.</summary>
private void DoAliasGUI(Rect area, NamedIndex alias, int aliasIndex)
{
var removeArea = StealFromLeft(ref area, ButtonWidth, StandardSpacing);
EditorGUI.BeginChangeCheck();
var name = StringAssetDrawer.DrawGUI(area, GUIContent.none, alias.Name, Window.SourceObject, out _);
if (EditorGUI.EndChangeCheck())
{
Window.RecordUndo().Aliases[aliasIndex] = alias.With(name as StringAsset);
}
if (GUI.Button(removeArea, "Remove"))
{
Window.RecordUndo().RemoveAlias(aliasIndex);
}
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -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:

View File

@@ -0,0 +1,54 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// A <see cref="TransitionLibraryWindowPage"/> for editing
/// <see cref="TransitionModifierDefinition.FadeDuration"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryFadeDurationsPage
[Serializable]
public class TransitionLibraryFadeDurationsPage : TransitionLibraryModifiersPage
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string DisplayName
=> "Fade Duration 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 TransitionLibraryFadeDurationsPage()
: base(Units.AnimationTimeAttribute.Units.Seconds)
{ }
/// <inheritdoc/>
public override float GetValue(ITransition transition)
=> transition.FadeDuration;
/// <inheritdoc/>
public override float GetValue(TransitionModifierDefinition modifier)
=> modifier.FadeDuration;
/// <inheritdoc/>
public override void SetValue(ref TransitionModifierDefinition modifier, float value)
=> modifier = modifier.WithFadeDuration(value);
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: db8c8079cb5b99142af9cbcc29f6c9ef
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,92 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using Animancer.Units;
using Animancer.Units.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 abstract class TransitionLibraryModifiersPage : TransitionLibraryWindowPage
{
/************************************************************************************************************************/
[SerializeField]
private TransitionModifierTableGUI _TableGUI;
/************************************************************************************************************************/
/// <summary>The drawer used for time fields on this page.</summary>
public readonly AnimationTimeAttributeDrawer
TimeDrawer = new();
/// <summary>Creates a new <see cref="TransitionLibraryModifiersPage"/>.</summary>
public TransitionLibraryModifiersPage(AnimationTimeAttribute.Units units)
{
TimeDrawer.Initialize(new AnimationTimeAttribute(units));
TimeDrawer.Attribute.Rule = Validate.Value.IsFiniteOrNaN;
TimeDrawer.Attribute.IsOptional = true;
}
/// <summary>Configures this page to display a single field or not.</summary>
public virtual void ConfigureForSingleField(bool singleField, ref float value) { }
/************************************************************************************************************************/
/// <summary>Gets the value controlled by this page.</summary>
public abstract float GetValue(ITransition transition);
/// <summary>Gets the value controlled by this page.</summary>
public abstract float GetValue(TransitionModifierDefinition modifier);
/// <summary>Sets the value controlled by this page.</summary>
public abstract void SetValue(ref TransitionModifierDefinition modifier, float value);
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnGUI(Rect area)
{
_TableGUI ??= new();
_TableGUI.Page = this;
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." +
" Drag and Drop Transition Assets into this window or use the Create Transition button.");
AnimancerGUI.NextVerticalArea(ref area);
if (GUI.Button(area, "Create Transition"))
TransitionLibraryOperations.CreateTransition(Window);
}
else
{
_TableGUI.DoGUI(area, Window);
}
TransitionLibraryOperations.HandleBackgroundInput(area, Window);
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -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:

View File

@@ -0,0 +1,80 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// A <see cref="TransitionLibraryWindowPage"/> for editing
/// <see cref="TransitionModifierDefinition.FadeDuration"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryStartTimesPage
[Serializable]
public class TransitionLibraryStartTimesPage : TransitionLibraryModifiersPage
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string DisplayName
=> "Start Time Modifiers";
/// <inheritdoc/>
public override string HelpTooltip
=> "Modifiers allow you to replace the usual start time for specific combinations of transitions.";
/// <inheritdoc/>
public override int Index
=> 1;
private readonly string[] ConvertedZeroes;
/// <inheritdoc/>
public TransitionLibraryStartTimesPage()
: base(Units.AnimationTimeAttribute.Units.Normalized)
{
TimeDrawer.Attribute.DisabledText = Strings.Tooltips.StartTimeDisabled;
var converters = TimeDrawer.DisplayConverters;
ConvertedZeroes = new string[converters.Length];
for (int i = 0; i < converters.Length; i++)
ConvertedZeroes[i] = converters[i].ConvertedZero;
}
/// <inheritdoc/>
public override void ConfigureForSingleField(bool singleField, ref float value)
{
var isSingleFieldNaN = singleField && float.IsNaN(value);
if (isSingleFieldNaN)
value = 0;
var converters = TimeDrawer.DisplayConverters;
for (int i = 0; i < converters.Length; i++)
{
var converter = converters[i];
converter.ConvertedZero = isSingleFieldNaN
? Strings.Tooltips.StartTimeDisabled
: ConvertedZeroes[i];
}
}
/// <inheritdoc/>
public override float GetValue(ITransition transition)
=> transition.NormalizedStartTime;
/// <inheritdoc/>
public override float GetValue(TransitionModifierDefinition modifier)
=> modifier.NormalizedStartTime;
/// <inheritdoc/>
public override void SetValue(ref TransitionModifierDefinition modifier, float value)
=> modifier = modifier.WithNormalizedStartTime(value);
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 80fa90c6d42e9064ca6c83b652444790
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,49 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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

View File

@@ -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:

View File

@@ -0,0 +1,598 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using Animancer.Units;
using Animancer.Units.Editor;
using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using static Animancer.Editor.TransitionDrawer;
using static Animancer.Editor.TransitionLibraries.TransitionLibrarySelection;
using Object = UnityEngine.Object;
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>The page displaying this table.</summary>
[NonSerialized] public TransitionLibraryModifiersPage Page;
/************************************************************************************************************************/
/// <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);
DoTableGUI(area, window.Items.Count, window.Items.Count);
}
/************************************************************************************************************************/
/// <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 transitions = _Window.Data.Transitions;
transitions.TryGet(selection.FromIndex, out var fromTransition);
transitions.TryGet(selection.ToIndex, out var toTransition);
var cell = new Vector2Int(
_Window.Items.IndexOf(toTransition),
_Window.Items.IndexOf(fromTransition));
if (cell.x < 0)
cell.x = int.MinValue;
if (cell.y < 0)
cell.y = int.MinValue;
return cell;
case SelectionType.Group:
var index = _Window.Items.IndexOf(selection.Selected);
return index >= 0
? new(index, index)
: new(int.MinValue, int.MinValue);
}
}
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
{
DoCellBodyGUI(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 deleteTransitionArea = toArea;
deleteTransitionArea.y -= deleteTransitionArea.height - Padding;
var createTransitionArea = deleteTransitionArea;
createTransitionArea.y -= createTransitionArea.height - Padding;
var createGroupArea = createTransitionArea;
createGroupArea.y -= createGroupArea.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;
DoCreateGroupButtonGUI(createGroupArea);
DoCreateTransitionButtonGUI(createTransitionArea);
DoRemoveButtonGUI(deleteTransitionArea);
}
/************************************************************************************************************************/
/// <summary>Draws a button to create a new group.</summary>
private void DoCreateGroupButtonGUI(Rect area)
{
if (GUI.Button(area, "Create Group"))
TransitionLibraryOperations.CreateGroup(_Window, _Window.EditorData);
}
/************************************************************************************************************************/
/// <summary>Draws a button to create a new transition.</summary>
private void DoCreateTransitionButtonGUI(Rect area)
{
if (GUI.Button(area, "Create Transition"))
TransitionLibraryOperations.CreateTransition(_Window);
}
/************************************************************************************************************************/
/// <summary>Draws a button to remove the selected object.</summary>
private void DoRemoveButtonGUI(Rect area)
{
var enabled = GUI.enabled;
var selection = _Window.Selection;
string label = "Remove Selection";
switch (selection.Type)
{
case SelectionType.FromTransition:
GUI.enabled = selection.FromIndex >= 0 && selection.FromIndex < _Window.Data.Transitions.Length;
label = "Remove Transition";
break;
case SelectionType.ToTransition:
GUI.enabled = selection.ToIndex >= 0 && selection.ToIndex < _Window.Data.Transitions.Length;
label = "Remove Transition";
break;
case SelectionType.Modifier:
label = "Remove Modifier";
break;
case SelectionType.Group:
label = "Remove Group";
break;
default:
GUI.enabled = false;
break;
}
using var content = PooledGUIContent.Acquire(label,
"Remove the selected object from this library. [Delete]");
if (GUI.Button(area, content))
{
TransitionLibraryOperations.HandleDelete(_Window);
Deselect();
}
GUI.enabled = enabled;
}
/************************************************************************************************************************/
/// <summary>Draws a row or column label.</summary>
private void DoLabelGUI(
Rect area,
int index,
GUIStyle style,
SelectionType selectionType)
{
if (!_Window.Items.TryGet(index, out var transitionOrGroup))
return;
if (transitionOrGroup is TransitionAssetBase transition)
{
var group = _Window.Items.GetGroup(index);
if (group != null)
StealGroupFoldoutSpace(ref area, style);
HandleTransitionLabelInput(
ref area,
_Window,
transition,
selectionType,
CalculateTarget);
GUI.Label(area, GetTransitionName(transition), style);
}
else if (transitionOrGroup is TransitionGroup group)
{
var foldoutArea = StealGroupFoldoutSpace(ref area, style);
HandleTransitionLabelInput(
ref area,
_Window,
group,
SelectionType.Group,
CalculateTarget);
GUI.Label(area, group.Name, style);
EditorGUI.BeginChangeCheck();
group.IsExpanded = EditorGUI.Foldout(foldoutArea, group.IsExpanded, GUIContent.none);
if (EditorGUI.EndChangeCheck())
_Window.Selection.Select(_Window, group, index, SelectionType.Group);
}
}
/************************************************************************************************************************/
/// <summary>Calculates the area for a group foldout and subtracts it from the appropriate side of the `area`.</summary>
private Rect StealGroupFoldoutSpace(ref Rect area, GUIStyle style)
{
if (style.alignment == TextAnchor.MiddleRight)
{
return StealFromRight(ref area, LineHeight, StandardSpacing);
}
else
{
var space = StealFromLeft(ref area, LineHeight, StandardSpacing);
space.x += StandardSpacing;
return space;
}
}
/************************************************************************************************************************/
/// <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,
object item,
SelectionType selectionType,
Func<Rect, int, Event, ListTargetCalculation> calculateTarget)
{
var control = new GUIControl(area, LabelHint);
switch (control.EventType)
{
case EventType.MouseDown:
if (control.Event.button == 0 &&
control.TryUseMouseDown())
{
if (control.Event.clickCount == 2)
{
if (item is Object unityObject)
EditorGUIUtility.PingObject(unityObject);
}
else
{
var index = IndexOf(window, item as TransitionAssetBase);
window.Selection.Select(window, item, index, selectionType);
}
_IsLabelDrag = false;
}
break;
case EventType.MouseUp:
if (control.TryUseMouseUp() && _IsLabelDrag)
{
var index = window.Items.IndexOf(item);
var target = calculateTarget(area, index, control.Event);
window.OnDropItem(item, target, 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;
}
}
/************************************************************************************************************************/
private static int IndexOf(TransitionLibraryWindow window, TransitionAssetBase transition)
=> Array.IndexOf(window.Data.Transitions, transition);
/************************************************************************************************************************/
/// <summary>Calculates the target index for a drag and drop operation.</summary>
private static ListTargetCalculation CalculateTarget(
Rect area,
int index,
Event currentEvent)
{
var target = new ListTargetCalculation(area.y, area.height, currentEvent.mousePosition.y);
target.Index += index;
return target;
}
/************************************************************************************************************************/
private static readonly int GroupHint = "Group".GetHashCode();
/// <summary>
/// Calls <see cref="DoModifierValueGUI"/> if the specified cell
/// contains a transition combination rather than a group.
/// </summary>
public void DoCellBodyGUI(
Rect area,
TransitionLibraryWindow window,
int from,
int to)
{
if (!_Window.Items.TryGet(from, out var fromTransitionOrGroup) ||
!_Window.Items.TryGet(to, out var toTransitionOrGroup))
return;
if (fromTransitionOrGroup is TransitionAssetBase fromTransition &&
toTransitionOrGroup is TransitionAssetBase toTransition)
{
from = IndexOf(window, fromTransition);
to = IndexOf(window, toTransition);
DoModifierValueGUI(area, _Window, Page, from, to, "", true);
}
else
{
var control = new GUIControl(area, GroupHint);
if (control.EventType == EventType.MouseDown &&
control.TryUseMouseDown())
{
var group = fromTransitionOrGroup is TransitionGroup
? fromTransitionOrGroup
: toTransitionOrGroup;
window.Selection.Select(
window,
group,
from,
SelectionType.Group);
}
}
}
/************************************************************************************************************************/
/// <summary>Draws the fade duration for a particular transition combination.</summary>
public static void DoModifierValueGUI(
Rect area,
TransitionLibraryWindow window,
TransitionLibraryModifiersPage page,
int from,
int to,
string label,
bool singleField)
{
var previousHotControl = GUIUtility.hotControl;
var hasModifier = window.Data.TryGetModifier(from, to, out var modifier);
var hasModifierWithValue = hasModifier;
var value = page.GetValue(modifier);
window.Data.Transitions.TryGet(to, out var transition);
if (float.IsNaN(value))
{
hasModifierWithValue = false;
if (transition != null)
value = page.GetValue(transition);
}
var labelStyle = EditorStyles.label.fontStyle;
var numberAlignment = EditorStyles.numberField.alignment;
var numberStyle = EditorStyles.numberField.fontStyle;
var numberSize = EditorStyles.numberField.fontSize;
try
{
EditorStyles.numberField.alignment = TextAnchor.MiddleLeft;
if (hasModifierWithValue)
{
EditorStyles.label.fontStyle = FontStyle.Bold;
EditorStyles.numberField.fontStyle = FontStyle.Bold;
}
else
{
EditorStyles.numberField.fontSize = EditorStyles.numberField.fontSize * 4 / 5;
}
EditorGUI.BeginChangeCheck();
page.ConfigureForSingleField(singleField, ref value);
if (singleField)
transition = null;
using (new DrawerContext(transition))
using (var content = PooledGUIContent.Acquire(label))
page.TimeDrawer.OnGUI(area, content, ref value);
if (TryUseClickEvent(area, 2))
value = float.NaN;
if (EditorGUI.EndChangeCheck())
{
if (EditorGUIUtility.editingTextField &&
!float.TryParse(CurrentFieldText, out value))
value = float.NaN;
if (!hasModifier)
modifier = modifier.WithDetails(float.NaN, float.NaN);
var data = window.RecordUndo();
page.SetValue(ref modifier, value);
data.SetModifier(modifier);
hasModifier = true;
RepaintEverything();
}
}
finally
{
EditorStyles.label.fontStyle = labelStyle;
EditorStyles.numberField.alignment = numberAlignment;
EditorStyles.numberField.fontStyle = numberStyle;
EditorStyles.numberField.fontSize = numberSize;
}
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

View File

@@ -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:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 61919166e1e7d634f95c77e80360ab63
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,313 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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 modifier for a particular from-to transition combination.</summary>
Modifier,
/// <summary>A <see cref="TransitionGroup"/>.</summary>
Group,
}
/************************************************************************************************************************/
[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>The <see cref="ITransition.NormalizedStartTime"/> of the current selection.</summary>
public float NormalizedStartTime { 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;
NormalizedStartTime = 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();
NormalizedStartTime = transition.TryGetNormalizedStartTime();
_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();
NormalizedStartTime = transition.TryGetNormalizedStartTime();
_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)
{
return false;
}
FadeDuration = modifier.FadeDuration;
NormalizedStartTime = modifier.NormalizedStartTime;
_Selected = modifier;
return true;
case SelectionType.Group:
name = "Transition Group";
return _Selected is TransitionGroup;
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;
}
case SelectionType.Group:
if (select is TransitionGroup group)
{
_FromIndex = index;
_ToIndex = index;
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

View File

@@ -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:

View File

@@ -0,0 +1,273 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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;
case TransitionLibrarySelection.SelectionType.Group:
DoGroupGUI(target, (TransitionGroup)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");
if (selection.Window.TryGetPage<TransitionLibraryFadeDurationsPage>(out var fadeDurations))
{
var area = AnimancerGUI.LayoutSingleLineRect();
TransitionModifierTableGUI.DoModifierValueGUI(
area,
selection.Window,
fadeDurations,
modifier.FromIndex,
modifier.ToIndex,
"Fade Duration",
false);
}
if (selection.Window.TryGetPage<TransitionLibraryStartTimesPage>(out var startTimes))
{
var area = AnimancerGUI.LayoutSingleLineRect();
TransitionModifierTableGUI.DoModifierValueGUI(
area,
selection.Window,
startTimes,
modifier.FromIndex,
modifier.ToIndex,
"Start Time",
false);
}
}
/************************************************************************************************************************/
/// <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
/************************************************************************************************************************/
#region Groups
/************************************************************************************************************************/
/// <summary>Draws the GUI for the `group`.</summary>
private void DoGroupGUI(
TransitionLibrarySelection selection,
TransitionGroup group)
{
group.Name = EditorGUILayout.TextField("Group Name", group.Name);
var enabled = GUI.enabled;
GUI.enabled = false;
EditorGUILayout.LabelField("Transition Count", group.TransitionIndices.Count.ToStringCached());
var transitions = selection.Window.Data.Transitions;
for (int i = 0; i < group.TransitionIndices.Count; i++)
{
var index = group.TransitionIndices[i];
if (!transitions.TryGetObject(index, out var transition))
continue;
EditorGUILayout.ObjectField(
$"Transition {i.ToStringCached()}",
transition,
typeof(TransitionAssetBase),
false);
}
GUI.enabled = enabled;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

@@ -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:

View File

@@ -0,0 +1,524 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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.NormalizedStartTime = _Target.NormalizedStartTime;
_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

View File

@@ -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:

View File

@@ -0,0 +1,53 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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

View File

@@ -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:

View File

@@ -0,0 +1,163 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryEditorDataInternal
public partial class TransitionLibraryEditorDataInternal
{
/************************************************************************************************************************/
[SerializeField]
private List<TransitionGroup> _TransitionGroups;
/// <summary>[<see cref="SerializeField"/>] The groups which transitions are organised in.</summary>
public ref List<TransitionGroup> TransitionGroups
{
get
{
_TransitionGroups ??= new();
return ref _TransitionGroups;
}
}
/************************************************************************************************************************/
}
/// <summary>[Editor-Only]
/// A group of transitions for display organisation in the <see cref="TransitionLibraryWindow"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionGroup
[Serializable]
public class TransitionGroup :
ICopyable<TransitionGroup>,
IEquatable<TransitionGroup>
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
private string _Name;
/// <summary>[<see cref="SerializeField"/>] The display name of this group.</summary>
public ref string Name
=> ref _Name;
/************************************************************************************************************************/
[SerializeField]
private int _Index;
/// <summary>[<see cref="SerializeField"/>]
/// The display index of this group within the <see cref="TransitionGroupCache.Items"/>.
/// </summary>
public ref int Index
=> ref _Index;
/************************************************************************************************************************/
[SerializeField]
private bool _IsExpanded = true;
/// <summary>[<see cref="SerializeField"/>] Is this group currently showing its contents?</summary>
public ref bool IsExpanded
=> ref _IsExpanded;
/************************************************************************************************************************/
[SerializeField]
private List<int> _TransitionIndices;
/// <summary>[<see cref="SerializeField"/>]
/// The indices of the transitions in the <see cref="Animancer.TransitionLibraries.TransitionLibraryDefinition"/>.
/// </summary>
public ref List<int> TransitionIndices
{
get
{
_TransitionIndices ??= new();
return ref _TransitionIndices;
}
}
/************************************************************************************************************************/
[NonSerialized]
private List<TransitionAssetBase> _Transitions;
/// <summary>The transitions referenced by <see cref="TransitionIndices"/>.</summary>
/// <remarks>This list is temporarily filled during GUI calls in the <see cref="TransitionLibraryWindow"/>.</remarks>
public List<TransitionAssetBase> Transitions
=> _Transitions ??= new();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Equality
/************************************************************************************************************************/
/// <summary>Are all fields in this object equal to the equivalent in `obj`?</summary>
public override bool Equals(object obj)
=> Equals(obj as TransitionGroup);
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
public bool Equals(TransitionGroup other)
=> other != null
&& _Name == other._Name
&& _Index == other._Index
&& _IsExpanded == other._IsExpanded
&& AnimancerUtilities.ContentsAreEqual(TransitionIndices, other.TransitionIndices);
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
public static bool operator ==(TransitionGroup a, TransitionGroup b)
=> a is null
? b is null
: a.Equals(b);
/// <summary>Are any fields in `a` not equal to the equivalent fields in `b`?</summary>
public static bool operator !=(TransitionGroup a, TransitionGroup b)
=> !(a == b);
/************************************************************************************************************************/
/// <summary>Returns a hash code based on the values of this object's fields.</summary>
public override int GetHashCode()
=> AnimancerUtilities.Hash(1598151553,
_Name.GetHashCode(),
_Index.GetHashCode(),
_IsExpanded.GetHashCode(),
TransitionIndices.GetHashCode());
/************************************************************************************************************************/
/// <inheritdoc/>
public void CopyFrom(TransitionGroup copyFrom, CloneContext context)
{
_Name = copyFrom._Name;
_Index = copyFrom._Index;
_IsExpanded = copyFrom._IsExpanded;
TransitionIndices.Clear();
TransitionIndices.AddRange(copyFrom.TransitionIndices);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override string ToString()
=> $"{nameof(TransitionGroup)}({_Name}, {_Index}, {TransitionIndices.Count})";
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89e6a854b9e635f4a87a5c33389f6720
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,225 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>A list of items in the <see cref="TransitionLibraryWindow"/> organised by group.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionGroupCache
public class TransitionGroupCache
{
/************************************************************************************************************************/
private static readonly List<TransitionAssetBase> Transitions = new();
private readonly List<object> Items = new();
private readonly List<TransitionGroup> ItemToGroup = new();
private readonly Dictionary<object, int> ItemToIndex = new();
/************************************************************************************************************************/
/// <summary>The total number of items in this cache.</summary>
public int Count
=> Items.Count;
/************************************************************************************************************************/
/// <summary>Returns the index of the specified `item` in this cache.</summary>
public int IndexOf(object item)
=> item != null && ItemToIndex.TryGetValue(item, out var index)
? index
: -1;
/************************************************************************************************************************/
/// <summary>Tries to get the item at the specified index.</summary>
public bool TryGet(int index, out object item)
=> Items.TryGet(index, out item);
/************************************************************************************************************************/
/// <summary>Returns the item at the specified index.</summary>
public object GetItem(int index)
=> Items[index];
/// <summary>Returns the group containing the item at the specified index.</summary>
public TransitionGroup GetGroup(int index)
=> ItemToGroup[index];
/************************************************************************************************************************/
/// <summary>
/// Converts the `index` to a value for the <see cref="TransitionGroup.Index"/>,
/// meaning it skips any items inside groups.
/// </summary>
public int ItemToGroupIndex(int index)
{
index = Mathf.Clamp(index, 0, Items.Count - 1);
for (int i = index; i >= 0; i--)
{
var group = ItemToGroup[i];
if (group != null && !ReferenceEquals(group, Items[i]))
index--;
}
return index;
}
/************************************************************************************************************************/
/// <summary>Gathers the items from the specified library.</summary>
public void GatherTransitionsAndGroups(
TransitionAssetBase[] transitions,
TransitionLibraryEditorDataInternal editorData)
=> GatherTransitionsAndGroups(transitions, editorData.TransitionGroups);
/// <summary>Gathers the items from the specified library.</summary>
public void GatherTransitionsAndGroups(
TransitionAssetBase[] transitions,
List<TransitionGroup> groups)
{
Items.Clear();
Transitions.Clear();
ItemToGroup.Clear();
ItemToIndex.Clear();
Transitions.AddRange(transitions);
SortGroups(groups);
GatherGroupedTransitions(groups);
GatherUnGroupedItems();
GatherGroupedItems(groups);
GatherItemIndices();
Transitions.Clear();
}
/************************************************************************************************************************/
/// <summary>
/// Sorts the `groups` by <see cref="TransitionGroup.Index"/>
/// and removes any nulls.
/// </summary>
public static void SortGroups(List<TransitionGroup> groups)
{
var previousGroupIndex = int.MinValue;
var outOfOrder = false;
for (int i = 0; i < groups.Count; i++)
{
var group = groups[i];
if (group == null)
{
groups.RemoveAt(i);
i--;
continue;
}
var groupIndex = group.Index;
if (groupIndex < previousGroupIndex)
{
outOfOrder = true;
}
else if (groupIndex == previousGroupIndex)
{
groupIndex++;
group.Index = groupIndex;
}
previousGroupIndex = groupIndex;
}
if (outOfOrder)
groups.Sort(static (a, b) => a.Index.CompareTo(b.Index));
}
/************************************************************************************************************************/
/// <summary>
/// Grabs items from the <see cref="Transitions"/>
/// to fill the <see cref="TransitionGroup.Transitions"/>.
/// </summary>
private void GatherGroupedTransitions(List<TransitionGroup> groups)
{
for (int iGroup = 0; iGroup < groups.Count; iGroup++)
{
var group = groups[iGroup];
group.Transitions.Clear();
for (int iTransition = 0; iTransition < group.TransitionIndices.Count; iTransition++)
{
var transitionIndex = group.TransitionIndices[iTransition];
if (!Transitions.TryGetObject(transitionIndex, out var transition))
continue;
Transitions[transitionIndex] = null;
group.Transitions.Add(transition);
}
}
}
/************************************************************************************************************************/
/// <summary>Copies un-grouped transitions over to the <see cref="Items"/>.</summary>
private void GatherUnGroupedItems()
{
for (int i = 0; i < Transitions.Count; i++)
{
var transition = Transitions[i];
if (transition == null)
continue;
Items.Add(transition);
ItemToGroup.Add(null);
}
}
/************************************************************************************************************************/
/// <summary>Copies groups and grouped transitions over to the <see cref="Items"/>.</summary>
private void GatherGroupedItems(List<TransitionGroup> groups)
{
var expandedItemOffset = 0;
for (int iGroup = 0; iGroup < groups.Count; iGroup++)
{
var group = groups[iGroup];
group.Index = Mathf.Clamp(group.Index, 0, Transitions.Count + iGroup + 1);
var index = group.Index + expandedItemOffset;
index = Mathf.Clamp(index, 0, Items.Count);
Items.Insert(index, group);
ItemToGroup.Insert(index, group);
if (!group.IsExpanded)
continue;
expandedItemOffset += group.Transitions.Count;
for (int iTransition = 0; iTransition < group.Transitions.Count; iTransition++)
{
index++;
Items.Insert(index, group.Transitions[iTransition]);
ItemToGroup.Insert(index, group);
}
}
}
/************************************************************************************************************************/
/// <summary>Assigns the <see cref="ItemToIndex"/> for each of the <see cref="Items"/>.</summary>
private void GatherItemIndices()
{
for (int i = 0; i < Items.Count; i++)
ItemToIndex[Items[i]] = i;
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc224f4f0f2cbf944ad6f9a6c37deebd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,191 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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);
DoDescriptionGUI(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 several labels describing the contents of the `library`.</summary>
private void DoDescriptionGUI(TransitionLibraryAsset library)
{
var definition = library.Definition;
EditorGUILayout.LabelField("Transitions", definition.Transitions.Length.ToString());
EditorGUILayout.LabelField("Modifiers", definition.Modifiers.Length.ToString());
EditorGUILayout.LabelField("Aliases", definition.Aliases.Length.ToString());
}
/************************************************************************************************************************/
/// <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="TransitionLibraryEditorDataAsset"/>.</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

View File

@@ -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:

View File

@@ -0,0 +1,199 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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/TransitionLibraryEditorDataAsset
[AnimancerHelpUrl(typeof(TransitionLibraryEditorDataAsset))]
public partial class TransitionLibraryEditorDataAsset : ScriptableObject
{
/************************************************************************************************************************/
/// <summary>Libraries mapped to their editor data.</summary>
/// <remarks>
/// Libraries can't have a direct reference to this class
/// because it's in the Editor assembly which the Runtime assembly doesn't reference.
/// </remarks>
private static readonly Dictionary<TransitionLibraryAsset, TransitionLibraryEditorDataAsset>
LibraryToEditorData = new();
/************************************************************************************************************************/
/// <summary>The name of the serialized backing field of <see cref="Library"/>.</summary>
internal const string LibraryFieldName = nameof(_Library);
[SerializeField]
private TransitionLibraryAsset _Library;
/// <summary>The library this data is associated with.</summary>
public TransitionLibraryAsset Library
=> _Library;
/************************************************************************************************************************/
/// <summary>The name of the serialized backing field of <see cref="Data"/>.</summary>
internal const string DataFieldName = nameof(_Data);
[SerializeField]
private TransitionLibraryEditorDataInternal _Data;
/// <summary>[<see cref="SerializeField"/>] The data contained in this asset.</summary>
public TransitionLibraryEditorDataInternal Data
{
get => _Data ??= new();
set
{
SetLibrary(this, _Library);
_Data = value;
EditorUtility.SetDirty(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>Sets the <see cref="Library"/>.</summary>
public static void SetLibrary(TransitionLibraryEditorDataAsset data, TransitionLibraryAsset library)
{
if (library != null)
LibraryToEditorData.Remove(library);
data._Library = library;
if (library != null)
LibraryToEditorData.Add(library, data);
}
/************************************************************************************************************************/
/// <summary>Tries to get the `data` associated with the `library`.</summary>
private static bool TryGet(
TransitionLibraryAsset library,
out TransitionLibraryEditorDataAsset asset)
{
if (!LibraryToEditorData.TryGetValue(library, out asset))
return false;
if (asset != null)
{
SetLibrary(asset, library);
return true;
}
LibraryToEditorData.Remove(library);
return false;
}
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="TransitionLibraryEditorDataInternal"/> sub-asset of the `library` if one exists.
/// </summary>
public static TransitionLibraryEditorDataAsset GetEditorData(TransitionLibraryAsset library)
{
if (TryGet(library, out var asset))
return asset;
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 TransitionLibraryEditorDataAsset editorData)
{
asset = editorData;
SetLibrary(asset, library);
return asset;
}
}
return null;
}
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="TransitionLibraryEditorDataAsset"/> sub-asset of the `library` if one exists.
/// Otherwise, creates and saves a new one.
/// </summary>
public static TransitionLibraryEditorDataAsset GetOrCreateEditorData(TransitionLibraryAsset library)
{
var data = library.GetEditorData();
if (data != null)
return data;
data = CreateInstance<TransitionLibraryEditorDataAsset>();
data.name = "Editor Data";
data.hideFlags = HideFlags.DontSaveInBuild | HideFlags.HideInHierarchy;
SetLibrary(data, 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="TransitionLibraryEditorDataAsset"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryEditorDataExtensions
public static class TransitionLibraryEditorDataExtensions
{
/************************************************************************************************************************/
/// <summary><see cref="TransitionLibraryEditorDataAsset.GetEditorData"/></summary>
public static TransitionLibraryEditorDataAsset GetEditorData(this TransitionLibraryAsset library)
=> TransitionLibraryEditorDataAsset.GetEditorData(library);
/// <summary><see cref="TransitionLibraryEditorDataAsset.GetOrCreateEditorData"/></summary>
public static TransitionLibraryEditorDataAsset GetOrCreateEditorData(this TransitionLibraryAsset library)
=> TransitionLibraryEditorDataAsset.GetOrCreateEditorData(library);
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f04d4a67566152e40aaa01b8b745ff6b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only] Custom Inspector for <see cref="TransitionLibraryEditorDataAsset"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryEditorDataEditor
[CustomEditor(typeof(TransitionLibraryEditorDataAsset), true)]
public class TransitionLibraryEditorDataEditor : UnityEditor.Editor
{
/************************************************************************************************************************/
private SerializedProperty _Library;
private SerializedProperty _Sort;
/************************************************************************************************************************/
/// <inheritdoc/>
protected virtual void OnEnable()
{
_Library = serializedObject.FindProperty(TransitionLibraryEditorDataAsset.LibraryFieldName);
var data = serializedObject.FindProperty(TransitionLibraryEditorDataAsset.DataFieldName);
_Sort = data.FindPropertyRelative(TransitionLibraryEditorDataInternal.TransitionSortModeFieldName);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void OnInspectorGUI()
{
var target = this.target as TransitionLibraryEditorDataAsset;
if (target == null)
return;
if (_Library != null)
{
var enabled = GUI.enabled;
if (_Library.objectReferenceValue != null)
GUI.enabled = false;
EditorGUILayout.PropertyField(_Library);
GUI.enabled = enabled;
}
if (_Sort != null)
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(_Sort);
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
TransitionLibrarySort.Sort(target.Library);
}
}
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -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:

View File

@@ -0,0 +1,79 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
using UnityEngine;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// Additional data for a <see cref="TransitionLibraryAsset"/> which is excluded from Runtime Builds.
/// </summary>
/// <remarks>
/// This class isn't called <c>TransitionLibraryEditorData</c> because
/// <see cref="TransitionLibraryEditorDataAsset"/> previously had that name
/// and changing from a <see cref="ScriptableObject"/> to a regular class with the same name
/// causes errors for any already existing assets of that type.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryEditorDataInternal
[Serializable]
public partial class TransitionLibraryEditorDataInternal :
ICopyable<TransitionLibraryEditorDataInternal>,
IEquatable<TransitionLibraryEditorDataInternal>
{
/************************************************************************************************************************/
#region Equality
/************************************************************************************************************************/
/// <summary>Are all fields in this object equal to the equivalent in `obj`?</summary>
public override bool Equals(object obj)
=> Equals(obj as TransitionLibraryEditorDataInternal);
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
public bool Equals(TransitionLibraryEditorDataInternal other)
=> other != null
&& _TransitionSortMode == other._TransitionSortMode
&& AnimancerUtilities.ContentsAreEqual(_TransitionGroups, other._TransitionGroups);
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
public static bool operator ==(TransitionLibraryEditorDataInternal a, TransitionLibraryEditorDataInternal b)
=> a is null
? b is null
: a.Equals(b);
/// <summary>Are any fields in `a` not equal to the equivalent fields in `b`?</summary>
public static bool operator !=(TransitionLibraryEditorDataInternal a, TransitionLibraryEditorDataInternal b)
=> !(a == b);
/************************************************************************************************************************/
/// <summary>Returns a hash code based on the values of this object's fields.</summary>
public override int GetHashCode()
=> AnimancerUtilities.Hash(287475157,
_TransitionSortMode.GetHashCode(),
_TransitionGroups.SafeGetHashCode());
/************************************************************************************************************************/
/// <inheritdoc/>
public void CopyFrom(TransitionLibraryEditorDataInternal copyFrom, CloneContext context)
{
_TransitionSortMode = copyFrom._TransitionSortMode;
var myGroups = TransitionGroups;
var copyGroups = copyFrom.TransitionGroups;
myGroups.Clear();
for (int i = 0; i < copyGroups.Count; i++)
myGroups.Add(copyGroups[i].CopyableClone(context));
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4bca901fbb36184486c4e519e7b82fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,296 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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 group in the <see cref="TransitionLibraryEditorDataInternal.TransitionGroups"/>.</summary>
public static TransitionGroup CreateGroup(
TransitionLibraryWindow window,
TransitionLibraryEditorDataInternal data)
{
window.Repaint();
var groups = data.TransitionGroups;
var group = new TransitionGroup
{
Name = $"Transition Group {groups.Count}",
};
groups.Add(group);
window.Selection.Select(window, group, groups.Count - 1, SelectionType.Group);
return group;
}
/************************************************************************************************************************/
/// <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 (currentEvent.type == EventType.ExecuteCommand)
HandleDelete(window);
currentEvent.Use();
}
/// <summary>Handles a delete event.</summary>
public static void HandleDelete(
TransitionLibraryWindow window)
{
if (!window.Selection.Validate())
return;
switch (window.Selection.Type)
{
case SelectionType.FromTransition:
if (window.Selection.Selected is TransitionAssetBase fromTransition)
AskHowToDeleteTransition(
fromTransition,
window.Selection.FromIndex,
window);
break;
case SelectionType.ToTransition:
if (window.Selection.Selected is TransitionAssetBase toTransition)
AskHowToDeleteTransition(
toTransition,
window.Selection.ToIndex,
window);
break;
case SelectionType.Modifier:
if (window.Selection.Selected is TransitionModifierDefinition modifier)
{
window.Selection.Deselect();
window.RecordUndo().RemoveModifier(modifier);
}
break;
case SelectionType.Group:
if (window.Selection.Selected is TransitionGroup group)
{
window.Selection.Deselect();
window.RecordUndo();
window.EditorData.TransitionGroups.Remove(group);
}
break;
}
}
/************************************************************************************************************************/
/// <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.Selection.Deselect();
window.RecordUndo().RemoveTransition(index);
break;
case 2:// Delete.
window.Selection.Deselect();
if (isMainAsset)
{
AssetDatabase.DeleteAsset(assetPath);
}
else
{
AnimancerEditorUtilities.DeleteSubAsset(transition);
}
window.Data.RemoveTransition(index);
Undo.ClearUndo(window);
break;
default:
return;
}
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -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:

View File

@@ -0,0 +1,175 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using Animancer.TransitionLibraries;
using System;
using UnityEngine;
using static Animancer.Editor.TransitionLibraries.TransitionLibrarySelection;
namespace Animancer.Editor.TransitionLibraries
{
/// <summary>[Editor-Only]
/// Operations for modifying the order of items in a <see cref="TransitionLibraryAsset"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.TransitionLibraries/TransitionLibraryOrdering
public static class TransitionLibraryOrdering
{
/************************************************************************************************************************/
/// <summary>Handles a drag and drop operation.</summary>
public static void OnDropItem(
this TransitionLibraryWindow window,
object item,
ListTargetCalculation target,
SelectionType selectionType)
{
window.RecordUndo();
window.EditorData.TransitionSortMode = TransitionSortMode.Custom;
if (item is TransitionAssetBase transition)
OnDropTransition(window, transition, target, selectionType);
else if (item is TransitionGroup group)
OnDropGroup(window, group, target);
else
Debug.LogWarning($"Unhandled item type: {item}");
}
/************************************************************************************************************************/
/// <summary>Handles a drag and drop operation for a `transition`.</summary>
private static void OnDropTransition(
TransitionLibraryWindow window,
TransitionAssetBase transition,
ListTargetCalculation target,
SelectionType selectionType)
{
var transitions = window.Data.Transitions;
var fromTransitionIndex = Array.IndexOf(transitions, transition);
var fromItemIndex = window.Items.IndexOf(transition);
var fromGroup = window.Items.GetGroup(fromItemIndex);
var fromIndexWithinGroup = int.MaxValue;
if (fromGroup != null)
{
fromIndexWithinGroup = fromGroup.TransitionIndices.IndexOf(fromTransitionIndex);
if (fromIndexWithinGroup >= 0)
fromGroup.TransitionIndices.RemoveAt(fromIndexWithinGroup);
}
var toGroup = window.Items.TryGet(target.Index, out var targetItem)
? window.Items.GetGroup(target.Index)
: null;
// If dropping onto the top half of a group, drop outside that group.
if (target.LocalOffset < 0.5f && ReferenceEquals(toGroup, targetItem))
{
toGroup = null;
if (fromItemIndex < target.Index)
target.Index--;
}
// Drop onto group or a transition in a group.
if (toGroup != null)
{
var groupIndex = window.Items.IndexOf(toGroup);
var indexWithinGroup = target.Index - groupIndex;
// If dropping into the top half of an item, insert above that item instead of below.
if (target.LocalOffset < 0.5f)
indexWithinGroup--;
// If this item was just removed from earlier in the same list, adjust the new index.
if (fromGroup == toGroup && fromIndexWithinGroup < indexWithinGroup)
indexWithinGroup--;
indexWithinGroup = Mathf.Clamp(indexWithinGroup, 0, toGroup.TransitionIndices.Count);
toGroup.TransitionIndices.Insert(indexWithinGroup, fromTransitionIndex);
}
else// Drop onto a transition with no group.
{
var toTransitionIndex = Array.IndexOf(transitions, targetItem);
if (toTransitionIndex >= 0)
{
// If dropping into the top half of an item, insert above that item instead of below.
if (target.LocalOffset >= 0.5f)
toTransitionIndex++;
// If this item was just removed from earlier in the transition list, adjust the new index.
if (fromTransitionIndex < toTransitionIndex)
toTransitionIndex--;
}
else if (target.Index < 0)// Above everything.
{
toTransitionIndex = 0;
}
else// Below everything.
{
toTransitionIndex = transitions.Length;
}
AdjustGroupIndices(window, fromItemIndex, target.Index);
TransitionLibrarySort.MoveTransition(window, fromTransitionIndex, toTransitionIndex);
}
window.Selection.Select(window, targetItem, fromTransitionIndex, selectionType);
}
/************************************************************************************************************************/
/// <summary>Handles a drag and drop operation for a `group`.</summary>
private static void OnDropGroup(
TransitionLibraryWindow window,
TransitionGroup group,
ListTargetCalculation target)
{
var fromItemIndex = window.Items.IndexOf(group);
if (target.LocalOffset > 0.5f)
target.Index++;
var previousIndex = group.Index;
AdjustGroupIndices(window, fromItemIndex, target.Index);
if (target.Index > fromItemIndex)
target.Index += group.Index - previousIndex;
group.Index = window.Items.ItemToGroupIndex(target.Index);
TransitionGroupCache.SortGroups(window.EditorData.TransitionGroups);
window.Selection.Select(window, group, target.Index, SelectionType.Group);
}
/************************************************************************************************************************/
/// <summary>Adjusts the <see cref="TransitionGroup.Index"/> for any groups an item is moved over.</summary>
private static void AdjustGroupIndices(
TransitionLibraryWindow window,
int movedFromItemIndex,
int movedToItemIndex)
{
var direction = Math.Sign(movedToItemIndex - movedFromItemIndex);
movedFromItemIndex = Mathf.Clamp(movedFromItemIndex, 0, window.Items.Count - 1);
movedToItemIndex = Mathf.Clamp(movedToItemIndex, 0, window.Items.Count - 1);
while (true)
{
if (window.Items.GetItem(movedFromItemIndex) is TransitionGroup group)
group.Index -= direction;
if (movedFromItemIndex == movedToItemIndex)
break;
movedFromItemIndex += direction;
}
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: bd5c4687712e975429ffa99477f82996
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,423 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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 static class TransitionLibrarySort
{
/************************************************************************************************************************/
#region Sort Modes
/************************************************************************************************************************/
/// <summary>Applies the <see cref="TransitionLibraryEditorDataInternal.TransitionSortMode"/>.</summary>
public static void Sort(
TransitionLibraryAsset asset,
TransitionLibraryDefinition definition)
{
if (asset == null)
return;
// Can't have editor data if not an asset, so the sort mode will be custom anyway.
if (!AssetDatabase.Contains(asset))
return;
var editorData = asset.GetOrCreateEditorData();
Sort(definition, editorData.Data);
}
/// <summary>Applies the <see cref="TransitionLibraryEditorDataInternal.TransitionSortMode"/>.</summary>
public static void Sort(TransitionLibraryAsset asset)
=> Sort(asset, asset.Definition);
/************************************************************************************************************************/
/// <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,
TransitionLibraryEditorDataInternal editorData)
{
var mode = editorData.TransitionSortMode;
if (mode == TransitionSortMode.Custom)
return;
NameCache.Clear();
switch (mode)
{
case TransitionSortMode.Name:
Sort(library, editorData, Static<CompareName>.Instance);
break;
case TransitionSortMode.Path:
Sort(library, editorData, Static<ComparePath>.Instance);
break;
case TransitionSortMode.TypeThenName:
Sort(library, editorData, Static<CompareTypeThenName>.Instance);
break;
case TransitionSortMode.TypeThenPath:
Sort(library, editorData, Static<CompareTypeThenPath>.Instance);
break;
}
}
/************************************************************************************************************************/
/// <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,
TransitionLibraryEditorDataInternal editorData,
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, editorData, _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,
TransitionLibraryEditorDataInternal editorData,
TransitionAssetBase[] newTransitions,
int[] oldIndexToNew,
int count)
{
var libraryTransitions = library.Transitions;
if (libraryTransitions != newTransitions)
{
AnimancerUtilities.SetLength(ref libraryTransitions, count);
Array.Copy(newTransitions, 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();
var groups = editorData.TransitionGroups;
for (int iGroup = 0; iGroup < groups.Count; iGroup++)
{
var group = groups[iGroup];
var transitionIndices = group.TransitionIndices;
for (int iTransition = transitionIndices.Count - 1; iTransition >= 0; iTransition--)
{
var index = transitionIndices[iTransition];
var isValid = true;
index = ConvertIndex(index, oldIndexToNew, count, ref isValid);
if (isValid)
transitionIndices[iTransition] = index;
else
transitionIndices.RemoveAt(iTransition);
}
}
}
/************************************************************************************************************************/
/// <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 definition = window.RecordUndo();
var editorData = window.EditorData;
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,
editorData,
transitions,
indices,
transitions.Length);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

@@ -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:

View File

@@ -0,0 +1,434 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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)
{
#if UNITY_6000_3_OR_NEWER
var asset = EditorUtility.EntityIdToObject(instanceID);
#else
var asset = EditorUtility.InstanceIDToObject(instanceID);
#endif
if (asset is not TransitionLibraryAsset library)
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;
}
/************************************************************************************************************************/
/// <summary>The <see cref="TransitionLibraryEditorDataInternal"/> of the source asset.</summary>
public TransitionLibraryEditorDataInternal SourceEditorData
{
get => SourceObject.GetOrCreateEditorData().Data;
set => SourceObject.GetOrCreateEditorData().Data = value;
}
[SerializeField]
private TransitionLibraryEditorDataInternal _EditorData;
/// <summary>A copy of the <see cref="SourceEditorData"/> being managed by this window.</summary>
public ref TransitionLibraryEditorDataInternal EditorData
=> ref _EditorData;
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool HasDataChanged
{
get
{
if (base.HasDataChanged)
return true;
if (_EditorData == null)
return false;
var sourceEditorData = SourceEditorData;
return sourceEditorData != null && !_EditorData.Equals(sourceEditorData);
}
}
/************************************************************************************************************************/
[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>Tries to find a page of the specified type and returns true if successful.</summary>
public bool TryGetPage<T>(out T page)
where T : TransitionLibraryWindowPage
{
for (int i = 0; i < _Pages.Count; i++)
{
page = _Pages[i] as T;
if (page != null)
return true;
}
page = null;
return false;
}
/************************************************************************************************************************/
/// <summary>Object highlight manager.</summary>
public readonly TransitionLibraryWindowHighlighter
Highlighter = new();
/// <summary>Transitions and groups ordered by group.</summary>
public readonly TransitionGroupCache
Items = 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;
}
Items.GatherTransitionsAndGroups(Data.Transitions, EditorData);
DoHeaderGUI();
DoBodyGUI();
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void CaptureData()
{
_EditorData = SourceEditorData?.CopyableClone() ?? new();
AnimancerReflection.TryInvoke(_EditorData, "OnValidate");
base.CaptureData();
Data.SortAliases();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Apply()
{
var editorData = SourceObject.GetOrCreateEditorData();
using (new ModifySerializedField(editorData, name, false))
{
editorData.Data = _EditorData.CopyableClone();
}
TransitionLibrarySort.Sort(Data, editorData.Data);
base.Apply();
for (int i = 0; i < Data.Transitions.Length; i++)
{
var transition = Data.Transitions[i];
if (transition == null ||
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;
Deselect();
});
}
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

View File

@@ -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:

View File

@@ -0,0 +1,83 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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

View File

@@ -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:

View File

@@ -0,0 +1,58 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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/TransitionLibraryEditorDataInternal
public partial class TransitionLibraryEditorDataInternal
{
/************************************************************************************************************************/
/// <summary>The name of the serialized backing field of <see cref="TransitionSortMode"/>.</summary>
internal const string TransitionSortModeFieldName = nameof(_TransitionSortMode);
[SerializeField]
private TransitionSortMode _TransitionSortMode;
/// <summary>[<see cref="SerializeField"/>] The algorithm to use for sorting transitions.</summary>
public TransitionSortMode TransitionSortMode
{
get => _TransitionSortMode;
set => _TransitionSortMode = value;
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9387a41aa99644146aed014d11ba8bc5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: