387 lines
15 KiB
C#
387 lines
15 KiB
C#
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
|
|
|
#if UNITY_EDITOR && UNITY_IMGUI
|
|
|
|
using System;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Animancer.Editor.Previews
|
|
{
|
|
/// <summary>[Editor-Only]
|
|
/// An <see cref="EditorWindow"/> which allows the user to preview animation transitions separately from the rest
|
|
/// of the scene in Edit Mode or Play Mode.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <strong>Documentation:</strong>
|
|
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions#previews">
|
|
/// Previews</see>
|
|
/// </remarks>
|
|
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/TransitionPreviewWindow
|
|
///
|
|
[HelpURL(Strings.DocsURLs.TransitionPreviews)]
|
|
[EditorWindowTitle]// Prevent the base SceneView from trying to use this type name to find the icon.
|
|
public partial class TransitionPreviewWindow : SceneView
|
|
{
|
|
/************************************************************************************************************************/
|
|
#region Public API
|
|
/************************************************************************************************************************/
|
|
|
|
private static Texture _Icon;
|
|
|
|
/// <summary>The icon image used by this window.</summary>
|
|
public static Texture Icon
|
|
{
|
|
get
|
|
{
|
|
if (_Icon == null)
|
|
{
|
|
// Possible icons: "UnityEditor.LookDevView", "SoftlockInline", "ViewToolOrbit", "ClothInspector.ViewValue".
|
|
var name = EditorGUIUtility.isProSkin ? "ViewToolOrbit On" : "ViewToolOrbit";
|
|
|
|
_Icon = AnimancerIcons.Load(name);
|
|
if (_Icon == null)
|
|
_Icon = EditorGUIUtility.whiteTexture;
|
|
}
|
|
|
|
return _Icon;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Focusses the <see cref="TransitionPreviewWindow"/> or creates one if none exists.
|
|
/// Or closes the existing window if it was already previewing the `transitionProperty`.
|
|
/// </summary>
|
|
public static void OpenOrClose(SerializedProperty transitionProperty)
|
|
{
|
|
transitionProperty = transitionProperty.Copy();
|
|
|
|
EditorApplication.delayCall += () =>
|
|
{
|
|
if (!IsPreviewing(transitionProperty))
|
|
{
|
|
// To avoid Unity giving a warning about camera rotation in 2D Mode:
|
|
// Set all scene views to not 2D mode and store their previous state.
|
|
var sceneViews = SceneView.sceneViews.ToArray();
|
|
var was2D = new bool[sceneViews.Length];
|
|
for (int i = 0; i < sceneViews.Length; i++)
|
|
{
|
|
var sceneView = (SceneView)sceneViews[i];
|
|
was2D[i] = sceneView.in2DMode;
|
|
sceneView.in2DMode = false;
|
|
}
|
|
|
|
GetWindow<TransitionPreviewWindow>(typeof(SceneView))
|
|
.SetTargetProperty(transitionProperty);
|
|
|
|
// Then after opening the window immediately return each scene view back to its previous state.
|
|
for (int i = 0; i < sceneViews.Length; i++)
|
|
{
|
|
var sceneView = (SceneView)sceneViews[i];
|
|
sceneView.in2DMode = was2D[i];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_Instance.Close();
|
|
}
|
|
};
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// The <see cref="AnimancerState.NormalizedTime"/> of the current transition. Can only be set if the property
|
|
/// being previewed matches the current <see cref="TransitionDrawer.Context"/>.
|
|
/// </summary>
|
|
public static float PreviewNormalizedTime
|
|
{
|
|
get => _Instance._Animations.NormalizedTime;
|
|
set
|
|
{
|
|
if (value.IsFinite() &&
|
|
IsPreviewingCurrentProperty())
|
|
_Instance._Animations.NormalizedTime = value;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Returns the <see cref="AnimancerState"/> of the current transition if the property being previewed matches
|
|
/// the <see cref="TransitionDrawer.Context"/>. Otherwise returns null.
|
|
/// </summary>
|
|
public static AnimancerState GetCurrentState()
|
|
{
|
|
if (!IsPreviewingCurrentProperty())
|
|
return null;
|
|
|
|
var previewObject = _Instance._Scene.PreviewObject;
|
|
if (previewObject == null || previewObject.Graph == null)
|
|
return null;
|
|
|
|
previewObject.Graph.States.TryGet(Transition, out var state);
|
|
return state;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Is the current <see cref="TransitionDrawer.DrawerContext.Property"/> being previewed at the moment?
|
|
/// </summary>
|
|
public static bool IsPreviewingCurrentProperty()
|
|
=> IsPreviewing(TransitionDrawer.Context.Property);
|
|
|
|
/// <summary>Is the `property` being previewed at the moment?</summary>
|
|
public static bool IsPreviewing(SerializedProperty property)
|
|
=> property != null
|
|
&& _Instance != null
|
|
&& _Instance._TransitionProperty.IsValid()
|
|
&& Serialization.AreSameProperty(property, _Instance._TransitionProperty);
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Messages
|
|
/************************************************************************************************************************/
|
|
|
|
private static TransitionPreviewWindow _Instance;
|
|
|
|
[SerializeField] private Object[] _PreviousSelection;
|
|
[SerializeField] private Animations _Animations;
|
|
[SerializeField] private Scene _Scene;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
public override void OnEnable()
|
|
{
|
|
_Instance = this;
|
|
|
|
base.OnEnable();
|
|
|
|
name = "Transition Preview Window";
|
|
titleContent = new("Transition Preview", Icon);
|
|
autoRepaintOnSceneChange = true;
|
|
sceneViewState.showSkybox = TransitionPreviewSettings.ShowSkybox;
|
|
sceneLighting = TransitionPreviewSettings.SceneLighting;
|
|
|
|
_Scene ??= new();
|
|
_Animations ??= new();
|
|
|
|
if (_TransitionProperty.IsValid() &&
|
|
!CanBePreviewed(_TransitionProperty))
|
|
{
|
|
DestroyTransitionProperty();
|
|
}
|
|
|
|
_Scene.OnEnable();
|
|
|
|
Selection.selectionChanged += OnSelectionChanged;
|
|
AssemblyReloadEvents.beforeAssemblyReload += DeselectPreviewSceneObjects;
|
|
|
|
// Re-select next frame.
|
|
// This fixes an issue where the Inspector header displays differently after a domain reload.
|
|
if (Selection.activeObject == this)
|
|
{
|
|
Selection.activeObject = null;
|
|
EditorApplication.delayCall += () => Selection.activeObject = this;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
public override void OnDisable()
|
|
{
|
|
base.OnDisable();
|
|
_Scene.OnDisable();
|
|
_Instance = null;
|
|
Selection.selectionChanged -= OnSelectionChanged;
|
|
AssemblyReloadEvents.beforeAssemblyReload -= DeselectPreviewSceneObjects;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Cleans up this window.</summary>
|
|
protected virtual new void OnDestroy()
|
|
{
|
|
base.OnDestroy();
|
|
_Scene.OnDestroy();
|
|
DestroyTransitionProperty();
|
|
|
|
using (ListPool<Object>.Instance.Acquire(out var objects))
|
|
{
|
|
for (int i = 0; i < _PreviousSelection.Length; i++)
|
|
{
|
|
var obj = _PreviousSelection[i];
|
|
if (obj != null)
|
|
objects.Add(obj);
|
|
}
|
|
Selection.objects = objects.ToArray();
|
|
}
|
|
|
|
_TransitionProperty = null;
|
|
|
|
AnimancerGUI.RepaintEverything();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
protected override void OnSceneGUI()
|
|
{
|
|
_Instance = this;
|
|
|
|
base.OnSceneGUI();
|
|
|
|
_Scene.OnGUI();
|
|
|
|
TransitionPreviewSettings.ShowSkybox = sceneViewState.showSkybox;
|
|
TransitionPreviewSettings.SceneLighting = sceneLighting;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Called multiple times per second while this window is visible.</summary>
|
|
private void Update()
|
|
{
|
|
if (Selection.activeObject == null)
|
|
Selection.activeObject = this;
|
|
|
|
if (TransitionPreviewSettings.AutoClose &&
|
|
!_TransitionProperty.IsValid())
|
|
{
|
|
Close();
|
|
return;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns false.</summary>
|
|
/// <remarks>Returning true makes it draw the main scene instead of the custom scene in Unity 2020.</remarks>
|
|
protected override bool SupportsStageHandling() => false;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private void OnSelectionChanged()
|
|
{
|
|
if (Selection.activeObject == null)
|
|
EditorApplication.delayCall += () => Selection.activeObject = this;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private void DeselectPreviewSceneObjects()
|
|
{
|
|
using (ListPool<Object>.Instance.Acquire(out var objects))
|
|
{
|
|
var selection = Selection.objects;
|
|
for (int i = 0; i < selection.Length; i++)
|
|
{
|
|
var obj = selection[i];
|
|
if (!_Scene.IsSceneObject(obj))
|
|
objects.Add(obj);
|
|
}
|
|
Selection.objects = objects.ToArray();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Transition Property
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField]
|
|
private Serialization.PropertyReference _TransitionProperty;
|
|
|
|
/// <summary>The <see cref="SerializedProperty"/> currently being previewed.</summary>
|
|
public static SerializedProperty TransitionProperty => _Instance._TransitionProperty;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The <see cref="ITransitionDetailed"/> currently being previewed.</summary>
|
|
public static ITransitionDetailed Transition
|
|
{
|
|
get
|
|
{
|
|
var property = _Instance._TransitionProperty;
|
|
if (!property.IsValid())
|
|
return null;
|
|
|
|
return property.Property.GetValue<ITransitionDetailed>();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Indicates whether the `property` is able to be previewed by this system.</summary>
|
|
public static bool CanBePreviewed(SerializedProperty property)
|
|
{
|
|
var accessor = property.GetAccessor();
|
|
if (accessor == null)
|
|
return false;
|
|
|
|
var type = accessor.GetFieldElementType(property);
|
|
if (typeof(ITransitionDetailed).IsAssignableFrom(type))
|
|
return true;
|
|
|
|
var value = accessor.GetValue(property);
|
|
return
|
|
value != null &&
|
|
typeof(ITransitionDetailed).IsAssignableFrom(value.GetType());
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private void SetTargetProperty(SerializedProperty property)
|
|
{
|
|
if (property.serializedObject.targetObjects.Length != 1)
|
|
{
|
|
Close();
|
|
throw new ArgumentException($"{nameof(TransitionPreviewWindow)} does not support multi-object selection.");
|
|
}
|
|
|
|
if (!CanBePreviewed(property))
|
|
{
|
|
Close();
|
|
throw new ArgumentException($"The specified property does not implement {nameof(ITransitionDetailed)}.");
|
|
}
|
|
|
|
if (!_TransitionProperty.IsValid())
|
|
_PreviousSelection = Selection.objects;
|
|
Selection.activeObject = this;
|
|
|
|
DestroyTransitionProperty();
|
|
|
|
_TransitionProperty = property;
|
|
_Scene.OnTargetPropertyChanged();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private void DestroyTransitionProperty()
|
|
{
|
|
if (_TransitionProperty == null)
|
|
return;
|
|
|
|
_Scene.PreviewObject.DestroyInstanceObject();
|
|
|
|
_TransitionProperty.Dispose();
|
|
_TransitionProperty = null;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|