A game about forced loneliness, made by TACStudios
at master 534 lines 22 kB view raw
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}