A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using UnityEngine;
5using UnityEngine.Timeline;
6using UnityEngine.Playables;
7
8namespace UnityEditor.Timeline
9{
10 /// <summary>
11 /// 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.
12 /// </summary>
13 public static class TrackExtensions
14 {
15 static readonly double kMinOverlapTime = TimeUtility.kTimeEpsilon * 1000;
16
17 /// <summary>
18 /// Queries whether the children of the Track are currently visible in the Timeline Editor.
19 /// </summary>
20 /// <param name="track">The track asset to query.</param>
21 /// <returns>True if the track is collapsed and false otherwise.</returns>
22 public static bool IsCollapsed(this TrackAsset track)
23 {
24 return TimelineWindowViewPrefs.IsTrackCollapsed(track);
25 }
26
27 /// <summary>
28 /// Sets whether the children of the Track are currently visible in the Timeline Editor.
29 /// </summary>
30 /// <param name="track">The track asset to collapsed state to modify.</param>
31 /// <param name="collapsed">`true` to collapse children, false otherwise.</param>
32 /// <remarks> The track collapsed state is not serialized inside the asset and is lost from one checkout of the project to another. </remarks>>
33 public static void SetCollapsed(this TrackAsset track, bool collapsed)
34 {
35 TimelineWindowViewPrefs.SetTrackCollapsed(track, collapsed);
36 }
37
38 /// <summary>
39 /// Queries whether any parent of the track is collapsed, rendering the track not visible to the user.
40 /// </summary>
41 /// <param name="track">The track asset to query.</param>
42 /// <returns>True if all parents are not collapsed, false otherwise.</returns>
43 public static bool IsVisibleInHierarchy(this TrackAsset track)
44 {
45 var t = track;
46 while ((t = t.parent as TrackAsset) != null)
47 {
48 if (t.IsCollapsed())
49 return false;
50 }
51
52 return true;
53 }
54
55 internal static AnimationClip GetOrCreateClip(this AnimationTrack track)
56 {
57 if (track.infiniteClip == null && !track.inClipMode)
58 track.CreateInfiniteClip(AnimationTrackRecorder.GetUniqueRecordedClipName(track, AnimationTrackRecorder.kRecordClipDefaultName));
59
60 return track.infiniteClip;
61 }
62
63 internal static TimelineClip CreateClip(this TrackAsset track, double time)
64 {
65 var attr = track.GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute), true);
66
67 if (attr.Length == 0)
68 return null;
69
70 if (TimelineWindow.instance.state == null)
71 return null;
72
73 if (attr.Length == 1)
74 {
75 var clipClass = (TrackClipTypeAttribute)attr[0];
76
77 var clip = TimelineHelpers.CreateClipOnTrack(clipClass.inspectedType, track, time);
78 return clip;
79 }
80
81 return null;
82 }
83
84 static bool Overlaps(TimelineClip blendOut, TimelineClip blendIn)
85 {
86 if (blendIn == blendOut)
87 return false;
88
89 if (Math.Abs(blendIn.start - blendOut.start) < TimeUtility.kTimeEpsilon)
90 {
91 return blendIn.duration >= blendOut.duration;
92 }
93
94 return blendIn.start >= blendOut.start && blendIn.start < blendOut.end;
95 }
96
97 internal static void ComputeBlendsFromOverlaps(this TrackAsset asset)
98 {
99 ComputeBlendsFromOverlaps(asset.clips);
100 }
101
102 internal static void ComputeBlendsFromOverlaps(TimelineClip[] clips)
103 {
104 foreach (var clip in clips)
105 {
106 clip.blendInDuration = -1;
107 clip.blendOutDuration = -1;
108 }
109
110 Array.Sort(clips, (c1, c2) =>
111 Math.Abs(c1.start - c2.start) < TimeUtility.kTimeEpsilon ? c1.duration.CompareTo(c2.duration) : c1.start.CompareTo(c2.start));
112
113 for (var i = 0; i < clips.Length; i++)
114 {
115 var clip = clips[i];
116 if (!clip.SupportsBlending())
117 continue;
118 var blendIn = clip;
119 TimelineClip blendOut = null;
120
121 var blendOutCandidate = clips[Math.Max(i - 1, 0)];
122 if (Overlaps(blendOutCandidate, blendIn))
123 blendOut = blendOutCandidate;
124
125 if (blendOut != null)
126 {
127 UpdateClipIntersection(blendOut, blendIn);
128 }
129 }
130 }
131
132 static void UpdateClipIntersection(TimelineClip blendOutClip, TimelineClip blendInClip)
133 {
134 if (!blendOutClip.SupportsBlending() || !blendInClip.SupportsBlending())
135 return;
136
137 if (blendInClip.start - blendOutClip.start < blendOutClip.duration - blendInClip.duration)
138 return;
139
140 double duration = Math.Max(0, blendOutClip.start + blendOutClip.duration - blendInClip.start);
141 duration = duration <= kMinOverlapTime ? 0 : duration;
142 blendOutClip.blendOutDuration = duration;
143 blendInClip.blendInDuration = duration;
144
145 var blendInMode = blendInClip.blendInCurveMode;
146 var blendOutMode = blendOutClip.blendOutCurveMode;
147
148 if (blendInMode == TimelineClip.BlendCurveMode.Manual && blendOutMode == TimelineClip.BlendCurveMode.Auto)
149 {
150 blendOutClip.mixOutCurve = CurveEditUtility.CreateMatchingCurve(blendInClip.mixInCurve);
151 }
152 else if (blendInMode == TimelineClip.BlendCurveMode.Auto && blendOutMode == TimelineClip.BlendCurveMode.Manual)
153 {
154 blendInClip.mixInCurve = CurveEditUtility.CreateMatchingCurve(blendOutClip.mixOutCurve);
155 }
156 else if (blendInMode == TimelineClip.BlendCurveMode.Auto && blendOutMode == TimelineClip.BlendCurveMode.Auto)
157 {
158 blendInClip.mixInCurve = null; // resets to default curves
159 blendOutClip.mixOutCurve = null;
160 }
161 }
162
163 static void RecursiveSubtrackClone(TrackAsset source, TrackAsset duplicate, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, PlayableAsset assetOwner)
164 {
165 var subtracks = source.GetChildTracks();
166 foreach (var sub in subtracks)
167 {
168 var newSub = TimelineHelpers.Clone(duplicate, sub, sourceTable, destTable, assetOwner);
169 duplicate.AddChild(newSub);
170 RecursiveSubtrackClone(sub, newSub, sourceTable, destTable, assetOwner);
171
172 // Call the custom editor on Create
173 var customEditor = CustomTimelineEditorCache.GetTrackEditor(newSub);
174 customEditor.OnCreate_Safe(newSub, sub);
175
176 // registration has to happen AFTER recursion
177 TimelineCreateUtilities.SaveAssetIntoObject(newSub, assetOwner);
178 TimelineUndo.RegisterCreatedObjectUndo(newSub, L10n.Tr("Duplicate"));
179 }
180 }
181
182 internal static TrackAsset Duplicate(this TrackAsset track, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable,
183 TimelineAsset destinationTimeline = null)
184 {
185 if (track == null)
186 return null;
187
188 // if the destination is us, clear to avoid bad parenting (case 919421)
189 if (destinationTimeline == track.timelineAsset)
190 destinationTimeline = null;
191
192 var timelineParent = track.parent as TimelineAsset;
193 var trackParent = track.parent as TrackAsset;
194 if (timelineParent == null && trackParent == null)
195 {
196 Debug.LogWarning("Cannot duplicate track because it is not parented to known type");
197 return null;
198 }
199
200 // Determine who the final parent is. If we are pasting into another track, it's always the timeline.
201 // Otherwise it's the original parent
202 PlayableAsset finalParent = destinationTimeline != null ? destinationTimeline : track.parent;
203
204 // grab the list of tracks to generate a name from (923360) to get the list of names
205 // no need to do this part recursively
206 var finalTrackParent = finalParent as TrackAsset;
207 var finalTimelineAsset = finalParent as TimelineAsset;
208 var otherTracks = (finalTimelineAsset != null) ? finalTimelineAsset.trackObjects : finalTrackParent.subTracksObjects;
209
210 // Important to create the new objects before pushing the original undo, or redo breaks the
211 // sequence
212 var newTrack = TimelineHelpers.Clone(finalParent, track, sourceTable, destTable, finalParent);
213 newTrack.name = TimelineCreateUtilities.GenerateUniqueActorName(otherTracks, newTrack.name);
214
215 RecursiveSubtrackClone(track, newTrack, sourceTable, destTable, finalParent);
216 TimelineCreateUtilities.SaveAssetIntoObject(newTrack, finalParent);
217 TimelineUndo.RegisterCreatedObjectUndo(newTrack, L10n.Tr("Duplicate"));
218 UndoExtensions.RegisterPlayableAsset(finalParent, L10n.Tr("Duplicate"));
219
220 if (destinationTimeline != null) // other timeline
221 destinationTimeline.AddTrackInternal(newTrack);
222 else if (timelineParent != null) // this timeline, no parent
223 ReparentTracks(new List<TrackAsset> { newTrack }, timelineParent, timelineParent.GetRootTracks().Last(), false);
224 else // this timeline, with parent
225 trackParent.AddChild(newTrack);
226
227 // Call the custom editor. this check prevents the call when copying to the clipboard
228 if (destinationTimeline == null || destinationTimeline == TimelineEditor.inspectedAsset)
229 {
230 var customEditor = CustomTimelineEditorCache.GetTrackEditor(newTrack);
231 customEditor.OnCreate_Safe(newTrack, track);
232 }
233
234 return newTrack;
235 }
236
237 // Reparents a list of tracks to a new parent
238 // the new parent cannot be null (has to be track asset or sequence)
239 // the insertAfter can be null (will not reorder)
240 internal static bool ReparentTracks(List<TrackAsset> tracksToMove, PlayableAsset targetParent,
241 TrackAsset insertMarker = null, bool insertBefore = false)
242 {
243 var targetParentTrack = targetParent as TrackAsset;
244 var targetSequenceTrack = targetParent as TimelineAsset;
245
246 if (tracksToMove == null || tracksToMove.Count == 0 || (targetParentTrack == null && targetSequenceTrack == null))
247 return false;
248
249 // invalid parent type on a track
250 if (targetParentTrack != null && tracksToMove.Any(x => !TimelineCreateUtilities.ValidateParentTrack(targetParentTrack, x.GetType())))
251 return false;
252
253 // no valid tracks means this is simply a rearrangement
254 var validTracks = tracksToMove.Where(x => x.parent != targetParent).ToList();
255 if (insertMarker == null && !validTracks.Any())
256 return false;
257
258 var parents = validTracks.Select(x => x.parent).Where(x => x != null).Distinct().ToList();
259 // push the current state of the tracks that will change
260 foreach (var p in parents)
261 {
262 UndoExtensions.RegisterPlayableAsset(p, "Reparent");
263 }
264 UndoExtensions.RegisterTracks(validTracks, "Reparent");
265 UndoExtensions.RegisterPlayableAsset(targetParent, "Reparent");
266
267 // need to reparent tracks first, before moving them.
268 foreach (var t in validTracks)
269 {
270 if (t.parent != targetParent)
271 {
272 TrackAsset toMoveParent = t.parent as TrackAsset;
273 TimelineAsset toMoveTimeline = t.parent as TimelineAsset;
274 if (toMoveTimeline != null)
275 {
276 toMoveTimeline.RemoveTrack(t);
277 }
278 else if (toMoveParent != null)
279 {
280 toMoveParent.RemoveSubTrack(t);
281 }
282
283 if (targetParentTrack != null)
284 {
285 targetParentTrack.AddChild(t);
286 targetParentTrack.SetCollapsed(false);
287 }
288 else
289 {
290 targetSequenceTrack.AddTrackInternal(t);
291 }
292 }
293 }
294
295
296 if (insertMarker != null)
297 {
298 // re-ordering track. This is using internal APIs, so invalidation of the tracks must be done manually to avoid
299 // cache mismatches
300 var children = targetParentTrack != null ? targetParentTrack.subTracksObjects : targetSequenceTrack.trackObjects;
301 TimelineUtility.ReorderTracks(children, tracksToMove, insertMarker, insertBefore);
302 if (targetParentTrack != null)
303 targetParentTrack.Invalidate();
304 if (insertMarker.timelineAsset != null)
305 insertMarker.timelineAsset.Invalidate();
306 }
307
308 return true;
309 }
310
311 internal static IEnumerable<TrackAsset> FilterTracks(IEnumerable<TrackAsset> tracks)
312 {
313 var nTracks = tracks.Count();
314 // Duplicate is recursive. If should not have parent and child in the list
315 var hash = new HashSet<TrackAsset>(tracks);
316 var take = new Dictionary<TrackAsset, bool>(nTracks);
317
318 foreach (var track in tracks)
319 {
320 var parent = track.parent as TrackAsset;
321 var foundParent = false;
322 // go up the hierarchy
323 while (parent != null && !foundParent)
324 {
325 if (hash.Contains(parent))
326 {
327 foundParent = true;
328 }
329
330 parent = parent.parent as TrackAsset;
331 }
332
333 take[track] = !foundParent;
334 }
335
336 foreach (var track in tracks)
337 {
338 if (take[track])
339 yield return track;
340 }
341 }
342
343 internal static bool GetShowMarkers(this TrackAsset track)
344 {
345 return TimelineWindowViewPrefs.IsShowMarkers(track);
346 }
347
348 internal static void SetShowMarkers(this TrackAsset track, bool collapsed)
349 {
350 TimelineWindowViewPrefs.SetTrackShowMarkers(track, collapsed);
351 }
352
353 internal static bool GetShowInlineCurves(this TrackAsset track)
354 {
355 return TimelineWindowViewPrefs.GetShowInlineCurves(track);
356 }
357
358 internal static void SetShowInlineCurves(this TrackAsset track, bool inlineOn)
359 {
360 TimelineWindowViewPrefs.SetShowInlineCurves(track, inlineOn);
361 }
362
363 internal static bool ShouldShowInfiniteClipEditor(this AnimationTrack track)
364 {
365 return track != null && !track.inClipMode && track.infiniteClip != null;
366 }
367
368 // Special method to remove a track that is in a broken state. i.e. the script won't load
369 internal static bool RemoveBrokenTrack(PlayableAsset parent, ScriptableObject track)
370 {
371 var parentTrack = parent as TrackAsset;
372 var parentTimeline = parent as TimelineAsset;
373
374 if (parentTrack == null && parentTimeline == null)
375 throw new ArgumentException("parent is not a valid parent type", "parent");
376
377 // this object must be a Unity null, but not actually null;
378 object trackAsObject = track;
379 if (trackAsObject == null || track as TrackAsset != null) // yes, this is correct
380 throw new ArgumentException("track is not in a broken state");
381
382 // this belongs to a parent track
383 if (parentTrack != null)
384 {
385 int index = parentTrack.subTracksObjects.FindIndex(t => t.GetInstanceID() == track.GetInstanceID());
386 if (index >= 0)
387 {
388 UndoExtensions.RegisterTrack(parentTrack, L10n.Tr("Remove Track"));
389 parentTrack.subTracksObjects.RemoveAt(index);
390 parentTrack.Invalidate();
391 Undo.DestroyObjectImmediate(track);
392 return true;
393 }
394 }
395 else if (parentTimeline != null)
396 {
397 int index = parentTimeline.trackObjects.FindIndex(t => t.GetInstanceID() == track.GetInstanceID());
398 if (index >= 0)
399 {
400 UndoExtensions.RegisterPlayableAsset(parentTimeline, L10n.Tr("Remove Track"));
401 parentTimeline.trackObjects.RemoveAt(index);
402 parentTimeline.Invalidate();
403 Undo.DestroyObjectImmediate(track);
404 return true;
405 }
406 }
407
408 return false;
409 }
410
411 // Find the gap at the given time
412 // return true if there is a gap, false if there is an intersection
413 // endGap will be Infinity if the gap has no end
414 internal static bool GetGapAtTime(this TrackAsset track, double time, out double startGap, out double endGap)
415 {
416 startGap = 0;
417 endGap = Double.PositiveInfinity;
418
419 if (track == null || !track.GetClips().Any())
420 {
421 return false;
422 }
423
424 var discreteTime = new DiscreteTime(time);
425
426 track.SortClips();
427 var sortedByStartTime = track.clips;
428 for (int i = 0; i < sortedByStartTime.Length; i++)
429 {
430 var clip = sortedByStartTime[i];
431
432 // intersection
433 if (discreteTime >= new DiscreteTime(clip.start) && discreteTime < new DiscreteTime(clip.end))
434 {
435 endGap = time;
436 startGap = time;
437 return false;
438 }
439
440 if (clip.end < time)
441 {
442 startGap = clip.end;
443 }
444 if (clip.start > time)
445 {
446 endGap = clip.start;
447 break;
448 }
449 }
450
451 if (endGap - startGap < TimelineClip.kMinDuration)
452 {
453 startGap = time;
454 endGap = time;
455 return false;
456 }
457
458 return true;
459 }
460
461 internal static bool IsCompatibleWithClip(this TrackAsset track, TimelineClip clip)
462 {
463 if (track == null || clip == null || clip.asset == null)
464 return false;
465
466 return TypeUtility.GetPlayableAssetsHandledByTrack(track.GetType()).Contains(clip.asset.GetType());
467 }
468
469 // Get a flattened list of all child tracks
470 static void GetFlattenedChildTracks(this TrackAsset asset, List<TrackAsset> list)
471 {
472 if (asset == null || list == null)
473 return;
474
475 foreach (var track in asset.GetChildTracks())
476 {
477 list.Add(track);
478 GetFlattenedChildTracks(track, list);
479 }
480 }
481
482 internal static IEnumerable<TrackAsset> GetFlattenedChildTracks(this TrackAsset asset)
483 {
484 if (asset == null || !asset.GetChildTracks().Any())
485 return Enumerable.Empty<TrackAsset>();
486
487 var flattenedChildTracks = new List<TrackAsset>();
488 GetFlattenedChildTracks(asset, flattenedChildTracks);
489 return flattenedChildTracks;
490 }
491
492 internal static void ArmForRecord(this TrackAsset track)
493 {
494 TimelineWindow.instance.state.ArmForRecord(track);
495 }
496 internal static void UnarmForRecord(this TrackAsset track)
497 {
498 TimelineWindow.instance.state.UnarmForRecord(track);
499 }
500
501 internal static void SetShowTrackMarkers(this TrackAsset track, bool showMarkers)
502 {
503 var currentValue = track.GetShowMarkers();
504 if (currentValue != showMarkers)
505 {
506 TimelineUndo.PushUndo(TimelineWindow.instance.state.editSequence.viewModel, L10n.Tr("Toggle Show Markers"));
507 track.SetShowMarkers(showMarkers);
508 if (!showMarkers)
509 {
510 foreach (var marker in track.GetMarkers())
511 {
512 SelectionManager.Remove(marker);
513 }
514 }
515 }
516 }
517
518 internal static IEnumerable<TrackAsset> RemoveTimelineMarkerTrackFromList(this IEnumerable<TrackAsset> tracks, TimelineAsset asset)
519 {
520 return tracks.Where(t => t != asset.markerTrack);
521 }
522
523 internal static bool ContainsTimelineMarkerTrack(this IEnumerable<TrackAsset> tracks, TimelineAsset asset)
524 {
525 return tracks.Contains(asset.markerTrack);
526 }
527
528 internal static void SetNameWithUndo(this TrackAsset track, string newName)
529 {
530 UndoExtensions.RegisterTrack(track, L10n.Tr("Rename Track"));
531 track.name = newName;
532 }
533 }
534}