// 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 { /// [Editor-Only] /// An which allows the user to preview animation transitions separately from the rest /// of the scene in Edit Mode or Play Mode. /// /// /// Documentation: /// /// Previews /// /// 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; /// The icon image used by this window. 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; } } /************************************************************************************************************************/ /// /// Focusses the or creates one if none exists. /// Or closes the existing window if it was already previewing the `transitionProperty`. /// 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(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(); } }; } /************************************************************************************************************************/ /// /// The of the current transition. Can only be set if the property /// being previewed matches the current . /// public static float PreviewNormalizedTime { get => _Instance._Animations.NormalizedTime; set { if (value.IsFinite() && IsPreviewingCurrentProperty()) _Instance._Animations.NormalizedTime = value; } } /************************************************************************************************************************/ /// /// Returns the of the current transition if the property being previewed matches /// the . Otherwise returns null. /// 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; } /************************************************************************************************************************/ /// /// Is the current being previewed at the moment? /// public static bool IsPreviewingCurrentProperty() => IsPreviewing(TransitionDrawer.Context.Property); /// Is the `property` being previewed at the moment? 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; /************************************************************************************************************************/ /// 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; } } /************************************************************************************************************************/ /// public override void OnDisable() { base.OnDisable(); _Scene.OnDisable(); _Instance = null; Selection.selectionChanged -= OnSelectionChanged; AssemblyReloadEvents.beforeAssemblyReload -= DeselectPreviewSceneObjects; } /************************************************************************************************************************/ /// Cleans up this window. protected virtual new void OnDestroy() { base.OnDestroy(); _Scene.OnDestroy(); DestroyTransitionProperty(); using (ListPool.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(); } /************************************************************************************************************************/ /// protected override void OnSceneGUI() { _Instance = this; base.OnSceneGUI(); _Scene.OnGUI(); TransitionPreviewSettings.ShowSkybox = sceneViewState.showSkybox; TransitionPreviewSettings.SceneLighting = sceneLighting; } /************************************************************************************************************************/ /// Called multiple times per second while this window is visible. private void Update() { if (Selection.activeObject == null) Selection.activeObject = this; if (TransitionPreviewSettings.AutoClose && !_TransitionProperty.IsValid()) { Close(); return; } } /************************************************************************************************************************/ /// Returns false. /// Returning true makes it draw the main scene instead of the custom scene in Unity 2020. protected override bool SupportsStageHandling() => false; /************************************************************************************************************************/ private void OnSelectionChanged() { if (Selection.activeObject == null) EditorApplication.delayCall += () => Selection.activeObject = this; } /************************************************************************************************************************/ private void DeselectPreviewSceneObjects() { using (ListPool.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; /// The currently being previewed. public static SerializedProperty TransitionProperty => _Instance._TransitionProperty; /************************************************************************************************************************/ /// The currently being previewed. public static ITransitionDetailed Transition { get { var property = _Instance._TransitionProperty; if (!property.IsValid()) return null; return property.Property.GetValue(); } } /************************************************************************************************************************/ /// Indicates whether the `property` is able to be previewed by this system. 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