chore: initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e1eb37f0147d7d4a8ddf11a7d7b04ee
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d5be687cb9b8da4e9c61bfb49ab861d
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de81cf7262eb1514894f722e6050eead
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61210733873388e4e815c6628988b306
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78f4b212f3ad9cd4285498110975c389
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61919166e1e7d634f95c77e80360ab63
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d5cccea07064a74a91890855fd2aaa4
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cfcfbc7bdddf4d4fabcf7aa15635df6
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7de29fe26fe25b540987aef2f28c1ee0
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29d5d1e026917374788c9f8258fc1fca
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89e6a854b9e635f4a87a5c33389f6720
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc224f4f0f2cbf944ad6f9a6c37deebd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e85015084cb6e947947199df796b532
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f04d4a67566152e40aaa01b8b745ff6b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e70b9fb99ed77049978e82246db2d3d
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4bca901fbb36184486c4e519e7b82fb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 981e396a9df2f7741b95b02098bb3838
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1193a923ec8c6f847a7e7b6dfca91c19
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ce82e4a867719941a5e8f1fba513d28
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edd6e6ad3fd0dd14cadf6ffeeefacf73
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9387a41aa99644146aed014d11ba8bc5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user