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); } internal static void SetNameWithUndo(this TrackAsset track, string newName) { UndoExtensions.RegisterTrack(track, L10n.Tr("Rename Track")); track.name = newName; } } }