//********************************** 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.Text; using BansheeEngine; namespace BansheeEditor { /** @addtogroup Windows * @{ */ /// /// Displays animation curve editor window. /// [DefaultSize(900, 500)] internal class AnimationWindow : EditorWindow { private const int FIELD_DISPLAY_WIDTH = 200; private const int DRAG_START_DISTANCE = 3; private const float DRAG_SCALE = 10.0f; private const float ZOOM_SCALE = 0.1f/120.0f; // One scroll step is usually 120 units, we want 1/10 of that private bool isInitialized; 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; EditorInput.OnPointerPressed += OnPointerPressed; EditorInput.OnPointerMoved += OnPointerMoved; EditorInput.OnPointerReleased += OnPointerReleased; EditorInput.OnButtonUp += OnButtonUp; RebuildGUI(); } private void OnEditorUpdate() { if (!isInitialized) return; HandleDragAndZoomInput(); } private void OnDestroy() { Selection.OnSelectionChanged -= OnSelectionChanged; EditorInput.OnPointerPressed -= OnPointerPressed; EditorInput.OnPointerMoved -= OnPointerMoved; EditorInput.OnPointerReleased -= OnPointerReleased; EditorInput.OnButtonUp -= OnButtonUp; } protected override void WindowResized(int width, int height) { if (!isInitialized) return; ResizeGUI(width, height); } #endregion #region GUI private GUIButton playButton; private GUIButton 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 int scrollBarWidth; private int scrollBarHeight; private GUIResizeableScrollBarH horzScrollBar; private GUIResizeableScrollBarV vertScrollBar; private GUIPanel editorPanel; private GUIAnimFieldDisplay guiFieldDisplay; private GUICurveEditor guiCurveEditor; private void RebuildGUI() { GUI.Clear(); selectedFields.Clear(); curves.Clear(); isInitialized = false; if (selectedSO != Selection.SceneObject) { zoomAmount = 0.0f; selectedSO = Selection.SceneObject; } 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; } // TODO - Retrieve Animation & AnimationClip from the selected object, fill curves dictionary // - If not available, show a button to create new animation clip // 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 GUIButton(playIcon); recordButton = new GUIButton(recordIcon); prevFrameButton = new GUIButton(prevFrameIcon); frameInputField = new GUIIntField(); nextFrameButton = new GUIButton(nextFrameIcon); addKeyframeButton = new GUIButton(addKeyframeIcon); addEventButton = new GUIButton(addEventIcon); optionsButton = new GUIButton(optionsIcon); playButton.OnClick += () => { // TODO // - Record current state of the scene object hierarchy // - Evaluate all curves manually and update them // - On end, restore original values of the scene object hierarchy }; recordButton.OnClick += () => { // TODO // - Every frame read back current values of all the current curve's properties and assign it to the current frame }; prevFrameButton.OnClick += () => { SetCurrentFrame(currentFrameIdx - 1); }; frameInputField.OnChanged += SetCurrentFrame; nextFrameButton.OnClick += () => { SetCurrentFrame(currentFrameIdx + 1); }; addKeyframeButton.OnClick += () => { guiCurveEditor.AddKeyFrameAtMarker(); }; addEventButton.OnClick += () => { guiCurveEditor.AddEventAtMarker(); }; optionsButton.OnClick += () => { Vector2I openPosition = ScreenToWindowPos(Input.PointerPosition); AnimationOptions dropDown = DropDownWindow.Open(this, openPosition); dropDown.Initialize(this); }; // Property buttons addPropertyBtn = new GUIButton(new LocEdString("Add property")); delPropertyBtn = new GUIButton(new LocEdString("Delete selected")); addPropertyBtn.OnClick += () => { Vector2I windowPos = ScreenToWindowPos(Input.PointerPosition); FieldSelectionWindow fieldSelection = DropDownWindow.Open(this, windowPos); fieldSelection.OnFieldSelected += OnFieldAdded; }; delPropertyBtn.OnClick += () => { 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(); } }); }; GUILayout mainLayout = GUI.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; 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); horzScrollBar = new GUIResizeableScrollBarH(); horzScrollBar.OnScrollOrResize += OnHorzScrollOrResize; vertScrollBar = new GUIResizeableScrollBarV(); vertScrollBar.OnScrollOrResize += OnVertScrollOrResize; GUILayout curveLayout = contentLayout.AddLayoutY(); GUILayout curveLayoutHorz = curveLayout.AddLayoutX(); GUILayout horzScrollBarLayout = curveLayout.AddLayoutX(); horzScrollBarLayout.AddElement(horzScrollBar); horzScrollBarLayout.AddFlexibleSpace(); editorPanel = curveLayoutHorz.AddPanel(); curveLayoutHorz.AddElement(vertScrollBar); curveLayoutHorz.AddFlexibleSpace(); scrollBarHeight = horzScrollBar.Bounds.height; scrollBarWidth = vertScrollBar.Bounds.width; Vector2I curveEditorSize = GetCurveEditorSize(); guiCurveEditor = new GUICurveEditor(this, editorPanel, curveEditorSize.x, curveEditorSize.y); guiCurveEditor.OnFrameSelected += OnFrameSelected; guiCurveEditor.Redraw(); horzScrollBar.SetWidth(curveEditorSize.x); vertScrollBar.SetHeight(curveEditorSize.y); SetCurrentFrame(currentFrameIdx); UpdateScrollBarSize(); isInitialized = true; } 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(); horzScrollBar.SetWidth(curveEditorSize.x); vertScrollBar.SetHeight(curveEditorSize.y); UpdateScrollBarSize(); UpdateScrollBarPosition(); } #endregion #region Scroll, drag, zoom private Vector2I dragStartPos; private bool isButtonHeld; private bool isDragInProgress; private float zoomAmount; private void HandleDragAndZoomInput() { // Handle middle mouse dragging if (isDragInProgress) { float dragX = Input.GetAxisValue(InputAxis.MouseX) * DRAG_SCALE; float dragY = Input.GetAxisValue(InputAxis.MouseY) * DRAG_SCALE; Vector2 offset = guiCurveEditor.Offset; offset.x = Math.Max(0.0f, offset.x + dragX); offset.y += dragY; guiCurveEditor.Offset = offset; UpdateScrollBarSize(); UpdateScrollBarPosition(); } // Handle zoom in/out float scroll = Input.GetAxisValue(InputAxis.MouseZ); if (scroll != 0.0f) { Vector2I windowPos = ScreenToWindowPos(Input.PointerPosition); Vector2 curvePos; if (guiCurveEditor.WindowToCurveSpace(windowPos, out curvePos)) { float zoom = scroll * ZOOM_SCALE; Zoom(curvePos, zoom); } } } private void SetVertScrollbarProperties(float position, float size) { Vector2 visibleRange = guiCurveEditor.Range; Vector2 totalRange = GetTotalRange(); visibleRange.y = totalRange.y*size; guiCurveEditor.Range = visibleRange; float scrollableRange = totalRange.y - visibleRange.y; Vector2 offset = guiCurveEditor.Offset; offset.y = -scrollableRange * (position * 2.0f - 1.0f); guiCurveEditor.Offset = offset; } private void SetHorzScrollbarProperties(float position, float size) { Vector2 visibleRange = guiCurveEditor.Range; Vector2 totalRange = GetTotalRange(); visibleRange.x = totalRange.x * size; guiCurveEditor.Range = visibleRange; float scrollableRange = totalRange.x - visibleRange.x; Vector2 offset = guiCurveEditor.Offset; offset.x = scrollableRange * position; guiCurveEditor.Offset = offset; } private void UpdateScrollBarSize() { Vector2 visibleRange = guiCurveEditor.Range; Vector2 totalRange = GetTotalRange(); horzScrollBar.HandleSize = visibleRange.x / totalRange.x; vertScrollBar.HandleSize = visibleRange.y / totalRange.y; } private void UpdateScrollBarPosition() { Vector2 visibleRange = guiCurveEditor.Range; Vector2 totalRange = GetTotalRange(); Vector2 scrollableRange = totalRange - visibleRange; Vector2 offset = guiCurveEditor.Offset; if (scrollableRange.x > 0.0f) horzScrollBar.Position = offset.x / scrollableRange.x; else horzScrollBar.Position = 0.0f; if (scrollableRange.y > 0.0f) { float pos = offset.y/scrollableRange.y; float sign = MathEx.Sign(pos); pos = sign*MathEx.Clamp01(MathEx.Abs(pos)); pos = (1.0f - pos) /2.0f; vertScrollBar.Position = pos; } else vertScrollBar.Position = 0.0f; } private Vector2 GetZoomedRange() { float zoomLevel = MathEx.Pow(2, zoomAmount); Vector2 optimalRange = GetOptimalRange(); return optimalRange / zoomLevel; } private Vector2 GetTotalRange() { // Return optimal range (that covers the visible curve) Vector2 optimalRange = GetOptimalRange(); // Increase range in case user zoomed out Vector2 zoomedRange = GetZoomedRange(); return Vector2.Max(optimalRange, zoomedRange); } private void Zoom(Vector2 curvePos, float amount) { // Increase or decrease the visible range depending on zoom level Vector2 oldZoomedRange = GetZoomedRange(); zoomAmount = MathEx.Clamp(zoomAmount + amount, -10.0f, 10.0f); Vector2 zoomedRange = GetZoomedRange(); Vector2 zoomedDiff = zoomedRange - oldZoomedRange; Vector2 currentRange = guiCurveEditor.Range; Vector2 newRange = currentRange + zoomedDiff; guiCurveEditor.Range = newRange; // When zooming, make sure to focus on the point provided, so adjust the offset Vector2 rangeScale = newRange; rangeScale.x /= currentRange.x; rangeScale.y /= currentRange.y; Vector2 relativeCurvePos = curvePos - guiCurveEditor.Offset; Vector2 newCurvePos = relativeCurvePos * rangeScale; Vector2 diff = newCurvePos - relativeCurvePos; guiCurveEditor.Offset -= diff; UpdateScrollBarSize(); UpdateScrollBarPosition(); } #endregion #region Curve save/load /// /// A set of animation curves for a field of a certain type. /// private struct FieldCurves { public SerializableProperty.FieldType type; public EdAnimationCurve[] curves; } [SerializeObject] private class EditorVector3CurveTangents { public string name; public TangentMode[] tangentsX; public TangentMode[] tangentsY; public TangentMode[] tangentsZ; } [SerializeObject] private class EditorFloatCurveTangents { public string name; public TangentMode[] tangents; } [SerializeObject] private class EditorCurveData { public EditorVector3CurveTangents[] positionCurves; public EditorVector3CurveTangents[] rotationCurves; public EditorVector3CurveTangents[] scaleCurves; public EditorFloatCurveTangents[] floatCurves; } private Dictionary curves = new Dictionary(); private bool clipIsImported; private void LoadFromClip(AnimationClip clip) { curves.Clear(); selectedFields.Clear(); guiFieldDisplay.SetFields(new string[0]); clipIsImported = IsClipImported(clip); AnimationCurves clipCurves = clip.Curves; EditorCurveData editorCurveData = null; string resourcePath = ProjectLibrary.GetPath(clip); if (!string.IsNullOrEmpty(resourcePath)) { LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath); string clipName = PathEx.GetTail(resourcePath); if (entry != null && entry.Type == LibraryEntryType.File) { FileEntry fileEntry = (FileEntry)entry; ResourceMeta[] metas = fileEntry.ResourceMetas; for (int i = 0; i < metas.Length; i++) { if (clipName == metas[i].SubresourceName) { editorCurveData = metas[i].EditorData as EditorCurveData; break; } } } } if(editorCurveData == null) editorCurveData = new EditorCurveData(); Action loadVector3Curve = (curves, tangents, subPath) => { foreach (var curveEntry in curves) { TangentMode[] tangentsX = null; TangentMode[] tangentsY = null; TangentMode[] tangentsZ = null; foreach (var tangentEntry in tangents) { if (tangentEntry.name == curveEntry.Name) { tangentsX = tangentEntry.tangentsX; tangentsY = tangentEntry.tangentsY; tangentsZ = tangentEntry.tangentsZ; break; } } FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = SerializableProperty.FieldType.Vector3; fieldCurves.curves = new EdAnimationCurve[3]; fieldCurves.curves[0] = new EdAnimationCurve(curveEntry.X, tangentsX); fieldCurves.curves[1] = new EdAnimationCurve(curveEntry.Y, tangentsY); fieldCurves.curves[2] = new EdAnimationCurve(curveEntry.Z, tangentsZ); string curvePath = curveEntry.Name.TrimEnd('/') + subPath; guiFieldDisplay.AddField(curvePath); this.curves[curvePath] = fieldCurves; } }; loadVector3Curve(clipCurves.PositionCurves, editorCurveData.positionCurves, "/Position"); loadVector3Curve(clipCurves.RotationCurves, editorCurveData.rotationCurves, "/Rotation"); loadVector3Curve(clipCurves.ScaleCurves, editorCurveData.scaleCurves, "/Scale"); // Find which individual float curves belong to the same field Dictionary[]> floatCurveMapping = new Dictionary[]>(); { int curveIdx = 0; foreach (var curveEntry in clipCurves.FloatCurves) { string path = curveEntry.Name; string pathNoSuffix = null; string pathSuffix; if (path.Length >= 2) { pathSuffix = path.Substring(path.Length - 2, 2); pathNoSuffix = path.Substring(0, path.Length - 2); } else pathSuffix = ""; int tangentIdx = -1; int currentTangentIdx = 0; foreach (var tangentEntry in editorCurveData.floatCurves) { if (tangentEntry.name == curveEntry.Name) { tangentIdx = currentTangentIdx; break; } currentTangentIdx++; } Animation.PropertySuffixInfo suffixInfo; if (Animation.PropertySuffixInfos.TryGetValue(pathSuffix, out suffixInfo)) { Tuple[] curveInfo; if (!floatCurveMapping.TryGetValue(pathNoSuffix, out curveInfo)) curveInfo = new Tuple[4]; curveInfo[suffixInfo.elementIdx] = Tuple.Create(curveIdx, tangentIdx, suffixInfo.isVector); floatCurveMapping[pathNoSuffix] = curveInfo; } else { Tuple[] curveInfo = new Tuple[4]; curveInfo[0] = Tuple.Create(curveIdx, tangentIdx, suffixInfo.isVector); floatCurveMapping[path] = curveInfo; } curveIdx++; } } foreach (var KVP in floatCurveMapping) { int numCurves = 0; for (int i = 0; i < 4; i++) { if (KVP.Value[i] == null) continue; numCurves++; } if (numCurves == 0) continue; // Invalid curve FieldCurves fieldCurves = new FieldCurves(); // Deduce type (note that all single value types are assumed to be float even if their source type is int or bool) if (numCurves == 1) fieldCurves.type = SerializableProperty.FieldType.Float; else if (numCurves == 2) fieldCurves.type = SerializableProperty.FieldType.Vector2; else if (numCurves == 3) fieldCurves.type = SerializableProperty.FieldType.Vector3; else // 4 curves { bool isVector = KVP.Value[0].Item3; if (isVector) fieldCurves.type = SerializableProperty.FieldType.Vector4; else fieldCurves.type = SerializableProperty.FieldType.Color; } fieldCurves.curves = new EdAnimationCurve[numCurves]; for (int i = 0; i < numCurves; i++) { int curveIdx = KVP.Value[i].Item1; int tangentIdx = KVP.Value[i].Item2; TangentMode[] tangents = null; if (tangentIdx != -1) tangents = editorCurveData.floatCurves[tangentIdx].tangents; fieldCurves.curves[i] = new EdAnimationCurve(clipCurves.FloatCurves[curveIdx].Curve, tangents); } string curvePath = KVP.Key; guiFieldDisplay.AddField(curvePath); curves[curvePath] = fieldCurves; } // Add events guiCurveEditor.Events = clip.Events; } private void SaveToClip(AnimationClip clip, string saveToPath) { if (!clipIsImported) { List positionCurves = new List(); List rotationCurves = new List(); List scaleCurves = new List(); List floatCurves = new List(); List positionTangents = new List(); List rotationTangents = new List(); List scaleTangents = new List(); List floatTangents = new List(); foreach (var kvp in curves) { string[] pathEntries = kvp.Key.Split('/'); if (pathEntries.Length == 0) continue; string lastEntry = pathEntries[pathEntries.Length - 1]; if (lastEntry == "Position" || lastEntry == "Rotation" || lastEntry == "Scale") { StringBuilder sb = new StringBuilder(); for (int i = 0; i < pathEntries.Length - 2; i++) sb.Append(pathEntries[i] + "/"); if (pathEntries.Length > 1) sb.Append(pathEntries[pathEntries.Length - 2]); string curvePath = sb.ToString(); NamedVector3Curve curve = new NamedVector3Curve(curvePath, new AnimationCurve(kvp.Value.curves[0].KeyFrames), new AnimationCurve(kvp.Value.curves[1].KeyFrames), new AnimationCurve(kvp.Value.curves[2].KeyFrames)); EditorVector3CurveTangents tangents = new EditorVector3CurveTangents(); tangents.name = curvePath; tangents.tangentsX = kvp.Value.curves[0].TangentModes; tangents.tangentsY = kvp.Value.curves[1].TangentModes; tangents.tangentsZ = kvp.Value.curves[2].TangentModes; if (lastEntry == "Position") { positionCurves.Add(curve); positionTangents.Add(tangents); } else if (lastEntry == "Rotation") { rotationCurves.Add(curve); rotationTangents.Add(tangents); } else if (lastEntry == "Scale") { scaleCurves.Add(curve); scaleTangents.Add(tangents); } } else { Action addCurve = (idx, subPath) => { string path = kvp.Key + subPath; NamedFloatCurve curve = new NamedFloatCurve(path, new AnimationCurve(kvp.Value.curves[idx].KeyFrames)); EditorFloatCurveTangents tangents = new EditorFloatCurveTangents(); tangents.name = path; tangents.tangents = kvp.Value.curves[idx].TangentModes; floatCurves.Add(curve); floatTangents.Add(tangents); }; switch (kvp.Value.type) { case SerializableProperty.FieldType.Vector2: addCurve(0, ".x"); addCurve(1, ".y"); break; case SerializableProperty.FieldType.Vector3: addCurve(0, ".x"); addCurve(1, ".y"); addCurve(2, ".z"); break; case SerializableProperty.FieldType.Vector4: addCurve(0, ".x"); addCurve(1, ".y"); addCurve(2, ".z"); addCurve(3, ".w"); break; case SerializableProperty.FieldType.Color: addCurve(0, ".r"); addCurve(1, ".g"); addCurve(2, ".b"); addCurve(3, ".a"); break; case SerializableProperty.FieldType.Bool: case SerializableProperty.FieldType.Int: case SerializableProperty.FieldType.Float: addCurve(0, ""); break; } } } AnimationCurves newClipCurves = new AnimationCurves(); newClipCurves.PositionCurves = positionCurves.ToArray(); newClipCurves.RotationCurves = rotationCurves.ToArray(); newClipCurves.ScaleCurves = scaleCurves.ToArray(); newClipCurves.FloatCurves = floatCurves.ToArray(); clip.Curves = newClipCurves; clip.Events = guiCurveEditor.Events; string resourcePath = ProjectLibrary.GetPath(clip); if (string.IsNullOrEmpty(resourcePath)) ProjectLibrary.Create(clip, saveToPath); else ProjectLibrary.Save(clip); // Save tangents for editor only use LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath); string clipName = PathEx.GetTail(resourcePath); if (entry != null && entry.Type == LibraryEntryType.File) { FileEntry fileEntry = (FileEntry)entry; ResourceMeta[] metas = fileEntry.ResourceMetas; for (int i = 0; i < metas.Length; i++) { if (clipName == metas[i].SubresourceName) { EditorCurveData newCurveData = new EditorCurveData(); newCurveData.positionCurves = positionTangents.ToArray(); newCurveData.rotationCurves = rotationTangents.ToArray(); newCurveData.scaleCurves = scaleTangents.ToArray(); newCurveData.floatCurves = floatTangents.ToArray(); ProjectLibrary.SetEditorData(resourcePath, newCurveData); break; } } } } else { string resourcePath = ProjectLibrary.GetPath(clip); LibraryEntry entry = ProjectLibrary.GetEntry(resourcePath); if (entry != null && entry.Type == LibraryEntryType.File) { FileEntry fileEntry = (FileEntry) entry; MeshImportOptions meshImportOptions = (MeshImportOptions)fileEntry.Options; string clipName = PathEx.GetTail(resourcePath); List newEvents = new List(); newEvents.AddRange(meshImportOptions.AnimationEvents); bool isExisting = false; for (int i = 0; i < newEvents.Count; i++) { if (newEvents[i].name == clipName) { newEvents[i].events = guiCurveEditor.Events; isExisting = true; break; } } if (!isExisting) { ImportedAnimationEvents newEntry = new ImportedAnimationEvents(); newEntry.name = clipName; newEntry.events = guiCurveEditor.Events; newEvents.Add(newEntry); } meshImportOptions.AnimationEvents = newEvents.ToArray(); ProjectLibrary.Reimport(resourcePath, meshImportOptions, true); } } } private bool IsClipImported(AnimationClip clip) { string resourcePath = ProjectLibrary.GetPath(clip); return ProjectLibrary.IsSubresource(resourcePath); } #endregion #region Curve display private int currentFrameIdx; private int fps = 1; internal int FPS { get { return fps; } set { guiCurveEditor.SetFPS(value); fps = MathEx.Max(value, 1); } } 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 curves) { string suffix; SerializableProperty property = Animation.FindProperty(selectedSO, kvp.Key, out suffix); if (property != null) { 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.curves[i].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.curves[i].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.curves[i].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.curves[i].Evaluate(time, false); fieldValue.value = value; } break; case SerializableProperty.FieldType.Bool: case SerializableProperty.FieldType.Int: case SerializableProperty.FieldType.Float: fieldValue.value = kvp.Value.curves[0].Evaluate(time, false); ; break; } values.Add(fieldValue); } } guiFieldDisplay.SetDisplayValues(values.ToArray()); } private Vector2 GetOptimalRange() { List displayedCurves = new List(); for (int i = 0; i < selectedFields.Count; i++) { EdAnimationCurve curve; if (TryGetCurve(selectedFields[i], out curve)) displayedCurves.Add(curve); } float xRange; float yRange; CalculateRange(displayedCurves, out xRange, out yRange); // Add padding to y range yRange *= 1.05f; // Don't allow zero range if (xRange == 0.0f) xRange = 60.0f; if (yRange == 0.0f) yRange = 10.0f; return new Vector2(xRange, yRange); } private void UpdateDisplayedCurves() { List curvesToDisplay = new List(); for (int i = 0; i < selectedFields.Count; i++) { EdAnimationCurve curve; if (TryGetCurve(selectedFields[i], out curve)) curvesToDisplay.Add(curve); } guiCurveEditor.SetCurves(curvesToDisplay.ToArray()); Vector2 newRange = GetOptimalRange(); // Don't reduce visible range newRange.x = Math.Max(newRange.x, guiCurveEditor.Range.x); newRange.y = Math.Max(newRange.y, guiCurveEditor.Range.y); guiCurveEditor.Range = newRange; UpdateScrollBarSize(); } #endregion #region Field display private List selectedFields = new List(); private void AddNewField(string path, SerializableProperty.FieldType type) { guiFieldDisplay.AddField(path); switch (type) { case SerializableProperty.FieldType.Vector4: { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[4]; string[] subPaths = { ".x", ".y", ".z", ".w" }; for (int i = 0; i < subPaths.Length; i++) { string subFieldPath = path + subPaths[i]; fieldCurves.curves[i] = new EdAnimationCurve(); selectedFields.Add(subFieldPath); } curves[path] = fieldCurves; } break; case SerializableProperty.FieldType.Vector3: { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[3]; string[] subPaths = { ".x", ".y", ".z" }; for (int i = 0; i < subPaths.Length; i++) { string subFieldPath = path + subPaths[i]; fieldCurves.curves[i] = new EdAnimationCurve(); selectedFields.Add(subFieldPath); } curves[path] = fieldCurves; } break; case SerializableProperty.FieldType.Vector2: { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[2]; string[] subPaths = { ".x", ".y" }; for (int i = 0; i < subPaths.Length; i++) { string subFieldPath = path + subPaths[i]; fieldCurves.curves[i] = new EdAnimationCurve(); selectedFields.Add(subFieldPath); } curves[path] = fieldCurves; } break; case SerializableProperty.FieldType.Color: { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[4]; string[] subPaths = { ".r", ".g", ".b", ".a" }; for (int i = 0; i < subPaths.Length; i++) { string subFieldPath = path + subPaths[i]; fieldCurves.curves[i] = new EdAnimationCurve(); selectedFields.Add(subFieldPath); } curves[path] = fieldCurves; } break; default: // Primitive type { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[1]; fieldCurves.curves[0] = new EdAnimationCurve(); selectedFields.Add(path); curves[path] = fieldCurves; } break; } UpdateDisplayedCurves(); } 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(); } private void RemoveSelectedFields() { for (int i = 0; i < selectedFields.Count; i++) { selectedFields.Remove(selectedFields[i]); curves.Remove(GetSubPathParent(selectedFields[i])); } List existingFields = new List(); foreach (var KVP in curves) existingFields.Add(KVP.Key); guiFieldDisplay.SetFields(existingFields.ToArray()); selectedFields.Clear(); UpdateDisplayedCurves(); } #endregion #region Helpers private Vector2I GetCurveEditorSize() { Vector2I output = new Vector2I(); output.x = Math.Max(0, Width - FIELD_DISPLAY_WIDTH - scrollBarWidth); output.y = Math.Max(0, Height - buttonLayoutHeight - scrollBarHeight); return output; } private static void CalculateRange(List curves, out float xRange, out float yRange) { xRange = 0.0f; yRange = 0.0f; foreach (var curve in curves) { KeyFrame[] keyframes = curve.KeyFrames; foreach (var key in keyframes) { xRange = Math.Max(xRange, key.time); yRange = Math.Max(yRange, Math.Abs(key.value)); } } } private bool TryGetCurve(string path, out EdAnimationCurve curve) { 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); } FieldCurves fieldCurves; if (curves.TryGetValue(parentPath, out fieldCurves)) { if (!string.IsNullOrEmpty(subPathSuffix)) { if (subPathSuffix == ".x" || subPathSuffix == ".r") { curve = fieldCurves.curves[0]; return true; } else if (subPathSuffix == ".y" || subPathSuffix == ".g") { curve = fieldCurves.curves[1]; return true; } else if (subPathSuffix == ".z" || subPathSuffix == ".b") { curve = fieldCurves.curves[2]; return true; } else if (subPathSuffix == ".w" || subPathSuffix == ".a") { curve = fieldCurves.curves[3]; return true; } } else { curve = fieldCurves.curves[0]; return true; } } curve = null; return false; } 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; } private string GetSubPathParent(string path) { int index = path.LastIndexOf("."); if (index == -1) return path; return path.Substring(0, index); } #endregion #region Input callbacks private void OnPointerPressed(PointerEvent ev) { if (!isInitialized) return; guiCurveEditor.OnPointerPressed(ev); if (ev.button == PointerButton.Middle) { Vector2I windowPos = ScreenToWindowPos(ev.ScreenPos); Vector2 curvePos; if (guiCurveEditor.WindowToCurveSpace(windowPos, out curvePos)) { dragStartPos = windowPos; isButtonHeld = true; } } } private void OnPointerMoved(PointerEvent ev) { if (!isInitialized) return; guiCurveEditor.OnPointerMoved(ev); if (isButtonHeld) { Vector2I windowPos = ScreenToWindowPos(ev.ScreenPos); int distance = Vector2I.Distance(dragStartPos, windowPos); if (distance >= DRAG_START_DISTANCE) { isDragInProgress = true; Cursor.Hide(); Rect2I clipRect; clipRect.x = ev.ScreenPos.x - 2; clipRect.y = ev.ScreenPos.y - 2; clipRect.width = 4; clipRect.height = 4; Cursor.ClipToRect(clipRect); } } } private void OnPointerReleased(PointerEvent ev) { if (isDragInProgress) { Cursor.Show(); Cursor.ClipDisable(); } isButtonHeld = false; isDragInProgress = false; if (!isInitialized) return; guiCurveEditor.OnPointerReleased(ev); } private void OnButtonUp(ButtonEvent ev) { if (!isInitialized) return; guiCurveEditor.OnButtonUp(ev); } #endregion #region General callbacks 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); } private void OnHorzScrollOrResize(float position, float size) { SetHorzScrollbarProperties(position, size); } private void OnVertScrollOrResize(float position, float size) { SetVertScrollbarProperties(position, size); } private void OnFieldSelected(string path) { bool additive = Input.IsButtonHeld(ButtonCode.LeftShift) || Input.IsButtonHeld(ButtonCode.RightShift); SelectField(path, additive); } private void OnSelectionChanged(SceneObject[] sceneObjects, string[] resourcePaths) { RebuildGUI(); } private void OnFrameSelected(int frameIdx) { SetCurrentFrame(frameIdx); } #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(); } } /** @} */ }