using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
///
/// Extension Methods for Tracks that require the Unity Editor, and may require the Timeline containing the Track to be currently loaded in the Timeline Editor Window.
///
public static class TrackExtensions
{
static readonly double kMinOverlapTime = TimeUtility.kTimeEpsilon * 1000;
///
/// Queries whether the children of the Track are currently visible in the Timeline Editor.
///
/// The track asset to query.
/// True if the track is collapsed and false otherwise.
public static bool IsCollapsed(this TrackAsset track)
{
return TimelineWindowViewPrefs.IsTrackCollapsed(track);
}
///
/// Sets whether the children of the Track are currently visible in the Timeline Editor.
///
/// The track asset to collapsed state to modify.
/// `true` to collapse children, false otherwise.
/// The track collapsed state is not serialized inside the asset and is lost from one checkout of the project to another. >
public static void SetCollapsed(this TrackAsset track, bool collapsed)
{
TimelineWindowViewPrefs.SetTrackCollapsed(track, collapsed);
}
///
/// Queries whether any parent of the track is collapsed, rendering the track not visible to the user.
///
/// The track asset to query.
/// True if all parents are not collapsed, false otherwise.
public static bool IsVisibleInHierarchy(this TrackAsset track)
{
var t = track;
while ((t = t.parent as TrackAsset) != null)
{
if (t.IsCollapsed())
return false;
}
return true;
}
internal static AnimationClip GetOrCreateClip(this AnimationTrack track)
{
if (track.infiniteClip == null && !track.inClipMode)
track.CreateInfiniteClip(AnimationTrackRecorder.GetUniqueRecordedClipName(track, AnimationTrackRecorder.kRecordClipDefaultName));
return track.infiniteClip;
}
internal static TimelineClip CreateClip(this TrackAsset track, double time)
{
var attr = track.GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute), true);
if (attr.Length == 0)
return null;
if (TimelineWindow.instance.state == null)
return null;
if (attr.Length == 1)
{
var clipClass = (TrackClipTypeAttribute)attr[0];
var clip = TimelineHelpers.CreateClipOnTrack(clipClass.inspectedType, track, time);
return clip;
}
return null;
}
static bool Overlaps(TimelineClip blendOut, TimelineClip blendIn)
{
if (blendIn == blendOut)
return false;
if (Math.Abs(blendIn.start - blendOut.start) < TimeUtility.kTimeEpsilon)
{
return blendIn.duration >= blendOut.duration;
}
return blendIn.start >= blendOut.start && blendIn.start < blendOut.end;
}
internal static void ComputeBlendsFromOverlaps(this TrackAsset asset)
{
ComputeBlendsFromOverlaps(asset.clips);
}
internal static void ComputeBlendsFromOverlaps(TimelineClip[] clips)
{
foreach (var clip in clips)
{
clip.blendInDuration = -1;
clip.blendOutDuration = -1;
}
Array.Sort(clips, (c1, c2) =>
Math.Abs(c1.start - c2.start) < TimeUtility.kTimeEpsilon ? c1.duration.CompareTo(c2.duration) : c1.start.CompareTo(c2.start));
for (var i = 0; i < clips.Length; i++)
{
var clip = clips[i];
if (!clip.SupportsBlending())
continue;
var blendIn = clip;
TimelineClip blendOut = null;
var blendOutCandidate = clips[Math.Max(i - 1, 0)];
if (Overlaps(blendOutCandidate, blendIn))
blendOut = blendOutCandidate;
if (blendOut != null)
{
UpdateClipIntersection(blendOut, blendIn);
}
}
}
static void UpdateClipIntersection(TimelineClip blendOutClip, TimelineClip blendInClip)
{
if (!blendOutClip.SupportsBlending() || !blendInClip.SupportsBlending())
return;
if (blendInClip.start - blendOutClip.start < blendOutClip.duration - blendInClip.duration)
return;
double duration = Math.Max(0, blendOutClip.start + blendOutClip.duration - blendInClip.start);
duration = duration <= kMinOverlapTime ? 0 : duration;
blendOutClip.blendOutDuration = duration;
blendInClip.blendInDuration = duration;
var blendInMode = blendInClip.blendInCurveMode;
var blendOutMode = blendOutClip.blendOutCurveMode;
if (blendInMode == TimelineClip.BlendCurveMode.Manual && blendOutMode == TimelineClip.BlendCurveMode.Auto)
{
blendOutClip.mixOutCurve = CurveEditUtility.CreateMatchingCurve(blendInClip.mixInCurve);
}
else if (blendInMode == TimelineClip.BlendCurveMode.Auto && blendOutMode == TimelineClip.BlendCurveMode.Manual)
{
blendInClip.mixInCurve = CurveEditUtility.CreateMatchingCurve(blendOutClip.mixOutCurve);
}
else if (blendInMode == TimelineClip.BlendCurveMode.Auto && blendOutMode == TimelineClip.BlendCurveMode.Auto)
{
blendInClip.mixInCurve = null; // resets to default curves
blendOutClip.mixOutCurve = null;
}
}
static void RecursiveSubtrackClone(TrackAsset source, TrackAsset duplicate, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, PlayableAsset assetOwner)
{
var subtracks = source.GetChildTracks();
foreach (var sub in subtracks)
{
var newSub = TimelineHelpers.Clone(duplicate, sub, sourceTable, destTable, assetOwner);
duplicate.AddChild(newSub);
RecursiveSubtrackClone(sub, newSub, sourceTable, destTable, assetOwner);
// Call the custom editor on Create
var customEditor = CustomTimelineEditorCache.GetTrackEditor(newSub);
customEditor.OnCreate_Safe(newSub, sub);
// registration has to happen AFTER recursion
TimelineCreateUtilities.SaveAssetIntoObject(newSub, assetOwner);
TimelineUndo.RegisterCreatedObjectUndo(newSub, L10n.Tr("Duplicate"));
}
}
internal static TrackAsset Duplicate(this TrackAsset track, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable,
TimelineAsset destinationTimeline = null)
{
if (track == null)
return null;
// if the destination is us, clear to avoid bad parenting (case 919421)
if (destinationTimeline == track.timelineAsset)
destinationTimeline = null;
var timelineParent = track.parent as TimelineAsset;
var trackParent = track.parent as TrackAsset;
if (timelineParent == null && trackParent == null)
{
Debug.LogWarning("Cannot duplicate track because it is not parented to known type");
return null;
}
// Determine who the final parent is. If we are pasting into another track, it's always the timeline.
// Otherwise it's the original parent
PlayableAsset finalParent = destinationTimeline != null ? destinationTimeline : track.parent;
// grab the list of tracks to generate a name from (923360) to get the list of names
// no need to do this part recursively
var finalTrackParent = finalParent as TrackAsset;
var finalTimelineAsset = finalParent as TimelineAsset;
var otherTracks = (finalTimelineAsset != null) ? finalTimelineAsset.trackObjects : finalTrackParent.subTracksObjects;
// Important to create the new objects before pushing the original undo, or redo breaks the
// sequence
var newTrack = TimelineHelpers.Clone(finalParent, track, sourceTable, destTable, finalParent);
newTrack.name = TimelineCreateUtilities.GenerateUniqueActorName(otherTracks, newTrack.name);
RecursiveSubtrackClone(track, newTrack, sourceTable, destTable, finalParent);
TimelineCreateUtilities.SaveAssetIntoObject(newTrack, finalParent);
TimelineUndo.RegisterCreatedObjectUndo(newTrack, L10n.Tr("Duplicate"));
UndoExtensions.RegisterPlayableAsset(finalParent, L10n.Tr("Duplicate"));
if (destinationTimeline != null) // other timeline
destinationTimeline.AddTrackInternal(newTrack);
else if (timelineParent != null) // this timeline, no parent
ReparentTracks(new List { newTrack }, timelineParent, timelineParent.GetRootTracks().Last(), false);
else // this timeline, with parent
trackParent.AddChild(newTrack);
// Call the custom editor. this check prevents the call when copying to the clipboard
if (destinationTimeline == null || destinationTimeline == TimelineEditor.inspectedAsset)
{
var customEditor = CustomTimelineEditorCache.GetTrackEditor(newTrack);
customEditor.OnCreate_Safe(newTrack, track);
}
return newTrack;
}
// Reparents a list of tracks to a new parent
// the new parent cannot be null (has to be track asset or sequence)
// the insertAfter can be null (will not reorder)
internal static bool ReparentTracks(List tracksToMove, PlayableAsset targetParent,
TrackAsset insertMarker = null, bool insertBefore = false)
{
var targetParentTrack = targetParent as TrackAsset;
var targetSequenceTrack = targetParent as TimelineAsset;
if (tracksToMove == null || tracksToMove.Count == 0 || (targetParentTrack == null && targetSequenceTrack == null))
return false;
// invalid parent type on a track
if (targetParentTrack != null && tracksToMove.Any(x => !TimelineCreateUtilities.ValidateParentTrack(targetParentTrack, x.GetType())))
return false;
// no valid tracks means this is simply a rearrangement
var validTracks = tracksToMove.Where(x => x.parent != targetParent).ToList();
if (insertMarker == null && !validTracks.Any())
return false;
var parents = validTracks.Select(x => x.parent).Where(x => x != null).Distinct().ToList();
// push the current state of the tracks that will change
foreach (var p in parents)
{
UndoExtensions.RegisterPlayableAsset(p, "Reparent");
}
UndoExtensions.RegisterTracks(validTracks, "Reparent");
UndoExtensions.RegisterPlayableAsset(targetParent, "Reparent");
// need to reparent tracks first, before moving them.
foreach (var t in validTracks)
{
if (t.parent != targetParent)
{
TrackAsset toMoveParent = t.parent as TrackAsset;
TimelineAsset toMoveTimeline = t.parent as TimelineAsset;
if (toMoveTimeline != null)
{
toMoveTimeline.RemoveTrack(t);
}
else if (toMoveParent != null)
{
toMoveParent.RemoveSubTrack(t);
}
if (targetParentTrack != null)
{
targetParentTrack.AddChild(t);
targetParentTrack.SetCollapsed(false);
}
else
{
targetSequenceTrack.AddTrackInternal(t);
}
}
}
if (insertMarker != null)
{
// re-ordering track. This is using internal APIs, so invalidation of the tracks must be done manually to avoid
// cache mismatches
var children = targetParentTrack != null ? targetParentTrack.subTracksObjects : targetSequenceTrack.trackObjects;
TimelineUtility.ReorderTracks(children, tracksToMove, insertMarker, insertBefore);
if (targetParentTrack != null)
targetParentTrack.Invalidate();
if (insertMarker.timelineAsset != null)
insertMarker.timelineAsset.Invalidate();
}
return true;
}
internal static IEnumerable FilterTracks(IEnumerable tracks)
{
var nTracks = tracks.Count();
// Duplicate is recursive. If should not have parent and child in the list
var hash = new HashSet(tracks);
var take = new Dictionary(nTracks);
foreach (var track in tracks)
{
var parent = track.parent as TrackAsset;
var foundParent = false;
// go up the hierarchy
while (parent != null && !foundParent)
{
if (hash.Contains(parent))
{
foundParent = true;
}
parent = parent.parent as TrackAsset;
}
take[track] = !foundParent;
}
foreach (var track in tracks)
{
if (take[track])
yield return track;
}
}
internal static bool GetShowMarkers(this TrackAsset track)
{
return TimelineWindowViewPrefs.IsShowMarkers(track);
}
internal static void SetShowMarkers(this TrackAsset track, bool collapsed)
{
TimelineWindowViewPrefs.SetTrackShowMarkers(track, collapsed);
}
internal static bool GetShowInlineCurves(this TrackAsset track)
{
return TimelineWindowViewPrefs.GetShowInlineCurves(track);
}
internal static void SetShowInlineCurves(this TrackAsset track, bool inlineOn)
{
TimelineWindowViewPrefs.SetShowInlineCurves(track, inlineOn);
}
internal static bool ShouldShowInfiniteClipEditor(this AnimationTrack track)
{
return track != null && !track.inClipMode && track.infiniteClip != null;
}
// Special method to remove a track that is in a broken state. i.e. the script won't load
internal static bool RemoveBrokenTrack(PlayableAsset parent, ScriptableObject track)
{
var parentTrack = parent as TrackAsset;
var parentTimeline = parent as TimelineAsset;
if (parentTrack == null && parentTimeline == null)
throw new ArgumentException("parent is not a valid parent type", "parent");
// this object must be a Unity null, but not actually null;
object trackAsObject = track;
if (trackAsObject == null || track as TrackAsset != null) // yes, this is correct
throw new ArgumentException("track is not in a broken state");
// this belongs to a parent track
if (parentTrack != null)
{
int index = parentTrack.subTracksObjects.FindIndex(t => t.GetInstanceID() == track.GetInstanceID());
if (index >= 0)
{
UndoExtensions.RegisterTrack(parentTrack, L10n.Tr("Remove Track"));
parentTrack.subTracksObjects.RemoveAt(index);
parentTrack.Invalidate();
Undo.DestroyObjectImmediate(track);
return true;
}
}
else if (parentTimeline != null)
{
int index = parentTimeline.trackObjects.FindIndex(t => t.GetInstanceID() == track.GetInstanceID());
if (index >= 0)
{
UndoExtensions.RegisterPlayableAsset(parentTimeline, L10n.Tr("Remove Track"));
parentTimeline.trackObjects.RemoveAt(index);
parentTimeline.Invalidate();
Undo.DestroyObjectImmediate(track);
return true;
}
}
return false;
}
// Find the gap at the given time
// return true if there is a gap, false if there is an intersection
// endGap will be Infinity if the gap has no end
internal static bool GetGapAtTime(this TrackAsset track, double time, out double startGap, out double endGap)
{
startGap = 0;
endGap = Double.PositiveInfinity;
if (track == null || !track.GetClips().Any())
{
return false;
}
var discreteTime = new DiscreteTime(time);
track.SortClips();
var sortedByStartTime = track.clips;
for (int i = 0; i < sortedByStartTime.Length; i++)
{
var clip = sortedByStartTime[i];
// intersection
if (discreteTime >= new DiscreteTime(clip.start) && discreteTime < new DiscreteTime(clip.end))
{
endGap = time;
startGap = time;
return false;
}
if (clip.end < time)
{
startGap = clip.end;
}
if (clip.start > time)
{
endGap = clip.start;
break;
}
}
if (endGap - startGap < TimelineClip.kMinDuration)
{
startGap = time;
endGap = time;
return false;
}
return true;
}
internal static bool IsCompatibleWithClip(this TrackAsset track, TimelineClip clip)
{
if (track == null || clip == null || clip.asset == null)
return false;
return TypeUtility.GetPlayableAssetsHandledByTrack(track.GetType()).Contains(clip.asset.GetType());
}
// Get a flattened list of all child tracks
static void GetFlattenedChildTracks(this TrackAsset asset, List list)
{
if (asset == null || list == null)
return;
foreach (var track in asset.GetChildTracks())
{
list.Add(track);
GetFlattenedChildTracks(track, list);
}
}
internal static IEnumerable GetFlattenedChildTracks(this TrackAsset asset)
{
if (asset == null || !asset.GetChildTracks().Any())
return Enumerable.Empty();
var flattenedChildTracks = new List();
GetFlattenedChildTracks(asset, flattenedChildTracks);
return flattenedChildTracks;
}
internal static void ArmForRecord(this TrackAsset track)
{
TimelineWindow.instance.state.ArmForRecord(track);
}
internal static void UnarmForRecord(this TrackAsset track)
{
TimelineWindow.instance.state.UnarmForRecord(track);
}
internal static void SetShowTrackMarkers(this TrackAsset track, bool showMarkers)
{
var currentValue = track.GetShowMarkers();
if (currentValue != showMarkers)
{
TimelineUndo.PushUndo(TimelineWindow.instance.state.editSequence.viewModel, L10n.Tr("Toggle Show Markers"));
track.SetShowMarkers(showMarkers);
if (!showMarkers)
{
foreach (var marker in track.GetMarkers())
{
SelectionManager.Remove(marker);
}
}
}
}
internal static IEnumerable RemoveTimelineMarkerTrackFromList(this IEnumerable tracks, TimelineAsset asset)
{
return tracks.Where(t => t != asset.markerTrack);
}
internal static bool ContainsTimelineMarkerTrack(this IEnumerable tracks, TimelineAsset asset)
{
return tracks.Contains(asset.markerTrack);
}
}
}