//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using bs;
namespace bs.Editor
{
/** @addtogroup Windows
* @{
*/
///
/// Displays animation curve editor window. Allows the user to manipulate keyframes of animation curves, add/remove
/// curves from an animation clip, and manipulate animation events.
///
[DefaultSize(900, 500), UndoRedoLocal]
internal class AnimationWindow : EditorWindow
{
private const int FIELD_DISPLAY_WIDTH = 300;
private SceneObject selectedSO;
#region Overrides
///
/// Opens the animation window.
///
[MenuItem("Windows/Animation", ButtonModifier.CtrlAlt, ButtonCode.A, 6000)]
private static void OpenGameWindow()
{
OpenWindow();
}
///
protected override LocString GetDisplayName()
{
return new LocEdString("Animation");
}
private void OnInitialize()
{
Selection.OnSelectionChanged += OnSelectionChanged;
UpdateSelectedSO(true);
}
private void OnEditorUpdate()
{
if (selectedSO == null)
return;
guiCurveEditor.HandleDragAndZoomInput();
if (state == State.Playback)
{
Animation animation = selectedSO.GetComponent();
if (animation != null)
{
float time = animation.EditorGetTime();
int frameIdx = (int)(time * fps);
SetCurrentFrame(frameIdx);
animation.UpdateFloatProperties();
}
}
else if (state == State.Recording)
{
if (!delayRecord)
{
float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
if (RecordState(time))
{
ApplyClipChanges();
guiCurveEditor.Redraw();
}
}
}
delayRecord = false;
}
private void OnDestroy()
{
SwitchState(State.Empty);
Selection.OnSelectionChanged -= OnSelectionChanged;
if (selectedSO != null)
{
EditorInput.OnPointerPressed -= OnPointerPressed;
EditorInput.OnPointerDoubleClick -= OnPointerDoubleClicked;
EditorInput.OnPointerMoved -= OnPointerMoved;
EditorInput.OnPointerReleased -= OnPointerReleased;
EditorInput.OnButtonUp -= OnButtonUp;
}
}
///
protected override void WindowResized(int width, int height)
{
if (selectedSO == null)
return;
ResizeGUI(width, height);
}
#endregion
#region GUI
private GUIToggle playButton;
private GUIToggle recordButton;
private GUIButton prevFrameButton;
private GUIIntField frameInputField;
private GUIButton nextFrameButton;
private GUIButton addKeyframeButton;
private GUIButton addEventButton;
private GUIButton optionsButton;
private GUIButton addPropertyBtn;
private GUIButton delPropertyBtn;
private GUILayout buttonLayout;
private int buttonLayoutHeight;
private GUIAnimFieldDisplay guiFieldDisplay;
private GUICurveEditor guiCurveEditor;
///
/// Recreates the entire curve editor GUI depending on the currently selected scene object.
///
private void RebuildGUI()
{
GUI.Clear();
guiCurveEditor = null;
guiFieldDisplay = null;
if (selectedSO == null)
{
GUILabel warningLbl = new GUILabel(new LocEdString("Select an object to animate in the Hierarchy or Scene windows."));
GUILayoutY vertLayout = GUI.AddLayoutY();
vertLayout.AddFlexibleSpace();
GUILayoutX horzLayout = vertLayout.AddLayoutX();
vertLayout.AddFlexibleSpace();
horzLayout.AddFlexibleSpace();
horzLayout.AddElement(warningLbl);
horzLayout.AddFlexibleSpace();
return;
}
// Top button row
GUIContent playIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.Play),
new LocEdString("Play"));
GUIContent recordIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.Record),
new LocEdString("Record"));
GUIContent prevFrameIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.FrameBack),
new LocEdString("Previous frame"));
GUIContent nextFrameIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.FrameForward),
new LocEdString("Next frame"));
GUIContent addKeyframeIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.AddKeyframe),
new LocEdString("Add keyframe"));
GUIContent addEventIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.AddEvent),
new LocEdString("Add event"));
GUIContent optionsIcon = new GUIContent(EditorBuiltin.GetLibraryWindowIcon(LibraryWindowIcon.Options),
new LocEdString("Options"));
playButton = new GUIToggle(playIcon, EditorStyles.Button);
recordButton = new GUIToggle(recordIcon, EditorStyles.Button);
prevFrameButton = new GUIButton(prevFrameIcon);
frameInputField = new GUIIntField();
nextFrameButton = new GUIButton(nextFrameIcon);
addKeyframeButton = new GUIButton(addKeyframeIcon);
addEventButton = new GUIButton(addEventIcon);
optionsButton = new GUIButton(optionsIcon);
playButton.OnToggled += x =>
{
if(x)
SwitchState(State.Playback);
else
SwitchState(State.Normal);
};
recordButton.OnToggled += x =>
{
if (x)
SwitchState(State.Recording);
else
SwitchState(State.Normal);
};
prevFrameButton.OnClick += () =>
{
SetCurrentFrame(currentFrameIdx - 1);
switch (state)
{
case State.Recording:
case State.Normal:
PreviewFrame(currentFrameIdx);
break;
default:
SwitchState(State.Normal);
break;
}
};
frameInputField.OnChanged += x =>
{
SetCurrentFrame(x);
switch (state)
{
case State.Recording:
case State.Normal:
PreviewFrame(currentFrameIdx);
break;
default:
SwitchState(State.Normal);
break;
}
};
nextFrameButton.OnClick += () =>
{
SetCurrentFrame(currentFrameIdx + 1);
switch (state)
{
case State.Recording:
case State.Normal:
PreviewFrame(currentFrameIdx);
break;
default:
SwitchState(State.Normal);
break;
}
};
addKeyframeButton.OnClick += () =>
{
SwitchState(State.Normal);
guiCurveEditor.AddKeyFrameAtMarker();
};
addEventButton.OnClick += () =>
{
SwitchState(State.Normal);
guiCurveEditor.AddEventAtMarker();
};
optionsButton.OnClick += () =>
{
Vector2I openPosition = ScreenToWindowPos(Input.PointerPosition);
AnimationOptions dropDown = DropDownWindow.Open(GUI, openPosition);
dropDown.Initialize(this);
};
// Property buttons
addPropertyBtn = new GUIButton(new LocEdString("Add property"));
delPropertyBtn = new GUIButton(new LocEdString("Delete selected"));
addPropertyBtn.OnClick += () =>
{
Action openPropertyWindow = () =>
{
Vector2I windowPos = ScreenToWindowPos(Input.PointerPosition);
FieldSelectionWindow fieldSelection = DropDownWindow.Open(GUI, windowPos);
fieldSelection.OnFieldSelected += OnFieldAdded;
};
if (clipInfo.clip == null)
{
LocEdString title = new LocEdString("Warning");
LocEdString message =
new LocEdString("Selected object doesn't have an animation clip assigned. Would you like to create" +
" a new animation clip?");
DialogBox.Open(title, message, DialogBox.Type.YesNoCancel, type =>
{
if (type == DialogBox.ResultType.Yes)
{
string clipSavePath;
if (BrowseDialog.SaveFile(ProjectLibrary.ResourceFolder, "*.asset", out clipSavePath))
{
SwitchState(State.Empty);
clipSavePath = Path.ChangeExtension(clipSavePath, ".asset");
AnimationClip newClip = new AnimationClip();
ProjectLibrary.Create(newClip, clipSavePath);
LoadAnimClip(newClip);
Animation animation = selectedSO.GetComponent();
if (animation == null)
animation = selectedSO.AddComponent();
animation.DefaultClip = newClip;
EditorApplication.SetSceneDirty();
SwitchState(State.Normal);
openPropertyWindow();
}
}
});
}
else
{
if (clipInfo.isImported)
{
LocEdString title = new LocEdString("Warning");
LocEdString message =
new LocEdString("You cannot add/edit/remove curves from animation clips that" +
" are imported from an external file.");
DialogBox.Open(title, message, DialogBox.Type.OK);
}
else
{
SwitchState(State.Normal);
openPropertyWindow();
}
}
};
delPropertyBtn.OnClick += () =>
{
if (clipInfo.clip == null)
return;
SwitchState(State.Normal);
if (clipInfo.isImported)
{
LocEdString title = new LocEdString("Warning");
LocEdString message =
new LocEdString("You cannot add/edit/remove curves from animation clips that" +
" are imported from an external file.");
DialogBox.Open(title, message, DialogBox.Type.OK);
}
else
{
LocEdString title = new LocEdString("Warning");
LocEdString message = new LocEdString("Are you sure you want to remove all selected fields?");
DialogBox.Open(title, message, DialogBox.Type.YesNo, x =>
{
if (x == DialogBox.ResultType.Yes)
{
RemoveSelectedFields();
ApplyClipChanges();
}
});
}
};
GUIPanel mainPanel = GUI.AddPanel();
GUIPanel backgroundPanel = GUI.AddPanel(1);
GUILayout mainLayout = mainPanel.AddLayoutY();
buttonLayout = mainLayout.AddLayoutX();
buttonLayout.AddSpace(5);
buttonLayout.AddElement(playButton);
buttonLayout.AddElement(recordButton);
buttonLayout.AddSpace(5);
buttonLayout.AddElement(prevFrameButton);
buttonLayout.AddElement(frameInputField);
buttonLayout.AddElement(nextFrameButton);
buttonLayout.AddSpace(5);
buttonLayout.AddElement(addKeyframeButton);
buttonLayout.AddElement(addEventButton);
buttonLayout.AddSpace(5);
buttonLayout.AddElement(optionsButton);
buttonLayout.AddFlexibleSpace();
buttonLayoutHeight = playButton.Bounds.height;
GUITexture buttonBackground = new GUITexture(null, EditorStyles.HeaderBackground);
buttonBackground.Bounds = new Rect2I(0, 0, Width, buttonLayoutHeight);
backgroundPanel.AddElement(buttonBackground);
GUILayout contentLayout = mainLayout.AddLayoutX();
GUILayout fieldDisplayLayout = contentLayout.AddLayoutY(GUIOption.FixedWidth(FIELD_DISPLAY_WIDTH));
guiFieldDisplay = new GUIAnimFieldDisplay(fieldDisplayLayout, FIELD_DISPLAY_WIDTH,
Height - buttonLayoutHeight * 2, selectedSO);
guiFieldDisplay.OnEntrySelected += OnFieldSelected;
GUILayout bottomButtonLayout = fieldDisplayLayout.AddLayoutX();
bottomButtonLayout.AddElement(addPropertyBtn);
bottomButtonLayout.AddElement(delPropertyBtn);
GUITexture separator = new GUITexture(null, EditorStyles.Separator, GUIOption.FixedWidth(3));
contentLayout.AddElement(separator);
GUILayout curveLayout = contentLayout.AddLayoutY();
Vector2I curveEditorSize = GetCurveEditorSize();
guiCurveEditor = new GUICurveEditor(curveLayout, curveEditorSize.x, curveEditorSize.y, true);
guiCurveEditor.SetEventSceneObject(selectedSO);
guiCurveEditor.OnFrameSelected += OnFrameSelected;
guiCurveEditor.OnEventAdded += () =>
{
OnEventsChanged();
RecordClipState();
};
guiCurveEditor.OnEventModified += () =>
{
EditorApplication.SetProjectDirty();
RecordClipState();
};
guiCurveEditor.OnEventDeleted += () =>
{
OnEventsChanged();
RecordClipState();
};
guiCurveEditor.OnCurveModified += () =>
{
RecordClipState();
SwitchState(State.Normal);
ApplyClipChanges();
PreviewFrame(currentFrameIdx);
EditorApplication.SetProjectDirty();
};
guiCurveEditor.OnClicked += () =>
{
if(state != State.Recording)
SwitchState(State.Normal);
};
guiCurveEditor.Redraw();
}
///
/// Resizes GUI elements so they fit within the provided boundaries.
///
/// Width of the GUI bounds, in pixels.
/// Height of the GUI bounds, in pixels.
private void ResizeGUI(int width, int height)
{
guiFieldDisplay.SetSize(FIELD_DISPLAY_WIDTH, height - buttonLayoutHeight * 2);
Vector2I curveEditorSize = GetCurveEditorSize();
guiCurveEditor.SetSize(curveEditorSize.x, curveEditorSize.y);
guiCurveEditor.Redraw();
}
#endregion
#region Curve save/load
private EditorAnimClipInfo clipInfo;
///
/// Refreshes the contents of the curve and property display by loading animation curves from the provided
/// animation clip.
///
/// Clip containing the animation to load.
private void LoadAnimClip(AnimationClip clip)
{
EditorPersistentData persistentData = EditorApplication.PersistentData;
if (persistentData.dirtyAnimClips.TryGetValue(clip.UUID, out clipInfo))
{
// If an animation clip is imported, we don't care about it's cached curve values as they could have changed
// since last modification, so we re-load the clip. But we persist the events as those can only be set
// within the editor.
if (clipInfo.isImported)
{
EditorAnimClipInfo newClipInfo = EditorAnimClipInfo.Create(clip);
newClipInfo.events = clipInfo.events;
}
}
else
clipInfo = EditorAnimClipInfo.Create(clip);
persistentData.dirtyAnimClips[clip.UUID] = clipInfo;
AnimFieldInfo[] fieldInfos = new AnimFieldInfo[clipInfo.curves.Count];
int idx = 0;
foreach (var curve in clipInfo.curves)
fieldInfos[idx++] = new AnimFieldInfo(curve.Key, curve.Value);
guiFieldDisplay.SetFields(fieldInfos);
guiCurveEditor.Events = clipInfo.events;
guiCurveEditor.DisableCurveEdit = clipInfo.isImported;
guiCurveEditor.SetFPS(clipInfo.sampleRate);
SetCurrentFrame(0);
fps = clipInfo.sampleRate;
}
///
/// Applies any changes made to the animation curves and events to the actual animation clip resource.
///
private void ApplyClipChanges()
{
if (clipInfo == null)
return;
clipInfo.Apply(out _);
}
///
/// Checks if the currently selected object has changed, and rebuilds the GUI and loads the animation clip if needed.
///
/// If true the GUI rebuild and animation clip load will be forced regardless if the active
/// scene object changed.
private void UpdateSelectedSO(bool force)
{
SceneObject so = Selection.SceneObject;
if (selectedSO != so || force)
{
if (selectedSO != null && so == null)
{
EditorInput.OnPointerPressed -= OnPointerPressed;
EditorInput.OnPointerDoubleClick -= OnPointerDoubleClicked;
EditorInput.OnPointerMoved -= OnPointerMoved;
EditorInput.OnPointerReleased -= OnPointerReleased;
EditorInput.OnButtonUp -= OnButtonUp;
}
else if (selectedSO == null && so != null)
{
EditorInput.OnPointerPressed += OnPointerPressed;
EditorInput.OnPointerDoubleClick += OnPointerDoubleClicked;
EditorInput.OnPointerMoved += OnPointerMoved;
EditorInput.OnPointerReleased += OnPointerReleased;
EditorInput.OnButtonUp += OnButtonUp;
}
SwitchState(State.Empty);
selectedSO = so;
selectedFields.Clear();
clipInfo = null;
UndoRedo.Clear();
RebuildGUI();
// Load existing clip if one exists
if (selectedSO != null)
{
Animation animation = selectedSO.GetComponent();
if (animation != null)
{
AnimationClip clip = animation.DefaultClip.Value;
if (clip != null)
LoadAnimClip(clip);
}
}
if(clipInfo == null)
clipInfo = new EditorAnimClipInfo();
SwitchState(State.Normal);
currentClipState = CreateClipState();
if (selectedSO != null)
{
// Select first curve by default
foreach (var KVP in clipInfo.curves)
{
SelectField(KVP.Key, false);
break;
}
UpdateDisplayedCurves(true);
}
}
}
///
/// Stops animation preview on the selected object and resets it back to its non-animated state.
///
private void ClearSO()
{
if (selectedSO != null)
{
Animation animation = selectedSO.GetComponent();
if (animation != null)
animation.EditorStop();
// Reset generic curves to their initial values
UpdateGenericCurves(0.0f);
}
}
#endregion
#region Undo/Redo
private AnimationClipState currentClipState;
///
/// Records current clip state for undo/redo purposes.
///
private void RecordClipState()
{
AnimationClipState clipState = CreateClipState();
AnimationUndo undoCommand = new AnimationUndo(currentClipState, clipState);
UndoRedo.RegisterCommand(undoCommand);
currentClipState = clipState;
}
///
/// Records current clip state for undo/redo purposes.
///
private AnimationClipState CreateClipState()
{
AnimationClipState clipState = new AnimationClipState();
clipState.events = new AnimationEvent[clipInfo.events.Length];
for (int i = 0; i < clipState.events.Length; i++)
clipState.events[i] = new AnimationEvent(clipInfo.events[i].name, clipInfo.events[i].time);
foreach (var curveField in clipInfo.curves)
{
AnimationCurveState[] curveData = new AnimationCurveState[curveField.Value.curveInfos.Length];
for (int i = 0; i < curveData.Length; i++)
{
curveData[i] = new AnimationCurveState();
TangentMode[] tangentModes = curveField.Value.curveInfos[i].curve.TangentModes;
int numTangentModes = tangentModes.Length;
curveData[i].tangentModes = new TangentMode[numTangentModes];
Array.Copy(tangentModes, curveData[i].tangentModes, numTangentModes);
KeyFrame[] keyFrames = curveField.Value.curveInfos[i].curve.KeyFrames;
int numKeyframes = keyFrames.Length;
curveData[i].keyFrames = new KeyFrame[numKeyframes];
Array.Copy(keyFrames, curveData[i].keyFrames, numKeyframes);
}
clipState.curves[curveField.Key] = curveData;
}
return clipState;
}
///
/// Updates the current animation fields from the keyframes and events in the provided state.
///
/// Saved state of an animation clip.
internal void ApplyClipState(AnimationClipState animationClipState)
{
if (state == State.Empty)
return;
SwitchState(State.Normal);
AnimationEvent[] events = animationClipState.events;
clipInfo.events = new AnimationEvent[events.Length];
for(int i = 0; i < events.Length; i++)
clipInfo.events[i] = new AnimationEvent(events[i].name, events[i].time);
foreach (var KVP in animationClipState.curves)
{
FieldAnimCurves fieldCurves;
if (!clipInfo.curves.TryGetValue(KVP.Key, out fieldCurves))
continue;
for (int i = 0; i < fieldCurves.curveInfos.Length; i++)
{
AnimationCurve curve = new AnimationCurve(KVP.Value[i].keyFrames);
fieldCurves.curveInfos[i].curve = new EdAnimationCurve(curve, KVP.Value[i].tangentModes);
}
clipInfo.curves[KVP.Key] = fieldCurves;
}
// Clear all keyframes from curves not in the stored state
foreach (var currentKVP in clipInfo.curves)
{
bool found = false;
foreach (var stateKVP in animationClipState.curves)
{
if (currentKVP.Key == stateKVP.Key)
{
found = true;
break;
}
}
if (found)
continue;
FieldAnimCurves fieldCurves = currentKVP.Value;
for (int i = 0; i < fieldCurves.curveInfos.Length; i++)
{
AnimationCurve curve = new AnimationCurve(new KeyFrame[0]);
fieldCurves.curveInfos[i].curve = new EdAnimationCurve(curve, new TangentMode[0]);
}
}
currentClipState = animationClipState;
UpdateDisplayedCurves();
ApplyClipChanges();
PreviewFrame(currentFrameIdx);
EditorApplication.SetProjectDirty();
}
#endregion
#region Record/Playback
///
/// Possible states the animation window can be in.
///
private enum State
{
Empty,
Normal,
Recording,
Playback
}
private State state = State.Empty;
private bool delayRecord = false;
///
/// Transitions the window into a different state. Caller must validate state transitions.
///
/// New state to transition to.
private void SwitchState(State state)
{
switch (this.state)
{
case State.Normal:
{
switch (state)
{
case State.Playback:
StartPlayback();
break;
case State.Recording:
StartRecord();
break;
case State.Empty:
ClearSO();
break;
}
}
break;
case State.Playback:
{
switch (state)
{
case State.Normal:
EndPlayback();
PreviewFrame(currentFrameIdx);
break;
case State.Recording:
EndPlayback();
StartRecord();
break;
case State.Empty:
EndPlayback();
ClearSO();
break;
}
}
break;
case State.Recording:
{
switch (state)
{
case State.Normal:
EndRecord();
PreviewFrame(currentFrameIdx);
break;
case State.Playback:
EndRecord();
StartPlayback();
break;
case State.Empty:
EndRecord();
ClearSO();
break;
}
}
break;
case State.Empty:
{
switch (state)
{
case State.Normal:
PreviewFrame(currentFrameIdx);
break;
case State.Playback:
StartPlayback();
break;
case State.Recording:
StartRecord();
break;
}
}
break;
}
this.state = state;
}
///
/// Plays back the animation on the currently selected object.
///
private void StartPlayback()
{
clipInfo.Apply(out _);
Animation animation = selectedSO.GetComponent();
if (animation != null && clipInfo.clip != null)
{
float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
animation.EditorPlay(clipInfo.clip, time);
EditorApplication.ToggleOnDemandDrawing("__AnimationPreview", false);
}
playButton.Value = true;
}
///
/// Ends playback started with
///
private void EndPlayback()
{
PreviewFrame(currentFrameIdx);
EditorApplication.ToggleOnDemandDrawing("__AnimationPreview", true);
playButton.Value = false;
}
///
/// Updates the visible animation to display the provided frame.
///
/// Index of the animation frame to display.
private void PreviewFrame(int frameIdx)
{
if (selectedSO == null)
return;
float time = guiCurveEditor.GetTimeForFrame(frameIdx);
Animation animation = selectedSO.GetComponent();
if (animation != null && clipInfo.clip != null)
animation.EditorPlay(clipInfo.clip, time, true);
UpdateGenericCurves(time);
EditorApplication.NotifyNeedsRedraw();
}
///
/// Start recording modifications made to the selected scene object.
///
private void StartRecord()
{
float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
if (RecordState(time))
{
ApplyClipChanges();
guiCurveEditor.Redraw();
}
recordButton.Value = true;
}
///
/// Stops recording modifications made to the selected scene object.
///
private void EndRecord()
{
recordButton.Value = false;
}
///
/// Updates the states of all properties controlled by curves to the value of the curves at the provided time.
///
/// Time at which to evaluate the curves controlling the properties.
private void UpdateGenericCurves(float time)
{
foreach (var KVP in clipInfo.curves)
{
FieldAnimCurves curves;
if (!clipInfo.curves.TryGetValue(KVP.Key, out curves))
continue;
string suffix;
SerializableProperty property = Animation.FindProperty(selectedSO, KVP.Key, out suffix);
if (property == null)
continue;
float DoOnComponent(int index)
{
EdAnimationCurve curve = curves.curveInfos[index].curve;
return curve.Evaluate(time);
}
ForEachPropertyComponentSet(property, DoOnComponent);
}
}
///
/// Iterates over all curve path fields and records their current state. If the state differs from the current
/// curve values, new keyframes are added.
///
/// Time for which to record the state, in seconds.
/// True if any changes were recorded, false otherwise.
private bool RecordState(float time)
{
bool changesMade = false;
foreach (var KVP in clipInfo.curves)
{
if (RecordState(KVP.Key, time))
changesMade = true;
}
return changesMade;
}
///
/// Records the state of the provided property and adds it as a keyframe to the related animation curve (or updates
/// an existing keyframe if the value is different).
///
/// Path to the property whose state to record.
/// Time for which to record the state, in seconds.
/// True if any changes were recorded, false otherwise.
private bool RecordState(string path, float time)
{
FieldAnimCurves curves;
if (!clipInfo.curves.TryGetValue(path, out curves))
return false;
string suffix;
SerializableProperty property = Animation.FindProperty(selectedSO, path, out suffix);
if (property == null)
return false;
bool changesMade = false;
void DoOnComponent(float value, int index)
{
EdAnimationCurve curve = curves.curveInfos[index].curve;
float curveVal = curve.Evaluate(time);
if (!MathEx.ApproxEquals(value, curveVal, 0.001f))
{
curve.AddOrUpdateKeyframe(time, value);
curve.Apply();
changesMade = true;
}
}
ForEachPropertyComponentGet(property, DoOnComponent);
return changesMade;
}
#endregion
#region Curve display
private int currentFrameIdx;
private int fps = 1;
///
/// Sampling rate of the animation in frames per second. Determines granularity at which positions keyframes can be
/// placed.
///
internal int FPS
{
get { return fps; }
set
{
fps = MathEx.Max(value, 1);
guiCurveEditor.SetFPS(fps);
if (clipInfo != null)
clipInfo.sampleRate = fps;
}
}
///
/// Changes the currently selected frame in the curve display.
///
/// Index of the frame to select.
private void SetCurrentFrame(int frameIdx)
{
currentFrameIdx = Math.Max(0, frameIdx);
frameInputField.Value = currentFrameIdx;
guiCurveEditor.SetMarkedFrame(currentFrameIdx);
float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
List values = new List();
foreach (var kvp in clipInfo.curves)
{
GUIAnimFieldPathValue fieldValue = new GUIAnimFieldPathValue();
fieldValue.path = kvp.Key;
switch (kvp.Value.type)
{
case SerializableProperty.FieldType.Vector2:
{
Vector2 value = new Vector2();
for (int i = 0; i < 2; i++)
value[i] = kvp.Value.curveInfos[i].curve.Evaluate(time, false);
fieldValue.value = value;
}
break;
case SerializableProperty.FieldType.Vector3:
{
Vector3 value = new Vector3();
for (int i = 0; i < 3; i++)
value[i] = kvp.Value.curveInfos[i].curve.Evaluate(time, false);
fieldValue.value = value;
}
break;
case SerializableProperty.FieldType.Vector4:
{
Vector4 value = new Vector4();
for (int i = 0; i < 4; i++)
value[i] = kvp.Value.curveInfos[i].curve.Evaluate(time, false);
fieldValue.value = value;
}
break;
case SerializableProperty.FieldType.Color:
{
Color value = new Color();
for (int i = 0; i < 4; i++)
value[i] = kvp.Value.curveInfos[i].curve.Evaluate(time, false);
fieldValue.value = value;
}
break;
case SerializableProperty.FieldType.Bool:
case SerializableProperty.FieldType.Int:
case SerializableProperty.FieldType.Float:
fieldValue.value = kvp.Value.curveInfos[0].curve.Evaluate(time, false); ;
break;
}
values.Add(fieldValue);
}
guiFieldDisplay.SetDisplayValues(values.ToArray());
}
///
/// Returns a list of all animation curves that should be displayed in the curve display.
///
/// Array of curves to display.
private EdCurveDrawInfo[] GetDisplayedCurves()
{
List curvesToDisplay = new List();
if (clipInfo == null)
return curvesToDisplay.ToArray();
for (int i = 0; i < selectedFields.Count; i++)
{
EdCurveDrawInfo[] curveInfos;
if (TryGetCurve(selectedFields[i], out curveInfos))
curvesToDisplay.AddRange(curveInfos);
}
return curvesToDisplay.ToArray();
}
///
/// Calculates an unique color for each animation curve.
///
private void UpdateCurveColors()
{
int globalCurveIdx = 0;
foreach (var curveGroup in clipInfo.curves)
{
for (int i = 0; i < curveGroup.Value.curveInfos.Length; i++)
curveGroup.Value.curveInfos[i].color = EditorAnimClipInfo.GetUniqueColor(globalCurveIdx++);
}
}
///
/// Updates the curve display with currently selected curves.
///
/// If true the time offset/range will be recalculated, otherwise current time offset will
/// be kept as is.
private void UpdateDisplayedCurves(bool resetTime = false)
{
EdCurveDrawInfo[] curvesToDisplay = GetDisplayedCurves();
guiCurveEditor.SetCurves(curvesToDisplay);
guiCurveEditor.CenterAndResize(resetTime);
}
#endregion
#region Field display
private List selectedFields = new List();
///
/// Registers a new animation curve field.
///
/// Path of the field, see
/// Type of the field (float, vector, etc.)
private void AddNewField(string path, SerializableProperty.FieldType type)
{
bool isPropertyCurve = !clipInfo.isImported && !EditorAnimClipInfo.IsMorphShapeCurve(path);
switch (type)
{
case SerializableProperty.FieldType.Vector4:
{
FieldAnimCurves fieldCurves = new FieldAnimCurves();
fieldCurves.type = type;
fieldCurves.isPropertyCurve = isPropertyCurve;
fieldCurves.curveInfos = new EdCurveDrawInfo[4];
string[] subPaths = { ".x", ".y", ".z", ".w" };
for (int i = 0; i < subPaths.Length; i++)
{
string subFieldPath = path + subPaths[i];
fieldCurves.curveInfos[i].curve = new EdAnimationCurve();
selectedFields.Add(subFieldPath);
}
clipInfo.curves[path] = fieldCurves;
}
break;
case SerializableProperty.FieldType.Vector3:
{
FieldAnimCurves fieldCurves = new FieldAnimCurves();
fieldCurves.type = type;
fieldCurves.isPropertyCurve = isPropertyCurve;
fieldCurves.curveInfos = new EdCurveDrawInfo[3];
string[] subPaths = { ".x", ".y", ".z" };
for (int i = 0; i < subPaths.Length; i++)
{
string subFieldPath = path + subPaths[i];
fieldCurves.curveInfos[i].curve = new EdAnimationCurve();
selectedFields.Add(subFieldPath);
}
clipInfo.curves[path] = fieldCurves;
}
break;
case SerializableProperty.FieldType.Vector2:
{
FieldAnimCurves fieldCurves = new FieldAnimCurves();
fieldCurves.type = type;
fieldCurves.isPropertyCurve = isPropertyCurve;
fieldCurves.curveInfos = new EdCurveDrawInfo[2];
string[] subPaths = { ".x", ".y" };
for (int i = 0; i < subPaths.Length; i++)
{
string subFieldPath = path + subPaths[i];
fieldCurves.curveInfos[i].curve = new EdAnimationCurve();
selectedFields.Add(subFieldPath);
}
clipInfo.curves[path] = fieldCurves;
}
break;
case SerializableProperty.FieldType.Color:
{
FieldAnimCurves fieldCurves = new FieldAnimCurves();
fieldCurves.type = type;
fieldCurves.isPropertyCurve = isPropertyCurve;
fieldCurves.curveInfos = new EdCurveDrawInfo[4];
string[] subPaths = { ".r", ".g", ".b", ".a" };
for (int i = 0; i < subPaths.Length; i++)
{
string subFieldPath = path + subPaths[i];
fieldCurves.curveInfos[i].curve = new EdAnimationCurve();
selectedFields.Add(subFieldPath);
}
clipInfo.curves[path] = fieldCurves;
}
break;
default: // Primitive type
{
FieldAnimCurves fieldCurves = new FieldAnimCurves();
fieldCurves.type = type;
fieldCurves.isPropertyCurve = isPropertyCurve;
fieldCurves.curveInfos = new EdCurveDrawInfo[1];
fieldCurves.curveInfos[0].curve = new EdAnimationCurve();
selectedFields.Add(path);
clipInfo.curves[path] = fieldCurves;
}
break;
}
UpdateCurveColors();
UpdateDisplayedFields();
EditorApplication.SetProjectDirty();
UpdateDisplayedCurves();
}
///
/// Selects a new animation curve field, making the curve display in the curve display GUI element.
///
/// Path of the field to display.
/// If true the field will be shown along with any already selected fields, or if false
/// only the provided field will be shown.
private void SelectField(string path, bool additive)
{
if (!additive)
selectedFields.Clear();
if (!string.IsNullOrEmpty(path))
{
selectedFields.RemoveAll(x => { return x == path || IsPathParent(x, path); });
selectedFields.Add(path);
}
guiFieldDisplay.SetSelection(selectedFields.ToArray());
UpdateDisplayedCurves();
}
///
/// Deletes all currently selecting fields, removing them their curves permanently.
///
private void RemoveSelectedFields()
{
for (int i = 0; i < selectedFields.Count; i++)
clipInfo.curves.Remove(GetSubPathParent(selectedFields[i]));
UpdateCurveColors();
UpdateDisplayedFields();
selectedFields.Clear();
EditorApplication.SetProjectDirty();
UpdateDisplayedCurves();
}
///
/// Updates the GUI element displaying the current animation curve fields.
///
private void UpdateDisplayedFields()
{
List existingFields = new List();
foreach (var KVP in clipInfo.curves)
existingFields.Add(new AnimFieldInfo(KVP.Key, KVP.Value));
guiFieldDisplay.SetFields(existingFields.ToArray());
}
#endregion
#region Helpers
///
/// Returns the size of the curve editor GUI element.
///
/// Width/height of the curve editor, in pixels.
private Vector2I GetCurveEditorSize()
{
Vector2I output = new Vector2I();
output.x = Math.Max(0, Width - FIELD_DISPLAY_WIDTH);
output.y = Math.Max(0, Height - buttonLayoutHeight);
return output;
}
///
/// Attempts to find a curve field at the specified path.
///
/// Path of the curve field to look for.
/// One or multiple curves found for the specific path (one field can have multiple curves
/// if it is a complex type, like a vector).
/// True if the curve field was found, false otherwise.
private bool TryGetCurve(string path, out EdCurveDrawInfo[] curveInfos)
{
int index = path.LastIndexOf(".");
string parentPath;
string subPathSuffix = null;
if (index == -1)
{
parentPath = path;
}
else
{
parentPath = path.Substring(0, index);
subPathSuffix = path.Substring(index, path.Length - index);
}
FieldAnimCurves fieldCurves;
if (clipInfo.curves.TryGetValue(parentPath, out fieldCurves))
{
if (!string.IsNullOrEmpty(subPathSuffix))
{
if (subPathSuffix == ".x" || subPathSuffix == ".r")
{
curveInfos = new [] { fieldCurves.curveInfos[0] };
return true;
}
else if (subPathSuffix == ".y" || subPathSuffix == ".g")
{
curveInfos = new[] { fieldCurves.curveInfos[1] };
return true;
}
else if (subPathSuffix == ".z" || subPathSuffix == ".b")
{
curveInfos = new[] { fieldCurves.curveInfos[2] };
return true;
}
else if (subPathSuffix == ".w" || subPathSuffix == ".a")
{
curveInfos = new[] { fieldCurves.curveInfos[3] };
return true;
}
}
else
{
curveInfos = fieldCurves.curveInfos;
return true;
}
}
curveInfos = new EdCurveDrawInfo[0];
return false;
}
///
/// Checks if one curve field path a parent of the other.
///
/// Path to check if it is a child of .
/// Path to check if it is a parent of .
/// True if is a child of .
private bool IsPathParent(string child, string parent)
{
string[] childEntries = child.Split('/', '.');
string[] parentEntries = parent.Split('/', '.');
if (parentEntries.Length >= child.Length)
return false;
int compareLength = Math.Min(childEntries.Length, parentEntries.Length);
for (int i = 0; i < compareLength; i++)
{
if (childEntries[i] != parentEntries[i])
return false;
}
return true;
}
///
/// If a path has sub-elements (e.g. .x, .r), returns a path without those elements. Otherwise returns the original
/// path.
///
/// Path to check.
/// Path without sub-elements.
private string GetSubPathParent(string path)
{
int index = path.LastIndexOf(".");
if (index == -1)
return path;
return path.Substring(0, index);
}
///
/// Iterates over all components of a property and calls the provided action for every component with the current
/// value of the property. Only works with floating point (any dimension), integer, color and boolean property
/// types. Since reported values are always floating point booleans are encoded as -1.0f for false and 1.0f for
/// true, and integers are converted to floating point.
///
/// Property whose components to iterate over.
///
/// Callback to trigger for each component. The callback receives the current value of the property's component
/// and the sequential index of the component.
///
private void ForEachPropertyComponentGet(SerializableProperty property, Action action)
{
switch (property.Type)
{
case SerializableProperty.FieldType.Vector2:
{
Vector2 value = property.GetValue();
for (int i = 0; i < 2; i++)
action(value[i], i);
}
break;
case SerializableProperty.FieldType.Vector3:
{
Vector3 value = property.GetValue();
for (int i = 0; i < 3; i++)
action(value[i], i);
}
break;
case SerializableProperty.FieldType.Vector4:
{
Vector4 value = property.GetValue();
for (int i = 0; i < 4; i++)
action(value[i], i);
}
break;
case SerializableProperty.FieldType.Color:
{
Color value = property.GetValue();
for (int i = 0; i < 4; i++)
action(value[i], i);
}
break;
case SerializableProperty.FieldType.Bool:
{
bool value = property.GetValue();
action(value ? 1.0f : -1.0f, 0);
}
break;
case SerializableProperty.FieldType.Int:
{
int value = property.GetValue();
action(value, 0);
}
break;
case SerializableProperty.FieldType.Float:
{
float value = property.GetValue();
action(value, 0);
}
break;
}
}
///
/// Iterates over all components of a property, calls the provided action which returns a new value to be
/// assigned to the property component. Only works with floating point (any dimension), integer, color and boolean
/// property types. Since reported values are always floating point booleans are encoded as -1.0f for false and
/// 1.0f for true, and integers are converted to floating point.
///
/// Property whose components to iterate over.
///
/// Callback to trigger for each component. The callback receives the current value of the property's component
/// and the sequential index of the component.
///
private void ForEachPropertyComponentSet(SerializableProperty property, Func action)
{
switch (property.Type)
{
case SerializableProperty.FieldType.Vector2:
{
Vector2 value = new Vector2();
for (int i = 0; i < 2; i++)
value[i] = action(i);
property.SetValue(value);
}
break;
case SerializableProperty.FieldType.Vector3:
{
Vector3 value = new Vector3();
for (int i = 0; i < 3; i++)
value[i] = action(i);
property.SetValue(value);
}
break;
case SerializableProperty.FieldType.Vector4:
{
Vector4 value = new Vector4();
for (int i = 0; i < 4; i++)
value[i] = action(i);
property.SetValue(value);
}
break;
case SerializableProperty.FieldType.Color:
{
Color value = new Color();
for (int i = 0; i < 4; i++)
value[i] = action(i);
property.SetValue(value);
}
break;
case SerializableProperty.FieldType.Bool:
{
bool value = action(0) > 0.0f ? true : false;
property.SetValue(value);
}
break;
case SerializableProperty.FieldType.Int:
{
int value = (int)action(0);
property.SetValue(value);
}
break;
case SerializableProperty.FieldType.Float:
{
float value = action(0);
property.SetValue(value);
}
break;
}
}
#endregion
#region Input callbacks
///
/// Triggered when the user presses a mouse button.
///
/// Information about the mouse press event.
private void OnPointerPressed(PointerEvent ev)
{
guiCurveEditor.OnPointerPressed(ev);
}
///
/// Triggered when the user double clicks the left mouse button.
///
/// Information about the mouse event.
private void OnPointerDoubleClicked(PointerEvent ev)
{
guiCurveEditor.OnPointerDoubleClicked(ev);
}
///
/// Triggered when the user moves the mouse.
///
/// Information about the mouse move event.
private void OnPointerMoved(PointerEvent ev)
{
guiCurveEditor.OnPointerMoved(ev);
}
///
/// Triggered when the user releases a mouse button.
///
/// Information about the mouse release event.
private void OnPointerReleased(PointerEvent ev)
{
guiCurveEditor.OnPointerReleased(ev);
}
///
/// Triggered when the user releases a keyboard button.
///
/// Information about the keyboard release event.
private void OnButtonUp(ButtonEvent ev)
{
guiCurveEditor.OnButtonUp(ev);
}
#endregion
#region General callbacks
///
/// Triggered by the field selector, when user selects a new curve field.
///
/// Path of the selected curve field.
/// Type of the selected curve field (float, vector, etc.).
private void OnFieldAdded(string path, SerializableProperty.FieldType type)
{
// Remove the root scene object from the path (we know which SO it is, no need to hardcode its name in the path)
string pathNoRoot = path.TrimStart('/');
int separatorIdx = pathNoRoot.IndexOf("/");
if (separatorIdx == -1 || (separatorIdx + 1) >= pathNoRoot.Length)
return;
pathNoRoot = pathNoRoot.Substring(separatorIdx + 1, pathNoRoot.Length - separatorIdx - 1);
AddNewField(pathNoRoot, type);
RecordState(pathNoRoot, 0.0f);
ApplyClipChanges();
}
///
/// Triggered when the user selects a new curve field.
///
/// Path of the selected curve field.
private void OnFieldSelected(string path)
{
bool additive = Input.IsButtonHeld(ButtonCode.LeftShift) || Input.IsButtonHeld(ButtonCode.RightShift);
SelectField(path, additive);
}
///
/// Triggered when the user selects a new scene object or a resource.
///
/// Newly selected scene objects.
/// Newly selected resources.
private void OnSelectionChanged(SceneObject[] sceneObjects, string[] resourcePaths)
{
// While recording allow other objects to be selected so the user can modify them
if (state == State.Recording)
return;
UpdateSelectedSO(false);
}
///
/// Triggered when the user selects a new frame in the curve display.
///
/// Index of the selected frame.
private void OnFrameSelected(int frameIdx)
{
SetCurrentFrame(frameIdx);
PreviewFrame(currentFrameIdx);
// HACK: Skip checking for record changes this frame, to give the preview a chance to update, otherwise
// the changes would be detected any time a frame is delayed. A proper fix for this would be to force the
// animation to be evaluated synchronously when PreviewFrame is called.
delayRecord = true;
}
///
/// Triggered when the user changed (add, removed or modified) animation events in the curve display.
///
private void OnEventsChanged()
{
clipInfo.events = guiCurveEditor.Events;
EditorApplication.SetProjectDirty();
}
#endregion
}
///
/// Drop down window that displays options used by the animation window.
///
[DefaultSize(100, 50)]
internal class AnimationOptions : DropDownWindow
{
///
/// Initializes the drop down window by creating the necessary GUI. Must be called after construction and before
/// use.
///
/// Animation window that this drop down window is a part of.
internal void Initialize(AnimationWindow parent)
{
GUIIntField fpsField = new GUIIntField(new LocEdString("FPS"), 40);
fpsField.Value = parent.FPS;
fpsField.OnChanged += x => { parent.FPS = x; };
GUILayoutY vertLayout = GUI.AddLayoutY();
vertLayout.AddFlexibleSpace();
GUILayoutX contentLayout = vertLayout.AddLayoutX();
contentLayout.AddFlexibleSpace();
contentLayout.AddElement(fpsField);
contentLayout.AddFlexibleSpace();
vertLayout.AddFlexibleSpace();
}
}
///
/// Raw data representing a single animation curve.
///
[SerializeObject]
class AnimationCurveState
{
public KeyFrame[] keyFrames;
public TangentMode[] tangentModes;
}
///
/// Raw data representing a single animation clip state.
///
[SerializeObject]
class AnimationClipState
{
public Dictionary curves = new Dictionary();
public AnimationEvent[] events;
}
///
/// Undo command used in the AnimationWindow.
///
[SerializeObject]
internal class AnimationUndo : UndoableCommand
{
private AnimationClipState prevClipState;
private AnimationClipState clipState;
public AnimationUndo(AnimationClipState prevClipState, AnimationClipState clipState)
{
this.prevClipState = prevClipState;
this.clipState = clipState;
}
///
protected override void Commit()
{
AnimationWindow window = EditorWindow.GetWindow();
window?.ApplyClipState(clipState);
}
///
protected override void Revert()
{
AnimationWindow window = EditorWindow.GetWindow();
window?.ApplyClipState(prevClipState);
}
}
/** @} */
}