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,355 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor.Previews
{
/// <summary>[Editor-Only] Manages the selection and instantiation of models for previewing animations.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/AnimancerPreviewObject
[Serializable]
public class AnimancerPreviewObject : IDisposable
{
/************************************************************************************************************************/
/// <summary>[Editor-Only] Handles events from an <see cref="AnimancerPreviewObject"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/IEventHandler
public interface IEventHandler
{
/// <summary>Called after the <see cref="InstanceObject"/> is instantiated.</summary>
void OnInstantiateObject();
/// <summary>Called after the <see cref="SelectedInstanceAnimator"/> is changed.</summary>
void OnSetSelectedAnimator();
/// <summary>Called after the <see cref="Graph"/> is initialized.</summary>
void OnCreateGraph();
}
/// <summary>An optional listener for this object's events.</summary>
[field: NonSerialized] public IEventHandler EventHandler { get; set; }
/************************************************************************************************************************/
[SerializeField]
private Transform _OriginalObject;
/// <summary>[<see cref="SerializeField"/>]
/// The original model which was instantiated to create the <see cref="InstanceObject"/>.
/// </summary>
public Transform OriginalObject
{
get => _OriginalObject;
set
{
_OriginalObject = value;
InstantiateObject();
if (value != null)
TransitionPreviewSettings.AddModel(value.gameObject);
}
}
/************************************************************************************************************************/
/// <summary>The object to instantiate the <see cref="InstanceObject"/> under.</summary>
[field: NonSerialized]
public Transform InstanceRoot { get; private set; }
/// <summary>The preview copy of the <see cref="OriginalObject"/>.</summary>
[field: NonSerialized]
public Transform InstanceObject { get; private set; }
/// <summary>The bounds of the <see cref="InstanceObject"/>.</summary>
[field: NonSerialized]
public Bounds InstanceBounds { get; private set; }
/************************************************************************************************************************/
/// <summary>The <see cref="Animator"/>s on the <see cref="InstanceObject"/> and its children.</summary>
[field: NonSerialized]
public Animator[] InstanceAnimators { get; private set; }
/// <summary>The type of the <see cref="SelectedInstanceAnimator"/>.</summary>
[field: NonSerialized]
public AnimationType SelectedInstanceType { get; private set; }
[SerializeField]
private int _SelectedInstanceAnimator;
/// <summary>The <see cref="Animator"/> component currently being used for the preview.</summary>
public Animator SelectedInstanceAnimator
{
get
{
if (InstanceAnimators.IsNullOrEmpty())
return null;
if (_SelectedInstanceAnimator >= InstanceAnimators.Length)
_SelectedInstanceAnimator = InstanceAnimators.Length - 1;
return InstanceAnimators[_SelectedInstanceAnimator];
}
}
/************************************************************************************************************************/
[NonSerialized]
private AnimancerGraph _Graph;
/// <summary>The <see cref="AnimancerGraph"/> being used for the preview.</summary>
public AnimancerGraph Graph
{
get
{
if ((_Graph == null || !_Graph.IsValidOrDispose()) &&
InstanceObject != null)
{
_Graph = null;
var animator = SelectedInstanceAnimator;
if (animator != null)
{
AnimancerGraph.SetNextGraphName($"{animator.name} (Animancer Preview)");
_Graph = new AnimancerGraph();
_Graph.Initialize(
new DummyAnimancerComponent(animator, _Graph));
EventHandler?.OnCreateGraph();
}
}
return _Graph;
}
}
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="AnimancerPreviewObject"/>
/// and calls <see cref="Initialize(Transform)"/>.
/// </summary>
public static AnimancerPreviewObject Initialize(
ref AnimancerPreviewObject preview,
IEventHandler eventHandler,
Transform instanceRoot)
{
preview ??= new();
preview.EventHandler = eventHandler;
preview.Initialize(instanceRoot);
return preview;
}
/************************************************************************************************************************/
[NonSerialized] private bool _HasInitialized;
/// <summary>Sets the <see cref="InstanceRoot"/> for this preview to work under.</summary>
public void Initialize(Transform instanceRoot)
{
if (InstanceRoot != instanceRoot)
{
DestroyInstanceObject();
InstanceRoot = instanceRoot;
}
if (InstanceObject == null)
InstantiateObject();
if (_HasInitialized)
return;
_HasInitialized = true;
EditorSceneManager.sceneOpening += OnSceneOpening;
EditorApplication.playModeStateChanged += OnPlayModeChanged;
}
/************************************************************************************************************************/
/// <summary>Cleans up this preview.</summary>
public void Dispose()
{
EditorSceneManager.sceneOpening -= OnSceneOpening;
EditorApplication.playModeStateChanged -= OnPlayModeChanged;
}
/************************************************************************************************************************/
/// <summary>Called when entering or exiting Play Mode to destroy the <see cref="InstanceObject"/>.</summary>
private void OnPlayModeChanged(PlayModeStateChange change)
{
switch (change)
{
case PlayModeStateChange.ExitingEditMode:
case PlayModeStateChange.ExitingPlayMode:
DestroyInstanceObject();
break;
}
}
/************************************************************************************************************************/
/// <summary>Called when opening a scene to destroy the <see cref="InstanceObject"/>.</summary>
private void OnSceneOpening(string path, OpenSceneMode mode)
{
if (mode == OpenSceneMode.Single)
DestroyInstanceObject();
}
/************************************************************************************************************************/
/// <summary>Destroys and re-instantiates the <see cref="InstanceObject"/>.</summary>
private void InstantiateObject()
{
if (AnimancerEditorUtilities.IsChangingPlayMode)
return;
DestroyInstanceObject();
if (_OriginalObject == null ||
InstanceRoot == null)
return;
InstanceRoot.gameObject.SetActive(false);
InstanceObject = Object.Instantiate(_OriginalObject, InstanceRoot);
InstanceObject.localPosition = default;
InstanceObject.name = _OriginalObject.name;
InstanceBounds = AnimancerEditorUtilities.CalculateBounds(InstanceObject);
DisableUnnecessaryComponents(InstanceObject.gameObject);
InstanceAnimators = InstanceObject.GetComponentsInChildren<Animator>();
for (int i = 0; i < InstanceAnimators.Length; i++)
{
var animator = InstanceAnimators[i];
animator.enabled = false;
animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
animator.fireEvents = false;
animator.updateMode = AnimatorUpdateMode.Normal;
animator.applyRootMotion = true;
}
InstanceRoot.gameObject.SetActive(true);
SetSelectedAnimator(_SelectedInstanceAnimator);
EventHandler?.OnInstantiateObject();
}
/************************************************************************************************************************/
/// <summary>Disables all unnecessary components on the `root` or its children.</summary>
private static void DisableUnnecessaryComponents(GameObject root)
{
var behaviours = root.GetComponentsInChildren<Behaviour>();
for (int i = 0; i < behaviours.Length; i++)
{
var behaviour = behaviours[i];
// Other undesirable components aren't Behaviours anyway: Transform, MeshFilter, Renderer.
if (behaviour is Animator)
continue;
var type = behaviour.GetType();
if (type.IsDefined(typeof(ExecuteAlways), true) ||
type.IsDefined(typeof(ExecuteInEditMode), true))
continue;
behaviour.enabled = false;
behaviour.hideFlags |= HideFlags.NotEditable;
}
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="SelectedInstanceAnimator"/>.</summary>
public void SetSelectedAnimator(int index)
{
DestroyGraph();
var animator = SelectedInstanceAnimator;
if (animator != null && animator.enabled)
{
animator.Rebind();
animator.enabled = false;
return;
}
_SelectedInstanceAnimator = index;
animator = SelectedInstanceAnimator;
if (animator != null)
{
animator.enabled = true;
SelectedInstanceType = AnimationBindings.GetAnimationType(animator);
}
else
{
SelectedInstanceType = default;
}
EventHandler?.OnSetSelectedAnimator();
}
/************************************************************************************************************************/
/// <summary>Destroys the <see cref="InstanceObject"/>.</summary>
public void DestroyInstanceObject()
{
DestroyGraph();
if (InstanceObject == null)
return;
Object.DestroyImmediate(InstanceObject.gameObject);
InstanceObject = null;
InstanceAnimators = null;
}
/************************************************************************************************************************/
/// <summary>Destroys the <see cref="Graph"/>.</summary>
private void DestroyGraph()
{
if (_Graph == null)
return;
_Graph.Destroy();
_Graph = null;
}
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="TransitionPreviewSettings.TrySelectBestModel(object, Transform)"/>
/// if there is no <see cref="OriginalObject"/> yet.
/// </summary>
public void TrySelectBestModel(object animationClipSource)
{
if (OriginalObject == null)
OriginalObject = TransitionPreviewSettings.TrySelectBestModel(animationClipSource, InstanceRoot);
}
/************************************************************************************************************************/
/// <summary>
/// Creates an object using <see cref="HideFlags.HideAndDontSave"/>
/// without <see cref="HideFlags.NotEditable"/>.
/// </summary>
public static GameObject CreateEmpty(string name)
=> EditorUtility.CreateGameObjectWithHideFlags(
name,
HideFlags.HideInHierarchy | HideFlags.DontSave);
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,209 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
namespace Animancer.Editor.Previews
{
/// <summary>[Editor-Only] GUI utilities for <see cref="AnimancerPreviewObject"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/AnimancerPreviewObjectGUI
public static class AnimancerPreviewObjectGUI
{
/************************************************************************************************************************/
/// <summary>Calculates the pixel height required to draw the `preview`.</summary>
public static float CalculateHeight(AnimancerPreviewObject preview)
{
var lines = 1;
var instanceAnimators = preview.InstanceAnimators;
if (instanceAnimators != null &&
instanceAnimators.Length > 1)
lines++;
return AnimancerGUI.CalculateHeight(lines);
}
/************************************************************************************************************************/
/// <summary>Draws the model selection GUI.</summary>
public static void DoModelGUI(AnimancerPreviewObject preview)
{
var area = LayoutRect(CalculateHeight(preview));
DoModelGUI(area, preview);
}
/************************************************************************************************************************/
/// <summary>Draws the model selection GUI.</summary>
public static void DoModelGUI(Rect area, AnimancerPreviewObject preview)
{
var root = preview.OriginalObject;
var model = root != null ? root.gameObject : null;
EditorGUI.BeginChangeCheck();
var warning = GetModelWarning(model, preview);
var color = GUI.color;
if (warning != null)
GUI.color = WarningFieldColor;
using (var label = PooledGUIContent.Acquire("Model"))
{
var objectFieldArea = StealLineFromTop(ref area);
if (DoDropdownObjectFieldGUI(objectFieldArea, label, true, ref model))
{
var menu = new GenericMenu();
menu.AddItem(
new("Default Humanoid"),
model != null && model == TransitionPreviewSettings.GetOrCreateDefaultHumanoid(null),
() => preview.OriginalObject
= TransitionPreviewSettings.GetOrCreateDefaultHumanoid(preview.InstanceRoot).transform);
menu.AddItem(
new("Default Sprite"),
model != null && model == TransitionPreviewSettings.GetDefaultSpriteIfAlreadyCreated(),
() => preview.OriginalObject
= TransitionPreviewSettings.GetOrCreateDefaultSprite(preview.InstanceRoot).transform);
var persistentModels = TransitionPreviewSettings.Models;
var temporaryModels = TemporarySettings.PreviewModels;
if (persistentModels.Count == 0 && temporaryModels.Count == 0)
{
menu.AddDisabledItem(new("No model prefabs have been used yet"));
}
else
{
AddModelSelectionFunctions(menu, preview, persistentModels, model);
AddModelSelectionFunctions(menu, preview, temporaryModels, model);
}
menu.ShowAsContext();
}
}
GUI.color = color;
if (EditorGUI.EndChangeCheck())
preview.OriginalObject = model != null ? model.transform : null;
if (warning != null)
EditorGUILayout.HelpBox(warning, MessageType.Warning, true);
DoAnimatorSelectorGUI(preview);
}
/************************************************************************************************************************/
/// <summary>Adds menu functions for selecting each of the `models`.</summary>
private static void AddModelSelectionFunctions(
GenericMenu menu,
AnimancerPreviewObject preview,
List<GameObject> models,
GameObject selected)
{
for (int i = models.Count - 1; i >= 0; i--)
{
var model = models[i];
var path = AssetDatabase.GetAssetPath(model);
if (!string.IsNullOrEmpty(path))
path = path.Replace('/', '\\');
else
path = model.name;
menu.AddItem(new(path), model == selected,
() => preview.OriginalObject = model.transform);
}
}
/************************************************************************************************************************/
/// <summary>Returns a warning about the selected model or <c>null</c>.</summary>
private static string GetModelWarning(
GameObject model,
AnimancerPreviewObject preview)
{
if (model == null)
return "No Model is selected so nothing can be previewed.";
if (preview.SelectedInstanceAnimator == null)
return "The selected Model has no Animator component.";
return null;
}
/************************************************************************************************************************/
/// <summary>
/// Draws a button for selecting which <see cref="Animator"/> to control if there's more than one.
/// </summary>
private static void DoAnimatorSelectorGUI(AnimancerPreviewObject preview)
{
var instanceAnimators = preview.InstanceAnimators;
if (instanceAnimators == null ||
instanceAnimators.Length <= 1)
return;
var area = LayoutSingleLineRect(SpacingMode.After);
var labelArea = StealFromLeft(ref area, EditorGUIUtility.labelWidth, StandardSpacing);
GUI.Label(labelArea, nameof(Animator));
var selectedAnimator = preview.SelectedInstanceAnimator;
using (var label = PooledGUIContent.Acquire(
selectedAnimator != null ? selectedAnimator.name : "None"))
{
var clicked = EditorGUI.DropdownButton(area, label, FocusType.Passive);
if (!clicked)
return;
var menu = new GenericMenu();
for (int i = 0; i < instanceAnimators.Length; i++)
{
var animator = instanceAnimators[i];
var index = i;
menu.AddItem(new(animator.name), animator == selectedAnimator, () =>
{
preview.SetSelectedAnimator(index);
});
}
menu.ShowAsContext();
}
}
/************************************************************************************************************************/
private static DragAndDropHandler<GameObject> _ModelDropHandler;
private static AnimancerPreviewObject _ModelDropPreview;
/// <summary>Handles drag and drop events for preview models.</summary>
public static void HandleDragAndDrop(Rect area, AnimancerPreviewObject preview)
{
_ModelDropPreview = preview;
_ModelDropHandler ??= (gameObject, isDrop) =>
{
if (!gameObject.TryGetComponent<Animator>(out _))
return false;
if (isDrop)
_ModelDropPreview.OriginalObject = gameObject.transform;
return true;
};
_ModelDropPreview = null;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,352 @@
// 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;
using Object = UnityEngine.Object;
namespace Animancer.Editor.Previews
{
/// <summary>[Editor-Only] Utility for rendering previews of animated objects.</summary>
/// <remarks>Parts of this class are based on Unity's <see cref="MeshPreview"/>.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/AnimancerPreviewRenderer
[Serializable]
public class AnimancerPreviewRenderer :
AnimancerPreviewObject.IEventHandler,
IDisposable
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[NonSerialized] private PreviewRenderUtility _PreviewRenderUtility;
[NonSerialized] private Light[] _PreviewLights;
[SerializeField] private AnimancerPreviewObject _PreviewObject;
[SerializeField] private Vector3 _OrthographicPosition = new(0.5f, 0.5f, -1);
[SerializeField] private Vector2 _PreviewDirection = new(135, -30);
[SerializeField] private Vector2 _LightDirection = new(-40, -40);
[SerializeField] private Vector3 _PivotPositionOffset;
[SerializeField] private float _ZoomFactor = 1f;
/// <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; }
/************************************************************************************************************************/
/// <summary>[<see cref="SerializeField"/>] The object being previewed.</summary>
public AnimancerPreviewObject PreviewObject
{
get
{
InitializePreviewRenderUtility();
return AnimancerPreviewObject.Initialize(ref _PreviewObject, this, PreviewSceneRoot);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>Cleans up this renderer.</summary>
public void Dispose()
{
_PreviewObject?.Dispose();
CleanupPreviewRenderUtility();
}
/************************************************************************************************************************/
/// <summary>Calles when the <see cref="TransitionPreviewSettings.SceneEnvironment"/> is changed.</summary>
private void OnEnvironmentPrefabChanged()
{
Object.DestroyImmediate(EnvironmentInstance);
var prefab = TransitionPreviewSettings.SceneEnvironment;
if (prefab != null)
EnvironmentInstance = Object.Instantiate(prefab, PreviewSceneRoot);
}
/************************************************************************************************************************/
/// <inheritdoc/>
void AnimancerPreviewObject.IEventHandler.OnInstantiateObject()
{
_PivotPositionOffset = _PreviewObject.InstanceBounds.center;
}
/// <inheritdoc/>
void AnimancerPreviewObject.IEventHandler.OnSetSelectedAnimator() { }
/// <inheritdoc/>
void AnimancerPreviewObject.IEventHandler.OnCreateGraph() { }
/************************************************************************************************************************/
#region GUI
/************************************************************************************************************************/
/// <summary>Draws the preview.</summary>
public void DoGUI(Rect area, GUIStyle background)
{
if (!DrawIsRenderTextureSupported(area))
return;
var currentEvent = Event.current;
HandleCameraControlEvent(area, currentEvent);
if (currentEvent.type == EventType.Repaint)
DrawPreview(area, background);
}
/************************************************************************************************************************/
/// <summary>Shows a warning if the current device doesn't support render textures.</summary>
private bool DrawIsRenderTextureSupported(Rect area)
{
if (ShaderUtil.hardwareSupportsRectRenderTexture)
return true;
EditorGUI.DropShadowLabel(
area,
"Device doesn't support Render Textures\nUnable to render previews");
return false;
}
/************************************************************************************************************************/
/// <summary>Initializes the <see cref="_PreviewRenderUtility"/>.</summary>
private void InitializePreviewRenderUtility()
{
if (_PreviewRenderUtility != null)
return;
_PreviewRenderUtility = new();
_PreviewRenderUtility.camera.fieldOfView = 30;
_PreviewRenderUtility.camera.allowHDR = false;
_PreviewRenderUtility.camera.allowMSAA = false;
_PreviewRenderUtility.ambientColor = Grey(0.1f, 0);
_PreviewLights = _PreviewRenderUtility.lights;
_PreviewLights[0].intensity = 1.4f;
_PreviewLights[0].transform.rotation = Quaternion.Euler(40, 40, 0);
_PreviewLights[1].intensity = 1.4f;
var root = AnimancerPreviewObject.CreateEmpty(nameof(AnimancerPreviewRenderer));
_PreviewRenderUtility.AddSingleGO(root);
PreviewSceneRoot = root.transform;
OnEnvironmentPrefabChanged();
}
/************************************************************************************************************************/
/// <summary>Cleans up the <see cref="_PreviewRenderUtility"/>.</summary>
private void CleanupPreviewRenderUtility()
{
_PreviewRenderUtility?.Cleanup();
_PreviewRenderUtility = null;
_PreviewLights = null;
}
/************************************************************************************************************************/
/// <summary>Updates and renders the preview.</summary>
private void DrawPreview(Rect area, GUIStyle background)
{
InitializePreviewRenderUtility();
_PreviewRenderUtility.BeginPreview(area, background);
UpdatePreviewRenderUtility();
_PreviewRenderUtility.Render(true, true);
_PreviewRenderUtility.EndAndDrawPreview(area);
}
/************************************************************************************************************************/
/// <summary>Updates the preview rendering details.</summary>
private void UpdatePreviewRenderUtility()
{
var previewObject = PreviewObject;
if (previewObject.InstanceObject == null)
return;
var rotation = Quaternion.Euler(_PreviewDirection.y, 0, 0) * Quaternion.Euler(0, _PreviewDirection.x, 0);
previewObject.InstanceObject.rotation = rotation;
var size = previewObject.InstanceBounds.extents.magnitude;
var position = _ZoomFactor * -4f * size * Vector3.forward + _PivotPositionOffset;
var camera = _PreviewRenderUtility.camera;
camera.transform.SetPositionAndRotation(position, Quaternion.identity);
camera.nearClipPlane = 0.0001f;
camera.farClipPlane = 1000;
camera.orthographic = false;
var lights = _PreviewLights;
lights[0].intensity = 1.1f;
lights[0].transform.rotation = Quaternion.Euler(-_LightDirection.y, -_LightDirection.x, 0);
lights[1].intensity = 1.1f;
lights[1].transform.rotation = Quaternion.Euler(_LightDirection.y, _LightDirection.x, 0);
_PreviewRenderUtility.ambientColor = Grey(0.1f, 0);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Camera Controls
/************************************************************************************************************************/
private static readonly int CameraControlsHint = "CameraControls".GetHashCode();
/// <summary>Handles GUI events for controlling the preview camera.</summary>
private void HandleCameraControlEvent(Rect area, Event currentEvent)
{
if (currentEvent.button == 1)
{
if (currentEvent.alt)
_LightDirection = Drag2D(_LightDirection, area);// Could draw some lines to show the light directions.
else
_PreviewDirection = Drag2D(_PreviewDirection, area);
}
var control = new GUIControl(area, currentEvent, CameraControlsHint);
switch (control.EventType)
{
case EventType.ScrollWheel:
HandleScrollZoomEvent(area, currentEvent);
break;
case EventType.MouseDown:
if (currentEvent.button <= 0 || currentEvent.button == 2)
control.TryUseMouseDown();
break;
case EventType.MouseUp:
control.TryUseMouseUp();
break;
case EventType.MouseDrag:
if (control.TryUseHotControl())
HandleDragPanEvent(area, currentEvent);
break;
case EventType.ValidateCommand:
case EventType.ExecuteCommand:
switch (currentEvent.commandName)
{
case Commands.FrameSelected:
case Commands.FrameSelectedWithLock:
FrameTarget();
currentEvent.Use();
break;
}
break;
}
}
/************************************************************************************************************************/
private static readonly int SliderHash = "Slider".GetHashCode();
/// <summary>Handles drag input within a given `area`.</summary>
/// <remarks>Copied from Unity's <see cref="PreviewGUI.Drag2D"/>.</remarks>
public static Vector2 Drag2D(Vector2 scrollPosition, Rect area)
{
var control = new GUIControl(area, SliderHash);
switch (control.EventType)
{
case EventType.MouseDown:
if (control.TryUseMouseDown() && area.width > 50)
EditorGUIUtility.SetWantsMouseJumping(1);
break;
case EventType.MouseUp:
if (control.TryUseMouseUp())
EditorGUIUtility.SetWantsMouseJumping(0);
break;
case EventType.MouseDrag:
if (control.TryUseHotControl())
{
var multiplier = control.Event.shift ? 3 : 1;
var size = Mathf.Min(area.width, area.height);
scrollPosition -= control.Event.delta * multiplier / size * 140;
}
break;
}
return scrollPosition;
}
/************************************************************************************************************************/
/// <summary>Handles a mouse scroll event to zoom the preview camera.</summary>
private void HandleScrollZoomEvent(Rect area, Event currentEvent)
{
var delta = HandleUtility.niceMouseDeltaZoom * -0.025f;
var zoom = _ZoomFactor * (1 + delta);
zoom = Mathf.Clamp(zoom, 0.1f, 10);
var vector = new Vector2(
currentEvent.mousePosition.x / area.width,
1 - currentEvent.mousePosition.y / area.height);
var origin = _PreviewRenderUtility.camera.ViewportToWorldPoint(vector);
var direction = _OrthographicPosition - origin;
var position = origin + direction * (zoom / _ZoomFactor);
_PreviewRenderUtility.camera.transform.position = position;
_ZoomFactor = zoom;
currentEvent.Use();
}
/************************************************************************************************************************/
/// <summary>Handles a mouse drag event to pan the preview camera.</summary>
private void HandleDragPanEvent(Rect area, Event currentEvent)
{
var camera = _PreviewRenderUtility.camera;
var direction = new Vector3(
-currentEvent.delta.x * camera.pixelWidth / area.width,
currentEvent.delta.y * camera.pixelHeight / area.height,
0);
var position = camera.WorldToScreenPoint(_PivotPositionOffset);
position += direction;
direction = camera.ScreenToWorldPoint(position) - _PivotPositionOffset;
_PivotPositionOffset += direction;
}
/************************************************************************************************************************/
/// <summary>Frames the preview object in the middle of the camera.</summary>
private void FrameTarget()
{
_ZoomFactor = 1f;
_OrthographicPosition = new(0.5f, 0.5f, -1f);
_PivotPositionOffset = default;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,79 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEngine;
namespace Animancer.Editor.Previews
{
/// <summary>[Editor-Only]
/// An <see cref="IAnimancerComponent"/> which isn't actually a <see cref="Component"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/DummyAnimancerComponent
public class DummyAnimancerComponent : IAnimancerComponent
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="DummyAnimancerComponent"/>.</summary>
public DummyAnimancerComponent(Animator animator, AnimancerGraph playable)
{
Animator = animator;
Graph = playable;
InitialUpdateMode = animator.updateMode;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public bool enabled => true;
/// <inheritdoc/>
public GameObject gameObject => Animator.gameObject;
/// <inheritdoc/>
public Animator Animator { get; set; }
/// <inheritdoc/>
public AnimancerGraph Graph { get; private set; }
/// <inheritdoc/>
public bool IsGraphInitialized => true;
/// <inheritdoc/>
public bool ResetOnDisable => false;
/// <inheritdoc/>
public AnimatorUpdateMode UpdateMode
{
get => Animator.updateMode;
set => Animator.updateMode = value;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public object GetKey(AnimationClip clip) => clip;
/************************************************************************************************************************/
/// <inheritdoc/>
public string AnimatorFieldName => null;
/// <inheritdoc/>
public string ActionOnDisableFieldName => null;
/// <inheritdoc/>
public AnimatorUpdateMode? InitialUpdateMode { get; private set; }
/************************************************************************************************************************/
/// <summary>Describes this and the <see cref="Animator"/>.</summary>
public override string ToString()
=> $"{nameof(DummyAnimancerComponent)}({(Animator != null ? Animator.name : "Destroyed")})";
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,350 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor.Previews
{
/// <summary>[Editor-Only] Utility for playing through transition previews.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/TransitionPreviewPlayer
[Serializable]
public class TransitionPreviewPlayer : IDisposable
{
/************************************************************************************************************************/
[SerializeReference] private ITransition _FromTransition;
[SerializeReference] private ITransition _ToTransition;
/// <summary>The animation to play first.</summary>
public ref ITransition FromTransition
=> ref _FromTransition;
/// <summary>The animation to transition into.</summary>
public ref ITransition ToTransition
=> ref _ToTransition;
/************************************************************************************************************************/
private AnimancerGraph _Graph;
/// <summary>The graph used to play the animations.</summary>
public AnimancerGraph Graph
{
get => _Graph;
set
{
_Graph = value;
Evaluate();
}
}
/************************************************************************************************************************/
/// <summary>
/// The minimum amount of time to play the <see cref="FromTransition"/>
/// before the <see cref="ToTransition"/> starts (in seconds).
/// </summary>
public float FromDuration { get; set; }
/// <summary>
/// The minimum amount of time to continue playing
/// after the <see cref="ToTransition"/> started (in seconds).
/// </summary>
public float ToDuration { get; set; }
/// <summary>The speed at which the preview plays.</summary>
public float Speed { get; set; } = 1;
/************************************************************************************************************************/
private float _FadeDuration = float.NaN;
/// <summary>The <see cref="ITransition.FadeDuration"/>.</summary>
/// <remarks><see cref="float.NaN"/> uses the value from the <see cref="ToTransition"/>.</remarks>
public float FadeDuration
{
get => float.IsNaN(_FadeDuration) && ToTransition.IsValid()
? ToTransition.FadeDuration
: _FadeDuration;
set => _FadeDuration = value;
}
/************************************************************************************************************************/
private float _NormalizedStartTime = float.NaN;
/// <summary>The <see cref="ITransition.NormalizedStartTime"/>.</summary>
/// <remarks><see cref="float.NaN"/> uses the value from the <see cref="ToTransition"/>.</remarks>
public float NormalizedStartTime
{
get => float.IsNaN(_NormalizedStartTime) && ToTransition.IsValid()
? ToTransition.NormalizedStartTime
: _NormalizedStartTime;
set => _NormalizedStartTime = value;
}
/************************************************************************************************************************/
/// <summary>The lowest allowed <see cref="CurrentTime"/>.</summary>
/// <remarks>
/// This is the lower of the negative <see cref="FromDuration"/>
/// or the negative duration of the <see cref="FromTransition"/>.
/// </remarks>
public float MinTime { get; private set; }
/// <summary>The highest allowed <see cref="CurrentTime"/>.</summary>
/// <remarks>
/// This is the higher of the <see cref="ToDuration"/> or <see cref="FadeDuration"/>
/// or the duration of the <see cref="ToTransition"/>.
/// </remarks>
public float MaxTime { get; private set; }
/************************************************************************************************************************/
/// <summary>Recalculated the <see cref="MinTime"/> and <see cref="MaxTime"/>.</summary>
public void RecalculateTimeBounds()
{
MinTime = -FromDuration;
if (_FromTransition.IsValid() &&
AnimancerUtilities.TryCalculateDuration(_FromTransition, out var duration))
MinTime = Math.Min(MinTime, -duration);
MaxTime = ToDuration;
var fadeDuration = FadeDuration;
if (!float.IsNaN(fadeDuration))
MaxTime = Math.Max(ToDuration, fadeDuration);
if (_ToTransition.IsValid() &&
AnimancerUtilities.TryCalculateDuration(_ToTransition, out duration))
MaxTime = Math.Max(MaxTime, duration);
}
/************************************************************************************************************************/
/// <summary>Converts normalized time to seconds.</summary>
public float LerpTimeUnclamped(float normalizedTime)
=> Mathf.LerpUnclamped(MinTime, MaxTime, normalizedTime);
/// <summary>Converts seconds to normalized time.</summary>
public float InverseLerpTimeUnclamped(float time)
=> AnimancerUtilities.InverseLerpUnclamped(MinTime, MaxTime, time);
/************************************************************************************************************************/
private float _CurrentTime = float.NaN;
/// <summary>The amount of time that has passed since the <see cref="ToTransition"/> started (in seconds).</summary>
/// <remarks>This value goes from the <see cref="MinTime"/> to the <see cref="MaxTime"/>.</remarks>
public float CurrentTime
{
get => float.IsNaN(_CurrentTime)
? MinTime
: _CurrentTime;
set
{
_CurrentTime = value;
Evaluate();
}
}
/// <summary>The amount of time that has passed since the <see cref="ToTransition"/> started (normalized).</summary>
/// <remarks>0 is at the <see cref="MinTime"/> and 1 is at the <see cref="MaxTime"/>.</remarks>
public float NormalizedTime
{
get => InverseLerpTimeUnclamped(CurrentTime);
set => CurrentTime = LerpTimeUnclamped(value);
}
/************************************************************************************************************************/
private bool _IsPlaying;
/// <summary>Is the preview currently playing?</summary>
public bool IsPlaying
{
get => _IsPlaying;
set
{
if (_IsPlaying == value)
return;
_IsPlaying = value;
if (_IsPlaying)
{
_LastUpdateTime = TimeSinceStartup;
EditorApplication.update += Update;
}
else
{
EditorApplication.update -= Update;
}
}
}
/// <summary>Cleans up this player.</summary>
public void Dispose()
=> IsPlaying = false;
/************************************************************************************************************************/
/// <summary><see cref="EditorApplication.timeSinceStartup"/></summary>
private static double TimeSinceStartup
=> EditorApplication.timeSinceStartup;
private double _LastUpdateTime;
/// <summary>Updates the preview time while playing.</summary>
private void Update()
{
if (Graph == null || !Graph.IsValidOrDispose())
{
EditorApplication.update -= Update;
return;
}
var time = TimeSinceStartup;
var deltaTime = time - _LastUpdateTime;
_LastUpdateTime = time;
CurrentTime += ((float)deltaTime) * Speed;
AnimancerGUI.RepaintEverything();
}
/************************************************************************************************************************/
/// <summary>Applies the animations at the <see cref="CurrentTime"/>.</summary>
private void Evaluate()
{
if (Graph == null)
return;
Graph.PauseGraph();
Graph.Stop();
if (_FromTransition.IsValid())
{
if (_ToTransition.IsValid())
{
Apply(CurrentTime, _FromTransition, _ToTransition);
}
else
{
var minTime = MinTime;
Apply(CurrentTime - minTime, -minTime, _FromTransition, false);
}
}
else
{
if (_ToTransition.IsValid())
Apply(CurrentTime, MaxTime, _ToTransition, true);
else
return;
}
Graph.Evaluate();
}
/************************************************************************************************************************/
/// <summary>Applies the animations at the `currentTime`.</summary>
private void Apply(
float currentTime,
ITransition from,
ITransition to)
{
var layer = Graph.Layers[0];
// Playing From.
if (currentTime < 0)
{
var state = layer.Play(from);
state.Time += state.NormalizedEndTime * state.Length + currentTime;
return;
}
var maxTime = MaxTime;
// Fading.
if (currentTime < maxTime)
{
var state = layer.Play(from);
state.Time += state.NormalizedEndTime * state.Length + currentTime;
state = layer.Play(to, FadeDuration, to.FadeMode);
var normalizedStartTime = NormalizedStartTime;
if (!float.IsNaN(normalizedStartTime))
state.NormalizedTime = normalizedStartTime;
state.Time += currentTime;
var fade = state.FadeGroup;
if (fade != null)
{
fade.NormalizedTime += currentTime * fade.FadeSpeed;
fade.ApplyWeights();
}
}
// Playing To.
else
{
var state = layer.Play(to);
var normalizedStartTime = NormalizedStartTime;
if (!float.IsNaN(normalizedStartTime))
state.NormalizedTime = normalizedStartTime;
state.Time += currentTime;
// Finished.
if (currentTime >= maxTime)
{
_CurrentTime = MinTime;
state.Time = _CurrentTime;
}
}
}
/************************************************************************************************************************/
/// <summary>Applies the animation at the `currentTime`.</summary>
private void Apply(
float currentTime,
float endTime,
ITransition transition,
bool applyNormalizedStartTime)
{
var state = Graph.Layers[0].Play(transition);
if (currentTime < endTime)// Playing.
{
state.Time = currentTime;
if (applyNormalizedStartTime)
{
var normalizedStartTime = NormalizedStartTime;
if (!float.IsNaN(normalizedStartTime))
state.NormalizedTime += normalizedStartTime;
}
}
else// Finished.
{
_CurrentTime = MinTime;
state.Time = _CurrentTime;
}
}
/************************************************************************************************************************/
}
}
#endif

View File

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