using System; using System.Linq; using UnityEngine; using UnityEngine.Timeline; namespace UnityEditor.Timeline { /// /// Extension Methods for AnimationTracks that require the Unity Editor, and may require the Timeline containing the Animation Track to be currently loaded in the Timeline Editor Window. /// public static class AnimationTrackExtensions { /// /// Determines whether the Timeline window can enable recording mode on an AnimationTrack. /// For a track to support recording, it needs to have a valid scene binding, /// its offset mode should not be Auto and needs to be currently visible in the Timeline Window. /// /// The track to query. /// True if recording can start, False otherwise. public static bool CanStartRecording(this AnimationTrack track) { if (track == null) { throw new ArgumentNullException(nameof(track)); } if (TimelineEditor.state == null) { return false; } var director = TimelineEditor.inspectedDirector; var animTrack = TimelineUtility.GetSceneReferenceTrack(track) as AnimationTrack; return animTrack != null && animTrack.trackOffset != TrackOffset.Auto && TimelineEditor.inspectedAsset == animTrack.timelineAsset && director != null && TimelineUtility.GetSceneGameObject(director, animTrack) != null; } /// /// Method that allows querying if a track is current enabled for animation recording. /// /// The track to query. /// True if currently recording and False otherwise. public static bool IsRecording(this AnimationTrack track) { if (track == null) { throw new ArgumentNullException(nameof(track)); } return TimelineEditor.state != null && TimelineEditor.state.IsArmedForRecord(track); } /// /// Method that enables animation recording for an AnimationTrack. /// /// The AnimationTrack which will be put in recording mode. /// True if track was put successfully in recording mode, False otherwise. public static bool StartRecording(this AnimationTrack track) { if (!CanStartRecording(track)) { return false; } TimelineEditor.state.ArmForRecord(track); return true; } /// /// Disables recording mode of an AnimationTrack. /// /// The AnimationTrack which will be taken out of recording mode. public static void StopRecording(this AnimationTrack track) { if (!IsRecording(track) || TimelineEditor.state == null) { return; } TimelineEditor.state.UnarmForRecord(track); } internal static void ConvertToClipMode(this AnimationTrack track) { if (!track.CanConvertToClipMode()) return; UndoExtensions.RegisterTrack(track, L10n.Tr("Convert To Clip")); if (!track.infiniteClip.empty) { var animClip = track.infiniteClip; TimelineUndo.PushUndo(animClip, L10n.Tr("Convert To Clip")); UndoExtensions.RegisterTrack(track, L10n.Tr("Convert To Clip")); var start = AnimationClipCurveCache.Instance.GetCurveInfo(animClip).keyTimes.FirstOrDefault(); animClip.ShiftBySeconds(-start); track.infiniteClip = null; var clip = track.CreateClip(animClip); clip.start = start; clip.preExtrapolationMode = track.infiniteClipPreExtrapolation; clip.postExtrapolationMode = track.infiniteClipPostExtrapolation; clip.recordable = true; if (Mathf.Abs(animClip.length) < TimelineClip.kMinDuration) { clip.duration = 1; } var animationAsset = clip.asset as AnimationPlayableAsset; if (animationAsset) { animationAsset.position = track.infiniteClipOffsetPosition; animationAsset.eulerAngles = track.infiniteClipOffsetEulerAngles; // going to / from infinite mode should reset this. infinite mode animationAsset.removeStartOffset = track.infiniteClipRemoveOffset; animationAsset.applyFootIK = track.infiniteClipApplyFootIK; animationAsset.loop = track.infiniteClipLoop; track.infiniteClipOffsetPosition = Vector3.zero; track.infiniteClipOffsetEulerAngles = Vector3.zero; } track.CalculateExtrapolationTimes(); } track.infiniteClip = null; EditorUtility.SetDirty(track); } internal static void ConvertFromClipMode(this AnimationTrack track, TimelineAsset timeline) { if (!track.CanConvertFromClipMode()) return; UndoExtensions.RegisterTrack(track, L10n.Tr("Convert From Clip")); var clip = track.clips[0]; var delta = (float)clip.start; track.infiniteClipTimeOffset = 0.0f; track.infiniteClipPreExtrapolation = clip.preExtrapolationMode; track.infiniteClipPostExtrapolation = clip.postExtrapolationMode; var animAsset = clip.asset as AnimationPlayableAsset; if (animAsset) { track.infiniteClipOffsetPosition = animAsset.position; track.infiniteClipOffsetEulerAngles = animAsset.eulerAngles; track.infiniteClipRemoveOffset = animAsset.removeStartOffset; track.infiniteClipApplyFootIK = animAsset.applyFootIK; track.infiniteClipLoop = animAsset.loop; } // clone it, it may not be in the same asset var animClip = clip.animationClip; float scale = (float)clip.timeScale; if (!Mathf.Approximately(scale, 1.0f)) { if (!Mathf.Approximately(scale, 0.0f)) scale = 1.0f / scale; animClip.ScaleTime(scale); } TimelineUndo.PushUndo(animClip, L10n.Tr("Convert From Clip")); animClip.ShiftBySeconds(delta); // manually delete the clip var asset = clip.asset; clip.asset = null; // Remove the clip, remove old assets ClipModifier.Delete(timeline, clip); TimelineUndo.PushDestroyUndo(null, track, asset); track.infiniteClip = animClip; EditorUtility.SetDirty(track); } internal static bool CanConvertToClipMode(this AnimationTrack track) { if (track == null || track.inClipMode) return false; return (track.infiniteClip != null && !track.infiniteClip.empty); } // Requirements to go from clip mode // - one clip, recordable, and animation clip belongs to the same asset as the track internal static bool CanConvertFromClipMode(this AnimationTrack track) { if ((track == null) || (!track.inClipMode) || (track.clips.Length != 1) || (track.clips[0].start < 0) || (!track.clips[0].recordable)) return false; var asset = track.clips[0].asset as AnimationPlayableAsset; if (asset == null) return false; return TimelineHelpers.HaveSameContainerAsset(track, asset.clip); } } }