You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

797 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TimelineClipGUI : TimelineItemGUI, IClipCurveEditorOwner, ISnappable, IAttractable
{
EditorClip m_EditorItem;
Rect m_ClipCenterSection;
readonly List<Rect> m_LoopRects = new List<Rect>();
ClipDrawData m_ClipDrawData;
Rect m_MixOutRect;
Rect m_MixInRect;
int m_MinLoopIndex = 1;
// clip dirty detection
int m_LastDirtyIndex = Int32.MinValue;
bool m_ClipViewDirty = true;
bool supportResize { get; }
public ClipCurveEditor clipCurveEditor { get; set; }
public TimelineClipGUI previousClip { get; set; }
public TimelineClipGUI nextClip { get; set; }
static readonly float k_MinMixWidth = 2;
static readonly float k_MaxHandleWidth = 10f;
static readonly float k_MinHandleWidth = 1f;
bool? m_ShowDrillIcon;
ClipEditor m_ClipEditor;
static List<PlayableDirector> s_TempSubDirectors = new List<PlayableDirector>();
static readonly IconData k_DiggableClipIcon = new IconData(DirectorStyles.LoadIcon("TimelineDigIn"));
string name
{
get
{
if (string.IsNullOrEmpty(clip.displayName))
return "(Empty)";
return clip.displayName;
}
}
public bool inlineCurvesSelected => SelectionManager.IsCurveEditorFocused(this);
public Rect mixOutRect
{
get
{
var percent = clip.mixOutPercentage;
var x = Mathf.Round(treeViewRect.width * (1 - percent));
var width = Mathf.Round(treeViewRect.width * percent);
m_MixOutRect.Set(x, 0.0f, width, treeViewRect.height);
return m_MixOutRect;
}
}
public Rect mixInRect
{
get
{
var width = Mathf.Round(treeViewRect.width * clip.mixInPercentage);
m_MixInRect.Set(0.0f, 0.0f, width, treeViewRect.height);
return m_MixInRect;
}
}
public ClipBlends GetClipBlends()
{
var _mixInRect = mixInRect;
var _mixOutRect = mixOutRect;
var blendInKind = BlendKind.None;
if (_mixInRect.width > k_MinMixWidth && clip.hasBlendIn)
blendInKind = BlendKind.Mix;
else if (_mixInRect.width > k_MinMixWidth)
blendInKind = BlendKind.Ease;
var blendOutKind = BlendKind.None;
if (_mixOutRect.width > k_MinMixWidth && clip.hasBlendOut)
blendOutKind = BlendKind.Mix;
else if (_mixOutRect.width > k_MinMixWidth)
blendOutKind = BlendKind.Ease;
return new ClipBlends(blendInKind, _mixInRect, blendOutKind, _mixOutRect);
}
public override double start
{
get { return clip.start; }
}
public override double end
{
get { return clip.end; }
}
public bool supportsLooping
{
get { return clip.SupportsLooping(); }
}
// for the inline curve editor, only show loops if we recorded the asset
bool IClipCurveEditorOwner.showLoops
{
get { return clip.SupportsLooping() && (clip.asset is AnimationPlayableAsset); }
}
TrackAsset IClipCurveEditorOwner.owner
{
get { return clip.GetParentTrack(); }
}
public bool supportsSubTimelines
{
get { return m_ClipEditor.supportsSubTimelines; }
}
public int minLoopIndex
{
get { return m_MinLoopIndex; }
}
public Rect clippedRect { get; private set; }
public override void Select()
{
MoveToTop();
SelectionManager.Add(clip);
if (clipCurveEditor != null && SelectionManager.Count() == 1)
SelectionManager.SelectInlineCurveEditor(this);
}
public override bool IsSelected()
{
return SelectionManager.Contains(clip);
}
public override void Deselect()
{
SelectionManager.Remove(clip);
if (inlineCurvesSelected)
SelectionManager.SelectInlineCurveEditor(null);
}
public override bool CanSelect(Event evt)
{
ClipBlends clipBlends = GetClipBlends();
Vector2 mousePos = evt.mousePosition - rect.position;
return m_ClipCenterSection.Contains(mousePos) || IsPointLocatedInClipBlend(mousePos, clipBlends);
}
static bool IsPointLocatedInClipBlend(Vector2 pt, ClipBlends blends)
{
if (blends.inRect.Contains(pt))
{
if (blends.inKind == BlendKind.Mix)
return Sign(pt, blends.inRect.min, blends.inRect.max) < 0;
return true;
}
if (blends.outRect.Contains(pt))
{
if (blends.outKind == BlendKind.Mix)
return Sign(pt, blends.outRect.min, blends.outRect.max) >= 0;
return true;
}
return false;
}
static float Sign(Vector2 point, Vector2 linePoint1, Vector2 linePoint2)
{
return (point.x - linePoint2.x) * (linePoint1.y - linePoint2.y) - (linePoint1.x - linePoint2.x) * (point.y - linePoint2.y);
}
public override ITimelineItem item
{
get { return ItemsUtils.ToItem(clip); }
}
IZOrderProvider zOrderProvider { get; }
public TimelineClipHandle leftHandle { get; }
public TimelineClipHandle rightHandle { get; }
public TimelineClipGUI(TimelineClip clip, IRowGUI parent, IZOrderProvider provider) : base(parent)
{
zOrderProvider = provider;
zOrder = provider.Next();
m_EditorItem = EditorClipFactory.GetEditorClip(clip);
m_ClipEditor = CustomTimelineEditorCache.GetClipEditor(clip);
supportResize = true;
leftHandle = new TimelineClipHandle(this, TrimEdge.Start);
rightHandle = new TimelineClipHandle(this, TrimEdge.End);
ItemToItemGui.Add(clip, this);
}
void CreateInlineCurveEditor(WindowState state)
{
if (clipCurveEditor != null)
return;
var animationClip = clip.animationClip;
if (animationClip != null && animationClip.empty)
animationClip = null;
// prune out clips coming from FBX
if (animationClip != null && !clip.recordable)
return; // don't show, even if there are curves
if (animationClip == null && !clip.HasAnyAnimatableParameters())
return; // nothing to show
state.AddEndFrameDelegate((istate, currentEvent) =>
{
clipCurveEditor = new ClipCurveEditor(CurveDataSource.Create(this), TimelineWindow.instance, clip.GetParentTrack());
return true;
});
}
public TimelineClip clip
{
get { return m_EditorItem.clip; }
}
// Draw the actual clip. Defers to the track drawer for customization
void UpdateDrawData(WindowState state, Rect drawRect, string title, bool selected, bool previousClipSelected, float rectXOffset)
{
m_ClipDrawData.clip = clip;
m_ClipDrawData.targetRect = drawRect;
m_ClipDrawData.clipCenterSection = m_ClipCenterSection;
m_ClipDrawData.unclippedRect = treeViewRect;
m_ClipDrawData.title = title;
m_ClipDrawData.selected = selected;
m_ClipDrawData.inlineCurvesSelected = inlineCurvesSelected;
m_ClipDrawData.previousClip = previousClip != null ? previousClip.clip : null;
m_ClipDrawData.previousClipSelected = previousClipSelected;
Vector3 shownAreaTime = state.timeAreaShownRange;
m_ClipDrawData.localVisibleStartTime = clip.ToLocalTimeUnbound(Math.Max(clip.start, shownAreaTime.x));
m_ClipDrawData.localVisibleEndTime = clip.ToLocalTimeUnbound(Math.Min(clip.end, shownAreaTime.y));
m_ClipDrawData.clippedRect = new Rect(clippedRect.x - rectXOffset, 0.0f, clippedRect.width, clippedRect.height);
m_ClipDrawData.minLoopIndex = minLoopIndex;
m_ClipDrawData.loopRects = m_LoopRects;
m_ClipDrawData.supportsLooping = supportsLooping;
m_ClipDrawData.clipBlends = GetClipBlends();
m_ClipDrawData.clipEditor = m_ClipEditor;
m_ClipDrawData.ClipDrawOptions = UpdateClipDrawOptions(m_ClipEditor, clip);
UpdateClipIcons(state);
}
void UpdateClipIcons(WindowState state)
{
// Pass 1 - gather size
int required = 0;
bool requiresDigIn = ShowDrillIcon(state.editSequence.director);
if (requiresDigIn)
required++;
var icons = m_ClipDrawData.ClipDrawOptions.icons;
foreach (var icon in icons)
{
if (icon != null)
required++;
}
// Pass 2 - copy icon data
if (required == 0)
{
m_ClipDrawData.rightIcons = null;
return;
}
if (m_ClipDrawData.rightIcons == null || m_ClipDrawData.rightIcons.Length != required)
m_ClipDrawData.rightIcons = new IconData[required];
int index = 0;
if (requiresDigIn)
m_ClipDrawData.rightIcons[index++] = k_DiggableClipIcon;
foreach (var icon in icons)
{
if (icon != null)
m_ClipDrawData.rightIcons[index++] = new IconData(icon);
}
}
static ClipDrawOptions UpdateClipDrawOptions(ClipEditor clipEditor, TimelineClip clip)
{
try
{
return clipEditor.GetClipOptions(clip);
}
catch (Exception e)
{
Debug.LogException(e);
}
return CustomTimelineEditorCache.GetDefaultClipEditor().GetClipOptions(clip);
}
static void DrawClip(ClipDrawData drawData)
{
ClipDrawer.DrawDefaultClip(drawData);
if (drawData.clip.asset is AnimationPlayableAsset)
{
var state = TimelineWindow.instance.state;
if (state.recording && state.IsArmedForRecord(drawData.clip.GetParentTrack()))
{
ClipDrawer.DrawAnimationRecordBorder(drawData);
ClipDrawer.DrawRecordProhibited(drawData);
}
}
}
public void DrawGhostClip(Rect targetRect)
{
DrawSimpleClip(targetRect, ClipBorder.Selection(), new Color(1.0f, 1.0f, 1.0f, 0.5f));
}
public void DrawInvalidClip(Rect targetRect)
{
DrawSimpleClip(targetRect, ClipBorder.Selection(), DirectorStyles.Instance.customSkin.colorInvalidClipOverlay);
}
void DrawSimpleClip(Rect targetRect, ClipBorder border, Color overlay)
{
var drawOptions = UpdateClipDrawOptions(CustomTimelineEditorCache.GetClipEditor(clip), clip);
ClipDrawer.DrawSimpleClip(clip, targetRect, border, overlay, drawOptions);
}
void DrawInto(Rect drawRect, WindowState state)
{
if (Event.current.type != EventType.Repaint)
return;
// create the inline curve editor if not already created
CreateInlineCurveEditor(state);
// @todo optimization, most of the calculations (rect, offsets, colors, etc.) could be cached
// and rebuilt when the hash of the clip changes.
if (isInvalid)
{
DrawInvalidClip(treeViewRect);
return;
}
GUI.BeginClip(drawRect);
var originRect = new Rect(0.0f, 0.0f, drawRect.width, drawRect.height);
string clipLabel = name;
var selected = SelectionManager.Contains(clip);
var previousClipSelected = previousClip != null && SelectionManager.Contains(previousClip.clip);
if (selected && 1.0 != clip.timeScale)
clipLabel += " " + clip.timeScale.ToString("F2") + "x";
UpdateDrawData(state, originRect, clipLabel, selected, previousClipSelected, drawRect.x);
DrawClip(m_ClipDrawData);
GUI.EndClip();
if (clip.GetParentTrack() != null && !clip.GetParentTrack().lockedInHierarchy)
{
if (selected && supportResize)
{
var cursorRect = rect;
cursorRect.xMin += leftHandle.boundingRect.width;
cursorRect.xMax -= rightHandle.boundingRect.width;
EditorGUIUtility.AddCursorRect(cursorRect, MouseCursor.MoveArrow);
}
if (supportResize)
{
var handleWidth = Mathf.Clamp(drawRect.width * 0.3f, k_MinHandleWidth, k_MaxHandleWidth);
leftHandle.Draw(drawRect, handleWidth, state);
rightHandle.Draw(drawRect, handleWidth, state);
}
}
}
void CalculateClipRectangle(Rect trackRect, WindowState state)
{
if (m_ClipViewDirty)
{
var clipRect = RectToTimeline(trackRect, state);
treeViewRect = clipRect;
// calculate clipped rect
clipRect.xMin = Mathf.Max(clipRect.xMin, trackRect.xMin);
clipRect.xMax = Mathf.Min(clipRect.xMax, trackRect.xMax);
if (clipRect.width > 0 && clipRect.width < 2)
{
clipRect.width = 5.0f;
}
clippedRect = clipRect;
}
}
void AddToSpacePartitioner(WindowState state)
{
if (Event.current.type == EventType.Repaint && !parent.locked)
state.spacePartitioner.AddBounds(this, rect);
}
void CalculateBlendRect()
{
m_ClipCenterSection = treeViewRect;
m_ClipCenterSection.x = 0;
m_ClipCenterSection.y = 0;
m_ClipCenterSection.xMin = mixInRect.xMax;
m_ClipCenterSection.width = Mathf.Round(treeViewRect.width - mixInRect.width - mixOutRect.width);
m_ClipCenterSection.xMax = m_ClipCenterSection.xMin + m_ClipCenterSection.width;
}
// Entry point to the Clip Drawing...
public override void Draw(Rect trackRect, bool trackRectChanged, WindowState state)
{
// if the clip has changed, fire the appropriate callback
DetectClipChanged(trackRectChanged);
// update the clip projected rectangle on the timeline
CalculateClipRectangle(trackRect, state);
AddToSpacePartitioner(state);
// update the blend rects (when clip overlaps with others)
CalculateBlendRect();
// update the loop rects (when clip loops)
CalculateLoopRects(trackRect, state);
DrawExtrapolation(trackRect, treeViewRect);
DrawInto(treeViewRect, state);
ResetClipChanged();
}
void DetectClipChanged(bool trackRectChanged)
{
if (Event.current.type == EventType.Layout)
{
if (clip.DirtyIndex != m_LastDirtyIndex)
{
m_ClipViewDirty = true;
try
{
m_ClipEditor.OnClipChanged(clip);
}
catch (Exception e)
{
Debug.LogException(e);
}
m_LastDirtyIndex = clip.DirtyIndex;
}
m_ClipViewDirty |= trackRectChanged;
}
}
void ResetClipChanged()
{
if (Event.current.type == EventType.Repaint)
m_ClipViewDirty = false;
}
internal void MoveToTop()
{
zOrder = zOrderProvider.Next();
}
GUIStyle GetExtrapolationIcon(TimelineClip.ClipExtrapolation mode)
{
GUIStyle extrapolationIcon = null;
switch (mode)
{
case TimelineClip.ClipExtrapolation.None: return null;
case TimelineClip.ClipExtrapolation.Hold: extrapolationIcon = m_Styles.extrapolationHold; break;
case TimelineClip.ClipExtrapolation.Loop: extrapolationIcon = m_Styles.extrapolationLoop; break;
case TimelineClip.ClipExtrapolation.PingPong: extrapolationIcon = m_Styles.extrapolationPingPong; break;
case TimelineClip.ClipExtrapolation.Continue: extrapolationIcon = m_Styles.extrapolationContinue; break;
}
return extrapolationIcon;
}
Rect GetPreExtrapolationBounds(Rect trackRect, Rect clipRect, GUIStyle icon)
{
float x = clipRect.xMin - (icon.fixedWidth + 10.0f);
float y = trackRect.yMin + (trackRect.height - icon.fixedHeight) / 2.0f;
if (previousClip != null)
{
float distance = Mathf.Abs(treeViewRect.xMin - previousClip.treeViewRect.xMax);
if (distance < icon.fixedWidth)
return new Rect(0.0f, 0.0f, 0.0f, 0.0f);
if (distance < icon.fixedWidth + 20.0f)
{
float delta = (distance - icon.fixedWidth) / 2.0f;
x = clipRect.xMin - (icon.fixedWidth + delta);
}
}
return new Rect(x, y, icon.fixedWidth, icon.fixedHeight);
}
Rect GetPostExtrapolationBounds(Rect trackRect, Rect clipRect, GUIStyle icon)
{
float x = clipRect.xMax + 10.0f;
float y = trackRect.yMin + (trackRect.height - icon.fixedHeight) / 2.0f;
if (nextClip != null)
{
float distance = Mathf.Abs(nextClip.treeViewRect.xMin - treeViewRect.xMax);
if (distance < icon.fixedWidth)
return new Rect(0.0f, 0.0f, 0.0f, 0.0f);
if (distance < icon.fixedWidth + 20.0f)
{
float delta = (distance - icon.fixedWidth) / 2.0f;
x = clipRect.xMax + delta;
}
}
return new Rect(x, y, icon.fixedWidth, icon.fixedHeight);
}
static void DrawExtrapolationIcon(Rect rect, GUIStyle icon)
{
GUI.Label(rect, GUIContent.none, icon);
}
void DrawExtrapolation(Rect trackRect, Rect clipRect)
{
if (clip.hasPreExtrapolation)
{
GUIStyle icon = GetExtrapolationIcon(clip.preExtrapolationMode);
if (icon != null)
{
Rect iconBounds = GetPreExtrapolationBounds(trackRect, clipRect, icon);
if (iconBounds.width > 1 && iconBounds.height > 1)
DrawExtrapolationIcon(iconBounds, icon);
}
}
if (clip.hasPostExtrapolation)
{
GUIStyle icon = GetExtrapolationIcon(clip.postExtrapolationMode);
if (icon != null)
{
Rect iconBounds = GetPostExtrapolationBounds(trackRect, clipRect, icon);
if (iconBounds.width > 1 && iconBounds.height > 1)
DrawExtrapolationIcon(iconBounds, icon);
}
}
}
static Rect ProjectRectOnTimeline(Rect rect, Rect trackRect, WindowState state)
{
Rect newRect = rect;
// transform clipRect into pixel-space
newRect.x *= state.timeAreaScale.x;
newRect.width *= state.timeAreaScale.x;
newRect.x += state.timeAreaTranslation.x + trackRect.xMin;
// adjust clipRect height and vertical centering
const int clipPadding = 2;
newRect.y = trackRect.y + clipPadding;
newRect.height = trackRect.height - (2 * clipPadding);
return newRect;
}
void CalculateLoopRects(Rect trackRect, WindowState state)
{
if (!m_ClipViewDirty)
return;
m_LoopRects.Clear();
if (clip.duration < WindowState.kTimeEpsilon)
return;
var times = TimelineHelpers.GetLoopTimes(clip);
var loopDuration = TimelineHelpers.GetLoopDuration(clip);
m_MinLoopIndex = -1;
// we have a hold, no need to compute all loops
if (!supportsLooping)
{
if (times.Length > 1)
{
var t = times[1];
float loopTime = (float)(clip.duration - t);
m_LoopRects.Add(ProjectRectOnTimeline(new Rect((float)(t + clip.start), 0, loopTime, 0), trackRect, state));
}
return;
}
var range = state.timeAreaShownRange;
var visibleStartTime = range.x - clip.start;
var visibleEndTime = range.y - clip.start;
for (int i = 1; i < times.Length; i++)
{
var t = times[i];
// don't draw off screen loops
if (t > visibleEndTime)
break;
float loopTime = Mathf.Min((float)(clip.duration - t), (float)loopDuration);
var loopEnd = t + loopTime;
if (loopEnd < visibleStartTime)
continue;
m_LoopRects.Add(ProjectRectOnTimeline(new Rect((float)(t + clip.start), 0, loopTime, 0), trackRect, state));
if (m_MinLoopIndex == -1)
m_MinLoopIndex = i;
}
}
public override Rect RectToTimeline(Rect trackRect, WindowState state)
{
var offsetFromTimeSpaceToPixelSpace = state.timeAreaTranslation.x + trackRect.xMin;
var start = (float)(DiscreteTime)clip.start;
var end = (float)(DiscreteTime)clip.end;
return Rect.MinMaxRect(
Mathf.Round(start * state.timeAreaScale.x + offsetFromTimeSpaceToPixelSpace), Mathf.Round(trackRect.yMin),
Mathf.Round(end * state.timeAreaScale.x + offsetFromTimeSpaceToPixelSpace), Mathf.Round(trackRect.yMax)
);
}
public IEnumerable<Edge> SnappableEdgesFor(IAttractable attractable, ManipulateEdges manipulateEdges)
{
var edges = new List<Edge>();
bool canAddEdges = !parent.muted;
if (canAddEdges)
{
// Hack: Trim Start in Ripple mode should not have any snap point added
if (EditMode.editType == EditMode.EditType.Ripple && manipulateEdges == ManipulateEdges.Left)
return edges;
if (attractable != this)
{
if (EditMode.editType == EditMode.EditType.Ripple)
{
bool skip = false;
// Hack: Since Trim End and Move in Ripple mode causes other snap point to move on the same track (which is not supported), disable snapping for this special cases...
// TODO Find a proper way to have different snap edges for each edit mode.
if (manipulateEdges == ManipulateEdges.Right)
{
var otherClipGUI = attractable as TimelineClipGUI;
skip = otherClipGUI != null && otherClipGUI.parent == parent;
}
else if (manipulateEdges == ManipulateEdges.Both)
{
var moveHandler = attractable as MoveItemHandler;
skip = moveHandler != null && moveHandler.movingItems.Any(clips => clips.targetTrack == clip.GetParentTrack() && clip.start >= clips.start);
}
if (skip)
return edges;
}
AddEdge(edges, clip.start);
AddEdge(edges, clip.end);
}
else
{
if (manipulateEdges == ManipulateEdges.Right)
{
var d = TimelineHelpers.GetClipAssetEndTime(clip);
if (d < double.MaxValue)
{
if (clip.SupportsLooping())
{
var l = TimelineHelpers.GetLoopDuration(clip);
var shownTime = TimelineWindow.instance.state.timeAreaShownRange;
do
{
AddEdge(edges, d, false);
d += l;
}
while (d < shownTime.y);
}
else
{
AddEdge(edges, d, false);
}
}
}
if (manipulateEdges == ManipulateEdges.Left)
{
var clipInfo = AnimationClipCurveCache.Instance.GetCurveInfo(clip.animationClip);
if (clipInfo != null && clipInfo.keyTimes.Any())
AddEdge(edges, clip.FromLocalTimeUnbound(clipInfo.keyTimes.Min()), false);
}
}
}
return edges;
}
public bool ShouldSnapTo(ISnappable snappable)
{
return true;
}
bool ShowDrillIcon(PlayableDirector resolver)
{
if (!m_ShowDrillIcon.HasValue || TimelineWindow.instance.hierarchyChangedThisFrame)
{
var nestable = m_ClipEditor.supportsSubTimelines;
m_ShowDrillIcon = nestable && resolver != null;
if (m_ShowDrillIcon.Value)
{
s_TempSubDirectors.Clear();
try
{
m_ClipEditor.GetSubTimelines(clip, resolver, s_TempSubDirectors);
}
catch (Exception e)
{
Debug.LogException(e);
}
m_ShowDrillIcon &= s_TempSubDirectors.Count > 0;
}
}
return m_ShowDrillIcon.Value;
}
static void AddEdge(List<Edge> edges, double time, bool showEdgeHint = true)
{
var shownTime = TimelineWindow.instance.state.timeAreaShownRange;
if (time >= shownTime.x && time <= shownTime.y)
edges.Add(new Edge(time, showEdgeHint));
}
public void SelectCurves()
{
SelectionManager.SelectOnly(clip);
SelectionManager.SelectInlineCurveEditor(this);
}
public void ValidateCurvesSelection()
{
if (!IsSelected()) //if clip is not selected, deselect the inline curve
SelectionManager.SelectInlineCurveEditor(null);
}
}
}