Improve Animation Scene curve keyframe editing
Improves Animation Scene timeline curve editing, including vector keyframe selection/editing, tangent interaction, draggable edit popup, and several related editor UI fixes.
This commit is contained in:
@@ -55,6 +55,21 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
private Window _window;
|
||||
private Control _previouslyFocused;
|
||||
|
||||
private static bool IsFinite(float value)
|
||||
{
|
||||
return !float.IsNaN(value) && !float.IsInfinity(value);
|
||||
}
|
||||
|
||||
private static bool IsValidWindowBounds(Float2 location, Float2 size)
|
||||
{
|
||||
return IsFinite(location.X) &&
|
||||
IsFinite(location.Y) &&
|
||||
IsFinite(size.X) &&
|
||||
IsFinite(size.Y) &&
|
||||
size.X > 0.0f &&
|
||||
size.Y > 0.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether use automatic popup direction fix based on the screen dimensions.
|
||||
/// </summary>
|
||||
@@ -190,6 +205,11 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
var dpiSize = Size * dpiScale;
|
||||
var locationWS = parent.PointToWindow(location);
|
||||
var locationSS = parentWin.PointToScreen(locationWS);
|
||||
if (!IsValidWindowBounds(locationSS, dpiSize))
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
var monitorBounds = Platform.GetMonitorBounds(locationSS);
|
||||
var rightBottomLocationSS = locationSS + dpiSize;
|
||||
bool isUp = false, isLeft = false;
|
||||
@@ -403,7 +423,31 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.ClientSize = Size * _window.DpiScale;
|
||||
var size = Size * _window.DpiScale;
|
||||
if (!IsValidWindowBounds(_window.ClientBounds.Location, size))
|
||||
return;
|
||||
_window.ClientSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the popup window location in screen coordinates.
|
||||
/// </summary>
|
||||
protected Float2 WindowLocation => _window != null ? _window.ClientBounds.Location : Float2.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Moves the popup window to the given screen-space location.
|
||||
/// </summary>
|
||||
/// <param name="screenLocation">The popup window location in screen coordinates.</param>
|
||||
protected void MoveWindowTo(Float2 screenLocation)
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
var bounds = _window.ClientBounds;
|
||||
if (!IsValidWindowBounds(screenLocation, bounds.Size))
|
||||
return;
|
||||
bounds.Location = screenLocation;
|
||||
_window.ClientBounds = bounds;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,16 @@ namespace FlaxEditor.GUI
|
||||
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
|
||||
protected class ContentsBase : ContainerControl
|
||||
{
|
||||
private const float DragStartDistance = 1.5f;
|
||||
private const float DragStartDistanceSquared = DragStartDistance * DragStartDistance;
|
||||
|
||||
private enum SelectionMode
|
||||
{
|
||||
Replace,
|
||||
Add,
|
||||
Remove,
|
||||
}
|
||||
|
||||
private readonly CurveEditor<T> _editor;
|
||||
internal bool _leftMouseDown;
|
||||
private bool _rightMouseDown;
|
||||
@@ -46,6 +56,89 @@ namespace FlaxEditor.GUI
|
||||
_editor = editor;
|
||||
}
|
||||
|
||||
private KeyframePoint GetKeyframePointAt(Float2 location, bool cycle)
|
||||
{
|
||||
return GetKeyframePointAt(location, cycle, out _);
|
||||
}
|
||||
|
||||
private KeyframePoint GetKeyframePointAt(Float2 location, bool cycle, out bool isStacked)
|
||||
{
|
||||
KeyframePoint firstHit = null;
|
||||
KeyframePoint selectedHit = null;
|
||||
KeyframePoint nextHitAfterSelected = null;
|
||||
int hitsCount = 0;
|
||||
for (int i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var point = _editor._points[i];
|
||||
if (!point.Visible || !point.Bounds.Contains(ref location))
|
||||
continue;
|
||||
|
||||
if (hitsCount == 0)
|
||||
firstHit = point;
|
||||
if (selectedHit != null && nextHitAfterSelected == null)
|
||||
nextHitAfterSelected = point;
|
||||
if (selectedHit == null && point.IsSelected)
|
||||
selectedHit = point;
|
||||
hitsCount++;
|
||||
}
|
||||
isStacked = hitsCount > 1;
|
||||
if (hitsCount == 0)
|
||||
return null;
|
||||
if (hitsCount == 1)
|
||||
return firstHit;
|
||||
if (selectedHit != null)
|
||||
return cycle ? nextHitAfterSelected ?? firstHit : selectedHit;
|
||||
return firstHit;
|
||||
}
|
||||
|
||||
private static string GetComponentName(int component)
|
||||
{
|
||||
switch (component)
|
||||
{
|
||||
case 0: return "X";
|
||||
case 1: return "Y";
|
||||
case 2: return "Z";
|
||||
case 3: return "W";
|
||||
default: return (component + 1).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectKeyframePoint(KeyframePoint keyframe, bool addToSelection)
|
||||
{
|
||||
if (!addToSelection)
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
}
|
||||
keyframe.IsSelected = true;
|
||||
if (_editor.ShowCollapsed)
|
||||
{
|
||||
for (int i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var point = _editor._points[i];
|
||||
if (point.Index == keyframe.Index)
|
||||
point.IsSelected = true;
|
||||
}
|
||||
}
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
|
||||
private void SelectKeyframeComponent(int keyframeIndex, int component)
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
for (int i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var point = _editor._points[i];
|
||||
point.IsSelected = point.Index == keyframeIndex && point.Component == component;
|
||||
}
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
|
||||
private void UpdateSelectionRectangle()
|
||||
{
|
||||
var selectionRect = Rectangle.FromPoints(_leftMouseDownPos, _mousePos);
|
||||
@@ -55,14 +148,41 @@ namespace FlaxEditor.GUI
|
||||
UpdateSelection(ref selectionRect);
|
||||
}
|
||||
|
||||
private SelectionMode GetSelectionMode()
|
||||
{
|
||||
if (Root.GetKey(KeyboardKeys.Alt))
|
||||
return SelectionMode.Remove;
|
||||
if (Root.GetKey(KeyboardKeys.Shift))
|
||||
return SelectionMode.Add;
|
||||
return SelectionMode.Replace;
|
||||
}
|
||||
|
||||
internal void UpdateSelection(ref Rectangle selectionRect)
|
||||
{
|
||||
var mode = GetSelectionMode();
|
||||
|
||||
// Find controls to select
|
||||
var children = _children;
|
||||
for (int i = 0; i < children.Count; i++)
|
||||
{
|
||||
if (children[i] is KeyframePoint p)
|
||||
p.IsSelected = p.Bounds.Intersects(ref selectionRect);
|
||||
{
|
||||
var intersects = p.Bounds.Intersects(ref selectionRect);
|
||||
switch (mode)
|
||||
{
|
||||
case SelectionMode.Replace:
|
||||
p.IsSelected = intersects;
|
||||
break;
|
||||
case SelectionMode.Add:
|
||||
if (intersects)
|
||||
p.IsSelected = true;
|
||||
break;
|
||||
case SelectionMode.Remove:
|
||||
if (intersects)
|
||||
p.IsSelected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
@@ -82,19 +202,15 @@ namespace FlaxEditor.GUI
|
||||
_editor.OnEditingStart();
|
||||
}
|
||||
|
||||
internal void OnMove(Float2 location)
|
||||
private bool MoveSelectedKeyframes(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;
|
||||
var moved = false;
|
||||
for (var i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var p = _editor._points[i];
|
||||
@@ -154,7 +270,23 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
|
||||
_editor.SetKeyframeInternal(p.Index, time, value, p.Component);
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
return moved;
|
||||
}
|
||||
|
||||
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 moved = MoveSelectedKeyframes(location);
|
||||
if (moved)
|
||||
{
|
||||
_editor.UpdateKeyframes();
|
||||
_editor.UpdateTooltips();
|
||||
if (_editor.EnablePanning == UseMode.On)
|
||||
@@ -168,13 +300,15 @@ namespace FlaxEditor.GUI
|
||||
|
||||
internal void OnMoveEnd(Float2 location)
|
||||
{
|
||||
_isMovingSelection = false;
|
||||
if (_movedKeyframes)
|
||||
{
|
||||
_editor.OnEdited();
|
||||
_editor.OnEditingEnd();
|
||||
_editor.UpdateKeyframes();
|
||||
_editor.UpdateTooltips();
|
||||
_movedKeyframes = false;
|
||||
}
|
||||
_isMovingSelection = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -199,26 +333,32 @@ namespace FlaxEditor.GUI
|
||||
_mousePos = location;
|
||||
|
||||
// Start moving selection if movement started from the keyframe
|
||||
if (_leftMouseDown && !_isMovingSelection && GetChildAt(_leftMouseDownPos) is KeyframePoint)
|
||||
var leftMouseDownOverKeyframe = _leftMouseDown && GetKeyframePointAt(_leftMouseDownPos, false) != null;
|
||||
if (leftMouseDownOverKeyframe && !_isMovingSelection)
|
||||
{
|
||||
if (Float2.DistanceSquared(ref _leftMouseDownPos, ref location) < DragStartDistanceSquared)
|
||||
return;
|
||||
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, location, true, false);
|
||||
_editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, _leftMouseDownPos, true, false);
|
||||
else
|
||||
OnMoveStart(location);
|
||||
OnMoveStart(_leftMouseDownPos);
|
||||
}
|
||||
|
||||
// Moving view
|
||||
if (_rightMouseDown)
|
||||
{
|
||||
var movingViewPos = Parent.PointToParent(PointToParent(location));
|
||||
var delta = movingViewPos - _movingViewLastPos;
|
||||
if (_editor.CustomViewPanning != null)
|
||||
delta = _editor.CustomViewPanning(delta);
|
||||
delta *= GetUseModeMask(_editor.EnablePanning);
|
||||
if (delta.LengthSquared > 0.01f)
|
||||
var mousePosition = Root.MousePosition;
|
||||
var mouseDelta = mousePosition - _movingViewLastPos;
|
||||
if (mouseDelta.LengthSquared > 0.01f)
|
||||
{
|
||||
_editor._mainPanel.ViewOffset += delta;
|
||||
_movingViewLastPos = movingViewPos;
|
||||
var delta = mouseDelta;
|
||||
if (_editor.CustomViewPanning != null)
|
||||
delta = _editor.CustomViewPanning(delta);
|
||||
var viewDelta = delta * GetUseModeMask(_editor.EnablePanning);
|
||||
if (viewDelta.LengthSquared > 0.0f)
|
||||
_editor.ViewOffset += viewDelta;
|
||||
_movingViewLastPos = mousePosition;
|
||||
_movedView = true;
|
||||
if (_editor.CustomViewPanning != null)
|
||||
{
|
||||
@@ -257,7 +397,7 @@ namespace FlaxEditor.GUI
|
||||
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;
|
||||
tangent = tangent * UnitsPerSecond / _movingTangent.TangentOffset;
|
||||
_movingTangent.TangentValue = tangent;
|
||||
_editor.UpdateTangents();
|
||||
Cursor = CursorType.SizeNS;
|
||||
@@ -265,7 +405,7 @@ namespace FlaxEditor.GUI
|
||||
return;
|
||||
}
|
||||
// Selecting
|
||||
else if (_leftMouseDown)
|
||||
else if (_leftMouseDown && !leftMouseDownOverKeyframe)
|
||||
{
|
||||
UpdateSelectionRectangle();
|
||||
return;
|
||||
@@ -321,12 +461,13 @@ namespace FlaxEditor.GUI
|
||||
_rightMouseDown = true;
|
||||
_rightMouseDownPos = location;
|
||||
_movedView = false;
|
||||
_movingViewLastPos = Parent.PointToParent(PointToParent(location));
|
||||
_movingViewLastPos = Root.MousePosition;
|
||||
}
|
||||
|
||||
// Check if any node is under the mouse
|
||||
var underMouse = GetChildAt(location);
|
||||
if (underMouse is KeyframePoint keyframe)
|
||||
var keyframe = underMouse is KeyframePoint ? GetKeyframePointAt(location, false) : null;
|
||||
if (keyframe != null)
|
||||
{
|
||||
if (_leftMouseDown)
|
||||
{
|
||||
@@ -355,12 +496,7 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
// Select node
|
||||
if (!Root.GetKey(KeyboardKeys.Control))
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
}
|
||||
SelectKeyframePoint(keyframe, false);
|
||||
_toggledSelection = true;
|
||||
keyframe.IsSelected = true;
|
||||
_editor.UpdateTangents();
|
||||
@@ -402,11 +538,14 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
// Start selecting
|
||||
StartMouseCapture();
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
_editor.UpdateTangents();
|
||||
if (GetSelectionMode() == SelectionMode.Replace)
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
Focus();
|
||||
return true;
|
||||
}
|
||||
@@ -453,11 +592,21 @@ namespace FlaxEditor.GUI
|
||||
OnMoveEnd(location);
|
||||
}
|
||||
// Toggle selection
|
||||
else if (!_toggledSelection && Root.GetKey(KeyboardKeys.Control) && GetChildAt(location) is KeyframePoint keyframe)
|
||||
else if (!_toggledSelection && Root.GetKey(KeyboardKeys.Control) && GetKeyframePointAt(location, false) is KeyframePoint keyframe)
|
||||
{
|
||||
keyframe.IsSelected = !keyframe.IsSelected;
|
||||
_editor.UpdateTangents();
|
||||
}
|
||||
// Select next stacked keyframe component only after a click has completed, not before a possible drag.
|
||||
else if (!_toggledSelection && !Root.GetKey(KeyboardKeys.Control) && !Root.GetKey(KeyboardKeys.Shift) &&
|
||||
Float2.DistanceSquared(ref _leftMouseDownPos, ref location) < DragStartDistanceSquared &&
|
||||
GetKeyframePointAt(_leftMouseDownPos, false, out var mouseDownStacked) != null &&
|
||||
mouseDownStacked &&
|
||||
GetKeyframePointAt(location, true, out var mouseUpStacked) is KeyframePoint clickedKeyframe &&
|
||||
mouseUpStacked)
|
||||
{
|
||||
SelectKeyframePoint(clickedKeyframe, false);
|
||||
}
|
||||
|
||||
_isMovingSelection = false;
|
||||
_isMovingTangent = false;
|
||||
@@ -473,22 +622,12 @@ namespace FlaxEditor.GUI
|
||||
if (!_movedView)
|
||||
{
|
||||
var selectionCount = _editor.SelectionCount;
|
||||
var point = GetChildAt(location) as KeyframePoint;
|
||||
if (selectionCount == 0 && point != null)
|
||||
var point = GetKeyframePointAt(location, false);
|
||||
if (point != null && (selectionCount == 0 || !point.IsSelected))
|
||||
{
|
||||
// Select node
|
||||
selectionCount = 1;
|
||||
point.IsSelected = true;
|
||||
if (_editor.ShowCollapsed)
|
||||
{
|
||||
for (int i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var p = _editor._points[i];
|
||||
if (p.Index == point.Index)
|
||||
p.IsSelected = point.IsSelected;
|
||||
}
|
||||
}
|
||||
_editor.UpdateTangents();
|
||||
SelectKeyframePoint(point, false);
|
||||
}
|
||||
|
||||
var viewRect = _editor._mainPanel.GetClientArea();
|
||||
@@ -500,6 +639,16 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
cm.AddButton(selectionCount == 1 ? "Edit keyframe" : "Edit keyframes", () => _editor.EditKeyframes(this, location));
|
||||
}
|
||||
var components = _editor.Accessor.GetCurveComponents();
|
||||
if (point != null && !_editor.ShowCollapsed && components > 1)
|
||||
{
|
||||
var componentMenu = cm.AddChildMenu("Select component");
|
||||
for (int i = 0; i < components; i++)
|
||||
{
|
||||
var component = i;
|
||||
componentMenu.ContextMenu.AddButton(GetComponentName(component), () => SelectKeyframeComponent(point.Index, component));
|
||||
}
|
||||
}
|
||||
var totalSelectionCount = _editor.KeyframesEditorContext?.OnKeyframesSelectionCount() ?? selectionCount;
|
||||
if (totalSelectionCount > 0)
|
||||
{
|
||||
@@ -556,7 +705,7 @@ namespace FlaxEditor.GUI
|
||||
|
||||
// Add keyframe on double click
|
||||
var child = GetChildAt(location);
|
||||
if (child is not KeyframePoint &&
|
||||
if (GetKeyframePointAt(location, false) == null &&
|
||||
child is not TangentPoint &&
|
||||
_editor.KeyframesCount < _editor.MaxKeyframes)
|
||||
{
|
||||
@@ -588,7 +737,10 @@ namespace FlaxEditor.GUI
|
||||
// Scale relative to the curve size
|
||||
var scale = new Float2(delta * 0.1f);
|
||||
_editor._mainPanel.GetDesireClientArea(out var mainPanelArea);
|
||||
var curveScale = mainPanelArea.Size / _editor._contents.Size;
|
||||
var contentsSize = _editor._contents.Size;
|
||||
var curveScale = new Float2(
|
||||
GetSafeZoomRatio(mainPanelArea.Width, contentsSize.X),
|
||||
GetSafeZoomRatio(mainPanelArea.Height, contentsSize.Y));
|
||||
scale *= curveScale;
|
||||
if (zoomAlt)
|
||||
scale.X = 0; // Scale Y axis only
|
||||
|
||||
@@ -63,17 +63,29 @@ namespace FlaxEditor.GUI
|
||||
|
||||
private class Popup : ContextMenuBase
|
||||
{
|
||||
private const float HeaderHeight = 12.0f;
|
||||
private const float MinContentHeight = 120.0f;
|
||||
|
||||
private CustomEditorPresenter _presenter;
|
||||
private CurveEditor<T> _editor;
|
||||
private List<int> _keyframeIndices;
|
||||
private Panel _panel;
|
||||
private bool _isDirty;
|
||||
private bool _isDragging;
|
||||
private Float2 _dragStartScreenPos;
|
||||
private Float2 _dragStartWindowPos;
|
||||
|
||||
public Popup(CurveEditor<T> editor, object[] selection, List<int> keyframeIndices = null, float maxHeight = 140.0f)
|
||||
: this(editor, maxHeight)
|
||||
{
|
||||
_presenter.Select(selection);
|
||||
_presenter.OpenAllGroups();
|
||||
Size = new Float2(Size.X, Mathf.Min(_presenter.ContainerControl.Size.Y, maxHeight));
|
||||
var maxContentHeight = Mathf.Max(maxHeight - HeaderHeight, 1.0f);
|
||||
var desiredHeight = _presenter.ContainerControl.Size.Y;
|
||||
if (desiredHeight <= 1.0f)
|
||||
desiredHeight = maxContentHeight;
|
||||
Size = new Float2(Size.X, HeaderHeight + Mathf.Min(Mathf.Max(desiredHeight, MinContentHeight), maxContentHeight));
|
||||
UpdateContentBounds();
|
||||
_keyframeIndices = keyframeIndices;
|
||||
if (keyframeIndices != null && selection.Length != keyframeIndices.Count)
|
||||
throw new Exception();
|
||||
@@ -84,18 +96,39 @@ namespace FlaxEditor.GUI
|
||||
_editor = editor;
|
||||
const float width = 340.0f;
|
||||
Size = new Float2(width, height);
|
||||
var panel1 = new Panel(ScrollBars.Vertical)
|
||||
_panel = new Panel(ScrollBars.Vertical)
|
||||
{
|
||||
Bounds = new Rectangle(0, 0.0f, width, height),
|
||||
Bounds = new Rectangle(0, HeaderHeight, width, Mathf.Max(height - HeaderHeight, 1.0f)),
|
||||
Parent = this
|
||||
};
|
||||
_presenter = new CustomEditorPresenter(null);
|
||||
_presenter.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
|
||||
_presenter.Panel.IsScrollable = true;
|
||||
_presenter.Panel.Parent = panel1;
|
||||
_presenter.Panel.Parent = _panel;
|
||||
_presenter.Modified += OnModified;
|
||||
}
|
||||
|
||||
private void UpdateContentBounds()
|
||||
{
|
||||
if (_panel != null)
|
||||
_panel.Bounds = new Rectangle(0, HeaderHeight, Width, Mathf.Max(Height - HeaderHeight, 1.0f));
|
||||
}
|
||||
|
||||
private bool IsOverHeader(ref Float2 location)
|
||||
{
|
||||
return location.X >= 0.0f && location.X <= Width && location.Y >= 0.0f && location.Y <= HeaderHeight;
|
||||
}
|
||||
|
||||
private void EndDragging()
|
||||
{
|
||||
if (_isDragging)
|
||||
{
|
||||
_isDragging = false;
|
||||
Cursor = CursorType.Default;
|
||||
EndMouseCapture();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModified()
|
||||
{
|
||||
if (!_isDirty)
|
||||
@@ -108,7 +141,7 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
for (int i = 0; i < _presenter.SelectionCount; i++)
|
||||
{
|
||||
_editor.SetKeyframeInternal(_keyframeIndices[i], _presenter.Selection[i]);
|
||||
_editor.SetKeyframeInternal(_keyframeIndices[i], _editor.GetKeyframeFromEditingProxy(_presenter.Selection[i]));
|
||||
}
|
||||
}
|
||||
else if (_presenter.Selection[0] is IAllKeyframesProxy proxy)
|
||||
@@ -129,12 +162,31 @@ namespace FlaxEditor.GUI
|
||||
base.OnShow();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
base.Draw();
|
||||
|
||||
var style = Style.Current;
|
||||
Render2D.FillRectangle(new Rectangle(0, 0, Width, HeaderHeight), style.BackgroundHighlighted);
|
||||
Render2D.FillRectangle(new Rectangle(0, HeaderHeight - 1.0f, Width, 1.0f), style.Background);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnSizeChanged()
|
||||
{
|
||||
base.OnSizeChanged();
|
||||
|
||||
UpdateContentBounds();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Hide()
|
||||
{
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
EndDragging();
|
||||
Focus(null);
|
||||
|
||||
if (_isDirty)
|
||||
@@ -145,13 +197,87 @@ namespace FlaxEditor.GUI
|
||||
|
||||
if (_editor._popup == this)
|
||||
_editor._popup = null;
|
||||
_presenter.Modified -= OnModified;
|
||||
_presenter = null;
|
||||
_editor = null;
|
||||
_keyframeIndices = null;
|
||||
_panel = null;
|
||||
|
||||
base.Hide();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDown(Float2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && IsOverHeader(ref location))
|
||||
{
|
||||
_isDragging = true;
|
||||
_dragStartScreenPos = FlaxEngine.Input.MouseScreenPosition;
|
||||
_dragStartWindowPos = WindowLocation;
|
||||
Cursor = CursorType.SizeAll;
|
||||
StartMouseCapture();
|
||||
Focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(location, button);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseMove(Float2 location)
|
||||
{
|
||||
if (_isDragging)
|
||||
{
|
||||
var screenPos = FlaxEngine.Input.MouseScreenPosition;
|
||||
MoveWindowTo(_dragStartWindowPos + screenPos - _dragStartScreenPos);
|
||||
Cursor = CursorType.SizeAll;
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor = IsOverHeader(ref location) ? CursorType.SizeAll : CursorType.Default;
|
||||
|
||||
base.OnMouseMove(location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseUp(Float2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && _isDragging)
|
||||
{
|
||||
EndDragging();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseUp(location, button);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnMouseLeave()
|
||||
{
|
||||
if (!_isDragging)
|
||||
Cursor = CursorType.Default;
|
||||
|
||||
base.OnMouseLeave();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLostFocus()
|
||||
{
|
||||
EndDragging();
|
||||
Cursor = CursorType.Default;
|
||||
|
||||
base.OnLostFocus();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnEndMouseCapture()
|
||||
{
|
||||
_isDragging = false;
|
||||
Cursor = CursorType.Default;
|
||||
|
||||
base.OnEndMouseCapture();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnKeyDown(KeyboardKeys key)
|
||||
{
|
||||
@@ -208,7 +334,8 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
var style = Style.Current;
|
||||
var rect = new Rectangle(Float2.Zero, Size);
|
||||
var color = Editor.ShowCollapsed ? style.ForegroundDisabled : Editor.Colors[Component];
|
||||
var axisColor = Editor.ShowCollapsed ? style.ForegroundDisabled : Editor.Colors[Component];
|
||||
var color = axisColor;
|
||||
if (IsSelected)
|
||||
color = Editor.ContainsFocus ? style.SelectionBorder : Color.Lerp(style.ForegroundDisabled, style.SelectionBorder, 0.4f);
|
||||
if (IsMouseOver)
|
||||
@@ -288,22 +415,14 @@ namespace FlaxEditor.GUI
|
||||
set => Editor.SetKeyframeTangentInternal(Index, IsIn, Component, value);
|
||||
}
|
||||
|
||||
internal float TangentOffset => 50.0f / Editor.ViewScale.X;
|
||||
private const float TangentVisualOffset = 50.0f;
|
||||
|
||||
internal float TangentOffset => TangentVisualOffset / Editor.ViewScale.X;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
var style = Style.Current;
|
||||
var thickness = 6.0f / Mathf.Max(Editor.ViewScale.X, 1.0f);
|
||||
var size = Size;
|
||||
var pointPos = PointFromParent(Point.Center);
|
||||
Render2D.DrawLine(size * 0.5f, pointPos, style.ForegroundDisabled, thickness);
|
||||
|
||||
var rect = new Rectangle(Float2.Zero, size);
|
||||
var color = style.BorderSelected;
|
||||
if (IsMouseOver)
|
||||
color *= 1.1f;
|
||||
Render2D.FillRectangle(rect, color);
|
||||
// Drawn by the editor overlay to keep a constant screen-space size.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -436,12 +555,41 @@ namespace FlaxEditor.GUI
|
||||
/// </summary>
|
||||
protected readonly TangentPoint[] _tangents = new TangentPoint[2];
|
||||
|
||||
private static bool IsFinite(float value)
|
||||
{
|
||||
return !float.IsNaN(value) && !float.IsInfinity(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the input value if finite, otherwise returns a finite fallback or zero.
|
||||
/// </summary>
|
||||
protected static float SanitizeFinite(float value, float fallback)
|
||||
{
|
||||
if (IsFinite(value))
|
||||
return value;
|
||||
return IsFinite(fallback) ? fallback : 0.0f;
|
||||
}
|
||||
|
||||
private static float SanitizeViewScale(float value, float fallback)
|
||||
{
|
||||
if (!IsFinite(value))
|
||||
value = IsFinite(fallback) ? fallback : 1.0f;
|
||||
return Mathf.Clamp(value, 0.0001f, 1000.0f);
|
||||
}
|
||||
|
||||
private static float GetSafeZoomRatio(float viewSize, float contentsSize)
|
||||
{
|
||||
return IsFinite(viewSize) && IsFinite(contentsSize) && viewSize > Mathf.Epsilon && contentsSize > Mathf.Epsilon ? viewSize / contentsSize : 0.0f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Float2 ViewOffset
|
||||
{
|
||||
get => _mainPanel.ViewOffset;
|
||||
set
|
||||
{
|
||||
value.X = SanitizeFinite(value.X, _mainPanel.ViewOffset.X);
|
||||
value.Y = SanitizeFinite(value.Y, _mainPanel.ViewOffset.Y);
|
||||
_mainPanel.ViewOffset = value;
|
||||
_mainPanel.FastScroll();
|
||||
}
|
||||
@@ -451,7 +599,13 @@ namespace FlaxEditor.GUI
|
||||
public override Float2 ViewScale
|
||||
{
|
||||
get => _contents.Scale;
|
||||
set => _contents.Scale = Float2.Clamp(value, new Float2(0.0001f), new Float2(1000.0f));
|
||||
set
|
||||
{
|
||||
var scale = _contents.Scale;
|
||||
value.X = SanitizeViewScale(value.X, scale.X);
|
||||
value.Y = SanitizeViewScale(value.Y, scale.Y);
|
||||
_contents.Scale = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -705,6 +859,36 @@ namespace FlaxEditor.GUI
|
||||
/// <returns>The proxy object.</returns>
|
||||
protected abstract IAllKeyframesProxy GetAllKeyframesEditingProxy();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an editing proxy for a single keyframe.
|
||||
/// </summary>
|
||||
/// <param name="index">The keyframe index.</param>
|
||||
/// <param name="keyframe">The keyframe.</param>
|
||||
/// <returns>The keyframe editing proxy.</returns>
|
||||
protected virtual object CreateKeyframeEditingProxy(int index, object keyframe)
|
||||
{
|
||||
return keyframe;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets keyframe data from its editing proxy.
|
||||
/// </summary>
|
||||
/// <param name="proxy">The keyframe editing proxy.</param>
|
||||
/// <returns>The keyframe data.</returns>
|
||||
protected virtual object GetKeyframeFromEditingProxy(object proxy)
|
||||
{
|
||||
return proxy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum height for keyframe editing popup.
|
||||
/// </summary>
|
||||
/// <returns>The maximum popup height.</returns>
|
||||
protected virtual float GetKeyframeEditingPopupHeight()
|
||||
{
|
||||
return 320.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for keyframes editing proxy objects.
|
||||
/// </summary>
|
||||
@@ -740,8 +924,8 @@ namespace FlaxEditor.GUI
|
||||
var selection = new object[keyframeIndices.Count];
|
||||
var keyframes = GetKeyframes();
|
||||
for (int i = 0; i < keyframeIndices.Count; i++)
|
||||
selection[i] = keyframes[keyframeIndices[i]];
|
||||
_popup = new Popup(this, selection, keyframeIndices);
|
||||
selection[i] = CreateKeyframeEditingProxy(keyframeIndices[i], keyframes[keyframeIndices[i]]);
|
||||
_popup = new Popup(this, selection, keyframeIndices, GetKeyframeEditingPopupHeight());
|
||||
_popup.Show(control, pos);
|
||||
}
|
||||
|
||||
@@ -1049,6 +1233,70 @@ namespace FlaxEditor.GUI
|
||||
/// <param name="viewRect">The main panel client area used as a view bounds.</param>
|
||||
protected abstract void DrawCurve(ref Rectangle viewRect);
|
||||
|
||||
private Float2 ContentsToEditor(Float2 location)
|
||||
{
|
||||
location = _contents.PointToParent(location);
|
||||
return _mainPanel.PointToParent(location);
|
||||
}
|
||||
|
||||
private Float2 GetControlCenterInEditor(Control control)
|
||||
{
|
||||
var center = control.PointToParent(control.Size * 0.5f);
|
||||
return ContentsToEditor(center);
|
||||
}
|
||||
|
||||
private void DrawTangentHandles()
|
||||
{
|
||||
var style = Style.Current;
|
||||
for (int i = 0; i < _tangents.Length; i++)
|
||||
{
|
||||
var tangent = _tangents[i];
|
||||
if (!tangent.Visible || tangent.Point == null || !tangent.Point.Visible)
|
||||
continue;
|
||||
|
||||
var tangentCenter = GetControlCenterInEditor(tangent);
|
||||
var keyframeCenter = GetControlCenterInEditor(tangent.Point);
|
||||
Render2D.DrawLine(tangentCenter, keyframeCenter, style.ForegroundDisabled, 2.0f);
|
||||
|
||||
var rect = new Rectangle(tangentCenter - KeyframesSize * 0.5f, KeyframesSize);
|
||||
var color = style.BorderSelected;
|
||||
if (tangent.IsMouseOver)
|
||||
color *= 1.1f;
|
||||
Render2D.FillRectangle(rect, color);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSelectedKeyframeLabels()
|
||||
{
|
||||
if (ShowCollapsed)
|
||||
return;
|
||||
|
||||
var style = Style.Current;
|
||||
for (int i = 0; i < _points.Count; i++)
|
||||
{
|
||||
var point = _points[i];
|
||||
if (!point.Visible || !point.IsSelected)
|
||||
continue;
|
||||
|
||||
var center = GetControlCenterInEditor(point);
|
||||
var label = GetComponentLabel(point.Component);
|
||||
var labelRect = new Rectangle(center.X + 12.0f, center.Y - 22.0f, 34.0f, 28.0f);
|
||||
Render2D.DrawText(style.FontMedium, label, labelRect, Colors[point.Component], TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, 1.25f);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetComponentLabel(int component)
|
||||
{
|
||||
switch (component)
|
||||
{
|
||||
case 0: return "x";
|
||||
case 1: return "y";
|
||||
case 2: return "z";
|
||||
case 3: return "w";
|
||||
default: return (component + 1).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
@@ -1097,14 +1345,18 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
var selectionRect = Rectangle.FromPoints
|
||||
(
|
||||
_mainPanel.PointToParent(_contents.PointToParent(_contents._leftMouseDownPos)),
|
||||
_mainPanel.PointToParent(_contents.PointToParent(_contents._mousePos))
|
||||
ContentsToEditor(_contents._leftMouseDownPos),
|
||||
ContentsToEditor(_contents._mousePos)
|
||||
);
|
||||
Render2D.FillRectangle(selectionRect, style.Selection);
|
||||
Render2D.DrawRectangle(selectionRect, style.SelectionBorder);
|
||||
}
|
||||
|
||||
base.Draw();
|
||||
Render2D.PushClip(ref viewRect);
|
||||
DrawTangentHandles();
|
||||
DrawSelectedKeyframeLabels();
|
||||
Render2D.PopClip();
|
||||
|
||||
// Draw border
|
||||
if (ContainsFocus)
|
||||
@@ -1465,6 +1717,85 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
}
|
||||
|
||||
sealed class KeyframeProxy
|
||||
{
|
||||
[HideInEditor, NoSerialize]
|
||||
public LinearCurveEditor<T> Editor;
|
||||
|
||||
[HideInEditor, NoSerialize]
|
||||
public int Index;
|
||||
|
||||
private float _time;
|
||||
private readonly float _originalTime;
|
||||
|
||||
[EditorDisplay("Time"), EditorOrder(0), VisibleIf(nameof(HasFPS))]
|
||||
[Tooltip("The keyframe frame number.")]
|
||||
public int Frame
|
||||
{
|
||||
get => HasFPS ? Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * Editor.FPS.Value) : 0;
|
||||
set
|
||||
{
|
||||
if (Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon)
|
||||
_time = value / Editor.FPS.Value;
|
||||
}
|
||||
}
|
||||
|
||||
[EditorDisplay("Time"), EditorOrder(1), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The time of the keyframe.")]
|
||||
public float Time
|
||||
{
|
||||
get => _time;
|
||||
set => _time = SanitizeFinite(value, _time);
|
||||
}
|
||||
|
||||
[EditorDisplay("Value"), EditorOrder(10), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The value of the curve at keyframe.")]
|
||||
public T Value;
|
||||
|
||||
private bool HasFPS => Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon;
|
||||
|
||||
public KeyframeProxy(LinearCurveEditor<T> editor, int index, LinearCurve<T>.Keyframe keyframe)
|
||||
{
|
||||
Editor = editor;
|
||||
Index = index;
|
||||
_time = _originalTime = keyframe.Time;
|
||||
Value = keyframe.Value;
|
||||
}
|
||||
|
||||
private float GetValidTime()
|
||||
{
|
||||
if (Editor == null)
|
||||
return SanitizeFinite(_time, _originalTime);
|
||||
if (Editor.FPS.HasValue)
|
||||
{
|
||||
var fps = Editor.FPS.Value;
|
||||
if (fps <= Mathf.Epsilon)
|
||||
return SanitizeFinite(_time, _originalTime);
|
||||
var frame = Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * fps);
|
||||
var minFrame = int.MinValue;
|
||||
var maxFrame = int.MaxValue;
|
||||
if (Index > 0)
|
||||
minFrame = Mathf.FloorToInt(Editor._keyframes[Index - 1].Time * fps) + 1;
|
||||
if (Index < Editor._keyframes.Count - 1)
|
||||
maxFrame = Mathf.FloorToInt(Editor._keyframes[Index + 1].Time * fps) - 1;
|
||||
if (minFrame > maxFrame)
|
||||
return _originalTime;
|
||||
frame = Mathf.Clamp(frame, minFrame, maxFrame);
|
||||
return frame / fps;
|
||||
}
|
||||
var minTime = Index > 0 ? Editor._keyframes[Index - 1].Time + Mathf.Epsilon : float.MinValue;
|
||||
var maxTime = Index < Editor._keyframes.Count - 1 ? Editor._keyframes[Index + 1].Time - Mathf.Epsilon : float.MaxValue;
|
||||
if (minTime > maxTime)
|
||||
return _originalTime;
|
||||
return Mathf.Clamp(SanitizeFinite(_time, _originalTime), minTime, maxTime);
|
||||
}
|
||||
|
||||
public LinearCurve<T>.Keyframe ToKeyframe()
|
||||
{
|
||||
return new LinearCurve<T>.Keyframe(GetValidTime(), Value);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AllKeyframesProxy : IAllKeyframesProxy
|
||||
{
|
||||
[HideInEditor, NoSerialize]
|
||||
@@ -1489,6 +1820,24 @@ namespace FlaxEditor.GUI
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object CreateKeyframeEditingProxy(int index, object keyframe)
|
||||
{
|
||||
return new KeyframeProxy(this, index, (LinearCurve<T>.Keyframe)keyframe);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object GetKeyframeFromEditingProxy(object proxy)
|
||||
{
|
||||
return proxy is KeyframeProxy keyframeProxy ? keyframeProxy.ToKeyframe() : proxy;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override float GetKeyframeEditingPopupHeight()
|
||||
{
|
||||
return 240.0f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object[] GetKeyframes()
|
||||
{
|
||||
@@ -2213,6 +2562,95 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
}
|
||||
|
||||
sealed class KeyframeProxy
|
||||
{
|
||||
[HideInEditor, NoSerialize]
|
||||
public BezierCurveEditor<T> Editor;
|
||||
|
||||
[HideInEditor, NoSerialize]
|
||||
public int Index;
|
||||
|
||||
private float _time;
|
||||
private readonly float _originalTime;
|
||||
|
||||
[EditorDisplay("Time"), EditorOrder(0), VisibleIf(nameof(HasFPS))]
|
||||
[Tooltip("The keyframe frame number.")]
|
||||
public int Frame
|
||||
{
|
||||
get => HasFPS ? Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * Editor.FPS.Value) : 0;
|
||||
set
|
||||
{
|
||||
if (Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon)
|
||||
_time = value / Editor.FPS.Value;
|
||||
}
|
||||
}
|
||||
|
||||
[EditorDisplay("Time"), EditorOrder(1), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The time of the keyframe.")]
|
||||
public float Time
|
||||
{
|
||||
get => _time;
|
||||
set => _time = SanitizeFinite(value, _time);
|
||||
}
|
||||
|
||||
[EditorDisplay("Value"), EditorOrder(10), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The value of the curve at keyframe.")]
|
||||
public T Value;
|
||||
|
||||
[EditorDisplay("Tangents", "Tangent In"), EditorOrder(20), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The input tangent (going from the previous key to this one) of the key.")]
|
||||
public T TangentIn;
|
||||
|
||||
[EditorDisplay("Tangents", "Tangent Out"), EditorOrder(21), Limit(float.MinValue, float.MaxValue, 0.01f)]
|
||||
[Tooltip("The output tangent (going from this key to next one) of the key.")]
|
||||
public T TangentOut;
|
||||
|
||||
private bool HasFPS => Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon;
|
||||
|
||||
public KeyframeProxy(BezierCurveEditor<T> editor, int index, BezierCurve<T>.Keyframe keyframe)
|
||||
{
|
||||
Editor = editor;
|
||||
Index = index;
|
||||
_time = _originalTime = keyframe.Time;
|
||||
Value = keyframe.Value;
|
||||
TangentIn = keyframe.TangentIn;
|
||||
TangentOut = keyframe.TangentOut;
|
||||
}
|
||||
|
||||
private float GetValidTime()
|
||||
{
|
||||
if (Editor == null)
|
||||
return SanitizeFinite(_time, _originalTime);
|
||||
if (Editor.FPS.HasValue)
|
||||
{
|
||||
var fps = Editor.FPS.Value;
|
||||
if (fps <= Mathf.Epsilon)
|
||||
return SanitizeFinite(_time, _originalTime);
|
||||
var frame = Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * fps);
|
||||
var minFrame = int.MinValue;
|
||||
var maxFrame = int.MaxValue;
|
||||
if (Index > 0)
|
||||
minFrame = Mathf.FloorToInt(Editor._keyframes[Index - 1].Time * fps) + 1;
|
||||
if (Index < Editor._keyframes.Count - 1)
|
||||
maxFrame = Mathf.FloorToInt(Editor._keyframes[Index + 1].Time * fps) - 1;
|
||||
if (minFrame > maxFrame)
|
||||
return _originalTime;
|
||||
frame = Mathf.Clamp(frame, minFrame, maxFrame);
|
||||
return frame / fps;
|
||||
}
|
||||
var minTime = Index > 0 ? Editor._keyframes[Index - 1].Time + Mathf.Epsilon : float.MinValue;
|
||||
var maxTime = Index < Editor._keyframes.Count - 1 ? Editor._keyframes[Index + 1].Time - Mathf.Epsilon : float.MaxValue;
|
||||
if (minTime > maxTime)
|
||||
return _originalTime;
|
||||
return Mathf.Clamp(SanitizeFinite(_time, _originalTime), minTime, maxTime);
|
||||
}
|
||||
|
||||
public BezierCurve<T>.Keyframe ToKeyframe()
|
||||
{
|
||||
return new BezierCurve<T>.Keyframe(GetValidTime(), Value, TangentIn, TangentOut);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AllKeyframesProxy : IAllKeyframesProxy
|
||||
{
|
||||
[HideInEditor, NoSerialize]
|
||||
@@ -2237,6 +2675,24 @@ namespace FlaxEditor.GUI
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object CreateKeyframeEditingProxy(int index, object keyframe)
|
||||
{
|
||||
return new KeyframeProxy(this, index, (BezierCurve<T>.Keyframe)keyframe);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override object GetKeyframeFromEditingProxy(object proxy)
|
||||
{
|
||||
return proxy is KeyframeProxy keyframeProxy ? keyframeProxy.ToKeyframe() : proxy;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override float GetKeyframeEditingPopupHeight()
|
||||
{
|
||||
return 320.0f;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object[] GetKeyframes()
|
||||
{
|
||||
|
||||
@@ -36,6 +36,11 @@ namespace FlaxEditor.GUI.Timeline.GUI
|
||||
_timeline.OnKeyframesSelection(null, this, selectionRect);
|
||||
}
|
||||
|
||||
private bool HasSelectionModifier()
|
||||
{
|
||||
return Root.GetKey(KeyboardKeys.Shift) || Root.GetKey(KeyboardKeys.Alt);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDown(Float2 location, MouseButton button)
|
||||
{
|
||||
@@ -48,7 +53,8 @@ namespace FlaxEditor.GUI.Timeline.GUI
|
||||
// Start selecting
|
||||
_isSelecting = true;
|
||||
_selectingStartPos = location;
|
||||
_timeline.OnKeyframesDeselect(null);
|
||||
if (!HasSelectionModifier())
|
||||
_timeline.OnKeyframesDeselect(null);
|
||||
Focus();
|
||||
StartMouseCapture();
|
||||
return true;
|
||||
|
||||
@@ -77,6 +77,13 @@ namespace FlaxEditor.GUI
|
||||
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
|
||||
private class Contents : ContainerControl
|
||||
{
|
||||
private enum SelectionMode
|
||||
{
|
||||
Replace,
|
||||
Add,
|
||||
Remove,
|
||||
}
|
||||
|
||||
private readonly KeyframesEditor _editor;
|
||||
internal bool _leftMouseDown;
|
||||
private bool _rightMouseDown;
|
||||
@@ -108,14 +115,39 @@ namespace FlaxEditor.GUI
|
||||
UpdateSelection(ref selectionRect);
|
||||
}
|
||||
|
||||
private SelectionMode GetSelectionMode()
|
||||
{
|
||||
if (Root.GetKey(KeyboardKeys.Alt))
|
||||
return SelectionMode.Remove;
|
||||
if (Root.GetKey(KeyboardKeys.Shift))
|
||||
return SelectionMode.Add;
|
||||
return SelectionMode.Replace;
|
||||
}
|
||||
|
||||
internal void UpdateSelection(ref Rectangle selectionRect)
|
||||
{
|
||||
var mode = GetSelectionMode();
|
||||
|
||||
// Find controls to select
|
||||
for (int i = 0; i < Children.Count; i++)
|
||||
{
|
||||
if (Children[i] is KeyframePoint p)
|
||||
{
|
||||
p.IsSelected = p.Bounds.Intersects(ref selectionRect);
|
||||
var intersects = p.Bounds.Intersects(ref selectionRect);
|
||||
switch (mode)
|
||||
{
|
||||
case SelectionMode.Replace:
|
||||
p.IsSelected = intersects;
|
||||
break;
|
||||
case SelectionMode.Add:
|
||||
if (intersects)
|
||||
p.IsSelected = true;
|
||||
break;
|
||||
case SelectionMode.Remove:
|
||||
if (intersects)
|
||||
p.IsSelected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +170,7 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
var viewRect = _editor._mainPanel.GetClientArea();
|
||||
var locationKeyframes = PointToKeyframes(location, ref viewRect);
|
||||
var moved = false;
|
||||
for (var i = 0; i < _editor._points.Count; i++)
|
||||
{
|
||||
var p = _editor._points[i];
|
||||
@@ -160,7 +193,12 @@ namespace FlaxEditor.GUI
|
||||
// TODO: snapping keyframes to grid when moving
|
||||
|
||||
_editor._keyframes[p.Index] = k;
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (moved)
|
||||
{
|
||||
_editor.UpdateKeyframes();
|
||||
if (_editor.EnablePanning)
|
||||
{
|
||||
@@ -351,10 +389,13 @@ namespace FlaxEditor.GUI
|
||||
{
|
||||
// Start selecting
|
||||
StartMouseCapture();
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
if (GetSelectionMode() == SelectionMode.Replace)
|
||||
{
|
||||
if (_editor.KeyframesEditorContext != null)
|
||||
_editor.KeyframesEditorContext.OnKeyframesDeselect(_editor);
|
||||
else
|
||||
_editor.ClearSelection();
|
||||
}
|
||||
Focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
|
||||
|
||||
private void OnSplitterMoved(Float2 location)
|
||||
{
|
||||
var height = Mathf.Clamp(PointToParent(location).Y, 40.0f, 1000.0f);
|
||||
var height = Mathf.Clamp(_splitter.PointToParent(location).Y + Height - Curve.Height, 40.0f, 1000.0f);
|
||||
if (!Mathf.NearEqual(height, _expandedHeight))
|
||||
{
|
||||
Height = _expandedHeight = height;
|
||||
|
||||
@@ -755,6 +755,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
}
|
||||
_previewPlayerPicker.Parent.Visible = !_previewButton.Checked;
|
||||
_timeline.CanPlayPause = _previewButton.Checked || Editor.IsPlayMode;
|
||||
_timeline.CanPlayStop = _previewButton.Checked || Editor.IsPlayMode;
|
||||
}
|
||||
|
||||
private void OnRenderButtonClicked()
|
||||
@@ -927,7 +928,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
|
||||
UpdateToolstrip();
|
||||
_timeline.CanPlayPause = _previewButton.Checked;
|
||||
_timeline.CanPlayStop = false;
|
||||
_timeline.CanPlayStop = _previewButton.Checked;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -982,7 +983,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
// Preview is playing
|
||||
_previewPlayer.Tick(Time.UnscaledDeltaTime);
|
||||
}
|
||||
else if (Mathf.NearEqual(_previewPlayer.Time, _timeline.CurrentFrame))
|
||||
else if (Mathf.NearEqual(_previewPlayer.Time, _timeline.CurrentTime))
|
||||
{
|
||||
// Preview is paused
|
||||
_previewPlayer.Time = _timeline.CurrentTime;
|
||||
|
||||
@@ -463,6 +463,8 @@ namespace FlaxEngine.GUI
|
||||
// Click change
|
||||
Value = _value + (mousePosition < _thumbCenter ? -1 : 1) * _clickChange;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(location, button);
|
||||
|
||||
Reference in New Issue
Block a user