458 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			458 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.Collections.Generic;
 | |
| using System.Linq;
 | |
| using UnityEngine;
 | |
| using UnityEngine.Timeline;
 | |
| using UnityEngine.Playables;
 | |
| 
 | |
| namespace UnityEditor.Timeline
 | |
| {
 | |
|     /// <summary>
 | |
|     /// 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.
 | |
|     /// </summary>
 | |
|     public static class TrackExtensions
 | |
|     {
 | |
|         /// <summary>
 | |
|         /// Queries whether the children of the Track are currently visible in the Timeline Editor.
 | |
|         /// </summary>
 | |
|         /// <param name="track">The track asset to query.</param>
 | |
|         /// <returns>True if the track is collapsed and false otherwise.</returns>
 | |
|         public static bool IsCollapsed(this TrackAsset track)
 | |
|         {
 | |
|             return TimelineWindowViewPrefs.IsTrackCollapsed(track);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Sets whether the children of the Track are currently visible in the Timeline Editor.
 | |
|         /// </summary>
 | |
|         /// <param name="track">The track asset to collapsed state to modify.</param>
 | |
|         /// <param name="collapsed">`true` to collapse children, false otherwise.</param>
 | |
|         /// <remarks> The track collapsed state is not serialized inside the asset and is lost from one checkout of the project to another. </remarks>
 | |
|         public static void SetCollapsed(this TrackAsset track, bool collapsed)
 | |
|         {
 | |
|             TimelineWindowViewPrefs.SetTrackCollapsed(track, collapsed);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Queries whether any parent of the track is collapsed, rendering the track not visible to the user.
 | |
|         /// </summary>
 | |
|         /// <param name="track">The track asset to query.</param>
 | |
|         /// <returns>True if all parents are not collapsed, false otherwise.</returns>
 | |
|         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;
 | |
|         }
 | |
| 
 | |
|         internal static void ComputeBlendsFromOverlaps(this TrackAsset asset) => ComputeBlendsFromOverlaps(asset.clips);
 | |
| 
 | |
|         internal static void ComputeBlendsFromOverlaps(TimelineClip[] clips) => BlendUtility.ComputeBlendsFromOverlaps(clips);
 | |
| 
 | |
|         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<TrackAsset> { 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<TrackAsset> 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<TrackAsset> FilterTracks(IEnumerable<TrackAsset> tracks)
 | |
|         {
 | |
|             var nTracks = tracks.Count();
 | |
|             // Duplicate is recursive. If should not have parent and child in the list
 | |
|             var hash = new HashSet<TrackAsset>(tracks);
 | |
|             var take = new Dictionary<TrackAsset, bool>(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<TrackAsset> list)
 | |
|         {
 | |
|             if (asset == null || list == null)
 | |
|                 return;
 | |
| 
 | |
|             foreach (var track in asset.GetChildTracks())
 | |
|             {
 | |
|                 list.Add(track);
 | |
|                 GetFlattenedChildTracks(track, list);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         internal static IEnumerable<TrackAsset> GetFlattenedChildTracks(this TrackAsset asset)
 | |
|         {
 | |
|             if (asset == null || !asset.GetChildTracks().Any())
 | |
|                 return Enumerable.Empty<TrackAsset>();
 | |
| 
 | |
|             var flattenedChildTracks = new List<TrackAsset>();
 | |
|             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<TrackAsset> RemoveTimelineMarkerTrackFromList(this IEnumerable<TrackAsset> tracks, TimelineAsset asset)
 | |
|         {
 | |
|             return tracks.Where(t => t != asset.markerTrack);
 | |
|         }
 | |
| 
 | |
|         internal static bool ContainsTimelineMarkerTrack(this IEnumerable<TrackAsset> tracks, TimelineAsset asset)
 | |
|         {
 | |
|             return tracks.Contains(asset.markerTrack);
 | |
|         }
 | |
| 
 | |
|         internal static void SetNameWithUndo(this TrackAsset track, string newName)
 | |
|         {
 | |
|             UndoExtensions.RegisterTrack(track, L10n.Tr("Rename Track"));
 | |
|             track.name = newName;
 | |
|         }
 | |
|     }
 | |
| }
 |