Browse Source

Animation editor:
- Neater way of selecting keyframe and converting between pixel and curve coordinates
- A way to detect clicks on tangent handles

BearishSun 9 years ago
parent
commit
f1d2e99a1b

+ 24 - 0
Source/MBansheeEditor/Utility/EdAnimationCurve.cs

@@ -311,4 +311,28 @@ namespace BansheeEditor
             }
         }
     }
+
+    internal struct KeyframeRef
+    {
+        public KeyframeRef(int curveIdx, int keyIdx)
+        {
+            this.curveIdx = curveIdx;
+            this.keyIdx = keyIdx;
+        }
+
+        public int curveIdx;
+        public int keyIdx;
+    }
+
+    internal struct TangentRef
+    {
+        public TangentRef(KeyframeRef keyframeRef, TangentType type)
+        {
+            this.keyframeRef = keyframeRef;
+            this.type = type;
+        }
+
+        public KeyframeRef keyframeRef;
+        public TangentType type;
+    }
 }

+ 116 - 84
Source/MBansheeEditor/Windows/Animation/GUICurveDrawing.cs

@@ -115,21 +115,20 @@ namespace BansheeEditor
         /// <summary>
         /// Marks the specified key-frame as selected, changing the way it is displayed.
         /// </summary>
-        /// <param name="curveIdx">Index of the curve the keyframe is on.</param>
-        /// <param name="keyIdx">Index of the keyframe.</param>
+        /// <param name="keyframeRef">Keyframe reference containing the curve and keyframe index.</param>
         /// <param name="selected">True to select it, false to deselect it.</param>
-        public void SelectKeyframe(int curveIdx, int keyIdx, bool selected)
+        public void SelectKeyframe(KeyframeRef keyframeRef, bool selected)
         {
             if (selectedKeyframes == null)
                 return;
 
-            if (curveIdx < 0 || curveIdx >= selectedKeyframes.Length)
+            if (keyframeRef.curveIdx < 0 || keyframeRef.curveIdx >= selectedKeyframes.Length)
                 return;
 
-            if (keyIdx < 0 || keyIdx >= selectedKeyframes[curveIdx].Length)
+            if (keyframeRef.keyIdx < 0 || keyframeRef.keyIdx >= selectedKeyframes[keyframeRef.curveIdx].Length)
                 return;
 
-            selectedKeyframes[curveIdx][keyIdx] = selected;
+            selectedKeyframes[keyframeRef.curveIdx][keyframeRef.keyIdx] = selected;
         }
 
         /// <summary>
@@ -161,50 +160,55 @@ namespace BansheeEditor
         }
 
         /// <summary>
-        /// Retrieves information under the provided window coordinates. This involves coordinates of the curve, as well
-        /// as curve and key-frame indices that were under the coordinates (if any).
+        /// Attempts to find a keyframe under the provided coordinates.
         /// </summary>
-        /// <param name="windowCoords">Coordinate relative to the window the GUI element is on.</param>
-        /// <param name="curveCoords">Curve coordinates within the range as specified by <see cref="SetRange"/>. Only
-        ///                           Valid when function returns true.</param>
-        /// <param name="curveIdx">Sequential index of the curve that's under the coordinates. -1 if no curve. Index
-        ///                        corresponds to the curve index as provided by the curve array in the constructor or
-        ///                        <see cref="SetCurves"/>.</param>
-        /// <param name="keyIdx">Index of a keyframe that that's under the coordinates, on the curve as referenced by
-        ///                      <paramref name="curveIdx"/>. -1 if no keyframe.</param>
-        /// <returns>True if the window coordinates were within the curve area, false otherwise.</returns>
-        public bool GetCoordInfo(Vector2I windowCoords, out Vector2 curveCoords, out int curveIdx, out int keyIdx)
+        /// <param name="pixelCoords">Coordinates relative to this GUI element in pixels.</param>
+        /// <param name="keyframe">Output object containing keyframe index and index of the curve it belongs to. Only valid
+        ///                        if method returns true.</param>
+        /// <returns>True if there is a keyframe under the coordinates, false otherwise.</returns>
+        public bool FindKeyFrame(Vector2I pixelCoords, out KeyframeRef keyframe)
         {
-            Rect2I bounds = canvas.Bounds;
+            keyframe = new KeyframeRef();
 
-            // Check if outside of curve drawing bounds
-            if (windowCoords.x < (bounds.x + GUIGraphTime.PADDING) || windowCoords.x >= (bounds.x + bounds.width - GUIGraphTime.PADDING) ||
-                windowCoords.y < bounds.y || windowCoords.y >= (bounds.y + bounds.height))
+            float nearestDistance = float.MaxValue;
+            for (int i = 0; i < curves.Length; i++)
             {
-                curveCoords = new Vector2();
-                curveIdx = -1;
-                keyIdx = -1;
-                return false;
-            }
+                EdAnimationCurve curve = curves[i];
+                KeyFrame[] keyframes = curve.Native.KeyFrames;
 
-            // Find time and value of the place under the coordinates
-            Vector2I relativeCoords = windowCoords - new Vector2I(bounds.x + GUIGraphTime.PADDING, bounds.y);
+                for (int j = 0; j < keyframes.Length; j++)
+                {
+                    Vector2 keyframeCurveCoords = new Vector2(keyframes[j].time, keyframes[j].value);
+                    Vector2I keyframeCoords = CurveToPixelSpace(keyframeCurveCoords);
 
-            float lengthPerPixel = xRange / drawableWidth;
-            float heightPerPixel = yRange / height;
+                    float distanceToKey = Vector2I.Distance(pixelCoords, keyframeCoords);
+                    if (distanceToKey < nearestDistance)
+                    {
+                        nearestDistance = distanceToKey;
+                        keyframe.keyIdx = j;
+                        keyframe.curveIdx = i;
+                    }
+                }
+            }
 
-            float yOffset = yRange/2.0f;
+            // We're not near any keyframe
+            if (nearestDistance > 5.0f)
+                return false;
 
-            float t = relativeCoords.x*lengthPerPixel;
-            float value = yOffset - relativeCoords.y*heightPerPixel;
+            return true;
+        }
 
-            curveCoords = new Vector2();
-            curveCoords.x = t;
-            curveCoords.y = value;
+        /// <summary>
+        /// Attempts to find a a tangent handle under the provided coordinates.
+        /// </summary>
+        /// <param name="pixelCoords">Coordinates relative to this GUI element in pixels.</param>
+        /// <param name="tangent">Output object containing keyframe information and tangent type. Only valid if method
+        ///                       returns true.</param>
+        /// <returns>True if there is a tangent handle under the coordinates, false otherwise.</returns>
+        public bool FindTangent(Vector2I pixelCoords, out TangentRef tangent)
+        {
+            tangent = new TangentRef();
 
-            // Find nearest keyframe, if any
-            keyIdx = -1;
-            curveIdx = -1;
             float nearestDistance = float.MaxValue;
             for (int i = 0; i < curves.Length; i++)
             {
@@ -213,55 +217,81 @@ namespace BansheeEditor
 
                 for (int j = 0; j < keyframes.Length; j++)
                 {
-                    Vector2I keyframeCoords = new Vector2I((int)(keyframes[j].time / lengthPerPixel),
-                        (int)((yOffset - keyframes[j].value) / heightPerPixel));
+                    if (!IsSelected(i, j))
+                        continue;
 
-                    float distanceToKey = Vector2I.Distance(relativeCoords, keyframeCoords);
-                    if (distanceToKey < nearestDistance)
+                    TangentMode tangentMode = curve.TangentModes[j];
+
+                    if (IsTangentDisplayed(tangentMode, TangentType.In))
                     {
-                        nearestDistance = distanceToKey;
-                        keyIdx = j;
-                        curveIdx = i;
+                        Vector2I tangentCoords = GetTangentPosition(keyframes[j], TangentType.In);
+
+                        float distanceToHandle = Vector2I.Distance(pixelCoords, tangentCoords);
+                        if (distanceToHandle < nearestDistance)
+                        {
+                            nearestDistance = distanceToHandle;
+                            tangent.keyframeRef.keyIdx = j;
+                            tangent.keyframeRef.curveIdx = i;
+                            tangent.type = TangentType.In;
+                        }
+;                    }
+
+                    if (IsTangentDisplayed(tangentMode, TangentType.Out))
+                    {
+                        Vector2I tangentCoords = GetTangentPosition(keyframes[j], TangentType.Out);
+
+                        float distanceToHandle = Vector2I.Distance(pixelCoords, tangentCoords);
+                        if (distanceToHandle < nearestDistance)
+                        {
+                            nearestDistance = distanceToHandle;
+                            tangent.keyframeRef.keyIdx = j;
+                            tangent.keyframeRef.curveIdx = i;
+                            tangent.type = TangentType.Out;
+                        }
                     }
                 }
-
-                // We're not near any keyframe
-                if (nearestDistance > 5.0f)
-                    keyIdx = -1;
             }
 
-            // Find nearest curve, if any
-            if (keyIdx == -1)
+            // We're not near any keyframe
+            if (nearestDistance > 5.0f)
+                return false;
+
+            return true;
+        }
+        
+        /// <summary>
+        /// Converts pixel coordinates into coordinates in curve space.
+        /// </summary>
+        /// <param name="pixelCoords">Coordinates relative to this GUI element, in pixels.</param>
+        /// <param name="curveCoords">Curve coordinates within the range as specified by <see cref="SetRange"/>. Only
+        ///                           valid when function returns true.</param>
+        /// <returns>True if the window coordinates were within the curve area, false otherwise.</returns>
+        public bool PixelToCurveSpace(Vector2I pixelCoords, out Vector2 curveCoords)
+        {
+            Rect2I bounds = canvas.Bounds;
+
+            // Check if outside of curve drawing bounds
+            if (pixelCoords.x < (bounds.x + GUIGraphTime.PADDING) || pixelCoords.x >= (bounds.x + bounds.width - GUIGraphTime.PADDING) ||
+                pixelCoords.y < bounds.y || pixelCoords.y >= (bounds.y + bounds.height))
             {
-                // Note: This will not detect a curve if coordinate is over a step, and in general this works poorly with large slopes
-                curveIdx = -1;
-                nearestDistance = float.MaxValue;
-                for (int i = 0; i < curves.Length; i++)
-                {
-                    EdAnimationCurve curve = curves[i];
-                    KeyFrame[] keyframes = curve.Native.KeyFrames;
+                curveCoords = new Vector2();
+                return false;
+            }
 
-                    if (keyframes.Length == 0)
-                        continue;
+            // Find time and value of the place under the coordinates
+            Vector2I relativeCoords = pixelCoords - new Vector2I(bounds.x + GUIGraphTime.PADDING, bounds.y);
 
-                    if (t < keyframes[0].time || t > keyframes[keyframes.Length - 1].time)
-                        continue;
+            float lengthPerPixel = GetRange() / drawableWidth;
+            float heightPerPixel = yRange / height;
 
-                    float curveValue = curves[i].Native.Evaluate(t);
+            float yOffset = yRange / 2.0f;
 
-                    float distanceToCurve = Math.Abs(curveValue - value);
-                    if (distanceToCurve < nearestDistance)
-                    {
-                        nearestDistance = distanceToCurve;
-                        curveIdx = i;
-                    }
-                }
+            float t = relativeCoords.x * lengthPerPixel;
+            float value = yOffset - relativeCoords.y * heightPerPixel;
 
-                // We're not near any curve
-                float nearestDistancePx = nearestDistance/heightPerPixel;
-                if (nearestDistancePx > 15.0f)
-                    curveIdx = -1;
-            }
+            curveCoords = new Vector2();
+            curveCoords.x = t;
+            curveCoords.y = value;
 
             return true;
         }
@@ -271,7 +301,7 @@ namespace BansheeEditor
         /// </summary>
         /// <param name="curveCoords">Time and value of the location to convert.</param>
         /// <returns>Coordinates relative to this element's origin, in pixels.</returns>
-        private Vector2I CurveToPixelSpace(Vector2 curveCoords)
+        public Vector2I CurveToPixelSpace(Vector2 curveCoords)
         {
             int heightOffset = height / 2; // So that y = 0 is at center of canvas
 
@@ -542,7 +572,9 @@ namespace BansheeEditor
         /// <param name="color">Color to draw the curve with.</param>
         private void DrawCurve(EdAnimationCurve curve, Color color)
         {
-            float lengthPerPixel = xRange/drawableWidth;
+            float range = GetRange();
+
+            float lengthPerPixel = range / drawableWidth;
             float pixelsPerHeight = height/yRange;
 
             int heightOffset = height/2; // So that y = 0 is at center of canvas
@@ -553,7 +585,7 @@ namespace BansheeEditor
 
             // Draw start line
             {
-                float start = MathEx.Clamp(keyframes[0].time, 0.0f, xRange);
+                float start = MathEx.Clamp(keyframes[0].time, 0.0f, range);
                 int startPixel = (int)(start / lengthPerPixel);
 
                 int xPosStart = 0;
@@ -573,8 +605,8 @@ namespace BansheeEditor
             // Draw in between keyframes
             for (int i = 0; i < keyframes.Length - 1; i++)
             {
-                float start = MathEx.Clamp(keyframes[i].time, 0.0f, xRange);
-                float end = MathEx.Clamp(keyframes[i + 1].time, 0.0f, xRange);
+                float start = MathEx.Clamp(keyframes[i].time, 0.0f, range);
+                float end = MathEx.Clamp(keyframes[i + 1].time, 0.0f, range);
                 
                 int startPixel = (int)(start / lengthPerPixel);
                 int endPixel = (int)(end / lengthPerPixel);
@@ -640,13 +672,13 @@ namespace BansheeEditor
 
             // Draw end line
             {
-                float end = MathEx.Clamp(keyframes[keyframes.Length - 1].time, 0.0f, xRange);
+                float end = MathEx.Clamp(keyframes[keyframes.Length - 1].time, 0.0f, range);
                 int endPixel = (int)(end / lengthPerPixel);
 
                 int xPosStart = GUIGraphTime.PADDING + endPixel;
                 int xPosEnd = width;
 
-                int yPos = (int)(curve.Native.Evaluate(xRange, false) * pixelsPerHeight);
+                int yPos = (int)(curve.Native.Evaluate(range, false) * pixelsPerHeight);
                 yPos = heightOffset - yPos; // Offset and flip height (canvas Y goes down)
 
                 Vector2I a = new Vector2I(xPosStart, yPos);

+ 19 - 35
Source/MBansheeEditor/Windows/Animation/GUICurveEditor.cs

@@ -12,18 +12,6 @@ namespace BansheeEditor
 
     internal class GUICurveEditor
     {
-        public struct KeyframeRef
-        {
-            public KeyframeRef(int curveIdx, int keyIdx)
-            {
-                this.curveIdx = curveIdx;
-                this.keyIdx = keyIdx;
-            }
-
-            public int curveIdx;
-            public int keyIdx;
-        }
-
         private const int TIMELINE_HEIGHT = 20;
         private const int SIDEBAR_WIDTH = 30;
 
@@ -106,18 +94,17 @@ namespace BansheeEditor
             if (ev.Button == PointerButton.Left)
             {
                 Vector2 curveCoord;
-                int curveIdx;
-                int keyIdx;
-                if (guiCurveDrawing.GetCoordInfo(drawingPos, out curveCoord, out curveIdx, out keyIdx))
+                if (guiCurveDrawing.PixelToCurveSpace(drawingPos, out curveCoord))
                 {
-                    if (keyIdx == -1)
+                    KeyframeRef keyframeRef;
+                    if (!guiCurveDrawing.FindKeyFrame(drawingPos, out keyframeRef))
                         ClearSelection();
                     else
                     {
                         if (!Input.IsButtonHeld(ButtonCode.LeftShift) && !Input.IsButtonHeld(ButtonCode.RightShift))
                             ClearSelection();
 
-                        SelectKeyframe(curveIdx, keyIdx);
+                        SelectKeyframe(keyframeRef);
 
                         isMousePressedOverKey = true;
                         dragStart = curveCoord;
@@ -138,30 +125,29 @@ namespace BansheeEditor
             else if (ev.Button == PointerButton.Right)
             {
                 Vector2 curveCoord;
-                int curveIdx;
-                int keyIdx;
-                if (guiCurveDrawing.GetCoordInfo(drawingPos, out curveCoord, out curveIdx, out keyIdx))
+                if (guiCurveDrawing.PixelToCurveSpace(drawingPos, out curveCoord))
                 {
-                    contextClickPosition = pointerPos;
+                    contextClickPosition = drawingPos;
 
-                    if (keyIdx == -1)
+                    KeyframeRef keyframeRef;
+                    if (!guiCurveDrawing.FindKeyFrame(drawingPos, out keyframeRef))
                     {
                         ClearSelection();
 
-                        blankContextMenu.Open(contextClickPosition, gui);
+                        blankContextMenu.Open(pointerPos, gui);
                     }
                     else
                     {
                         // If clicked outside of current selection, just select the one keyframe
-                        if (!IsSelected(curveIdx, keyIdx))
+                        if (!IsSelected(keyframeRef))
                         {
                             ClearSelection();
-                            SelectKeyframe(curveIdx, keyIdx);
+                            SelectKeyframe(keyframeRef);
 
                             guiCurveDrawing.Rebuild();
                         }
 
-                        keyframeContextMenu.Open(contextClickPosition, gui);
+                        keyframeContextMenu.Open(pointerPos, gui);
                     }
                 }
             }
@@ -343,9 +329,7 @@ namespace BansheeEditor
         private void AddKeyframeAtPosition()
         {
             Vector2 curveCoord;
-            int curveIdx;
-            int keyIdx;
-            if (guiCurveDrawing.GetCoordInfo(contextClickPosition, out curveCoord, out curveIdx, out keyIdx))
+            if (guiCurveDrawing.PixelToCurveSpace(contextClickPosition, out curveCoord))
             {
                 ClearSelection();
 
@@ -397,19 +381,19 @@ namespace BansheeEditor
             isMousePressedOverKey = false;
         }
 
-        private void SelectKeyframe(int curveIdx, int keyIdx)
+        private void SelectKeyframe(KeyframeRef keyframeRef)
         {
-            guiCurveDrawing.SelectKeyframe(curveIdx, keyIdx, true);
+            guiCurveDrawing.SelectKeyframe(keyframeRef, true);
 
-            if (!IsSelected(curveIdx, keyIdx))
-                selectedKeyframes.Add(new GUICurveEditor.KeyframeRef(curveIdx, keyIdx));
+            if (!IsSelected(keyframeRef))
+                selectedKeyframes.Add(keyframeRef);
         }
 
-        private bool IsSelected(int curveIdx, int keyIdx)
+        private bool IsSelected(KeyframeRef keyframeRef)
         {
             int existingIdx = selectedKeyframes.FindIndex(x =>
             {
-                return x.curveIdx == curveIdx && x.keyIdx == keyIdx;
+                return x.curveIdx == keyframeRef.curveIdx && x.keyIdx == keyframeRef.keyIdx;
             });
 
             return (existingIdx != -1);