using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using UnityEditor.MemoryProfiler; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; using Component = UnityEngine.Component; using Object = UnityEngine.Object; namespace UnityEditor.Timeline { static class TimelineHelpers { static List s_SubClassesOfTrackDrawer; // check whether the exposed reference is explicitly named static bool IsExposedReferenceExplicitlyNamed(string name) { if (string.IsNullOrEmpty(name)) return false; GUID guid; return !GUID.TryParse(name, out guid); } static string GenerateExposedReferenceName() { return UnityEditor.GUID.Generate().ToString(); } public static void CloneExposedReferences(ScriptableObject clone, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable) { var cloneObject = new SerializedObject(clone); SerializedProperty prop = cloneObject.GetIterator(); while (prop.Next(true)) { if (prop.propertyType == SerializedPropertyType.ExposedReference) { var exposedNameProp = prop.FindPropertyRelative("exposedName"); var sourceKey = exposedNameProp.stringValue; var destKey = sourceKey; if (!IsExposedReferenceExplicitlyNamed(sourceKey)) destKey = GenerateExposedReferenceName(); exposedNameProp.stringValue = destKey; var requiresCopy = sourceTable != destTable || sourceKey != destKey; if (requiresCopy && sourceTable != null && destTable != null) { var valid = false; var target = sourceTable.GetReferenceValue(sourceKey, out valid); if (valid && target != null) { var existing = destTable.GetReferenceValue(destKey, out valid); if (!valid || existing != target) { var destTableObj = destTable as UnityEngine.Object; if (destTableObj != null) TimelineUndo.PushUndo(destTableObj, L10n.Tr("Create Clip")); destTable.SetReferenceValue(destKey, target); } } } } } cloneObject.ApplyModifiedPropertiesWithoutUndo(); } public static ScriptableObject CloneReferencedPlayableAsset(ScriptableObject original, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, Object newOwner) { var clone = Object.Instantiate(original); SaveCloneToAsset(clone, newOwner); if (clone == null || (clone as IPlayableAsset) == null) { throw new InvalidCastException("could not cast instantiated object into IPlayableAsset"); } CloneExposedReferences(clone, sourceTable, destTable); TimelineUndo.RegisterCreatedObjectUndo(clone, L10n.Tr("Create clip")); return clone; } static void SaveCloneToAsset(Object clone, Object newOwner) { if (newOwner == null) return; var containerPath = AssetDatabase.GetAssetPath(newOwner); var containerAsset = AssetDatabase.LoadAssetAtPath(containerPath); if (containerAsset != null) { TimelineCreateUtilities.SaveAssetIntoObject(clone, containerAsset); EditorUtility.SetDirty(containerAsset); } } static AnimationClip CloneAnimationClip(AnimationClip clip, Object owner) { if (clip == null) return null; var newClip = Object.Instantiate(clip); newClip.name = AnimationTrackRecorder.GetUniqueRecordedClipName(owner, clip.name); SaveAnimClipIntoObject(newClip, owner); TimelineUndo.RegisterCreatedObjectUndo(newClip, L10n.Tr("Create clip")); return newClip; } public static TimelineClip Clone(TimelineClip clip, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, double time, PlayableAsset newOwner = null) { if (newOwner == null) newOwner = clip.GetParentTrack(); TimelineClip newClip = DuplicateClip(clip, sourceTable, destTable, newOwner); newClip.start = time; var track = newClip.GetParentTrack(); track.SortClips(); return newClip; } // Creates a complete clone of a track and returns it. // Does not parent, or add the track to the sequence public static TrackAsset Clone(PlayableAsset parent, TrackAsset trackAsset, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, PlayableAsset assetOwner = null) { if (trackAsset == null) return null; var timelineAsset = trackAsset.timelineAsset; if (timelineAsset == null) return null; if (assetOwner == null) assetOwner = parent; // create a duplicate, then clear the clips and subtracks var newTrack = Object.Instantiate(trackAsset); newTrack.name = trackAsset.name; newTrack.ClearClipsInternal(); newTrack.parent = parent; newTrack.ClearSubTracksInternal(); if (trackAsset.hasCurves) newTrack.curves = CloneAnimationClip(trackAsset.curves, assetOwner); var animTrack = trackAsset as AnimationTrack; if (animTrack != null && animTrack.infiniteClip != null) ((AnimationTrack)newTrack).infiniteClip = CloneAnimationClip(animTrack.infiniteClip, assetOwner); foreach (var clip in trackAsset.clips) { var newClip = DuplicateClip(clip, sourceTable, destTable, assetOwner); newClip.SetParentTrack_Internal(newTrack); } newTrack.ClearMarkers(); foreach (var e in trackAsset.GetMarkersRaw()) { var newMarker = Object.Instantiate(e); newTrack.AddMarker(newMarker); SaveCloneToAsset(newMarker, assetOwner); if (newMarker is IMarker) { (newMarker as IMarker).Initialize(newTrack); } } newTrack.SetCollapsed(trackAsset.IsCollapsed()); // calling code is responsible for adding to asset, adding to sequence, and parenting, // and duplicating subtracks return newTrack; } public static IEnumerable DuplicateItemsUsingCurrentEditMode(IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, ItemsPerTrack items, TrackAsset targetParent, double candidateTime, string undoOperation) { if (targetParent != null) { var aTrack = targetParent as AnimationTrack; if (aTrack != null) aTrack.ConvertToClipMode(); var duplicatedItems = DuplicateItems(items, targetParent, sourceTable, destTable, undoOperation); FinalizeInsertItemsUsingCurrentEditMode(new[] {duplicatedItems}, candidateTime); return duplicatedItems.items; } return Enumerable.Empty(); } public static IEnumerable DuplicateItemsUsingCurrentEditMode(IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, IEnumerable items, double candidateTime, string undoOperation) { var duplicatedItemsGroups = new List(); foreach (var i in items) duplicatedItemsGroups.Add(DuplicateItems(i, i.targetTrack, sourceTable, destTable, undoOperation)); FinalizeInsertItemsUsingCurrentEditMode(duplicatedItemsGroups, candidateTime); return duplicatedItemsGroups.SelectMany(i => i.items); } internal static ItemsPerTrack DuplicateItems(ItemsPerTrack items, TrackAsset target, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, string undoOperation) { var duplicatedItems = new List(); var clips = items.clips.ToList(); if (clips.Any()) { TimelineUndo.PushUndo(target, undoOperation); duplicatedItems.AddRange(DuplicateClips(clips, sourceTable, destTable, target).ToItems()); TimelineUndo.PushUndo(target, undoOperation); // second undo causes reference fixups on redo (case 1063868) } var markers = items.markers.ToList(); if (markers.Any()) { duplicatedItems.AddRange(MarkerModifier.CloneMarkersToParent(markers, target).ToItems()); } return new ItemsPerTrack(target, duplicatedItems.ToArray()); } static void FinalizeInsertItemsUsingCurrentEditMode(IList itemsGroups, double candidateTime) { EditMode.FinalizeInsertItemsAtTime(itemsGroups, candidateTime); SelectionManager.Clear(); foreach (var itemsGroup in itemsGroups) { var track = itemsGroup.targetTrack; var items = itemsGroup.items; EditModeUtils.SetParentTrack(items, track); track.SortClips(); TrackExtensions.ComputeBlendsFromOverlaps(track.clips); track.CalculateExtrapolationTimes(); foreach (var item in items) if (item.gui != null) item.gui.Select(); } var allItems = itemsGroups.SelectMany(x => x.items).ToList(); foreach (var item in allItems) { SelectionManager.Add(item); } FrameItems(allItems); } internal static TimelineClip Clone(TimelineClip clip, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, PlayableAsset newOwner) { var editorClip = EditorClipFactory.GetEditorClip(clip); // Workaround for Clips not being unity object, assign it to a editor clip wrapper, clone it, and pull the clip back out var newClip = Object.Instantiate(editorClip).clip; // perform fix ups for what Instantiate cannot properly detect SelectionManager.Remove(newClip); newClip.SetParentTrack_Internal(null); newClip.curves = null; // instantiate might copy the reference, we need to clear it // curves are explicitly owned by the clip if (clip.curves != null) { newClip.CreateCurves(AnimationTrackRecorder.GetUniqueRecordedClipName(newOwner, clip.curves.name)); EditorUtility.CopySerialized(clip.curves, newClip.curves); TimelineCreateUtilities.SaveAssetIntoObject(newClip.curves, newOwner); } ScriptableObject playableAsset = newClip.asset as ScriptableObject; if (playableAsset != null && newClip.asset is IPlayableAsset) { var clone = CloneReferencedPlayableAsset(playableAsset, sourceTable, destTable, newOwner); newClip.asset = clone; // special case to make the name match the recordable clips, but only if they match on the original clip var originalRecordedAsset = clip.asset as AnimationPlayableAsset; if (clip.recordable && originalRecordedAsset != null && originalRecordedAsset.clip != null) { AnimationPlayableAsset clonedAnimationAsset = clone as AnimationPlayableAsset; if (clonedAnimationAsset != null && clonedAnimationAsset.clip != null) { clonedAnimationAsset.clip = CloneAnimationClip(originalRecordedAsset.clip, newOwner); if (clip.displayName == originalRecordedAsset.clip.name && newClip.recordable) { clonedAnimationAsset.name = clonedAnimationAsset.clip.name; newClip.displayName = clonedAnimationAsset.name; } } } } return newClip; } static TimelineClip[] DuplicateClips(IEnumerable clips, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, PlayableAsset newOwner) { var newClips = new TimelineClip[clips.Count()]; int i = 0; foreach (var clip in clips) { var newParent = newOwner == null ? clip.GetParentTrack() : newOwner; var newClip = DuplicateClip(clip, sourceTable, destTable, newParent); newClip.SetParentTrack_Internal(null); newClips[i++] = newClip; } return newClips; } static TimelineClip DuplicateClip(TimelineClip clip, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, PlayableAsset newOwner) { var newClip = Clone(clip, sourceTable, destTable, newOwner); var track = clip.GetParentTrack(); if (track != null) { newClip.SetParentTrack_Internal(track); track.AddClip(newClip); } var editor = CustomTimelineEditorCache.GetClipEditor(clip); try { editor.OnCreate(newClip, track, clip); } catch (Exception e) { Debug.LogException(e); } return newClip; } // Given a track type, return all the playable asset types that should be // visible to the user via menus // Given a track type, return all the playable asset types public static Type GetCustomDrawer(Type trackType) { if (s_SubClassesOfTrackDrawer == null) { s_SubClassesOfTrackDrawer = TypeCache.GetTypesDerivedFrom().ToList(); } foreach (var drawer in s_SubClassesOfTrackDrawer) { var attr = Attribute.GetCustomAttribute(drawer, typeof(CustomTrackDrawerAttribute), false) as CustomTrackDrawerAttribute; if (attr != null && attr.assetType.IsAssignableFrom(trackType)) return drawer; } return typeof(TrackDrawer); } public static bool HaveSameContainerAsset(Object assetA, Object assetB) { if (assetA == null || assetB == null) return false; if ((assetA.hideFlags & HideFlags.DontSave) != 0 && (assetB.hideFlags & HideFlags.DontSave) != 0) return true; return AssetDatabase.GetAssetPath(assetA) == AssetDatabase.GetAssetPath(assetB); } public static void SaveAnimClipIntoObject(AnimationClip clip, Object asset) { if (asset != null) { clip.hideFlags = asset.hideFlags & ~HideFlags.HideInHierarchy; // show animation clips, even if the parent track isn't if ((clip.hideFlags & HideFlags.DontSave) == 0) { AssetDatabase.AddObjectToAsset(clip, asset); } } } // Make sure a gameobject has all the required component for the given TrackAsset public static Component AddRequiredComponent(GameObject go, TrackAsset asset) { if (go == null || asset == null) return null; var bindings = asset.outputs; if (!bindings.Any()) return null; var binding = bindings.First(); if (binding.outputTargetType == null || !typeof(Component).IsAssignableFrom(binding.outputTargetType)) return null; var component = go.GetComponent(binding.outputTargetType); if (component == null) { component = Undo.AddComponent(go, binding.outputTargetType); } return component; } public static string GetTrackCategoryName(System.Type trackType) { if (trackType == null) return string.Empty; string s = GetItemCategoryName(trackType); if (!String.IsNullOrEmpty(s)) return s; // if as display name with a path is specified use that var attr = Attribute.GetCustomAttribute(trackType, typeof(DisplayNameAttribute)) as DisplayNameAttribute; if (attr != null && attr.DisplayName.Contains('/')) return string.Empty; if (trackType.Namespace == null || trackType.Namespace.Contains("UnityEngine")) return string.Empty; return trackType.Namespace + "/"; } public static string GetItemCategoryName(System.Type itemType) { if (itemType == null) return string.Empty; var attribute = itemType.GetCustomAttribute(typeof(MenuCategoryAttribute)) as MenuCategoryAttribute; if (attribute != null) { var s = attribute.category; if (!s.EndsWith("/")) s += "/"; return s; } return string.Empty; } public static string GetTrackMenuName(System.Type trackType) { return L10n.Tr(TypeUtility.GetDisplayName(trackType)); } // retrieve the duration of a single loop, taking into account speed public static double GetLoopDuration(TimelineClip clip) { double length = clip.clipAssetDuration; if (double.IsNegativeInfinity(length) || double.IsNaN(length)) return TimelineClip.kMinDuration; if (length == double.MaxValue || double.IsInfinity(length)) { return double.MaxValue; } return Math.Max(TimelineClip.kMinDuration, length / clip.timeScale); } public static double GetClipAssetEndTime(TimelineClip clip) { var d = GetLoopDuration(clip); if (d < double.MaxValue) d = clip.FromLocalTimeUnbound(d); return d; } // Checks if the underlying asset duration is usable. This means the clip // can loop or hold public static bool HasUsableAssetDuration(TimelineClip clip) { double length = clip.clipAssetDuration; return (length < TimelineClip.kMaxTimeValue) && !double.IsInfinity(length) && !double.IsNaN(length); } // Retrieves the starting point of each loop of a clip, relative to the start of the clip // Note that if clip-in is bigger than the loopDuration, negative times will be added public static double[] GetLoopTimes(TimelineClip clip) { if (!HasUsableAssetDuration(clip)) return new[] {-clip.clipIn / clip.timeScale}; var times = new List(); double loopDuration = GetLoopDuration(clip); if (loopDuration <= TimeUtility.kTimeEpsilon) return new double[] {}; double start = -clip.clipIn / clip.timeScale; double end = start + loopDuration; times.Add(start); while (end < clip.duration - WindowState.kTimeEpsilon) { times.Add(end); end += loopDuration; } return times.ToArray(); } public static double GetCandidateTime(Vector2? mousePosition, params TrackAsset[] trackAssets) { // Right-Click if (mousePosition != null) { return TimeReferenceUtility.GetSnappedTimeAtMousePosition(mousePosition.Value); } // Playhead if (TimelineEditor.inspectedDirector != null) { return TimeReferenceUtility.SnapToFrameIfRequired(TimelineEditor.inspectedSequenceTime); } // Specific tracks end if (trackAssets != null && trackAssets.Any()) { var items = trackAssets.SelectMany(t => t.GetItems()).ToList(); return items.Any() ? items.Max(i => i.end) : 0; } // Timeline tracks end if (TimelineEditor.inspectedAsset != null) return TimelineEditor.inspectedAsset.flattenedTracks.Any() ? TimelineEditor.inspectedAsset.flattenedTracks.Max(t => t.end) : 0; return 0.0; } public static TimelineClip CreateClipOnTrack(Object asset, TrackAsset parentTrack, WindowState state) { return CreateClipOnTrack(asset, parentTrack, GetCandidateTime(null, parentTrack), state); } public static TimelineClip CreateClipOnTrack(Object asset, TrackAsset parentTrack, double candidateTime) { WindowState state = null; if (TimelineWindow.instance != null) state = TimelineWindow.instance.state; return CreateClipOnTrack(asset, parentTrack, candidateTime, state); } public static TimelineClip CreateClipOnTrack(Type playableAssetType, TrackAsset parentTrack, WindowState state) { return CreateClipOnTrack(playableAssetType, null, parentTrack, GetCandidateTime(null, parentTrack), state); } public static TimelineClip CreateClipOnTrack(Type playableAssetType, TrackAsset parentTrack, double candidateTime) { return CreateClipOnTrack(playableAssetType, null, parentTrack, candidateTime); } public static TimelineClip CreateClipOnTrack(Object asset, TrackAsset parentTrack, double candidateTime, WindowState state) { if (parentTrack == null) return null; // pick the first clip type available, unless there is one that matches the asset var clipType = TypeUtility.GetPlayableAssetsHandledByTrack(parentTrack.GetType()).FirstOrDefault(); if (asset != null) clipType = TypeUtility.GetAssetTypesForObject(parentTrack.GetType(), asset).FirstOrDefault(); if (clipType == null) return null; return CreateClipOnTrack(clipType, asset, parentTrack, candidateTime, state); } public static TimelineClip CreateClipOnTrack(Type playableAssetType, Object assignableObject, TrackAsset parentTrack, double candidateTime) { WindowState state = null; if (TimelineWindow.instance != null) state = TimelineWindow.instance.state; return CreateClipOnTrack(playableAssetType, assignableObject, parentTrack, candidateTime, state); } public static TimelineClip CreateClipOnTrack(Type playableAssetType, Object assignableObject, TrackAsset parentTrack, double candidateTime, WindowState state) { if (parentTrack == null) return null; bool revertClipMode = false; // Ideally this is done automatically by the animation track, // but it's editor only because it does animation clip manipulation var animTrack = parentTrack as AnimationTrack; if (animTrack != null && animTrack.CanConvertToClipMode()) { animTrack.ConvertToClipMode(); revertClipMode = true; } TimelineClip newClip = null; if (TypeUtility.IsConcretePlayableAsset(playableAssetType)) { try { newClip = parentTrack.CreateClipOfType(playableAssetType); } catch (InvalidOperationException) {} // expected on a mismatch } if (newClip == null) { if (revertClipMode) animTrack.ConvertFromClipMode(animTrack.timelineAsset); Debug.LogWarningFormat("Cannot create a clip of type {0} on a track of type {1}", playableAssetType.Name, parentTrack.GetType().Name); return null; } AddClipOnTrack(newClip, parentTrack, candidateTime, assignableObject, state); return newClip; } /// /// Create a clip on track from an existing PlayableAsset /// public static TimelineClip CreateClipOnTrackFromPlayableAsset(IPlayableAsset asset, TrackAsset parentTrack, double candidateTime) { if (parentTrack == null || asset == null || !TypeUtility.IsConcretePlayableAsset(asset.GetType())) return null; TimelineClip newClip = null; try { newClip = parentTrack.CreateClipFromPlayableAsset(asset); } catch { return null; } WindowState state = null; if (TimelineWindow.instance != null) state = TimelineWindow.instance.state; AddClipOnTrack(newClip, parentTrack, candidateTime, null, state); return newClip; } public static void CreateClipsFromObjects(Type assetType, TrackAsset targetTrack, double candidateTime, IEnumerable objects) { foreach (var obj in objects) { if (ObjectReferenceField.FindObjectReferences(assetType).Any(f => f.IsAssignable(obj))) { var clip = CreateClipOnTrack(assetType, obj, targetTrack, candidateTime); candidateTime += clip.duration; } } } public static void CreateMarkersFromObjects(Type assetType, TrackAsset targetTrack, double candidateTime, IEnumerable objects) { var mList = new List(); foreach (var obj in objects) { if (ObjectReferenceField.FindObjectReferences(assetType).Any(f => f.IsAssignable(obj))) { var marker = CreateMarkerOnTrack(assetType, obj, targetTrack, candidateTime); mList.Add(marker.ToItem()); } } var state = TimelineWindow.instance.state; for (var i = 1; i < mList.Count; ++i) { var delta = ItemsUtils.TimeGapBetweenItems(mList[i - 1], mList[i]); mList[i].start += delta; } FinalizeInsertItemsUsingCurrentEditMode(new[] {new ItemsPerTrack(targetTrack, mList)}, candidateTime); state.Refresh(); } public static IMarker CreateMarkerOnTrack(Type markerType, Object assignableObject, TrackAsset parentTrack, double candidateTime) { WindowState state = null; if (TimelineWindow.instance != null) state = TimelineWindow.instance.state; var newMarker = parentTrack.CreateMarker(markerType, candidateTime); //Throws if marker is not an object var obj = newMarker as ScriptableObject; if (obj != null) obj.name = TypeUtility.GetDisplayName(markerType); if (assignableObject != null) { var director = state != null ? state.editSequence.director : null; foreach (var field in ObjectReferenceField.FindObjectReferences(markerType)) { if (field.IsAssignable(assignableObject)) { field.Assign(newMarker as ScriptableObject, assignableObject, director); break; } } } try { CustomTimelineEditorCache.GetMarkerEditor(newMarker).OnCreate(newMarker, null); } catch (Exception e) { Debug.LogException(e); } return newMarker; } public static void CreateClipsFromTypes(IEnumerable assetTypes, TrackAsset targetTrack, double candidateTime) { foreach (var assetType in assetTypes) { var clip = CreateClipOnTrack(assetType, targetTrack, candidateTime); candidateTime += clip.duration; } } public static void FrameItems(IEnumerable items) { if (items == null || !items.Any()) return; // if this is called before a repaint, the timeArea can be null if (TimelineEditor.window.timeArea == null) return; var start = (float)items.Min(x => x.start); var end = (float)items.Max(x => x.end); var timeRange = TimelineEditor.visibleTimeRange; // nothing to do if (timeRange.x <= start && timeRange.y >= end) return; timeRange.x = Mathf.Max(0, timeRange.x); timeRange.y = Mathf.Max(0, timeRange.y); var ds = start - timeRange.x; var de = end - timeRange.y; var padding = TimeReferenceUtility.PixelToTime(15) - TimeReferenceUtility.PixelToTime(0); var d = Math.Abs(ds) < Math.Abs(de) ? ds - padding : de + padding; TimelineEditor.visibleTimeRange = new Vector2(timeRange.x + d, timeRange.y + d); } public static void Frame(WindowState state, double start, double end) { var timeRange = state.timeAreaShownRange; // nothing to do if (timeRange.x <= start && timeRange.y >= end) return; var ds = (float)start - timeRange.x; var de = (float)end - timeRange.y; var padding = state.PixelDeltaToDeltaTime(15); var d = Math.Abs(ds) < Math.Abs(de) ? ds - padding : de + padding; state.SetTimeAreaShownRange(timeRange.x + d, timeRange.y + d); } public static void RangeSelect(IList totalCollection, IList currentSelection, T clickedItem, Action selector, Action remover) where T : class { var firstSelect = currentSelection.FirstOrDefault(); if (firstSelect == null) { selector(clickedItem); return; } var idxFirstSelect = totalCollection.IndexOf(firstSelect); var idxLastSelect = totalCollection.IndexOf(currentSelection.Last()); var idxClicked = totalCollection.IndexOf(clickedItem); //case 927807: selection is invalid if (idxFirstSelect < 0) { SelectionManager.Clear(); selector(clickedItem); return; } // Expand the selection between the first selected clip and clicked clip (insertion order is important) if (idxFirstSelect < idxClicked) for (var i = idxFirstSelect; i <= idxClicked; ++i) selector(totalCollection[i]); else for (var i = idxFirstSelect; i >= idxClicked; --i) selector(totalCollection[i]); // If clicked inside the selected range, shrink the selection between the the click and last selected clip if (Math.Min(idxFirstSelect, idxLastSelect) < idxClicked && idxClicked < Math.Max(idxFirstSelect, idxLastSelect)) for (var i = Math.Min(idxLastSelect, idxClicked); i <= Math.Max(idxLastSelect, idxClicked); ++i) remover(totalCollection[i]); // Ensure clicked clip is selected selector(clickedItem); } /// /// Shared code for adding a clip to a track /// static void AddClipOnTrack(TimelineClip newClip, TrackAsset parentTrack, double candidateTime, Object assignableObject, WindowState state) { var playableAsset = newClip.asset as IPlayableAsset; newClip.SetParentTrack_Internal(null); newClip.timeScale = 1.0; newClip.mixInCurve = AnimationCurve.EaseInOut(0, 0, 1, 1); newClip.mixOutCurve = AnimationCurve.EaseInOut(0, 1, 1, 0); var playableDirector = state != null ? state.editSequence.director : null; if (assignableObject != null) { foreach (var field in ObjectReferenceField.FindObjectReferences(playableAsset.GetType())) { if (field.IsAssignable(assignableObject)) { newClip.displayName = assignableObject.name; field.Assign(newClip.asset as PlayableAsset, assignableObject, playableDirector); break; } } } // get the clip editor try { CustomTimelineEditorCache.GetClipEditor(newClip).OnCreate(newClip, parentTrack, null); } catch (Exception e) { Debug.LogException(e); } // reset the duration as the newly assigned values may have changed the default if (playableAsset != null) { var candidateDuration = playableAsset.duration; if (!double.IsInfinity(candidateDuration) && candidateDuration > 0) newClip.duration = Math.Min(Math.Max(candidateDuration, TimelineClip.kMinDuration), TimelineClip.kMaxTimeValue); } var newClipsByTracks = new[] { new ItemsPerTrack(parentTrack, new[] {newClip.ToItem()}) }; FinalizeInsertItemsUsingCurrentEditMode(newClipsByTracks, candidateTime); if (state != null) state.Refresh(); } public static TrackAsset CreateTrack(TimelineAsset asset, Type type, TrackAsset parent = null, string name = null) { if (asset == null) return null; var track = asset.CreateTrack(type, parent, name); if (track != null) { if (parent != null) parent.SetCollapsed(false); var editor = CustomTimelineEditorCache.GetTrackEditor(track); editor.OnCreate_Safe(track, null); TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved); } return track; } public static TrackAsset CreateTrack(Type type, TrackAsset parent = null, string name = null) { return CreateTrack(TimelineEditor.inspectedAsset, type, parent, name); } public static T CreateTrack(TimelineAsset asset, TrackAsset parent = null, string name = null) where T : TrackAsset { return (T)CreateTrack(asset, typeof(T), parent, name); } public static T CreateTrack(TrackAsset parent = null, string name = null) where T : TrackAsset { return (T)CreateTrack(TimelineEditor.inspectedAsset, typeof(T), parent, name); } } }