chore: initial commit

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

View File

@@ -0,0 +1,98 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
#pragma warning disable CS0618 // Type or member is obsolete.
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.Previews
{
/// <summary>[Editor-Only] A minimal <see cref="ITransition"/> to preview an <see cref="AnimationClip"/>.</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/AnimationClipPreview
///
[AnimancerHelpUrl(typeof(AnimationClipPreview))]
internal class AnimationClipPreview : ScriptableObject
{
/************************************************************************************************************************/
[SerializeField]
private Transition _Transition;
/************************************************************************************************************************/
[Serializable]
[Obsolete("Only intended for internal use.")]// Prevent this type from showing up in [SerializeReference] fields.
private class Transition : ITransition, IAnimationClipCollection
{
/************************************************************************************************************************/
[SerializeField]
private AnimationClip _Clip;
public ref AnimationClip Clip => ref _Clip;
/************************************************************************************************************************/
public object Key => _Clip;
public float FadeDuration => 0;
public FadeMode FadeMode => default;
public AnimancerState CreateState() => new ClipState(_Clip);
public void Apply(AnimancerState state) { }
public bool IsValid => _Clip != null;
public bool IsLooping => _Clip.isLooping;
public float NormalizedStartTime { get => float.NaN; set => throw new NotSupportedException(); }
public float MaximumLength => _Clip.length;
public float Speed { get => 1; set => throw new NotSupportedException(); }
AnimancerEvent.Sequence IHasEvents.Events => null;
AnimancerEvent.Sequence.Serializable IHasEvents.SerializedEvents
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
/************************************************************************************************************************/
public void GatherAnimationClips(ICollection<AnimationClip> clips) => clips.Add(_Clip);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
[MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Preview")]
private static void Preview(MenuCommand command)
{
var preview = FindObjectOfType<AnimationClipPreview>();
if (preview == null)
{
preview = CreateInstance<AnimationClipPreview>();
preview.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSave;
}
preview._Transition = new Transition
{
Clip = (AnimationClip)command.context
};
var serializedObject = new SerializedObject(preview);
var property = serializedObject.FindProperty(nameof(_Transition));
TransitionPreviewWindow.OpenOrClose(property);
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,526 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using Animancer.Units;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.Previews
{
/// <summary>Persistent settings for the <see cref="TransitionPreviewWindow"/>.</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/TransitionPreviewSettings
[Serializable, InternalSerializableType]
public class TransitionPreviewSettings : AnimancerSettingsGroup
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override string DisplayName
=> "Transition Previews";
/// <inheritdoc/>
public override int Index
=> 3;
/************************************************************************************************************************/
private static TransitionPreviewSettings Instance
=> AnimancerSettingsGroup<TransitionPreviewSettings>.Instance;
/************************************************************************************************************************/
/// <summary>Draws the Inspector GUI for these settings.</summary>
public static void DoInspectorGUI()
{
AnimancerSettings.SerializedObject.Update();
EditorGUI.indentLevel++;
DoMiscGUI();
DoEnvironmentGUI();
DoModelsGUI();
DoHierarchyGUI();
EditorGUI.indentLevel--;
AnimancerSettings.SerializedObject.ApplyModifiedProperties();
}
/************************************************************************************************************************/
#region Misc
/************************************************************************************************************************/
private static void DoMiscGUI()
{
Instance.DoPropertyField(nameof(_AutoClose));
}
/************************************************************************************************************************/
[SerializeField]
[Tooltip("Should this window automatically close if the target object is destroyed?")]
private bool _AutoClose = true;
/// <summary>Should this window automatically close if the target object is destroyed?</summary>
public static bool AutoClose
=> Instance._AutoClose;
/************************************************************************************************************************/
[SerializeField]
[Tooltip("Should the scene lighting be enabled?")]
private bool _SceneLighting = false;
/// <summary>Should the scene lighting be enabled?</summary>
public static bool SceneLighting
{
get => Instance._SceneLighting;
set
{
if (SceneLighting == value)
return;
var property = Instance.GetSerializedProperty(nameof(_SceneLighting));
property.boolValue = value;
property.serializedObject.ApplyModifiedProperties();
}
}
/************************************************************************************************************************/
[SerializeField]
[Tooltip("Should the skybox be visible?")]
private bool _ShowSkybox = false;
/// <summary>Should the skybox be visible?</summary>
public static bool ShowSkybox
{
get => Instance._ShowSkybox;
set
{
if (ShowSkybox == value)
return;
var property = Instance.GetSerializedProperty(nameof(_ShowSkybox));
property.boolValue = value;
property.serializedObject.ApplyModifiedProperties();
}
}
/************************************************************************************************************************/
[SerializeField]
[Seconds(Rule = Validate.Value.IsNotNegative)]
[DefaultValue(0.02f)]
[Tooltip("The amount of time that will be added by a single frame step")]
private float _FrameStep = 0.02f;
/// <summary>The amount of time that will be added by a single frame step (in seconds).</summary>
public static float FrameStep
=> AnimancerSettingsGroup<TransitionPreviewSettings>.Instance._FrameStep;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Environment
/************************************************************************************************************************/
[SerializeField]
[Tooltip("If set, the default preview scene lighting will be replaced with this prefab.")]
private GameObject _SceneEnvironment;
/// <summary>If set, the default preview scene lighting will be replaced with this prefab.</summary>
public static GameObject SceneEnvironment
=> Instance._SceneEnvironment;
/************************************************************************************************************************/
private static void DoEnvironmentGUI()
{
EditorGUI.BeginChangeCheck();
var property = Instance.DoPropertyField(nameof(_SceneEnvironment));
if (EditorGUI.EndChangeCheck())
{
property.serializedObject.ApplyModifiedProperties();
TransitionPreviewWindow.InstanceScene.OnEnvironmentPrefabChanged();
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Models
/************************************************************************************************************************/
private static void DoModelsGUI()
{
var property = ModelsProperty;
var area = AnimancerGUI.LayoutSingleLineRect();
var valueArea = area;
valueArea.xMin = EditorGUIUtility.labelWidth - 10;
area.xMax = valueArea.xMin - AnimancerGUI.StandardSpacing;
EditorGUI.BeginChangeCheck();
var count = property.arraySize = EditorGUI.DelayedIntField(valueArea, property.arraySize);
if (EditorGUI.EndChangeCheck())
property.isExpanded = true;
HandleModelDragAndDrop(area);
if (count == 0)
return;
property.isExpanded = EditorGUI.Foldout(area, property.isExpanded, nameof(Models), true);
if (!property.isExpanded)
return;
EditorGUI.indentLevel++;
var model = property.GetArrayElementAtIndex(0);
for (int i = 0; i < count; i++)
{
GUILayout.BeginHorizontal();
EditorGUILayout.ObjectField(model);
if (GUILayout.Button(AnimancerIcons.ClearIcon("Remove model"), AnimancerGUI.NoPaddingButtonStyle))
{
Serialization.RemoveArrayElement(property, i);
property.serializedObject.ApplyModifiedProperties();
AnimancerGUI.Deselect();
GUIUtility.ExitGUI();
return;
}
GUILayout.Space(EditorStyles.objectField.margin.right);
GUILayout.EndHorizontal();
model.Next(false);
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
private static DragAndDropHandler<GameObject> _ModelDropHandler;
private static void HandleModelDragAndDrop(Rect area)
{
_ModelDropHandler ??= (gameObject, isDrop) =>
{
if (!EditorUtility.IsPersistent(gameObject) ||
Models.Contains(gameObject) ||
gameObject.GetComponentInChildren<Animator>() == null)
return false;
if (isDrop)
{
var modelsProperty = ModelsProperty;
modelsProperty.serializedObject.Update();
var i = modelsProperty.arraySize;
modelsProperty.arraySize = i + 1;
modelsProperty.GetArrayElementAtIndex(i).objectReferenceValue = gameObject;
modelsProperty.serializedObject.ApplyModifiedProperties();
}
return true;
};
_ModelDropHandler.Handle(area);
}
/************************************************************************************************************************/
[SerializeField]
private List<GameObject> _Models;
/// <summary>The models previously used in the <see cref="TransitionPreviewWindow"/>.</summary>
/// <remarks>Accessing this property removes missing and duplicate models from the list.</remarks>
public static List<GameObject> Models
{
get
{
var instance = Instance;
AnimancerEditorUtilities.RemoveMissingAndDuplicates(ref instance._Models);
return instance._Models;
}
}
private static SerializedProperty ModelsProperty
=> Instance.GetSerializedProperty(nameof(_Models));
/************************************************************************************************************************/
/// <summary>Adds a `model` to the list of preview models.</summary>
public static void AddModel(GameObject model)
{
if (model == GetOrCreateDefaultHumanoid(null) ||
model == GetOrCreateDefaultSprite(null))
return;
if (EditorUtility.IsPersistent(model))
{
AddModel(Models, model);
AnimancerSettings.SetDirty();
}
else
{
AddModel(TemporarySettings.PreviewModels, model);
}
}
private static void AddModel(List<GameObject> models, GameObject model)
{
// Remove if it was already there so that when we add it, it will be moved to the end.
var index = models.LastIndexOf(model);// Search backwards because it's more likely to be near the end.
if (index >= 0 && index < models.Count)
models.RemoveAt(index);
models.Add(model);
}
/************************************************************************************************************************/
private static GameObject _DefaultHumanoid;
/// <summary>
/// Returns the default preview object for Humanoid animations
/// if it has already been loaded.
/// </summary>
public static GameObject GetDefaultHumanoidIfAlreadyLoaded()
=> _DefaultHumanoid;
/// <summary>Returns the default preview object for Humanoid animations.</summary>
/// <remarks>A `parent` is only required if Animancer's or Unity's default objects fail to load.</remarks>
public static GameObject GetOrCreateDefaultHumanoid(Transform parent)
{
if (_DefaultHumanoid != null)
return _DefaultHumanoid;
// Try to load Animancer Humanoid.
var path = AssetDatabase.GUIDToAssetPath("f976ca0fb1329b44a8bc3dcca706751a");
if (!string.IsNullOrEmpty(path))
{
_DefaultHumanoid = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (_DefaultHumanoid != null)
return _DefaultHumanoid;
}
// Otherwise try to load Unity's DefaultAvatar.
_DefaultHumanoid = EditorGUIUtility.Load("Avatar/DefaultAvatar.fbx") as GameObject;
if (_DefaultHumanoid != null)
return _DefaultHumanoid;
if (parent == null)
return null;
// Otherwise just create an empty object.
_DefaultHumanoid = EditorUtility.CreateGameObjectWithHideFlags(
"DummyAvatar",
HideFlags.HideAndDontSave,
typeof(Animator));
_DefaultHumanoid.transform.parent = parent;
return _DefaultHumanoid;
}
/************************************************************************************************************************/
private static GameObject _DefaultSprite;
/// <summary>
/// Returns the default preview object for <see cref="Sprite"/> animations
/// if it has already been created.
/// </summary>
public static GameObject GetDefaultSpriteIfAlreadyCreated()
=> _DefaultSprite;
/// <summary>Returns the default preview object for <see cref="Sprite"/> animations.</summary>
/// <remarks>A `parent` is required to create the object.</remarks>
public static GameObject GetOrCreateDefaultSprite(Transform parent)
{
if (_DefaultSprite == null && parent != null)
{
_DefaultSprite = EditorUtility.CreateGameObjectWithHideFlags(
"DefaultSprite",
HideFlags.HideAndDontSave,
typeof(Animator),
typeof(SpriteRenderer));
_DefaultSprite.transform.parent = parent;
}
return _DefaultSprite;
}
/************************************************************************************************************************/
/// <summary>
/// Tries to choose the most appropriate model to use
/// based on the properties animated by the `animationClipSource`.
/// </summary>
public static Transform TrySelectBestModel(
object animationClipSource,
Transform parent)
{
if (animationClipSource.IsNullOrDestroyed())
return null;
using (SetPool<AnimationClip>.Instance.Acquire(out var clips))
{
clips.GatherFromSource(animationClipSource);
if (clips.Count == 0)
return null;
var model = TrySelectBestModel(clips, TemporarySettings.PreviewModels);
if (model != null)
return model;
model = TrySelectBestModel(clips, Models);
if (model != null)
return model;
foreach (var clip in clips)
{
var type = AnimationBindings.GetAnimationType(clip);
switch (type)
{
case AnimationType.Humanoid:
return GetOrCreateDefaultHumanoid(parent).transform;
case AnimationType.Sprite:
return GetOrCreateDefaultSprite(parent).transform;
}
}
return null;
}
}
/************************************************************************************************************************/
private static Transform TrySelectBestModel(HashSet<AnimationClip> clips, List<GameObject> models)
{
var animatableBindings = new HashSet<EditorCurveBinding>[models.Count];
for (int i = 0; i < models.Count; i++)
{
animatableBindings[i] = AnimationBindings.GetBindings(models[i]).ObjectBindings;
}
var bestMatchIndex = -1;
var bestMatchCount = 0;
foreach (var clip in clips)
{
var clipBindings = AnimationBindings.GetBindings(clip);
for (int iModel = animatableBindings.Length - 1; iModel >= 0; iModel--)
{
var modelBindings = animatableBindings[iModel];
var matches = 0;
for (int iBinding = 0; iBinding < clipBindings.Length; iBinding++)
{
if (modelBindings.Contains(clipBindings[iBinding]))
matches++;
}
if (bestMatchCount < matches && matches > clipBindings.Length / 2)
{
bestMatchCount = matches;
bestMatchIndex = iModel;
// If it matches all bindings, use it.
if (bestMatchCount == clipBindings.Length)
goto FoundBestMatch;
}
}
}
FoundBestMatch:
if (bestMatchIndex >= 0)
return models[bestMatchIndex].transform;
else
return null;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Scene Hierarchy
/************************************************************************************************************************/
private static void DoHierarchyGUI()
{
GUILayout.BeginVertical(GUI.skin.box);
GUILayout.Label("Preview Scene Hierarchy");
DoHierarchyGUI(TransitionPreviewWindow.InstanceScene.PreviewSceneRoot);
GUILayout.EndVertical();
}
/************************************************************************************************************************/
private static GUIStyle _HierarchyButtonStyle;
private static void DoHierarchyGUI(Transform root)
{
var area = AnimancerGUI.LayoutSingleLineRect();
_HierarchyButtonStyle ??= new(EditorStyles.miniButton)
{
alignment = TextAnchor.MiddleLeft,
};
if (GUI.Button(EditorGUI.IndentedRect(area), root.name, _HierarchyButtonStyle))
{
Selection.activeTransform = root;
GUIUtility.ExitGUI();
}
var childCount = root.childCount;
if (childCount == 0)
return;
var expandedHierarchy = TransitionPreviewWindow.InstanceScene.ExpandedHierarchy;
var index = expandedHierarchy != null ? expandedHierarchy.IndexOf(root) : -1;
var isExpanded = EditorGUI.Foldout(area, index >= 0, GUIContent.none);
if (isExpanded)
{
if (index < 0)
expandedHierarchy.Add(root);
EditorGUI.indentLevel++;
for (int i = 0; i < childCount; i++)
DoHierarchyGUI(root.GetChild(i));
EditorGUI.indentLevel--;
}
else if (index >= 0)
{
expandedHierarchy.RemoveAt(index);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,457 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
namespace Animancer.Editor.Previews
{
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/TransitionPreviewWindow
partial class TransitionPreviewWindow
{
/// <summary>Animation details for the <see cref="TransitionPreviewWindow"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions#previews">
/// Previews</see>
/// </remarks>
[Serializable]
internal class Animations
{
/************************************************************************************************************************/
public const string
PreviousAnimationKey = "Previous Animation",
NextAnimationKey = "Next Animation";
/************************************************************************************************************************/
[NonSerialized] private AnimationClip[] _OtherAnimations;
[SerializeField]
private AnimationClip _PreviousAnimation;
public AnimationClip PreviousAnimation => _PreviousAnimation;
[SerializeField]
private AnimationClip _NextAnimation;
public AnimationClip NextAnimation => _NextAnimation;
/************************************************************************************************************************/
private static AnimancerPreviewObject PreviewObject
=> _Instance._Scene.PreviewObject;
/************************************************************************************************************************/
public void DoGUI()
{
GUILayout.BeginVertical(GUI.skin.box);
EditorGUILayout.LabelField("Preview Details", "(Not Serialized)");
var previewObject = PreviewObject;
AnimancerPreviewObjectGUI.DoModelGUI(previewObject);
using (var label = PooledGUIContent.Acquire("Previous Animation",
"The animation for the preview to play before the target transition"))
{
DoAnimationFieldGUI(label, ref _PreviousAnimation, (clip) => _PreviousAnimation = clip);
}
var graph = previewObject.Graph;
DoCurrentAnimationGUI(graph);
using (var label = PooledGUIContent.Acquire("Next Animation",
"The animation for the preview to play after the target transition"))
{
DoAnimationFieldGUI(label, ref _NextAnimation, (clip) => _NextAnimation = clip);
}
if (graph != null)
{
using (new EditorGUI.DisabledScope(!Transition.IsValid()))
{
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (graph.IsGraphPlaying)
{
if (CompactMiniButton(AnimancerIcons.PauseIcon))
graph.PauseGraph();
}
else
{
if (CompactMiniButton(AnimancerIcons.StepBackwardIcon))
StepBackward();
if (CompactMiniButton(AnimancerIcons.PlayIcon))
PlaySequence(graph);
if (CompactMiniButton(AnimancerIcons.StepForwardIcon))
StepForward();
}
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
}
}
GUILayout.EndVertical();
}
/************************************************************************************************************************/
public void GatherAnimations()
{
AnimationGatherer.GatherFromGameObject(
PreviewObject.OriginalObject.gameObject,
ref _OtherAnimations,
true);
if (_OtherAnimations.Length > 0 &&
(_PreviousAnimation == null || _NextAnimation == null))
{
var defaultClip = _OtherAnimations[0];
var defaultClipIsIdle = false;
for (int i = 0; i < _OtherAnimations.Length; i++)
{
var clip = _OtherAnimations[i];
if (defaultClipIsIdle && clip.name.Length > defaultClip.name.Length)
continue;
if (clip.name.IndexOf("idle", StringComparison.CurrentCultureIgnoreCase) >= 0)
{
defaultClip = clip;
break;
}
}
if (_PreviousAnimation == null)
_PreviousAnimation = defaultClip;
if (_NextAnimation == null)
_NextAnimation = defaultClip;
}
}
/************************************************************************************************************************/
private void DoAnimationFieldGUI(GUIContent label, ref AnimationClip clip, Action<AnimationClip> setClip)
{
var showDropdown = !_OtherAnimations.IsNullOrEmpty();
var area = LayoutSingleLineRect();
if (DoDropdownObjectFieldGUI(area, label, showDropdown, ref clip))
{
var menu = new GenericMenu();
menu.AddItem(new("None"), clip == null, () => setClip(null));
for (int i = 0; i < _OtherAnimations.Length; i++)
{
var animation = _OtherAnimations[i];
menu.AddItem(new(animation.name), animation == clip, () => setClip(animation));
}
menu.ShowAsContext();
}
}
/************************************************************************************************************************/
private void DoCurrentAnimationGUI(AnimancerGraph animancer)
{
string text;
if (animancer != null)
{
var transition = Transition;
if (transition.IsValid() && transition.Key != null)
text = animancer.States.GetOrCreate(transition).ToString();
else
text = transition?.ToString();
}
else
{
text = _Instance._TransitionProperty.Property.GetFriendlyPath();
}
if (text != null)
EditorGUILayout.LabelField("Current Animation", text);
}
/************************************************************************************************************************/
private void PlaySequence(AnimancerGraph animancer)
{
if (_PreviousAnimation != null && _PreviousAnimation.length > 0)
{
PreviewObject.Graph.Stop();
var fromState = animancer.States.GetOrCreate(PreviousAnimationKey, _PreviousAnimation, true);
animancer.Layers[0].Play(fromState);
OnPlayAnimation();
fromState.TimeD = 0;
var warnings = OptionalWarning.UnsupportedEvents.DisableTemporarily();
fromState.Events(this).EndEvent = new(1 / fromState.Length, PlayTransition);
warnings.Enable();
}
else
{
PlayTransition();
}
PreviewObject.Graph.UnpauseGraph();
}
private void PlayTransition()
{
var transition = Transition;
var animancer = PreviewObject.Graph;
animancer.States.TryGet(transition, out var oldState);
var targetState = animancer.Layers[0].Play(transition);
OnPlayAnimation();
if (oldState != null && oldState != targetState)
oldState.Destroy();
var warnings = OptionalWarning.UnsupportedEvents.DisableTemporarily();
targetState.Events(this).OnEnd = () =>
{
if (_NextAnimation != null)
{
var fadeDuration = AnimancerEvent.GetFadeOutDuration(
targetState,
AnimancerGraph.DefaultFadeDuration);
PlayOther(NextAnimationKey, _NextAnimation, 0, fadeDuration);
OnPlayAnimation();
}
else
{
animancer.Layers[0].IncrementCommandCount();
}
};
warnings.Enable();
}
/************************************************************************************************************************/
/// <summary>Does nothing.</summary>
/// <remarks>
/// Doesn't trigger <see cref="OptionalWarning.UselessEvent"/>
/// (unlike <see cref="AnimancerEvent.DummyCallback"/>).
/// </remarks>
public static readonly Action
DummyCallback = () => { };
public void OnPlayAnimation()
{
var animancer = PreviewObject.Graph;
if (animancer == null ||
animancer.States.Current == null)
return;
var state = animancer.States.Current;
state.RecreatePlayableRecursive();
if (state.HasEvents)
{
var warnings = OptionalWarning.UnsupportedEvents | OptionalWarning.ProOnly;
warnings = warnings.DisableTemporarily();
var events = state.Events(this);
events.OnEnd = DummyCallback;
for (int i = events.Count - 1; i >= 0; i--)
events.SetCallback(i, DummyCallback);
warnings.Enable();
}
}
/************************************************************************************************************************/
private void StepBackward()
=> StepTime(-TransitionPreviewSettings.FrameStep);
private void StepForward()
=> StepTime(TransitionPreviewSettings.FrameStep);
private void StepTime(float timeOffset)
{
if (!TryShowTransitionPaused(out _, out _, out var state))
return;
var length = state.Length;
if (length != 0)
timeOffset /= length;
NormalizedTime += timeOffset;
}
/************************************************************************************************************************/
[SerializeField]
private float _NormalizedTime;
public float NormalizedTime
{
get => _NormalizedTime;
set
{
if (!value.IsFinite())
return;
_NormalizedTime = value;
if (!TryShowTransitionPaused(out var animancer, out var transition, out var state))
return;
var length = state.Length;
var speed = state.Speed;
var time = value * length;
var fadeDuration = transition.FadeDuration * Math.Abs(speed);
var startTime = TimelineGUI.GetStartTime(transition.NormalizedStartTime, speed, length);
var normalizedEndTime = state.NormalizedEndTime;
var endTime = normalizedEndTime * length;
var fadeOutEnd = TimelineGUI.GetFadeOutEnd(speed, endTime, length);
if (speed < 0)
{
time = length - time;
startTime = length - startTime;
value = 1 - value;
normalizedEndTime = 1 - normalizedEndTime;
endTime = length - endTime;
fadeOutEnd = length - fadeOutEnd;
}
if (time < startTime)// Previous animation.
{
if (_PreviousAnimation != null)
{
PlayOther(PreviousAnimationKey, _PreviousAnimation, value);
value = 0;
}
}
else if (time < startTime + fadeDuration)// Fade from previous animation to the target.
{
if (_PreviousAnimation != null)
{
var fromState = PlayOther(PreviousAnimationKey, _PreviousAnimation, value);
state.IsPlaying = true;
state.Weight = (time - startTime) / fadeDuration;
fromState.Weight = 1 - state.Weight;
}
}
else if (_NextAnimation != null)
{
if (value < normalizedEndTime)
{
// Just the main state.
}
else
{
var toState = PlayOther(NextAnimationKey, _NextAnimation, value - normalizedEndTime);
if (time < fadeOutEnd)// Fade from the target transition to the next animation.
{
state.IsPlaying = true;
toState.Weight = (time - endTime) / (fadeOutEnd - endTime);
state.Weight = 1 - toState.Weight;
}
// Else just the next animation.
}
}
if (speed < 0)
value = 1 - value;
state.MoveTime(state.Weight > 0 ? value : 0, true);
animancer.Evaluate();
RepaintEverything();
}
}
/************************************************************************************************************************/
private bool TryShowTransitionPaused(
out AnimancerGraph animancer, out ITransition transition, out AnimancerState state)
{
animancer = PreviewObject.Graph;
transition = Transition;
if (animancer == null || !transition.IsValid())
{
state = null;
return false;
}
state = animancer.Layers[0].Play(transition, 0);
OnPlayAnimation();
animancer.PauseGraph();
return true;
}
/************************************************************************************************************************/
private AnimancerState PlayOther(
object key,
AnimationClip animation,
float normalizedTime,
float fadeDuration = 0)
{
var animancer = PreviewObject.Graph;
var state = animancer.States.GetOrCreate(key, animation, true);
state = animancer.Layers[0].Play(state, fadeDuration);
OnPlayAnimation();
normalizedTime *= state.Length;
state.Time = normalizedTime.IsFinite() ? normalizedTime : 0;
return state;
}
/************************************************************************************************************************/
internal class WindowMatchStateTime : Updatable
{
/************************************************************************************************************************/
public override void Update()
{
if (_Instance == null ||
!AnimancerGraph.Current.IsGraphPlaying)
return;
var transition = Transition;
if (transition == null)
return;
if (AnimancerGraph.Current.States.TryGet(transition, out var state))
_Instance._Animations._NormalizedTime = state.NormalizedTime;
}
/************************************************************************************************************************/
public override string ToString()
=> nameof(WindowMatchStateTime);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,159 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor.Previews
{
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/TransitionPreviewWindow
partial class TransitionPreviewWindow
{
/// <summary>[Internal] Custom Inspector for the <see cref="TransitionPreviewWindow"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions#previews">
/// Previews</see>
/// </remarks>
[CustomEditor(typeof(TransitionPreviewWindow))]
internal class Inspector : UnityEditor.Editor
{
/************************************************************************************************************************/
private static readonly string[]
TabNames = { "Preview", "Settings" };
private const int
PreviewTab = 0,
SettingsTab = 1;
/************************************************************************************************************************/
[SerializeField]
private int _CurrentTab;
private readonly AnimancerGraphDrawer
PlayableDrawer = new();
public TransitionPreviewWindow Target { get; private set; }
/************************************************************************************************************************/
public override bool UseDefaultMargins()
=> false;
/************************************************************************************************************************/
public override void OnInspectorGUI()
{
GUILayout.Space(AnimancerGUI.StandardSpacing * 2);
Target = (TransitionPreviewWindow)target;
if (Event.current.type == EventType.MouseDown)
Target.ShowTab();
_CurrentTab = GUILayout.Toolbar(_CurrentTab, TabNames);
_CurrentTab = Mathf.Clamp(_CurrentTab, 0, TabNames.Length - 1);
switch (_CurrentTab)
{
case PreviewTab: DoPreviewInspectorGUI(); break;
case SettingsTab: TransitionPreviewSettings.DoInspectorGUI(); break;
default: GUILayout.Label("Tab index is out of bounds"); break;
}
}
/************************************************************************************************************************/
private void DoPreviewInspectorGUI()
{
if (!Target._TransitionProperty.IsValid())
{
EditorGUILayout.HelpBox("No target property", MessageType.Info, true);
Target.DestroyTransitionProperty();
return;
}
DoTransitionPropertyGUI();
DoTransitionGUI();
Target._Animations.DoGUI();
var animancer = Target._Scene.PreviewObject.Graph;
if (animancer != null)
{
PlayableDrawer.DoGUI(animancer.Component);
AnimancerGUI.SetGuiChanged(animancer.IsGraphPlaying);
}
}
/************************************************************************************************************************/
/// <summary>The tooltip used for the Close button.</summary>
public const string CloseTooltip = "Close the Transition Preview Window";
/// <summary>Draws the target object and path of the <see cref="TransitionProperty"/>.</summary>
private void DoTransitionPropertyGUI()
{
var property = Target._TransitionProperty;
property.Update();
using (new EditorGUI.DisabledScope(true))
{
EditorGUI.showMixedValue = property.TargetObjects.Length > 1;
EditorGUILayout.ObjectField(property.TargetObject, typeof(Object), true);
EditorGUI.showMixedValue = false;
GUILayout.BeginHorizontal();
{
GUILayout.Label(property.Property.GetFriendlyPath());
GUI.enabled = true;
using (var label = PooledGUIContent.Acquire("Close", CloseTooltip))
{
if (GUILayout.Button(label, EditorStyles.miniButton, AnimancerGUI.DontExpandWidth))
{
Target.Close();
GUIUtility.ExitGUI();
}
}
}
GUILayout.EndHorizontal();
}
}
/************************************************************************************************************************/
private void DoTransitionGUI()
{
var property = Target._TransitionProperty;
var isExpanded = property.Property.isExpanded;
property.Property.isExpanded = true;
var height = EditorGUI.GetPropertyHeight(property, true);
const float Indent = 12;
var padding = GUI.skin.box.padding;
var area = AnimancerGUI.LayoutRect(height + padding.horizontal - padding.bottom);
area.x += Indent + padding.left;
area.width -= Indent + padding.horizontal;
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(area, property, true);
property.Property.isExpanded = isExpanded;
if (EditorGUI.EndChangeCheck())
property.ApplyModifiedProperties();
}
/************************************************************************************************************************/
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,323 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
namespace Animancer.Editor.Previews
{
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/TransitionPreviewWindow
partial class TransitionPreviewWindow
{
/************************************************************************************************************************/
/// <summary>The <see cref="Scene"/> of the current <see cref="TransitionPreviewWindow"/> instance.</summary>
public static Scene InstanceScene
=> _Instance != null
? _Instance._Scene
: null;
/************************************************************************************************************************/
/// <summary>Temporary scene management for the <see cref="TransitionPreviewWindow"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions#previews">
/// Previews</see>
/// </remarks>
[Serializable]
public class Scene :
AnimancerPreviewObject.IEventHandler
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
/// <summary>The scene displayed by the <see cref="TransitionPreviewWindow"/>.</summary>
[SerializeField]
private UnityEngine.SceneManagement.Scene _Scene;
/// <summary>The root object in the preview scene.</summary>
public Transform PreviewSceneRoot { get; private set; }
/// <summary>
/// An instance of the <see cref="TransitionPreviewSettings.SceneEnvironment"/>.
/// A child of the <see cref="PreviewSceneRoot"/>.
/// </summary>
public GameObject EnvironmentInstance { get; private set; }
/************************************************************************************************************************/
[SerializeField]
private AnimancerPreviewObject _PreviewObject;
/// <summary>[<see cref="SerializeField"/>] The object being previewed.</summary>
public AnimancerPreviewObject PreviewObject
=> AnimancerPreviewObject.Initialize(ref _PreviewObject, this, PreviewSceneRoot);
/************************************************************************************************************************/
private Vector3 _PreviousPreviewObjectPosition;
private Vector3 CurrentPreviewObjectPosition
{
get
{
var animator = PreviewObject.SelectedInstanceAnimator;
return animator != null
? animator.transform.position
: default;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// <summary>Initializes this <see cref="Scene"/>.</summary>
public void OnEnable()
{
duringSceneGui += DoCustomGUI;
CreateScene();
PreviewObject.TrySelectBestModel(Transition);
}
/************************************************************************************************************************/
private void CreateScene()
{
_Scene = EditorSceneManager.NewPreviewScene();
_Scene.name = "Transition Preview";
_Instance.customScene = _Scene;
var root = AnimancerPreviewObject.CreateEmpty(nameof(TransitionPreviewWindow));
PreviewSceneRoot = root.transform;
SceneManager.MoveGameObjectToScene(root, _Scene);
_Instance.customParentForDraggedObjects = PreviewSceneRoot;
OnEnvironmentPrefabChanged();
}
/************************************************************************************************************************/
internal void OnEnvironmentPrefabChanged()
{
DestroyImmediate(EnvironmentInstance);
var prefab = TransitionPreviewSettings.SceneEnvironment;
if (prefab != null)
EnvironmentInstance = Instantiate(prefab, PreviewSceneRoot);
}
/************************************************************************************************************************/
/// <inheritdoc/>
void AnimancerPreviewObject.IEventHandler.OnInstantiateObject()
{
FocusCamera();
_Instance._Animations.GatherAnimations();
}
/************************************************************************************************************************/
/// <inheritdoc/>
void AnimancerPreviewObject.IEventHandler.OnSetSelectedAnimator()
{
_Instance.in2DMode = PreviewObject.SelectedInstanceType == AnimationType.Sprite;
}
/// <inheritdoc/>
void AnimancerPreviewObject.IEventHandler.OnCreateGraph()
{
PreviewObject.Graph.RequirePostUpdate(new Animations.WindowMatchStateTime());
_Instance._Animations.NormalizedTime = _Instance._Animations.NormalizedTime;
}
/************************************************************************************************************************/
/// <summary>Called when the target transition property is changed.</summary>
public void OnTargetPropertyChanged()
{
_ExpandedHierarchy?.Clear();
var previewObject = PreviewObject;
previewObject.OriginalObject = AnimancerUtilities.FindRoot(_Instance._TransitionProperty.TargetObject);
previewObject.TrySelectBestModel(Transition);
_Instance._Animations.NormalizedTime = 0;
_Instance.in2DMode = previewObject.SelectedInstanceType == AnimationType.Sprite;
}
/************************************************************************************************************************/
private void FocusCamera()
{
var instance = _PreviewObject.InstanceObject;
if (instance == null)
return;
var bounds = CalculateBounds(instance);
var rotation = _Instance.in2DMode ?
Quaternion.identity :
Quaternion.Euler(15, 225, 0);
var size = bounds.extents.magnitude * 1.2f;
if (size == float.PositiveInfinity)
return;
else if (size == 0)
size = 10;
_Instance.LookAt(bounds.center, rotation, size, _Instance.in2DMode, true);
}
/************************************************************************************************************************/
private static Bounds CalculateBounds(Transform transform)
{
if (transform == null)
return default;
var renderers = transform.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0)
return default;
var bounds = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++)
{
bounds.Encapsulate(renderers[i].bounds);
}
return bounds;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Execution
/************************************************************************************************************************/
/// <summary>Called when the window GUI is drawn.</summary>
public void OnGUI()
{
if (_PreviewObject != null &&
_PreviewObject.Graph != null &&
_PreviewObject.Graph.IsGraphPlaying)
AnimancerGUI.RepaintEverything();
if (Selection.activeObject == _Instance &&
Event.current.type == EventType.KeyUp &&
Event.current.keyCode == KeyCode.F)
FocusCamera();
}
/************************************************************************************************************************/
private void DoCustomGUI(SceneView sceneView)
{
FollowPreviewObject(sceneView);
var animancer = PreviewObject.Graph;
if (animancer == null ||
sceneView is not TransitionPreviewWindow instance ||
!AnimancerUtilities.TryGetWrappedObject(Transition, out ITransitionGUI gui) ||
instance._TransitionProperty == null)
return;
EditorGUI.BeginChangeCheck();
using (new TransitionDrawer.DrawerContext(instance._TransitionProperty))
{
try
{
gui.OnPreviewSceneGUI(new(animancer));
}
catch (Exception exception)
{
Debug.LogException(exception);
}
}
if (EditorGUI.EndChangeCheck())
AnimancerGUI.RepaintEverything();
}
/************************************************************************************************************************/
private void FollowPreviewObject(SceneView sceneView)
{
var currentPreviewObjectPosition = CurrentPreviewObjectPosition;
if (_PreviousPreviewObjectPosition == currentPreviewObjectPosition)
return;
sceneView.pivot += currentPreviewObjectPosition - _PreviousPreviewObjectPosition;
_PreviousPreviewObjectPosition = currentPreviewObjectPosition;
}
/************************************************************************************************************************/
/// <summary>Is the `obj` a <see cref="GameObject"/> in the preview scene?</summary>
public bool IsSceneObject(Object obj)
=> obj is GameObject gameObject
&& gameObject.transform.IsChildOf(PreviewSceneRoot);
/************************************************************************************************************************/
[SerializeField]
private List<Transform> _ExpandedHierarchy;
/// <summary>A list of all objects with their child hierarchy expanded.</summary>
public List<Transform> ExpandedHierarchy
=> _ExpandedHierarchy ??= new();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Cleanup
/************************************************************************************************************************/
/// <summary>Called by <see cref="TransitionPreviewWindow.OnDisable"/>.</summary>
public void OnDisable()
{
duringSceneGui -= DoCustomGUI;
_PreviewObject?.Dispose();
EditorSceneManager.ClosePreviewScene(_Scene);
}
/************************************************************************************************************************/
/// <summary>Called by <see cref="TransitionPreviewWindow.OnDestroy"/>.</summary>
public void OnDestroy()
{
if (PreviewSceneRoot != null)
{
DestroyImmediate(PreviewSceneRoot.gameObject);
PreviewSceneRoot = null;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,386 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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="ITransition"/> currently being previewed.</summary>
public static ITransition Transition
{
get
{
var property = _Instance._TransitionProperty;
if (!property.IsValid())
return null;
return property.Property.GetValue<ITransition>();
}
}
/************************************************************************************************************************/
/// <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(ITransition).IsAssignableFrom(type))
return true;
var value = accessor.GetValue(property);
return
value != null &&
typeof(ITransition).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(ITransition)}.");
}
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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cadc451eaffebdb48ad6dc32456ebbd5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: d4e06a71fc03595429cac47cd385c4c1, type: 3}
userData:
assetBundleName:
assetBundleVariant: