From fc98b5f1f06bd32b026d6bf0287c4132cd17f348 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 30 Jan 2025 17:26:04 +0100 Subject: [PATCH] Add snapping to grid with Ctrl key when moving keyframes in curve #2455 --- Source/Editor/GUI/CurveEditor.Contents.cs | 73 ++++++++++++++------ Source/Editor/GUI/CurveEditor.cs | 27 ++++++-- Source/Editor/GUI/Timeline/GUI/Background.cs | 2 +- Source/Editor/Gizmo/UIEditorGizmo.cs | 2 +- Source/Editor/Utilities/Utils.cs | 20 +++++- 5 files changed, 91 insertions(+), 33 deletions(-) diff --git a/Source/Editor/GUI/CurveEditor.Contents.cs b/Source/Editor/GUI/CurveEditor.Contents.cs index 94d2ceca7..510e429ae 100644 --- a/Source/Editor/GUI/CurveEditor.Contents.cs +++ b/Source/Editor/GUI/CurveEditor.Contents.cs @@ -30,8 +30,10 @@ namespace FlaxEditor.GUI internal bool _isMovingTangent; internal bool _movedView; internal bool _movedKeyframes; + internal bool _toggledSelection; private TangentPoint _movingTangent; private Float2 _movingSelectionStart; + private Float2 _movingSelectionStartPosLock; private Float2[] _movingSelectionOffsets; private Float2 _cmShowPos; @@ -56,12 +58,11 @@ namespace FlaxEditor.GUI internal void UpdateSelection(ref Rectangle selectionRect) { // Find controls to select - for (int i = 0; i < Children.Count; i++) + var children = _children; + for (int i = 0; i < children.Count; i++) { - if (Children[i] is KeyframePoint p) - { + if (children[i] is KeyframePoint p) p.IsSelected = p.Bounds.Intersects(ref selectionRect); - } } _editor.UpdateTangents(); } @@ -72,6 +73,7 @@ namespace FlaxEditor.GUI _isMovingSelection = true; _movedKeyframes = false; var viewRect = _editor._mainPanel.GetClientArea(); + _movingSelectionStartPosLock = location; _movingSelectionStart = PointToKeyframes(location, ref viewRect); if (_movingSelectionOffsets == null || _movingSelectionOffsets.Length != _editor._points.Count) _movingSelectionOffsets = new Float2[_editor._points.Count]; @@ -82,10 +84,17 @@ namespace FlaxEditor.GUI internal void OnMove(Float2 location) { + // Skip updating keyframes until move actual starts to be meaningful + if (Float2.Distance(ref _movingSelectionStartPosLock, ref location) < 1.5f) + return; + _movingSelectionStartPosLock = Float2.Minimum; + var viewRect = _editor._mainPanel.GetClientArea(); var locationKeyframes = PointToKeyframes(location, ref viewRect); var accessor = _editor.Accessor; var components = accessor.GetCurveComponents(); + var snapEnabled = Root.GetKey(KeyboardKeys.Control); + var snapGrid = snapEnabled ? _editor.GetGridSnap() : Float2.One; for (var i = 0; i < _editor._points.Count; i++) { var p = _editor._points[i]; @@ -122,7 +131,20 @@ namespace FlaxEditor.GUI if (isFirstSelected) { time = locationKeyframes.X + offset.X; + } + if (snapEnabled) + { + // Snap to the grid + var key = new Float2(time, value); + key = Float2.SnapToGrid(key, snapGrid); + time = key.X; + value = key.Y; + } + + // Clamp and snap time to the valid range + if (isFirstSelected) + { if (_editor.FPS.HasValue) { float fps = _editor.FPS.Value; @@ -131,8 +153,6 @@ namespace FlaxEditor.GUI time = Mathf.Clamp(time, minTime, maxTime); } - // TODO: snapping keyframes to grid when moving - _editor.SetKeyframeInternal(p.Index, time, value, p.Component); } _editor.UpdateKeyframes(); @@ -234,7 +254,11 @@ namespace FlaxEditor.GUI var k = _editor.GetKeyframe(_movingTangent.Index); var kv = _editor.GetKeyframeValue(k); var value = _editor.Accessor.GetCurveValue(ref kv, _movingTangent.Component); - _movingTangent.TangentValue = (PointToKeyframes(location, ref viewRect).Y - value) * _editor.ViewScale.X * 2; + var tangent = PointToKeyframes(location, ref viewRect).Y - value; + if (Root.GetKey(KeyboardKeys.Control)) + tangent = Float2.SnapToGrid(new Float2(0, tangent), _editor.GetGridSnap()).Y; // Snap tangent over Y axis + tangent = tangent * _editor.ViewScale.X * 2; + _movingTangent.TangentValue = tangent; _editor.UpdateTangents(); Cursor = CursorType.SizeNS; _movedKeyframes = true; @@ -283,6 +307,7 @@ namespace FlaxEditor.GUI } // Cache data + _toggledSelection = false; _isMovingSelection = false; _isMovingTangent = false; _mousePos = location; @@ -305,13 +330,7 @@ namespace FlaxEditor.GUI { if (_leftMouseDown) { - if (Root.GetKey(KeyboardKeys.Control)) - { - // Toggle selection - keyframe.IsSelected = !keyframe.IsSelected; - _editor.UpdateTangents(); - } - else if (Root.GetKey(KeyboardKeys.Shift)) + if (Root.GetKey(KeyboardKeys.Shift)) { // Select range keyframe.IsSelected = true; @@ -335,10 +354,14 @@ namespace FlaxEditor.GUI else if (!keyframe.IsSelected) { // Select node - if (_editor.KeyframesEditorContext != null) - _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); - else - _editor.ClearSelection(); + if (!Root.GetKey(KeyboardKeys.Control)) + { + if (_editor.KeyframesEditorContext != null) + _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); + else + _editor.ClearSelection(); + } + _toggledSelection = true; keyframe.IsSelected = true; _editor.UpdateTangents(); } @@ -429,6 +452,12 @@ namespace FlaxEditor.GUI else OnMoveEnd(location); } + // Toggle selection + else if (!_toggledSelection && Root.GetKey(KeyboardKeys.Control) && GetChildAt(location) is KeyframePoint keyframe) + { + keyframe.IsSelected = !keyframe.IsSelected; + _editor.UpdateTangents(); + } _isMovingSelection = false; _isMovingTangent = false; @@ -514,11 +543,11 @@ namespace FlaxEditor.GUI { if (base.OnMouseDoubleClick(location, button)) return true; - + // Add keyframe on double click var child = GetChildAt(location); - if (child is not KeyframePoint && - child is not TangentPoint && + if (child is not KeyframePoint && + child is not TangentPoint && _editor.KeyframesCount < _editor.MaxKeyframes) { var viewRect = _editor._mainPanel.GetClientArea(); @@ -545,7 +574,7 @@ namespace FlaxEditor.GUI var viewRect = _editor._mainPanel.GetClientArea(); var locationInKeyframes = PointToKeyframes(location, ref viewRect); var locationInEditorBefore = _editor.PointFromKeyframes(locationInKeyframes, ref viewRect); - + // Scale relative to the curve size var scale = new Float2(delta * 0.1f); _editor._mainPanel.GetDesireClientArea(out var mainPanelArea); diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index b6e3a1e15..9a774e519 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -867,7 +867,7 @@ namespace FlaxEditor.GUI private void DrawAxis(Float2 axis, Rectangle viewRect, float min, float max, float pixelRange) { - Utilities.Utils.DrawCurveTicks((decimal tick, float strength) => + Utilities.Utils.DrawCurveTicks((decimal tick, double step, float strength) => { var p = PointFromKeyframes(axis * (float)tick, ref viewRect); @@ -892,6 +892,24 @@ namespace FlaxEditor.GUI }, TickSteps, ref _tickStrengths, min, max, pixelRange); } + private void SetupGrid(out Float2 min, out Float2 max, out Float2 pixelRange) + { + var viewRect = _mainPanel.GetClientArea(); + var upperLeft = PointToKeyframes(viewRect.Location, ref viewRect); + var bottomRight = PointToKeyframes(viewRect.Size, ref viewRect); + + min = Float2.Min(upperLeft, bottomRight); + max = Float2.Max(upperLeft, bottomRight); + pixelRange = (max - min) * ViewScale * UnitsPerSecond; + } + + private Float2 GetGridSnap() + { + SetupGrid(out var min, out var max, out var pixelRange); + return new Float2(Utilities.Utils.GetCurveGridSnap(TickSteps, ref _tickStrengths, min.X, max.X, pixelRange.X), + Utilities.Utils.GetCurveGridSnap(TickSteps, ref _tickStrengths, min.Y, max.Y, pixelRange.Y)); + } + /// /// Draws the curve. /// @@ -921,12 +939,7 @@ namespace FlaxEditor.GUI // Draw time and values axes if (ShowAxes != UseMode.Off) { - var upperLeft = PointToKeyframes(viewRect.Location, ref viewRect); - var bottomRight = PointToKeyframes(viewRect.Size, ref viewRect); - - var min = Float2.Min(upperLeft, bottomRight); - var max = Float2.Max(upperLeft, bottomRight); - var pixelRange = (max - min) * ViewScale * UnitsPerSecond; + SetupGrid(out var min, out var max, out var pixelRange); Render2D.PushClip(ref viewRect); diff --git a/Source/Editor/GUI/Timeline/GUI/Background.cs b/Source/Editor/GUI/Timeline/GUI/Background.cs index b1087a682..70ae53058 100644 --- a/Source/Editor/GUI/Timeline/GUI/Background.cs +++ b/Source/Editor/GUI/Timeline/GUI/Background.cs @@ -176,7 +176,7 @@ namespace FlaxEditor.GUI.Timeline.GUI // Draw vertical lines for time axis var pixelsInRange = _timeline.Zoom; var pixelRange = pixelsInRange * (max - min); - var tickRange = Utilities.Utils.DrawCurveTicks((decimal tick, float strength) => + var tickRange = Utilities.Utils.DrawCurveTicks((decimal tick, double step, float strength) => { var time = (float)tick / _timeline.FramesPerSecond; var x = time * zoom + Timeline.StartOffset; diff --git a/Source/Editor/Gizmo/UIEditorGizmo.cs b/Source/Editor/Gizmo/UIEditorGizmo.cs index 7b4e79441..28ccf6ca4 100644 --- a/Source/Editor/Gizmo/UIEditorGizmo.cs +++ b/Source/Editor/Gizmo/UIEditorGizmo.cs @@ -565,7 +565,7 @@ namespace FlaxEditor var linesColor = style.ForegroundDisabled.RGBMultiplied(0.5f); var labelsColor = style.ForegroundDisabled; var labelsSize = 10.0f; - Utilities.Utils.DrawCurveTicks((decimal tick, float strength) => + Utilities.Utils.DrawCurveTicks((decimal tick, double step, float strength) => { var p = _view.PointToParent(axis * (float)tick); diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs index 36e847f2d..6197d9eb2 100644 --- a/Source/Editor/Utilities/Utils.cs +++ b/Source/Editor/Utilities/Utils.cs @@ -246,7 +246,7 @@ namespace FlaxEditor.Utilities 500000, 1000000, 5000000, 10000000, 100000000 }; - internal delegate void DrawCurveTick(decimal tick, float strength); + internal delegate void DrawCurveTick(decimal tick, double step, float strength); internal static Int2 DrawCurveTicks(DrawCurveTick drawTick, double[] tickSteps, ref float[] tickStrengths, float min, float max, float pixelRange, float minDistanceBetweenTicks = 20, float maxDistanceBetweenTicks = 60) { @@ -298,13 +298,29 @@ namespace FlaxEditor.Utilities if (l < biggestTick && (i % Mathd.RoundToInt(lNextStep / lStep) == 0)) continue; var tick = (decimal)lStep * i; - drawTick(tick, strength); + drawTick(tick, lStep, strength); } } return new Int2(smallestTick, biggestTick); } + internal static float GetCurveGridSnap(double[] tickSteps, ref float[] tickStrengths, float min, float max, float pixelRange, float minDistanceBetweenTicks = 20, float maxDistanceBetweenTicks = 60) + { + double gridStep = 0; // No grid + float gridWeight = 0.0f; + DrawCurveTicks((decimal tick, double step, float strength) => + { + // Find the smallest grid step that has meaningful strength (it's the most visible to the user) + if (strength > gridWeight && (step < gridStep || gridStep <= 0.0) && strength > 0.5f) + { + gridStep = Math.Abs(step); + gridWeight = strength; + } + }, tickSteps, ref tickStrengths, min, max, pixelRange, minDistanceBetweenTicks, maxDistanceBetweenTicks); + return (float)gridStep; + } + /// /// Determines whether the specified path string contains any invalid character. ///