398 lines
12 KiB
C#
398 lines
12 KiB
C#
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Linq;
|
||
|
using UnityEngine.Timeline;
|
||
|
using UnityEngine.Playables;
|
||
|
|
||
|
namespace UnityEditor.Timeline
|
||
|
{
|
||
|
static class ClipModifier
|
||
|
{
|
||
|
public static bool Delete(TimelineAsset timeline, TimelineClip clip)
|
||
|
{
|
||
|
return timeline.DeleteClip(clip);
|
||
|
}
|
||
|
|
||
|
public static bool Tile(IEnumerable<TimelineClip> clips)
|
||
|
{
|
||
|
if (clips.Count() < 2)
|
||
|
return false;
|
||
|
|
||
|
var clipsByTracks = clips.GroupBy(x => x.GetParentTrack())
|
||
|
.Select(track => new {track.Key, Items = track.OrderBy(c => c.start)});
|
||
|
|
||
|
foreach (var track in clipsByTracks)
|
||
|
{
|
||
|
UndoExtensions.RegisterTrack(track.Key, L10n.Tr("Tile"));
|
||
|
}
|
||
|
|
||
|
foreach (var track in clipsByTracks)
|
||
|
{
|
||
|
double newStart = track.Items.First().start;
|
||
|
foreach (var c in track.Items)
|
||
|
{
|
||
|
c.start = newStart;
|
||
|
newStart += c.duration;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static bool TrimStart(IEnumerable<TimelineClip> clips, double trimTime)
|
||
|
{
|
||
|
var result = false;
|
||
|
|
||
|
foreach (var clip in clips)
|
||
|
result |= TrimStart(clip, trimTime);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
public static bool TrimStart(TimelineClip clip, double trimTime)
|
||
|
{
|
||
|
if (clip.asset == null)
|
||
|
return false;
|
||
|
|
||
|
if (clip.start > trimTime)
|
||
|
return false;
|
||
|
|
||
|
if (clip.end < trimTime)
|
||
|
return false;
|
||
|
|
||
|
UndoExtensions.RegisterClip(clip, L10n.Tr("Trim Clip Start"));
|
||
|
|
||
|
// Note: We are NOT using edit modes in this case because we want the same result
|
||
|
// regardless of the selected EditMode: split at cursor and delete left part
|
||
|
SetStart(clip, trimTime, false);
|
||
|
clip.ConformEaseValues();
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static bool TrimEnd(IEnumerable<TimelineClip> clips, double trimTime)
|
||
|
{
|
||
|
var result = false;
|
||
|
|
||
|
foreach (var clip in clips)
|
||
|
result |= TrimEnd(clip, trimTime);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
public static bool TrimEnd(TimelineClip clip, double trimTime)
|
||
|
{
|
||
|
if (clip.asset == null)
|
||
|
return false;
|
||
|
|
||
|
if (clip.start > trimTime)
|
||
|
return false;
|
||
|
|
||
|
if (clip.end < trimTime)
|
||
|
return false;
|
||
|
|
||
|
UndoExtensions.RegisterClip(clip, L10n.Tr("Trim Clip End"));
|
||
|
TrimClipWithEditMode(clip, TrimEdge.End, trimTime);
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static bool MatchDuration(IEnumerable<TimelineClip> clips)
|
||
|
{
|
||
|
double referenceDuration = clips.First().duration;
|
||
|
UndoExtensions.RegisterClips(clips, L10n.Tr("Match Clip Duration"));
|
||
|
foreach (var clip in clips)
|
||
|
{
|
||
|
var newEnd = clip.start + referenceDuration;
|
||
|
TrimClipWithEditMode(clip, TrimEdge.End, newEnd);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static bool Split(IEnumerable<TimelineClip> clips, double splitTime, PlayableDirector director)
|
||
|
{
|
||
|
var result = false;
|
||
|
|
||
|
foreach (var clip in clips)
|
||
|
{
|
||
|
if (clip.start >= splitTime)
|
||
|
continue;
|
||
|
|
||
|
if (clip.end <= splitTime)
|
||
|
continue;
|
||
|
|
||
|
UndoExtensions.RegisterClip(clip, L10n.Tr("Split Clip"));
|
||
|
|
||
|
TimelineClip newClip = TimelineHelpers.Clone(clip, director, director, clip.start);
|
||
|
|
||
|
clip.easeInDuration = 0;
|
||
|
newClip.easeOutDuration = 0;
|
||
|
|
||
|
SetStart(clip, splitTime, false);
|
||
|
SetEnd(newClip, splitTime, false);
|
||
|
|
||
|
// Sort produced by cloning clips on top of each other is unpredictable (it varies between mono runtimes)
|
||
|
clip.GetParentTrack().SortClips();
|
||
|
|
||
|
result = true;
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
public static void SetStart(TimelineClip clip, double time, bool affectTimeScale)
|
||
|
{
|
||
|
var supportsClipIn = clip.SupportsClipIn();
|
||
|
var supportsPadding = TimelineUtility.IsRecordableAnimationClip(clip);
|
||
|
bool calculateTimeScale = (affectTimeScale && clip.SupportsSpeedMultiplier());
|
||
|
|
||
|
// treat empty recordable clips as not supporting clip in (there are no keys to modify)
|
||
|
if (supportsPadding && (clip.animationClip == null || clip.animationClip.empty))
|
||
|
{
|
||
|
supportsClipIn = false;
|
||
|
}
|
||
|
|
||
|
if (supportsClipIn && !supportsPadding && !calculateTimeScale)
|
||
|
{
|
||
|
var minStart = clip.FromLocalTimeUnbound(0.0);
|
||
|
if (time < minStart)
|
||
|
time = minStart;
|
||
|
}
|
||
|
|
||
|
var maxStart = clip.end - TimelineClip.kMinDuration;
|
||
|
if (time > maxStart)
|
||
|
time = maxStart;
|
||
|
|
||
|
var timeOffset = time - clip.start;
|
||
|
var duration = clip.duration - timeOffset;
|
||
|
|
||
|
if (calculateTimeScale)
|
||
|
{
|
||
|
var f = clip.duration / duration;
|
||
|
clip.timeScale *= f;
|
||
|
}
|
||
|
|
||
|
|
||
|
if (supportsClipIn && !calculateTimeScale)
|
||
|
{
|
||
|
if (supportsPadding)
|
||
|
{
|
||
|
double clipInGlobal = clip.clipIn / clip.timeScale;
|
||
|
double keyShift = -timeOffset;
|
||
|
if (timeOffset < 0) // left drag, eliminate clipIn before shifting
|
||
|
{
|
||
|
double clipInDelta = Math.Max(-clipInGlobal, timeOffset);
|
||
|
keyShift = -Math.Min(0, timeOffset - clipInDelta);
|
||
|
clip.clipIn += clipInDelta * clip.timeScale;
|
||
|
}
|
||
|
else if (timeOffset > 0) // right drag, elimate padding in animation clip before adding clip in
|
||
|
{
|
||
|
var clipInfo = AnimationClipCurveCache.Instance.GetCurveInfo(clip.animationClip);
|
||
|
double keyDelta = clip.FromLocalTimeUnbound(clipInfo.keyTimes.Min()) - clip.start;
|
||
|
keyShift = -Math.Max(0, Math.Min(timeOffset, keyDelta));
|
||
|
clip.clipIn += Math.Max(timeOffset + keyShift, 0) * clip.timeScale;
|
||
|
}
|
||
|
if (keyShift != 0)
|
||
|
{
|
||
|
AnimationTrackRecorder.ShiftAnimationClip(clip.animationClip, (float)(keyShift * clip.timeScale));
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
clip.clipIn += timeOffset * clip.timeScale;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
clip.start = time;
|
||
|
clip.duration = duration;
|
||
|
}
|
||
|
|
||
|
public static void SetEnd(TimelineClip clip, double time, bool affectTimeScale)
|
||
|
{
|
||
|
var duration = Math.Max(time - clip.start, TimelineClip.kMinDuration);
|
||
|
|
||
|
if (affectTimeScale && clip.SupportsSpeedMultiplier())
|
||
|
{
|
||
|
var f = clip.duration / duration;
|
||
|
clip.timeScale *= f;
|
||
|
}
|
||
|
|
||
|
clip.duration = duration;
|
||
|
}
|
||
|
|
||
|
public static bool ResetEditing(IEnumerable<TimelineClip> clips)
|
||
|
{
|
||
|
var result = false;
|
||
|
|
||
|
foreach (var clip in clips)
|
||
|
result = result || ResetEditing(clip);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
public static bool ResetEditing(TimelineClip clip)
|
||
|
{
|
||
|
if (clip.asset == null)
|
||
|
return false;
|
||
|
|
||
|
UndoExtensions.RegisterClip(clip, L10n.Tr("Reset Clip Editing"));
|
||
|
|
||
|
clip.clipIn = 0.0;
|
||
|
|
||
|
if (clip.clipAssetDuration < double.MaxValue)
|
||
|
{
|
||
|
var duration = clip.clipAssetDuration / clip.timeScale;
|
||
|
TrimClipWithEditMode(clip, TrimEdge.End, clip.start + duration);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static bool MatchContent(IEnumerable<TimelineClip> clips)
|
||
|
{
|
||
|
var result = false;
|
||
|
|
||
|
foreach (var clip in clips)
|
||
|
result = result || MatchContent(clip);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
public static bool MatchContent(TimelineClip clip)
|
||
|
{
|
||
|
if (clip.asset == null)
|
||
|
return false;
|
||
|
|
||
|
UndoExtensions.RegisterClip(clip, L10n.Tr("Match Clip Content"));
|
||
|
|
||
|
var newStartCandidate = clip.start - clip.clipIn / clip.timeScale;
|
||
|
var newStart = newStartCandidate < 0.0 ? 0.0 : newStartCandidate;
|
||
|
|
||
|
TrimClipWithEditMode(clip, TrimEdge.Start, newStart);
|
||
|
|
||
|
// In case resetting the start was blocked by edit mode or timeline start, we do the best we can
|
||
|
clip.clipIn = (clip.start - newStartCandidate) * clip.timeScale;
|
||
|
if (clip.clipAssetDuration > 0 && TimelineHelpers.HasUsableAssetDuration(clip))
|
||
|
{
|
||
|
var duration = TimelineHelpers.GetLoopDuration(clip);
|
||
|
var offset = (clip.clipIn / clip.timeScale) % duration;
|
||
|
TrimClipWithEditMode(clip, TrimEdge.End, clip.start - offset + duration);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static void TrimClipWithEditMode(TimelineClip clip, TrimEdge edge, double time)
|
||
|
{
|
||
|
var clipItem = ItemsUtils.ToItem(clip);
|
||
|
EditMode.BeginTrim(clipItem, edge);
|
||
|
if (edge == TrimEdge.Start)
|
||
|
EditMode.TrimStart(clipItem, time, false);
|
||
|
else
|
||
|
EditMode.TrimEnd(clipItem, time, false);
|
||
|
EditMode.FinishTrim();
|
||
|
}
|
||
|
|
||
|
public static bool CompleteLastLoop(IEnumerable<TimelineClip> clips)
|
||
|
{
|
||
|
foreach (var clip in clips)
|
||
|
{
|
||
|
CompleteLastLoop(clip);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static void CompleteLastLoop(TimelineClip clip)
|
||
|
{
|
||
|
FixLoops(clip, true);
|
||
|
}
|
||
|
|
||
|
public static bool TrimLastLoop(IEnumerable<TimelineClip> clips)
|
||
|
{
|
||
|
foreach (var clip in clips)
|
||
|
{
|
||
|
TrimLastLoop(clip);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static void TrimLastLoop(TimelineClip clip)
|
||
|
{
|
||
|
FixLoops(clip, false);
|
||
|
}
|
||
|
|
||
|
static void FixLoops(TimelineClip clip, bool completeLastLoop)
|
||
|
{
|
||
|
if (!TimelineHelpers.HasUsableAssetDuration(clip))
|
||
|
return;
|
||
|
|
||
|
var loopDuration = TimelineHelpers.GetLoopDuration(clip);
|
||
|
var firstLoopDuration = loopDuration - clip.clipIn * (1.0 / clip.timeScale);
|
||
|
|
||
|
// Making sure we don't trim to zero
|
||
|
if (!completeLastLoop && firstLoopDuration > clip.duration)
|
||
|
return;
|
||
|
|
||
|
var numLoops = (clip.duration - firstLoopDuration) / loopDuration;
|
||
|
var numCompletedLoops = Math.Floor(numLoops);
|
||
|
|
||
|
if (!(numCompletedLoops < numLoops))
|
||
|
return;
|
||
|
|
||
|
if (completeLastLoop)
|
||
|
numCompletedLoops += 1;
|
||
|
|
||
|
var newEnd = clip.start + firstLoopDuration + loopDuration * numCompletedLoops;
|
||
|
|
||
|
UndoExtensions.RegisterClip(clip, L10n.Tr("Trim Clip Last Loop"));
|
||
|
|
||
|
TrimClipWithEditMode(clip, TrimEdge.End, newEnd);
|
||
|
}
|
||
|
|
||
|
public static bool DoubleSpeed(IEnumerable<TimelineClip> clips)
|
||
|
{
|
||
|
foreach (var clip in clips)
|
||
|
{
|
||
|
if (clip.SupportsSpeedMultiplier())
|
||
|
{
|
||
|
UndoExtensions.RegisterClip(clip, L10n.Tr("Double Clip Speed"));
|
||
|
clip.timeScale = clip.timeScale * 2.0f;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static bool HalfSpeed(IEnumerable<TimelineClip> clips)
|
||
|
{
|
||
|
foreach (var clip in clips)
|
||
|
{
|
||
|
if (clip.SupportsSpeedMultiplier())
|
||
|
{
|
||
|
UndoExtensions.RegisterClip(clip, L10n.Tr("Half Clip Speed"));
|
||
|
clip.timeScale = clip.timeScale * 0.5f;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public static bool ResetSpeed(IEnumerable<TimelineClip> clips)
|
||
|
{
|
||
|
foreach (var clip in clips)
|
||
|
{
|
||
|
if (clip.timeScale != 1.0)
|
||
|
{
|
||
|
UndoExtensions.RegisterClip(clip, L10n.Tr("Reset Clip Speed"));
|
||
|
clip.timeScale = 1.0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|