293 lines
9.2 KiB
C#
293 lines
9.2 KiB
C#
using System;
|
|
using System.Linq;
|
|
using DG.Tweening;
|
|
using UnityEditor;
|
|
using UnityEditorInternal;
|
|
using UnityEngine;
|
|
|
|
namespace Dott.Editor
|
|
{
|
|
[CustomEditor(typeof(DOTweenTimeline))]
|
|
public class DOTweenTimelineEditor : UnityEditor.Editor
|
|
{
|
|
private DOTweenTimeline Timeline => (DOTweenTimeline)target;
|
|
|
|
private DottController controller;
|
|
private DottSelection selection;
|
|
private DottView view;
|
|
private float? dragTweenTimeShift;
|
|
private IDOTweenAnimation[] animations;
|
|
|
|
public override bool RequiresConstantRepaint() => true;
|
|
|
|
public override void OnInspectorGUI()
|
|
{
|
|
Timeline.OnValidate();
|
|
|
|
animations = Timeline.GetComponents<MonoBehaviour>().Select(DottAnimation.FromComponent).Where(animation => animation != null).ToArray();
|
|
selection.Validate(animations);
|
|
|
|
view.DrawTimeline(animations, selection.Animation, controller.IsPlaying, controller.ElapsedTime,
|
|
controller.Loop, controller.Paused);
|
|
|
|
if (selection.Animation != null)
|
|
{
|
|
view.DrawInspector(selection.GetAnimationEditor());
|
|
}
|
|
|
|
if (controller.Paused && Event.current.type == EventType.Repaint)
|
|
{
|
|
controller.GoTo(animations, controller.ElapsedTime);
|
|
}
|
|
|
|
// Smoother ui updates
|
|
if (controller.IsPlaying || view.IsTimeDragging || view.IsTweenDragging)
|
|
{
|
|
Repaint();
|
|
}
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
controller = new DottController();
|
|
selection = new DottSelection();
|
|
view = new DottView();
|
|
|
|
view.IsSnapping = EditorPrefs.GetBool("Dott.Snap", true);
|
|
|
|
view.TweenSelected += OnTweenSelected;
|
|
view.TweenDrag += DragSelectedAnimation;
|
|
|
|
view.TimeDragEnd += OnTimeDragEnd;
|
|
view.TimeDrag += GoTo;
|
|
view.PreviewDisabled += controller.Stop;
|
|
|
|
view.AddClicked += AddAnimation;
|
|
view.AddMore += AddMore;
|
|
view.RemoveClicked += Remove;
|
|
view.DuplicateClicked += Duplicate;
|
|
|
|
view.PlayClicked += Play;
|
|
view.StopClicked += controller.Stop;
|
|
view.LoopToggled += ToggleLoop;
|
|
view.SnapToggled += ToggleSnap;
|
|
|
|
view.InspectorUpButtonClicked += MoveSelectedUp;
|
|
view.InspectorDownButtonClicked += MoveSelectedDown;
|
|
|
|
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
view.TweenSelected -= OnTweenSelected;
|
|
view.TweenDrag -= DragSelectedAnimation;
|
|
|
|
view.TimeDragEnd -= OnTimeDragEnd;
|
|
view.TimeDrag -= GoTo;
|
|
view.PreviewDisabled -= controller.Stop;
|
|
|
|
view.AddClicked -= AddAnimation;
|
|
view.AddMore -= AddMore;
|
|
view.RemoveClicked -= Remove;
|
|
view.DuplicateClicked -= Duplicate;
|
|
|
|
view.PlayClicked -= Play;
|
|
view.StopClicked -= controller.Stop;
|
|
view.LoopToggled -= ToggleLoop;
|
|
view.SnapToggled -= ToggleSnap;
|
|
|
|
view.InspectorUpButtonClicked -= MoveSelectedUp;
|
|
view.InspectorDownButtonClicked -= MoveSelectedDown;
|
|
|
|
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
|
|
|
controller.Dispose();
|
|
controller = null;
|
|
|
|
selection.Dispose();
|
|
selection = null;
|
|
|
|
view = null;
|
|
|
|
animations = null;
|
|
}
|
|
|
|
private void Play()
|
|
{
|
|
controller.Play(animations);
|
|
}
|
|
|
|
private void GoTo(float time)
|
|
{
|
|
controller.GoTo(animations, time);
|
|
}
|
|
|
|
private void OnTimeDragEnd(Event mouseEvent)
|
|
{
|
|
const int mouseButtonMiddle = 2;
|
|
if (mouseEvent.IsRightMouseButton() || mouseEvent.button == mouseButtonMiddle)
|
|
{
|
|
controller.Stop();
|
|
return;
|
|
}
|
|
|
|
controller.Pause();
|
|
}
|
|
|
|
private void DragSelectedAnimation(float time)
|
|
{
|
|
// Sometimes (e.g., for Frame) undo is not recorded when dragging, so we force it
|
|
Undo.RecordObject(selection.Animation.Component, $"Drag {selection.Animation.Label}");
|
|
|
|
dragTweenTimeShift ??= time - selection.Animation.Delay;
|
|
|
|
var delay = time - dragTweenTimeShift.Value;
|
|
delay = Mathf.Max(0, delay);
|
|
delay = TrySnapTime(selection.Animation, delay, view.TimeScale);
|
|
delay = (float)Math.Round(delay, 2);
|
|
selection.Animation.Delay = delay;
|
|
|
|
// Complete undo record
|
|
Undo.FlushUndoRecordObjects();
|
|
}
|
|
|
|
private float TrySnapTime(IDOTweenAnimation target, float newDelay, float timeScale)
|
|
{
|
|
if (!IsSnapActive() || animations.Length < 2)
|
|
{
|
|
return newDelay;
|
|
}
|
|
|
|
var snapThreshold = 1f / 40f / timeScale;
|
|
var snapPoints = animations
|
|
.Where(animation => animation.Component != target.Component)
|
|
.SelectMany(animation => Enumerable.Empty<float>().Append(animation.Delay).Append(animation.Delay + animation.Duration * Mathf.Max(1, animation.Loops)))
|
|
.Distinct().ToArray();
|
|
|
|
var snapTime = snapPoints.OrderBy(snapPoint => Mathf.Abs(snapPoint - newDelay)).First();
|
|
if (Math.Abs(snapTime - newDelay) < snapThreshold)
|
|
{
|
|
return snapTime;
|
|
}
|
|
|
|
if (target.Loops == -1)
|
|
{
|
|
return newDelay;
|
|
}
|
|
|
|
var targetFullDuration = target.Duration * Mathf.Max(1, target.Loops);
|
|
var newEndTime = newDelay + targetFullDuration;
|
|
var snapEndTime = snapPoints.OrderBy(snapPoint => Mathf.Abs(snapPoint - newEndTime)).First();
|
|
if (Math.Abs(snapEndTime - newEndTime) < snapThreshold)
|
|
{
|
|
return snapEndTime - targetFullDuration;
|
|
}
|
|
|
|
return newDelay;
|
|
}
|
|
|
|
private bool IsSnapActive()
|
|
{
|
|
var reverseSnap = Event.current.control;
|
|
var snapEnabled = view.IsSnapping;
|
|
return reverseSnap ? !snapEnabled : snapEnabled;
|
|
}
|
|
|
|
private void OnTweenSelected(IDOTweenAnimation animation)
|
|
{
|
|
selection.Set(animation);
|
|
// clear focus to correctly update inspector
|
|
GUIUtility.keyboardControl = 0;
|
|
|
|
dragTweenTimeShift = null;
|
|
}
|
|
|
|
private void AddAnimation()
|
|
{
|
|
Add(Timeline, typeof(DOTweenAnimation));
|
|
}
|
|
|
|
private void AddMore(Type type)
|
|
{
|
|
Add(Timeline, type);
|
|
}
|
|
|
|
private void Add(DOTweenTimeline timeline, Type type)
|
|
{
|
|
var component = ObjectFactory.AddComponent(timeline.gameObject, type);
|
|
var animation = DottAnimation.FromComponent(component);
|
|
if (controller.Paused)
|
|
{
|
|
animation!.Delay = (float)Math.Round(controller.ElapsedTime, 2);
|
|
}
|
|
|
|
selection.Set(animation);
|
|
}
|
|
|
|
private void Remove()
|
|
{
|
|
Undo.DestroyObjectImmediate(selection.Animation.Component);
|
|
selection.Clear();
|
|
}
|
|
|
|
private void Duplicate()
|
|
{
|
|
Undo.SetCurrentGroupName($"Duplicate {selection.Animation.Label}");
|
|
|
|
var source = selection.Animation.Component;
|
|
|
|
var dest = Undo.AddComponent(source.gameObject, source.GetType());
|
|
EditorUtility.CopySerialized(source, dest);
|
|
|
|
var animation = DottAnimation.FromComponent(dest);
|
|
selection.Set(animation);
|
|
|
|
var components = source.GetComponents<Component>();
|
|
var targetIndex = Array.IndexOf(components, source) + 1;
|
|
var index = Array.IndexOf(components, dest);
|
|
while (index > targetIndex)
|
|
{
|
|
ComponentUtility.MoveComponentUp(dest);
|
|
index--;
|
|
}
|
|
}
|
|
|
|
private void ToggleLoop(bool value)
|
|
{
|
|
controller.Loop = value;
|
|
}
|
|
|
|
private void ToggleSnap()
|
|
{
|
|
EditorPrefs.SetBool("Dott.Snap", view.IsSnapping);
|
|
}
|
|
|
|
private void MoveSelectedUp()
|
|
{
|
|
var index = animations.FindIndex(animation => animation.Component == selection.Animation.Component);
|
|
if (index > 0)
|
|
{
|
|
ComponentUtility.MoveComponentUp(selection.Animation.Component);
|
|
}
|
|
}
|
|
|
|
private void MoveSelectedDown()
|
|
{
|
|
var index = animations.FindIndex(animation => animation.Component == selection.Animation.Component);
|
|
if (index < animations.Length - 1)
|
|
{
|
|
ComponentUtility.MoveComponentDown(selection.Animation.Component);
|
|
}
|
|
}
|
|
|
|
private void OnPlayModeStateChanged(PlayModeStateChange stateChange)
|
|
{
|
|
// Rewind tweens before play mode. OnDisable is too late (runs after dirty state is saved)
|
|
if (stateChange == PlayModeStateChange.ExitingEditMode)
|
|
{
|
|
controller.Stop();
|
|
}
|
|
}
|
|
}
|
|
} |