1219 lines
39 KiB
C#
1219 lines
39 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditorInternal;
|
|
#if UNITY_2021_2_OR_NEWER
|
|
using UnityEditor.SceneManagement;
|
|
#else
|
|
using UnityEditor.Experimental.SceneManagement;
|
|
#endif
|
|
using UnityEngine;
|
|
using UnityEngine.Playables;
|
|
using UnityEngine.Timeline;
|
|
#if !UNITY_2020_2_OR_NEWER
|
|
using UnityEngine.Experimental.Animations;
|
|
#endif
|
|
using UnityEngine.Animations;
|
|
using Object = System.Object;
|
|
|
|
namespace UnityEditor.Timeline
|
|
{
|
|
delegate bool PendingUpdateDelegate(WindowState state, Event currentEvent);
|
|
|
|
/// <summary>
|
|
/// Interface for faking purposes
|
|
/// </summary>
|
|
interface IWindowState
|
|
{
|
|
ISequenceState masterSequence { get; }
|
|
ISequenceState editSequence { get; }
|
|
IEnumerable<ISequenceState> allSequences { get; }
|
|
PlayRange playRange { get; set; }
|
|
void SetCurrentSequence(TimelineAsset timelineAsset, PlayableDirector director, TimelineClip hostClip);
|
|
void PopSequencesUntilCount(int count);
|
|
IEnumerable<SequenceContext> GetSubSequences();
|
|
void SetPlaying(bool start);
|
|
}
|
|
class WindowState : IWindowState
|
|
{
|
|
const int k_TimeCodeTextFieldId = 3790;
|
|
|
|
readonly TimelineWindow m_Window;
|
|
bool m_Recording;
|
|
|
|
readonly SpacePartitioner m_SpacePartitioner = new SpacePartitioner();
|
|
readonly SpacePartitioner m_HeaderSpacePartitioner = new SpacePartitioner();
|
|
readonly List<Manipulator> m_CaptureSession = new List<Manipulator>();
|
|
|
|
int m_DirtyStamp;
|
|
float m_BindingAreaWidth = WindowConstants.defaultBindingAreaWidth;
|
|
|
|
bool m_MustRebuildGraph;
|
|
|
|
float m_LastTime;
|
|
|
|
readonly PropertyCollector m_PropertyCollector = new PropertyCollector();
|
|
|
|
static AnimationModeDriver s_PreviewDriver;
|
|
PreviewedBindings<Animator> m_PreviewedAnimators;
|
|
List<Component> m_PreviewedComponents;
|
|
IEnumerable<IAnimationWindowPreview> previewedComponents =>
|
|
m_PreviewedComponents.Where(component => component != null).Cast<IAnimationWindowPreview>();
|
|
|
|
public static double kTimeEpsilon { get { return TimeUtility.kTimeEpsilon; } }
|
|
public static readonly float kMaxShownTime = (float)TimeUtility.k_MaxTimelineDurationInSeconds;
|
|
|
|
static readonly ISequenceState k_NullSequenceState = new NullSequenceState();
|
|
|
|
// which tracks are armed for record - only one allowed per 'actor'
|
|
Dictionary<TrackAsset, TrackAsset> m_ArmedTracks = new Dictionary<TrackAsset, TrackAsset>();
|
|
|
|
TimelineWindow.TimelineWindowPreferences m_Preferences;
|
|
|
|
List<PendingUpdateDelegate> m_OnStartFrameUpdates;
|
|
List<PendingUpdateDelegate> m_OnEndFrameUpdates;
|
|
|
|
readonly SequenceHierarchy m_SequenceHierarchy;
|
|
|
|
public event Action<WindowState, Event> windowOnGuiStarted;
|
|
|
|
public event Action<bool> OnPlayStateChange;
|
|
public event System.Action OnDirtyStampChange;
|
|
public event System.Action OnRebuildGraphChange;
|
|
public event System.Action OnTimeChange;
|
|
public event System.Action OnRecordingChange;
|
|
|
|
public event System.Action OnBeforeSequenceChange;
|
|
public event System.Action OnAfterSequenceChange;
|
|
|
|
public WindowState(TimelineWindow w, SequenceHierarchy hierarchy)
|
|
{
|
|
m_Window = w;
|
|
m_Preferences = w.preferences;
|
|
hierarchy.Init(this);
|
|
m_SequenceHierarchy = hierarchy;
|
|
TimelinePlayable.muteAudioScrubbing = muteAudioScrubbing;
|
|
}
|
|
|
|
public static AnimationModeDriver previewDriver
|
|
{
|
|
get
|
|
{
|
|
if (s_PreviewDriver == null)
|
|
{
|
|
s_PreviewDriver = ScriptableObject.CreateInstance<AnimationModeDriver>();
|
|
AnimationPreviewUtilities.s_PreviewDriver = s_PreviewDriver;
|
|
}
|
|
return s_PreviewDriver;
|
|
}
|
|
}
|
|
|
|
public EditorWindow editorWindow
|
|
{
|
|
get { return m_Window; }
|
|
}
|
|
|
|
public ISequenceState editSequence
|
|
{
|
|
get
|
|
{
|
|
// Using "null" ISequenceState to avoid checking against null all the time.
|
|
// This *should* be removed in a phase 2 of refactoring, where we make sure
|
|
// to pass around the correct state object instead of letting clients dig
|
|
// into the WindowState for whatever they want.
|
|
return m_SequenceHierarchy.editSequence ?? k_NullSequenceState;
|
|
}
|
|
}
|
|
|
|
public ISequenceState masterSequence
|
|
{
|
|
get { return m_SequenceHierarchy.masterSequence ?? k_NullSequenceState; }
|
|
}
|
|
|
|
public ISequenceState referenceSequence
|
|
{
|
|
get { return timeReferenceMode == TimeReferenceMode.Local ? editSequence : masterSequence; }
|
|
}
|
|
|
|
public IEnumerable<ISequenceState> allSequences
|
|
{
|
|
get { return m_SequenceHierarchy.allSequences; }
|
|
}
|
|
|
|
public bool rebuildGraph
|
|
{
|
|
get { return m_MustRebuildGraph; }
|
|
set { SyncNotifyValue(ref m_MustRebuildGraph, value, OnRebuildGraphChange); }
|
|
}
|
|
|
|
public float mouseDragLag { get; set; }
|
|
|
|
public SpacePartitioner spacePartitioner
|
|
{
|
|
get { return m_SpacePartitioner; }
|
|
}
|
|
|
|
public SpacePartitioner headerSpacePartitioner
|
|
{
|
|
get { return m_HeaderSpacePartitioner; }
|
|
}
|
|
|
|
public List<Manipulator> captured
|
|
{
|
|
get { return m_CaptureSession; }
|
|
}
|
|
|
|
public void AddCaptured(Manipulator manipulator)
|
|
{
|
|
if (!m_CaptureSession.Contains(manipulator))
|
|
m_CaptureSession.Add(manipulator);
|
|
}
|
|
|
|
public void RemoveCaptured(Manipulator manipulator)
|
|
{
|
|
m_CaptureSession.Remove(manipulator);
|
|
}
|
|
|
|
public bool isJogging { get; set; }
|
|
|
|
public int viewStateHash { get; private set; }
|
|
|
|
public float bindingAreaWidth
|
|
{
|
|
get { return m_BindingAreaWidth; }
|
|
set { m_BindingAreaWidth = value; }
|
|
}
|
|
|
|
public float sequencerHeaderWidth
|
|
{
|
|
get { return editSequence.viewModel.sequencerHeaderWidth; }
|
|
set
|
|
{
|
|
editSequence.viewModel.sequencerHeaderWidth = Mathf.Clamp(value, WindowConstants.minHeaderWidth, WindowConstants.maxHeaderWidth);
|
|
}
|
|
}
|
|
|
|
public float mainAreaWidth { get; set; }
|
|
|
|
public float trackScale
|
|
{
|
|
get { return editSequence.viewModel.trackScale; }
|
|
set
|
|
{
|
|
editSequence.viewModel.trackScale = value;
|
|
m_Window.treeView.CalculateRowRects();
|
|
}
|
|
}
|
|
|
|
public int dirtyStamp
|
|
{
|
|
get { return m_DirtyStamp; }
|
|
private set { SyncNotifyValue(ref m_DirtyStamp, value, OnDirtyStampChange); }
|
|
}
|
|
|
|
public bool showQuadTree { get; set; }
|
|
|
|
public bool canRecord
|
|
{
|
|
get { return AnimationMode.InAnimationMode(previewDriver) || !AnimationMode.InAnimationMode(); }
|
|
}
|
|
|
|
public bool recording
|
|
{
|
|
get
|
|
{
|
|
if (!previewMode)
|
|
m_Recording = false;
|
|
return m_Recording;
|
|
}
|
|
// set can only be used to disable recording
|
|
set
|
|
{
|
|
if (ignorePreview)
|
|
return;
|
|
|
|
// force preview mode on
|
|
if (value)
|
|
previewMode = true;
|
|
|
|
bool newValue = value;
|
|
if (!previewMode)
|
|
newValue = false;
|
|
|
|
if (newValue && m_ArmedTracks.Count == 0)
|
|
{
|
|
Debug.LogError("Cannot enable recording without an armed track");
|
|
newValue = false;
|
|
}
|
|
|
|
if (!newValue)
|
|
m_ArmedTracks.Clear();
|
|
|
|
if (newValue != m_Recording)
|
|
{
|
|
if (newValue)
|
|
AnimationMode.StartAnimationRecording();
|
|
else
|
|
AnimationMode.StopAnimationRecording();
|
|
|
|
InspectorWindow.RepaintAllInspectors();
|
|
}
|
|
|
|
SyncNotifyValue(ref m_Recording, newValue, OnRecordingChange);
|
|
}
|
|
}
|
|
|
|
public bool previewMode
|
|
{
|
|
get { return ignorePreview || AnimationMode.InAnimationMode(previewDriver); }
|
|
set
|
|
{
|
|
if (ignorePreview)
|
|
return;
|
|
bool inAnimationMode = AnimationMode.InAnimationMode(previewDriver);
|
|
if (!value)
|
|
{
|
|
if (inAnimationMode)
|
|
{
|
|
Stop();
|
|
|
|
OnStopPreview();
|
|
|
|
AnimationMode.StopAnimationMode(previewDriver);
|
|
|
|
AnimationPropertyContextualMenu.Instance.SetResponder(null);
|
|
previewedDirectors = null;
|
|
}
|
|
}
|
|
else if (!inAnimationMode)
|
|
{
|
|
editSequence.time = editSequence.viewModel.windowTime;
|
|
EvaluateImmediate(); // does appropriate caching prior to enabling
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool playing
|
|
{
|
|
get
|
|
{
|
|
return masterSequence.director != null && masterSequence.director.state == PlayState.Playing;
|
|
}
|
|
}
|
|
|
|
public float playbackSpeed { get; set; }
|
|
|
|
public bool frameSnap
|
|
{
|
|
get { return TimelinePreferences.instance.snapToFrame; }
|
|
set { TimelinePreferences.instance.snapToFrame = value; }
|
|
}
|
|
|
|
public bool edgeSnaps
|
|
{
|
|
get { return TimelinePreferences.instance.edgeSnap; }
|
|
set { TimelinePreferences.instance.edgeSnap = value; }
|
|
}
|
|
|
|
public bool muteAudioScrubbing
|
|
{
|
|
get { return !TimelinePreferences.instance.audioScrubbing; }
|
|
set
|
|
{
|
|
TimelinePreferences.instance.audioScrubbing = !value;
|
|
TimelinePlayable.muteAudioScrubbing = value;
|
|
RebuildPlayableGraph();
|
|
}
|
|
}
|
|
|
|
public TimeReferenceMode timeReferenceMode
|
|
{
|
|
get { return m_Preferences.timeReferenceMode; }
|
|
set { m_Preferences.timeReferenceMode = value; }
|
|
}
|
|
|
|
public TimeFormat timeFormat
|
|
{
|
|
get { return TimelinePreferences.instance.timeFormat; }
|
|
set { TimelinePreferences.instance.timeFormat = value; }
|
|
}
|
|
|
|
public bool showAudioWaveform
|
|
{
|
|
get { return TimelinePreferences.instance.showAudioWaveform; }
|
|
set { TimelinePreferences.instance.showAudioWaveform = value; }
|
|
}
|
|
|
|
public PlayRange playRange
|
|
{
|
|
get { return masterSequence.viewModel.timeAreaPlayRange; }
|
|
set { masterSequence.viewModel.timeAreaPlayRange = ValidatePlayRange(value, masterSequence); }
|
|
}
|
|
|
|
public bool showMarkerHeader
|
|
{
|
|
get { return editSequence.asset != null && editSequence.asset.markerTrack != null && editSequence.asset.markerTrack.GetShowMarkers(); }
|
|
set { GetWindow().SetShowMarkerHeader(value); }
|
|
}
|
|
|
|
public EditMode.EditType editType
|
|
{
|
|
get { return m_Preferences.editType; }
|
|
set { m_Preferences.editType = value; }
|
|
}
|
|
|
|
public PlaybackScrollMode autoScrollMode
|
|
{
|
|
get { return TimelinePreferences.instance.playbackScrollMode; }
|
|
set { TimelinePreferences.instance.playbackScrollMode = value; }
|
|
}
|
|
|
|
public List<PlayableDirector> previewedDirectors { get; private set; }
|
|
|
|
public void OnDestroy()
|
|
{
|
|
if (!ignorePreview)
|
|
Stop();
|
|
|
|
if (m_OnStartFrameUpdates != null)
|
|
m_OnStartFrameUpdates.Clear();
|
|
|
|
if (m_OnEndFrameUpdates != null)
|
|
m_OnEndFrameUpdates.Clear();
|
|
|
|
m_SequenceHierarchy.Clear();
|
|
windowOnGuiStarted = null;
|
|
}
|
|
|
|
public void OnSceneSaved()
|
|
{
|
|
// the director will reset it's time when the scene is saved.
|
|
EnsureWindowTimeConsistency();
|
|
}
|
|
|
|
public void SetCurrentSequence(TimelineAsset timelineAsset, PlayableDirector director, TimelineClip hostClip)
|
|
{
|
|
if (OnBeforeSequenceChange != null)
|
|
OnBeforeSequenceChange.Invoke();
|
|
|
|
OnCurrentDirectorWillChange();
|
|
|
|
if (hostClip == null || timelineAsset == null)
|
|
{
|
|
m_PropertyCollector.Clear();
|
|
m_SequenceHierarchy.Clear();
|
|
}
|
|
|
|
if (timelineAsset != null)
|
|
m_SequenceHierarchy.Add(timelineAsset, director, hostClip);
|
|
|
|
if (OnAfterSequenceChange != null)
|
|
OnAfterSequenceChange.Invoke();
|
|
}
|
|
|
|
public void PopSequencesUntilCount(int count)
|
|
{
|
|
if (count >= m_SequenceHierarchy.count) return;
|
|
if (count < 1) return;
|
|
|
|
if (OnBeforeSequenceChange != null)
|
|
OnBeforeSequenceChange.Invoke();
|
|
|
|
var nextDirector = m_SequenceHierarchy.GetStateAtIndex(count - 1).director;
|
|
OnCurrentDirectorWillChange();
|
|
|
|
m_SequenceHierarchy.RemoveUntilCount(count);
|
|
|
|
EnsureWindowTimeConsistency();
|
|
|
|
if (OnAfterSequenceChange != null)
|
|
OnAfterSequenceChange.Invoke();
|
|
}
|
|
|
|
public SequencePath GetCurrentSequencePath()
|
|
{
|
|
return m_SequenceHierarchy.ToSequencePath();
|
|
}
|
|
|
|
public void SetCurrentSequencePath(SequencePath path, bool forceRebuild)
|
|
{
|
|
if (!m_SequenceHierarchy.NeedsUpdate(path, forceRebuild))
|
|
return;
|
|
|
|
if (OnBeforeSequenceChange != null)
|
|
OnBeforeSequenceChange.Invoke();
|
|
|
|
m_SequenceHierarchy.FromSequencePath(path, forceRebuild);
|
|
|
|
if (OnAfterSequenceChange != null)
|
|
OnAfterSequenceChange.Invoke();
|
|
}
|
|
|
|
public IEnumerable<ISequenceState> GetAllSequences()
|
|
{
|
|
return m_SequenceHierarchy.allSequences;
|
|
}
|
|
|
|
public IEnumerable<SequenceContext> GetSubSequences()
|
|
{
|
|
var contexts =
|
|
editSequence.asset?.flattenedTracks
|
|
.SelectMany(x => x.clips)
|
|
.Where((TimelineUtility.HasCustomEditor))
|
|
.SelectMany((clip =>
|
|
TimelineUtility.GetSubTimelines(clip, TimelineEditor.inspectedDirector)
|
|
.Select(director => new SequenceContext(director, clip))));
|
|
|
|
return contexts;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
recording = false;
|
|
previewMode = false;
|
|
}
|
|
|
|
public double GetSnappedTimeAtMousePosition(Vector2 mousePos)
|
|
{
|
|
return TimeReferenceUtility.SnapToFrameIfRequired(ScreenSpacePixelToTimeAreaTime(mousePos.x));
|
|
}
|
|
|
|
static void SyncNotifyValue<T>(ref T oldValue, T newValue, System.Action changeStateCallback)
|
|
{
|
|
var stateChanged = false;
|
|
|
|
if (oldValue == null)
|
|
{
|
|
oldValue = newValue;
|
|
stateChanged = true;
|
|
}
|
|
else
|
|
{
|
|
if (!oldValue.Equals(newValue))
|
|
{
|
|
oldValue = newValue;
|
|
stateChanged = true;
|
|
}
|
|
}
|
|
|
|
if (stateChanged && changeStateCallback != null)
|
|
{
|
|
changeStateCallback.Invoke();
|
|
}
|
|
}
|
|
|
|
public TimelineWindowAnalytics analytics = new TimelineWindowAnalytics();
|
|
|
|
public void SetTimeAreaTransform(Vector2 newTranslation, Vector2 newScale)
|
|
{
|
|
m_Window.timeArea.SetTransform(newTranslation, newScale);
|
|
TimeAreaChanged();
|
|
}
|
|
|
|
public void SetTimeAreaShownRange(float min, float max)
|
|
{
|
|
m_Window.timeArea.SetShownHRange(min, max);
|
|
TimeAreaChanged();
|
|
}
|
|
|
|
internal void TimeAreaChanged()
|
|
{
|
|
if (editSequence.asset != null)
|
|
{
|
|
editSequence.viewModel.timeAreaShownRange = new Vector2(m_Window.timeArea.shownArea.x, m_Window.timeArea.shownArea.xMax);
|
|
}
|
|
}
|
|
|
|
public void ResetPreviewMode()
|
|
{
|
|
var mode = previewMode;
|
|
previewMode = false;
|
|
previewMode = mode;
|
|
}
|
|
|
|
public bool TimeIsInRange(float value)
|
|
{
|
|
Rect shownArea = m_Window.timeArea.shownArea;
|
|
return value >= shownArea.x && value <= shownArea.xMax;
|
|
}
|
|
|
|
public bool RangeIsVisible(Range range)
|
|
{
|
|
var shownArea = m_Window.timeArea.shownArea;
|
|
return range.start < shownArea.xMax && range.end > shownArea.xMin;
|
|
}
|
|
|
|
public void EnsurePlayHeadIsVisible()
|
|
{
|
|
double minDisplayedTime = PixelToTime(timeAreaRect.xMin);
|
|
double maxDisplayedTime = PixelToTime(timeAreaRect.xMax);
|
|
|
|
double currentTime = editSequence.time;
|
|
if (currentTime >= minDisplayedTime && currentTime <= maxDisplayedTime)
|
|
return;
|
|
|
|
float displayedTimeRange = (float)(maxDisplayedTime - minDisplayedTime);
|
|
float minimumTimeToDisplay = (float)currentTime - displayedTimeRange / 2.0f;
|
|
float maximumTimeToDisplay = (float)currentTime + displayedTimeRange / 2.0f;
|
|
SetTimeAreaShownRange(minimumTimeToDisplay, maximumTimeToDisplay);
|
|
}
|
|
|
|
public void SetPlayHeadToMiddle()
|
|
{
|
|
double minDisplayedTime = PixelToTime(timeAreaRect.xMin);
|
|
double maxDisplayedTime = PixelToTime(timeAreaRect.xMax);
|
|
|
|
double currentTime = editSequence.time;
|
|
float displayedTimeRange = (float)(maxDisplayedTime - minDisplayedTime);
|
|
|
|
if (currentTime >= minDisplayedTime && currentTime <= maxDisplayedTime)
|
|
{
|
|
if (currentTime < minDisplayedTime + displayedTimeRange / 2)
|
|
return;
|
|
}
|
|
|
|
const float kCatchUpSpeed = 3f;
|
|
float realDelta = Mathf.Clamp(Time.realtimeSinceStartup - m_LastTime, 0f, 1f) * kCatchUpSpeed;
|
|
float scrollCatchupAmount = kCatchUpSpeed * realDelta * displayedTimeRange / 2;
|
|
|
|
if (currentTime < minDisplayedTime)
|
|
{
|
|
SetTimeAreaShownRange((float)currentTime, (float)currentTime + displayedTimeRange);
|
|
}
|
|
else if (currentTime > maxDisplayedTime)
|
|
{
|
|
SetTimeAreaShownRange((float)currentTime - displayedTimeRange + scrollCatchupAmount, (float)currentTime + scrollCatchupAmount);
|
|
}
|
|
else if (currentTime > minDisplayedTime + displayedTimeRange / 2)
|
|
{
|
|
float targetMinDisplayedTime = Mathf.Min((float)minDisplayedTime + scrollCatchupAmount,
|
|
(float)(currentTime - displayedTimeRange / 2));
|
|
SetTimeAreaShownRange(targetMinDisplayedTime, targetMinDisplayedTime + displayedTimeRange);
|
|
}
|
|
}
|
|
|
|
internal void UpdateLastFrameTime()
|
|
{
|
|
m_LastTime = Time.realtimeSinceStartup;
|
|
}
|
|
|
|
public Vector2 timeAreaShownRange
|
|
{
|
|
get
|
|
{
|
|
if (m_Window.state.editSequence.asset != null)
|
|
return editSequence.viewModel.timeAreaShownRange;
|
|
|
|
return TimelineAssetViewModel.TimeAreaDefaultRange;
|
|
}
|
|
set
|
|
{
|
|
SetTimeAreaShownRange(value.x, value.y);
|
|
}
|
|
}
|
|
|
|
public Vector2 timeAreaTranslation
|
|
{
|
|
get { return m_Window.timeArea.translation; }
|
|
}
|
|
|
|
public Vector2 timeAreaScale
|
|
{
|
|
get { return m_Window.timeArea.scale; }
|
|
}
|
|
|
|
public Rect timeAreaRect
|
|
{
|
|
get
|
|
{
|
|
var sequenceContentRect = m_Window.sequenceContentRect;
|
|
return new Rect(
|
|
sequenceContentRect.x,
|
|
WindowConstants.timeAreaYPosition,
|
|
Mathf.Max(sequenceContentRect.width, WindowConstants.timeAreaMinWidth),
|
|
WindowConstants.timeAreaHeight
|
|
);
|
|
}
|
|
}
|
|
|
|
public float windowHeight
|
|
{
|
|
get { return m_Window.position.height; }
|
|
}
|
|
|
|
public bool playRangeEnabled
|
|
{
|
|
get { return !ignorePreview && masterSequence.viewModel.playRangeEnabled && !IsEditingASubTimeline(); }
|
|
set
|
|
{
|
|
if (!ignorePreview)
|
|
masterSequence.viewModel.playRangeEnabled = value;
|
|
}
|
|
}
|
|
|
|
public bool ignorePreview
|
|
{
|
|
get
|
|
{
|
|
var shouldIgnorePreview = masterSequence.asset != null && !masterSequence.asset.editorSettings.scenePreview;
|
|
return Application.isPlaying || shouldIgnorePreview;
|
|
}
|
|
}
|
|
|
|
|
|
public TimelineWindow GetWindow()
|
|
{
|
|
return m_Window;
|
|
}
|
|
|
|
public void Play()
|
|
{
|
|
if (masterSequence.director == null)
|
|
return;
|
|
|
|
if (!previewMode)
|
|
previewMode = true;
|
|
|
|
if (previewMode)
|
|
{
|
|
if (masterSequence.time > masterSequence.duration)
|
|
masterSequence.time = 0;
|
|
#if TIMELINE_FRAMEACCURATE
|
|
if (TimelinePreferences.instance.playbackLockedToFrame)
|
|
{
|
|
FrameRate frameRate = FrameRate.DoubleToFrameRate(masterSequence.asset.editorSettings.frameRate);
|
|
masterSequence.director.Play(frameRate);
|
|
}
|
|
else
|
|
{
|
|
masterSequence.director.Play();
|
|
}
|
|
#else
|
|
masterSequence.director.Play();
|
|
#endif
|
|
masterSequence.director.ProcessPendingGraphChanges();
|
|
PlayableDirector.ResetFrameTiming();
|
|
InvokePlayStateChangeCallback(true);
|
|
}
|
|
}
|
|
|
|
public void Pause()
|
|
{
|
|
if (masterSequence.director != null)
|
|
{
|
|
masterSequence.director.Pause();
|
|
masterSequence.director.ProcessPendingGraphChanges();
|
|
SynchronizeSequencesAfterPlayback();
|
|
InvokePlayStateChangeCallback(false);
|
|
}
|
|
}
|
|
|
|
public void SetPlaying(bool start)
|
|
{
|
|
if (start && !playing)
|
|
{
|
|
Play();
|
|
}
|
|
|
|
if (!start && playing)
|
|
{
|
|
Pause();
|
|
}
|
|
|
|
analytics.SendPlayEvent(start);
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
if (masterSequence.director != null)
|
|
{
|
|
masterSequence.director.Stop();
|
|
masterSequence.director.ProcessPendingGraphChanges();
|
|
InvokePlayStateChangeCallback(false);
|
|
}
|
|
}
|
|
|
|
void InvokePlayStateChangeCallback(bool isPlaying)
|
|
{
|
|
if (OnPlayStateChange != null)
|
|
OnPlayStateChange.Invoke(isPlaying);
|
|
}
|
|
|
|
public void RebuildPlayableGraph()
|
|
{
|
|
if (masterSequence.director != null)
|
|
{
|
|
masterSequence.director.RebuildGraph();
|
|
// rebuild both the parent and the edit sequences. control tracks don't necessary
|
|
// rebuild the subdirector on recreation
|
|
if (editSequence.director != null && editSequence.director != masterSequence.director)
|
|
{
|
|
editSequence.director.RebuildGraph();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Evaluate()
|
|
{
|
|
if (masterSequence.director != null)
|
|
{
|
|
if (!EditorApplication.isPlaying && !previewMode)
|
|
GatherProperties(masterSequence.director);
|
|
|
|
ForceTimeOnDirector(masterSequence.director);
|
|
masterSequence.director.DeferredEvaluate();
|
|
|
|
if (EditorApplication.isPlaying == false)
|
|
{
|
|
PlayModeView.RepaintAll();
|
|
SceneView.RepaintAll();
|
|
AudioMixerWindow.RepaintAudioMixerWindow();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void EvaluateImmediate()
|
|
{
|
|
if (masterSequence.director != null && masterSequence.director.isActiveAndEnabled)
|
|
{
|
|
if (!EditorApplication.isPlaying && !previewMode)
|
|
GatherProperties(masterSequence.director);
|
|
|
|
if (previewMode)
|
|
{
|
|
ForceTimeOnDirector(masterSequence.director);
|
|
masterSequence.director.ProcessPendingGraphChanges();
|
|
masterSequence.director.Evaluate();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Refresh()
|
|
{
|
|
CheckRecordingState();
|
|
dirtyStamp = dirtyStamp + 1;
|
|
|
|
rebuildGraph = true;
|
|
}
|
|
|
|
public void UpdateViewStateHash()
|
|
{
|
|
viewStateHash = timeAreaTranslation.GetHashCode()
|
|
.CombineHash(timeAreaScale.GetHashCode())
|
|
.CombineHash(trackScale.GetHashCode());
|
|
}
|
|
|
|
public bool IsEditingASubTimeline()
|
|
{
|
|
return editSequence != masterSequence;
|
|
}
|
|
|
|
public bool IsEditingAnEmptyTimeline()
|
|
{
|
|
return editSequence.asset == null;
|
|
}
|
|
|
|
public bool IsEditingAPrefabAsset()
|
|
{
|
|
var stage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
return stage != null && editSequence.director != null && stage.IsPartOfPrefabContents(editSequence.director.gameObject);
|
|
}
|
|
|
|
public bool IsCurrentEditingASequencerTextField()
|
|
{
|
|
if (editSequence.asset == null)
|
|
return false;
|
|
|
|
if (k_TimeCodeTextFieldId == GUIUtility.keyboardControl)
|
|
return true;
|
|
|
|
return editSequence.asset.flattenedTracks.Count(t => t.GetInstanceID() == GUIUtility.keyboardControl) != 0;
|
|
}
|
|
|
|
public float TimeToTimeAreaPixel(double t) // TimeToTimeAreaPixel
|
|
{
|
|
float pixelX = (float)t;
|
|
pixelX *= timeAreaScale.x;
|
|
pixelX += timeAreaTranslation.x + sequencerHeaderWidth;
|
|
return pixelX;
|
|
}
|
|
|
|
public float TimeToScreenSpacePixel(double time)
|
|
{
|
|
float pixelX = (float)time;
|
|
pixelX *= timeAreaScale.x;
|
|
pixelX += timeAreaTranslation.x;
|
|
return pixelX;
|
|
}
|
|
|
|
public float TimeToPixel(double time)
|
|
{
|
|
return m_Window.timeArea.TimeToPixel((float)time, timeAreaRect);
|
|
}
|
|
|
|
public float PixelToTime(float pixel)
|
|
{
|
|
return m_Window.timeArea.PixelToTime(pixel, timeAreaRect);
|
|
}
|
|
|
|
public float PixelDeltaToDeltaTime(float p)
|
|
{
|
|
return PixelToTime(p) - PixelToTime(0);
|
|
}
|
|
|
|
public float TimeAreaPixelToTime(float pixel)
|
|
{
|
|
return PixelToTime(pixel);
|
|
}
|
|
|
|
public float ScreenSpacePixelToTimeAreaTime(float p)
|
|
{
|
|
// transform into track space by offsetting the pixel by the screen-space offset of the time area
|
|
p -= timeAreaRect.x;
|
|
return TrackSpacePixelToTimeAreaTime(p);
|
|
}
|
|
|
|
public float TrackSpacePixelToTimeAreaTime(float p)
|
|
{
|
|
p -= timeAreaTranslation.x;
|
|
|
|
if (timeAreaScale.x > 0.0f)
|
|
return p / timeAreaScale.x;
|
|
|
|
return p;
|
|
}
|
|
|
|
public void OffsetTimeArea(int pixels)
|
|
{
|
|
Vector3 tx = timeAreaTranslation;
|
|
tx.x += pixels;
|
|
SetTimeAreaTransform(tx, timeAreaScale);
|
|
}
|
|
|
|
public GameObject GetSceneReference(TrackAsset asset)
|
|
{
|
|
if (editSequence.director == null)
|
|
return null; // no player bound
|
|
|
|
return TimelineUtility.GetSceneGameObject(editSequence.director, asset);
|
|
}
|
|
|
|
public void CalculateRowRects()
|
|
{
|
|
// arming a track might add inline curve tracks, recalc track heights
|
|
if (m_Window != null && m_Window.treeView != null)
|
|
m_Window.treeView.CalculateRowRects();
|
|
}
|
|
|
|
// Only one track within a 'track' hierarchy can be armed
|
|
public void ArmForRecord(TrackAsset track)
|
|
{
|
|
m_ArmedTracks[TimelineUtility.GetSceneReferenceTrack(track)] = track;
|
|
if (track != null && !recording)
|
|
recording = true;
|
|
if (!recording)
|
|
return;
|
|
|
|
track.OnRecordingArmed(editSequence.director);
|
|
CalculateRowRects();
|
|
}
|
|
|
|
public void UnarmForRecord(TrackAsset track)
|
|
{
|
|
m_ArmedTracks.Remove(TimelineUtility.GetSceneReferenceTrack(track));
|
|
if (m_ArmedTracks.Count == 0)
|
|
recording = false;
|
|
track.OnRecordingUnarmed(editSequence.director);
|
|
}
|
|
|
|
public void UpdateRecordingState()
|
|
{
|
|
if (recording)
|
|
{
|
|
foreach (var track in m_ArmedTracks.Values)
|
|
{
|
|
if (track != null)
|
|
track.OnRecordingTimeChanged(editSequence.director);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsTrackRecordable(TrackAsset track)
|
|
{
|
|
// A track with animated parameters can always be recorded to
|
|
return IsArmedForRecord(track) || track.HasAnyAnimatableParameters();
|
|
}
|
|
|
|
public bool IsArmedForRecord(TrackAsset track)
|
|
{
|
|
return track == GetArmedTrack(track);
|
|
}
|
|
|
|
public TrackAsset GetArmedTrack(TrackAsset track)
|
|
{
|
|
TrackAsset outTrack;
|
|
m_ArmedTracks.TryGetValue(TimelineUtility.GetSceneReferenceTrack(track), out outTrack);
|
|
return outTrack;
|
|
}
|
|
|
|
void CheckRecordingState()
|
|
{
|
|
// checks for deleted tracks, and makes sure the recording state matches
|
|
if (m_ArmedTracks.Any(t => t.Value == null))
|
|
{
|
|
m_ArmedTracks = m_ArmedTracks.Where(t => t.Value != null).ToDictionary(t => t.Key, t => t.Value);
|
|
if (m_ArmedTracks.Count == 0)
|
|
recording = false;
|
|
}
|
|
}
|
|
|
|
void OnCurrentDirectorWillChange()
|
|
{
|
|
if (ignorePreview)
|
|
return;
|
|
|
|
SynchronizeViewModelTime(editSequence);
|
|
Stop();
|
|
rebuildGraph = true; // needed for asset previews
|
|
}
|
|
|
|
public void GatherProperties(PlayableDirector director)
|
|
{
|
|
if (director == null || Application.isPlaying)
|
|
return;
|
|
|
|
var asset = director.playableAsset as TimelineAsset;
|
|
if (asset != null && !asset.editorSettings.scenePreview)
|
|
return;
|
|
|
|
if (!previewMode)
|
|
{
|
|
AnimationMode.StartAnimationMode(previewDriver);
|
|
|
|
OnStartPreview(director);
|
|
|
|
AnimationPropertyContextualMenu.Instance.SetResponder(new TimelineRecordingContextualResponder(this));
|
|
if (!previewMode)
|
|
return;
|
|
EnsureWindowTimeConsistency();
|
|
}
|
|
|
|
if (asset != null)
|
|
{
|
|
m_PropertyCollector.Reset();
|
|
m_PropertyCollector.PushActiveGameObject(null); // avoid overflow on unbound tracks
|
|
asset.GatherProperties(director, m_PropertyCollector);
|
|
}
|
|
}
|
|
|
|
void OnStartPreview(PlayableDirector director)
|
|
{
|
|
previewedDirectors = TimelineUtility.GetAllDirectorsInHierarchy(director).ToList();
|
|
|
|
if (previewedDirectors == null)
|
|
return;
|
|
|
|
m_PreviewedAnimators = PreviewedBindings<Animator>.GetPreviewedBindings(previewedDirectors);
|
|
|
|
m_PreviewedComponents = m_PreviewedAnimators.GetUniqueBindings()
|
|
.SelectMany(animator => animator.GetComponents<IAnimationWindowPreview>()
|
|
.Cast<Component>())
|
|
.ToList();
|
|
|
|
foreach (var previewedComponent in previewedComponents)
|
|
{
|
|
previewedComponent.StartPreview();
|
|
}
|
|
#if UNITY_2022_2_OR_NEWER
|
|
PrefabUtility.allowRecordingPrefabPropertyOverridesFor += AllowRecordingPrefabPropertyOverridesFor;
|
|
#endif //UNITY_2022_2_OR_NEWER
|
|
}
|
|
|
|
internal bool AllowRecordingPrefabPropertyOverridesFor(Object componentOrGameObject)
|
|
{
|
|
if (componentOrGameObject == null)
|
|
throw new ArgumentNullException(nameof(componentOrGameObject));
|
|
|
|
if (previewMode == false)
|
|
return true;
|
|
|
|
if (m_ArmedTracks.Count == 0)
|
|
return true;
|
|
|
|
GameObject inputGameObject = null;
|
|
if (componentOrGameObject is Component component)
|
|
{
|
|
inputGameObject = component.gameObject;
|
|
}
|
|
else if (componentOrGameObject is GameObject gameObject)
|
|
{
|
|
inputGameObject = gameObject;
|
|
}
|
|
|
|
if (inputGameObject == null)
|
|
return true;
|
|
|
|
var armedTracks = m_ArmedTracks.Keys;
|
|
foreach (var track in armedTracks)
|
|
{
|
|
var animators = m_PreviewedAnimators.GetBindingsForObject(track);
|
|
foreach (var animator in animators)
|
|
{
|
|
if (animator == null)
|
|
continue;
|
|
|
|
if (inputGameObject.transform.IsChildOf(animator.transform))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void OnStopPreview()
|
|
{
|
|
if (m_PreviewedComponents != null)
|
|
{
|
|
foreach (var previewComponent in previewedComponents)
|
|
{
|
|
previewComponent.StopPreview();
|
|
}
|
|
|
|
m_PreviewedComponents = null;
|
|
}
|
|
|
|
foreach (var previewAnimator in m_PreviewedAnimators.GetUniqueBindings())
|
|
{
|
|
if (previewAnimator != null)
|
|
previewAnimator.UnbindAllHandles();
|
|
}
|
|
m_PreviewedAnimators = default;
|
|
|
|
#if UNITY_2022_2_OR_NEWER
|
|
PrefabUtility.allowRecordingPrefabPropertyOverridesFor -= AllowRecordingPrefabPropertyOverridesFor;
|
|
#endif //UNITY_2022_2_OR_NEWER
|
|
}
|
|
|
|
internal void ProcessStartFramePendingUpdates()
|
|
{
|
|
if (m_OnStartFrameUpdates != null)
|
|
m_OnStartFrameUpdates.RemoveAll(callback => callback.Invoke(this, Event.current));
|
|
}
|
|
|
|
internal void ProcessEndFramePendingUpdates()
|
|
{
|
|
if (m_OnEndFrameUpdates != null)
|
|
m_OnEndFrameUpdates.RemoveAll(callback => callback.Invoke(this, Event.current));
|
|
}
|
|
|
|
public void AddStartFrameDelegate(PendingUpdateDelegate updateDelegate)
|
|
{
|
|
if (m_OnStartFrameUpdates == null)
|
|
m_OnStartFrameUpdates = new List<PendingUpdateDelegate>();
|
|
if (m_OnStartFrameUpdates.Contains(updateDelegate))
|
|
return;
|
|
m_OnStartFrameUpdates.Add(updateDelegate);
|
|
}
|
|
|
|
public void AddEndFrameDelegate(PendingUpdateDelegate updateDelegate)
|
|
{
|
|
if (m_OnEndFrameUpdates == null)
|
|
m_OnEndFrameUpdates = new List<PendingUpdateDelegate>();
|
|
if (m_OnEndFrameUpdates.Contains(updateDelegate))
|
|
return;
|
|
m_OnEndFrameUpdates.Add(updateDelegate);
|
|
}
|
|
|
|
internal void InvokeWindowOnGuiStarted(Event evt)
|
|
{
|
|
if (windowOnGuiStarted != null)
|
|
windowOnGuiStarted.Invoke(this, evt);
|
|
}
|
|
|
|
public void UpdateRootPlayableDuration(double duration)
|
|
{
|
|
if (editSequence.director != null)
|
|
{
|
|
if (editSequence.director.playableGraph.IsValid())
|
|
{
|
|
if (editSequence.director.playableGraph.GetRootPlayableCount() > 0)
|
|
{
|
|
var rootPlayable = editSequence.director.playableGraph.GetRootPlayable(0);
|
|
if (rootPlayable.IsValid())
|
|
rootPlayable.SetDuration(duration);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void InvokeTimeChangeCallback()
|
|
{
|
|
if (OnTimeChange != null)
|
|
OnTimeChange.Invoke();
|
|
}
|
|
|
|
PlayRange ValidatePlayRange(PlayRange range, ISequenceState sequenceState)
|
|
{
|
|
if (range == TimelineAssetViewModel.NoPlayRangeSet)
|
|
return range;
|
|
|
|
double minimumPlayRangeTime = (0.01 / Math.Max(1.0, sequenceState.frameRate));
|
|
|
|
// Validate min
|
|
if (range.end - range.start < minimumPlayRangeTime)
|
|
range.start = range.end - minimumPlayRangeTime;
|
|
|
|
if (range.start < 0.0)
|
|
range.start = 0.0;
|
|
|
|
// Validate max
|
|
if (range.end > sequenceState.duration)
|
|
range.end = sequenceState.duration;
|
|
|
|
if (range.end - range.start < minimumPlayRangeTime)
|
|
range.end = Math.Min(range.start + minimumPlayRangeTime, sequenceState.duration);
|
|
|
|
return range;
|
|
}
|
|
|
|
void EnsureWindowTimeConsistency()
|
|
{
|
|
if (masterSequence.director != null && masterSequence.viewModel != null && !ignorePreview)
|
|
masterSequence.time = masterSequence.viewModel.windowTime;
|
|
}
|
|
|
|
void SynchronizeSequencesAfterPlayback()
|
|
{
|
|
// Synchronizing editSequence will synchronize all view models up to the master
|
|
SynchronizeViewModelTime(editSequence);
|
|
}
|
|
|
|
static void SynchronizeViewModelTime(ISequenceState state)
|
|
{
|
|
if (state.director == null || state.viewModel == null)
|
|
return;
|
|
|
|
var t = state.time;
|
|
state.time = t;
|
|
}
|
|
|
|
// because we may be evaluating outside the duration of the root playable
|
|
// we explicitly set the time - this causes the graph to not 'advance' the time
|
|
// because advancing it can force it to change due to wrapping to the duration
|
|
// This can happen if the graph is force evaluated outside it's duration
|
|
// case 910114, 936844 and 943377
|
|
static void ForceTimeOnDirector(PlayableDirector director)
|
|
{
|
|
var directorTime = director.time;
|
|
director.time = directorTime;
|
|
}
|
|
|
|
public bool IsPlayableGraphDone()
|
|
{
|
|
return masterSequence.director != null
|
|
&& masterSequence.director.playableGraph.IsValid()
|
|
&& masterSequence.director.playableGraph.IsDone();
|
|
}
|
|
}
|
|
}
|