diff --git a/Source/Editor/GUI/CurveEditor.Access.cs b/Source/Editor/GUI/CurveEditor.Access.cs new file mode 100644 index 000000000..7cf113474 --- /dev/null +++ b/Source/Editor/GUI/CurveEditor.Access.cs @@ -0,0 +1,261 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +using FlaxEngine; + +// ReSharper disable RedundantAssignment + +namespace FlaxEditor.GUI +{ + partial class CurveEditor + { + /// + /// The generic keyframe value accessor object for curve editor. + /// + /// The keyframe value type. + public interface IKeyframeAccess where U : new() + { + /// + /// Gets the default value. + /// + /// The value. + void GetDefaultValue(out U value); + + /// + /// Gets the curve components count. Vector types should return amount of component to use for value editing. + /// + /// The components count. + int GetCurveComponents(); + + /// + /// Gets the value of the component for the curve. + /// + /// The keyframe value. + /// The component index. + /// The curve value. + float GetCurveValue(ref U value, int component); + + /// + /// Sets the curve value of the component. + /// + /// The curve value to assign. + /// The keyframe value. + /// The component index. + void SetCurveValue(float curve, ref U value, int component); + } + + private class KeyframeAccess : + IKeyframeAccess, + IKeyframeAccess, + IKeyframeAccess, + IKeyframeAccess, + IKeyframeAccess, + IKeyframeAccess, + IKeyframeAccess, + IKeyframeAccess, + IKeyframeAccess, + IKeyframeAccess + { + void IKeyframeAccess.GetDefaultValue(out bool value) + { + value = false; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 1; + } + + float IKeyframeAccess.GetCurveValue(ref bool value, int component) + { + return value ? 1 : 0; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref bool value, int component) + { + value = curve >= 0.5f; + } + + void IKeyframeAccess.GetDefaultValue(out int value) + { + value = 0; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 1; + } + + float IKeyframeAccess.GetCurveValue(ref int value, int component) + { + return value; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref int value, int component) + { + value = (int)curve; + } + + void IKeyframeAccess.GetDefaultValue(out double value) + { + value = 0.0; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 1; + } + + float IKeyframeAccess.GetCurveValue(ref double value, int component) + { + return (float)value; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref double value, int component) + { + value = curve; + } + + void IKeyframeAccess.GetDefaultValue(out float value) + { + value = 0.0f; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 1; + } + + float IKeyframeAccess.GetCurveValue(ref float value, int component) + { + return value; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref float value, int component) + { + value = curve; + } + + void IKeyframeAccess.GetDefaultValue(out Vector2 value) + { + value = Vector2.Zero; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 2; + } + + float IKeyframeAccess.GetCurveValue(ref Vector2 value, int component) + { + return value[component]; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref Vector2 value, int component) + { + value[component] = curve; + } + + void IKeyframeAccess.GetDefaultValue(out Vector3 value) + { + value = Vector3.Zero; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 3; + } + + float IKeyframeAccess.GetCurveValue(ref Vector3 value, int component) + { + return value[component]; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref Vector3 value, int component) + { + value[component] = curve; + } + + void IKeyframeAccess.GetDefaultValue(out Vector4 value) + { + value = Vector4.Zero; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 4; + } + + float IKeyframeAccess.GetCurveValue(ref Vector4 value, int component) + { + return value[component]; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref Vector4 value, int component) + { + value[component] = curve; + } + + public void GetDefaultValue(out Quaternion value) + { + value = Quaternion.Identity; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 3; + } + + float IKeyframeAccess.GetCurveValue(ref Quaternion value, int component) + { + return value.EulerAngles[component]; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref Quaternion value, int component) + { + var euler = value.EulerAngles; + euler[component] = curve; + Quaternion.Euler(euler.X, euler.Y, euler.Z, out value); + } + + void IKeyframeAccess.GetDefaultValue(out Color value) + { + value = Color.Black; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 4; + } + + float IKeyframeAccess.GetCurveValue(ref Color value, int component) + { + return value[component]; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref Color value, int component) + { + value[component] = curve; + } + + void IKeyframeAccess.GetDefaultValue(out Color32 value) + { + value = Color32.Black; + } + + int IKeyframeAccess.GetCurveComponents() + { + return 4; + } + + float IKeyframeAccess.GetCurveValue(ref Color32 value, int component) + { + return value[component]; + } + + void IKeyframeAccess.SetCurveValue(float curve, ref Color32 value, int component) + { + value[component] = (byte)Mathf.Clamp(curve, 0, 255); + } + } + } +} diff --git a/Source/Editor/GUI/CurveEditor.Base.cs b/Source/Editor/GUI/CurveEditor.Base.cs new file mode 100644 index 000000000..8621d443b --- /dev/null +++ b/Source/Editor/GUI/CurveEditor.Base.cs @@ -0,0 +1,255 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +using System; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.GUI +{ + /// + /// The base class for editors. Allows to use generic curve editor without type information at compile-time. + /// + [HideInEditor] + public abstract class CurveEditorBase : ContainerControl + { + /// + /// The UI use mode flags. + /// + [Flags] + public enum UseMode + { + /// + /// Disable usage. + /// + Off = 0, + + /// + /// Allow only vertical usage. + /// + Vertical = 1, + + /// + /// Allow only horizontal usage. + /// + Horizontal = 2, + + /// + /// Allow both vertical and horizontal usage. + /// + On = Vertical | Horizontal, + } + + /// + /// Occurs when curve gets edited. + /// + public event Action Edited; + + /// + /// Occurs when curve data editing starts (via UI). + /// + public event Action EditingStart; + + /// + /// Occurs when curve data editing ends (via UI). + /// + public event Action EditingEnd; + + /// + /// The maximum amount of keyframes to use in a single curve. + /// + public int MaxKeyframes = ushort.MaxValue; + + /// + /// True if enable view zooming. Otherwise user won't be able to zoom in or out. + /// + public UseMode EnableZoom = UseMode.On; + + /// + /// True if enable view panning. Otherwise user won't be able to move the view area. + /// + public UseMode EnablePanning = UseMode.On; + + /// + /// Gets or sets the scroll bars usage. + /// + public abstract ScrollBars ScrollBars { get; set; } + + /// + /// Enables drawing start/end values continuous lines. + /// + public bool ShowStartEndLines; + + /// + /// Enables drawing background. + /// + public bool ShowBackground = true; + + /// + /// Enables drawing time and values axes (lines and labels). + /// + public bool ShowAxes = true; + + /// + /// Gets the type of the curves keyframes value. + /// + public abstract Type ValueType { get; } + + /// + /// The amount of frames per second of the curve animation (optional). Can be used to restrict the keyframes time values to the given time quantization rate. + /// + public abstract float? FPS { get; set; } + + /// + /// Gets or sets a value indicating whether show curve collapsed as a list of keyframe points rather than a full curve. + /// + public abstract bool ShowCollapsed { get; set; } + + /// + /// Gets or sets the view offset (via scroll bars). + /// + public abstract Vector2 ViewOffset { get; set; } + + /// + /// Gets or sets the view scale. + /// + public abstract Vector2 ViewScale { get; set; } + + /// + /// Gets the amount of keyframes added to the curve. + /// + public abstract int KeyframesCount { get; } + + /// + /// Called when curve gets edited. + /// + public void OnEdited() + { + Edited?.Invoke(); + } + + /// + /// Called when curve data editing starts (via UI). + /// + public void OnEditingStart() + { + EditingStart?.Invoke(); + } + + /// + /// Called when curve data editing ends (via UI). + /// + public void OnEditingEnd() + { + EditingEnd?.Invoke(); + } + + /// + /// Updates the keyframes positioning. + /// + public abstract void UpdateKeyframes(); + + /// + /// Updates the tangents positioning. + /// + public abstract void UpdateTangents(); + + /// + /// Shows the whole curve. + /// + public abstract void ShowWholeCurve(); + + /// + /// Resets the view. + /// + public void ResetView() + { + ViewScale = ApplyUseModeMask(EnableZoom, Vector2.One, ViewScale); + ViewOffset = ApplyUseModeMask(EnablePanning, Vector2.Zero, ViewOffset); + UpdateKeyframes(); + } + + /// + /// Evaluates the animation curve value at the specified time. + /// + /// The interpolated value from the curve at provided time. + /// The time to evaluate the curve at. + /// If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped. + public abstract void Evaluate(out object result, float time, bool loop = false); + + /// + /// Gets the keyframes collection (as boxed objects). + /// + /// The array of boxed keyframe values of type or . + public abstract object[] GetKeyframes(); + + /// + /// Sets the keyframes collection (as boxed objects). + /// + /// The array of boxed keyframe values of type or . + public abstract void SetKeyframes(object[] keyframes); + + /// + /// Adds the new keyframe (as boxed object). + /// + /// The keyframe time. + /// The keyframe value. + public abstract void AddKeyframe(float time, object value); + + /// + /// Gets the keyframe data (as boxed objects). + /// + /// The keyframe index. + /// The keyframe time. + /// The keyframe value (boxed). + /// The keyframe 'In' tangent value (boxed). + /// The keyframe 'Out' tangent value (boxed). + public abstract void GetKeyframe(int index, out float time, out object value, out object tangentIn, out object tangentOut); + + /// + /// Gets the existing keyframe value (as boxed object). + /// + /// The keyframe index. + /// The keyframe value. + public abstract object GetKeyframe(int index); + + /// + /// Sets the existing keyframe value (as boxed object). + /// + /// The keyframe index. + /// The keyframe value. + public abstract void SetKeyframeValue(int index, object value); + + /// + /// Gets the keyframe point (in keyframes space). + /// + /// The keyframe index. + /// The keyframe value component index. + /// The point in time/value space. + public abstract Vector2 GetKeyframePoint(int index, int component); + + /// + /// Converts the into the mask. + /// + /// The mode. + /// The mask. + protected static Vector2 GetUseModeMask(UseMode mode) + { + return new Vector2((mode & UseMode.Horizontal) == UseMode.Horizontal ? 1.0f : 0.0f, (mode & UseMode.Vertical) == UseMode.Vertical ? 1.0f : 0.0f); + } + + /// + /// Filters the given value using the . + /// + /// The mode. + /// The value to process. + /// The default value. + /// The combined value. + protected static Vector2 ApplyUseModeMask(UseMode mode, Vector2 value, Vector2 defaultValue) + { + return new Vector2( + (mode & UseMode.Horizontal) == UseMode.Horizontal ? value.X : defaultValue.X, + (mode & UseMode.Vertical) == UseMode.Vertical ? value.Y : defaultValue.Y + ); + } + } +} diff --git a/Source/Editor/GUI/CurveEditor.Contents.cs b/Source/Editor/GUI/CurveEditor.Contents.cs new file mode 100644 index 000000000..2058a0aa7 --- /dev/null +++ b/Source/Editor/GUI/CurveEditor.Contents.cs @@ -0,0 +1,448 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.GUI +{ + partial class CurveEditor + { + /// + /// The curve contents container control. + /// + /// + protected class ContentsBase : ContainerControl + { + private readonly CurveEditor _editor; + internal bool _leftMouseDown; + private bool _rightMouseDown; + internal Vector2 _leftMouseDownPos = Vector2.Minimum; + private Vector2 _rightMouseDownPos = Vector2.Minimum; + private Vector2 _movingViewLastPos; + internal Vector2 _mousePos = Vector2.Minimum; + internal bool _isMovingSelection; + internal bool _isMovingTangent; + internal bool _movedKeyframes; + private TangentPoint _movingTangent; + private Vector2 _movingSelectionStart; + private Vector2[] _movingSelectionOffsets; + private Vector2 _cmShowPos; + + /// + /// Initializes a new instance of the class. + /// + /// The curve editor. + public ContentsBase(CurveEditor editor) + { + _editor = editor; + } + + private void UpdateSelectionRectangle() + { + var selectionRect = Rectangle.FromPoints(_leftMouseDownPos, _mousePos); + + // 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); + } + } + + _editor.UpdateTangents(); + } + + /// + public override bool IntersectsContent(ref Vector2 locationParent, out Vector2 location) + { + // Pass all events + location = PointFromParent(ref locationParent); + return true; + } + + /// + public override void OnMouseEnter(Vector2 location) + { + _mousePos = location; + + base.OnMouseEnter(location); + } + + /// + public override void OnMouseMove(Vector2 location) + { + _mousePos = location; + + // Moving view + if (_rightMouseDown) + { + Vector2 delta = location - _movingViewLastPos; + delta *= GetUseModeMask(_editor.EnablePanning) * _editor.ViewScale; + if (delta.LengthSquared > 0.01f) + { + _editor._mainPanel.ViewOffset += delta; + _movingViewLastPos = location; + Cursor = CursorType.SizeAll; + } + + return; + } + // Moving selection + else if (_isMovingSelection) + { + var viewRect = _editor._mainPanel.GetClientArea(); + var locationKeyframes = PointToKeyframes(location, ref viewRect); + var accessor = _editor.Accessor; + var components = accessor.GetCurveComponents(); + for (var i = 0; i < _editor._points.Count; i++) + { + var p = _editor._points[i]; + if (p.IsSelected) + { + var k = _editor.GetKeyframe(p.Index); + float time = _editor.GetKeyframeTime(k); + float value = _editor.GetKeyframeValue(k, p.Component); + + float minTime = p.Index != 0 ? _editor.GetKeyframeTime(_editor.GetKeyframe(p.Index - 1)) + Mathf.Epsilon : float.MinValue; + float maxTime = p.Index != _editor.KeyframesCount - 1 ? _editor.GetKeyframeTime(_editor.GetKeyframe(p.Index + 1)) - Mathf.Epsilon : float.MaxValue; + + var offset = _movingSelectionOffsets[i]; + + if (!_editor.ShowCollapsed) + { + // Move on value axis + value = locationKeyframes.Y + offset.Y; + } + + // Let the first selected point of this keyframe to edit time + bool isFirstSelected = false; + for (var j = 0; j < components; j++) + { + var idx = p.Index * components + j; + if (idx == i) + { + isFirstSelected = true; + break; + } + if (_editor._points[idx].IsSelected) + break; + } + if (isFirstSelected) + { + time = locationKeyframes.X + offset.X; + + if (_editor.FPS.HasValue) + { + float fps = _editor.FPS.Value; + time = Mathf.Floor(time * fps) / fps; + } + time = Mathf.Clamp(time, minTime, maxTime); + } + + // TODO: snapping keyframes to grid when moving + + _editor.SetKeyframeInternal(p.Index, time, value, p.Component); + } + _editor.UpdateKeyframes(); + _editor.UpdateTooltips(); + if (_editor.EnablePanning == UseMode.On) + { + //_editor._mainPanel.ScrollViewTo(PointToParent(_editor._mainPanel, location)); + } + Cursor = CursorType.SizeAll; + _movedKeyframes = true; + } + return; + } + // Moving tangent + else if (_isMovingTangent) + { + var viewRect = _editor._mainPanel.GetClientArea(); + var direction = _movingTangent.IsIn ? -1.0f : 1.0f; + var k = _editor.GetKeyframe(_movingTangent.Index); + var kv = _editor.GetKeyframeValue(k); + var value = _editor.Accessor.GetCurveValue(ref kv, _movingTangent.Component); + _movingTangent.TangentValue = direction * (PointToKeyframes(location, ref viewRect).Y - value); + _editor.UpdateTangents(); + Cursor = CursorType.SizeNS; + _movedKeyframes = true; + return; + } + // Selecting + else if (_leftMouseDown) + { + UpdateSelectionRectangle(); + return; + } + + base.OnMouseMove(location); + } + + /// + public override void OnLostFocus() + { + // Clear flags and state + if (_leftMouseDown) + { + _leftMouseDown = false; + } + if (_rightMouseDown) + { + _rightMouseDown = false; + Cursor = CursorType.Default; + } + _isMovingSelection = false; + _isMovingTangent = false; + + base.OnLostFocus(); + } + + /// + public override bool OnMouseDown(Vector2 location, MouseButton button) + { + if (base.OnMouseDown(location, button)) + { + // Clear flags + _isMovingSelection = false; + _isMovingTangent = false; + _rightMouseDown = false; + _leftMouseDown = false; + return true; + } + + // Cache data + _isMovingSelection = false; + _isMovingTangent = false; + _mousePos = location; + if (button == MouseButton.Left) + { + _leftMouseDown = true; + _leftMouseDownPos = location; + } + if (button == MouseButton.Right) + { + _rightMouseDown = true; + _rightMouseDownPos = location; + _movingViewLastPos = location; + } + + // Check if any node is under the mouse + var underMouse = GetChildAt(location); + if (underMouse is KeyframePoint keyframe) + { + if (_leftMouseDown) + { + // Check if user is pressing control + if (Root.GetKey(KeyboardKeys.Control)) + { + // Add to selection + keyframe.IsSelected = true; + _editor.UpdateTangents(); + } + // Check if node isn't selected + else if (!keyframe.IsSelected) + { + // Select node + _editor.ClearSelection(); + keyframe.IsSelected = true; + _editor.UpdateTangents(); + } + + // Start moving selected nodes + StartMouseCapture(); + _isMovingSelection = true; + _movedKeyframes = false; + var viewRect = _editor._mainPanel.GetClientArea(); + _movingSelectionStart = PointToKeyframes(location, ref viewRect); + if (_movingSelectionOffsets == null || _movingSelectionOffsets.Length != _editor._points.Count) + _movingSelectionOffsets = new Vector2[_editor._points.Count]; + for (int i = 0; i < _movingSelectionOffsets.Length; i++) + _movingSelectionOffsets[i] = _editor._points[i].Point - _movingSelectionStart; + _editor.OnEditingStart(); + Focus(); + Tooltip?.Hide(); + return true; + } + } + else if (underMouse is TangentPoint tangent && tangent.Visible) + { + if (_leftMouseDown) + { + // Start moving tangent + StartMouseCapture(); + _isMovingTangent = true; + _movedKeyframes = false; + _movingTangent = tangent; + _editor.OnEditingStart(); + Focus(); + Tooltip?.Hide(); + return true; + } + } + else + { + if (_leftMouseDown) + { + // Start selecting + StartMouseCapture(); + _editor.ClearSelection(); + _editor.UpdateTangents(); + Focus(); + return true; + } + if (_rightMouseDown) + { + // Start navigating + StartMouseCapture(); + Focus(); + return true; + } + } + + Focus(); + return true; + } + + /// + public override bool OnMouseUp(Vector2 location, MouseButton button) + { + _mousePos = location; + + if (_leftMouseDown && button == MouseButton.Left) + { + _leftMouseDown = false; + EndMouseCapture(); + Cursor = CursorType.Default; + + // Editing tangent + if (_isMovingTangent) + { + if (_movedKeyframes) + { + _editor.OnEdited(); + _editor.OnEditingEnd(); + _editor.UpdateKeyframes(); + } + } + // Moving keyframes + else if (_isMovingSelection) + { + if (_movedKeyframes) + { + _editor.OnEdited(); + _editor.OnEditingEnd(); + } + } + // Selecting + else + { + UpdateSelectionRectangle(); + } + + _isMovingSelection = false; + _isMovingTangent = false; + _movedKeyframes = false; + } + if (_rightMouseDown && button == MouseButton.Right) + { + _rightMouseDown = false; + EndMouseCapture(); + Cursor = CursorType.Default; + + // Check if no move has been made at all + if (Vector2.Distance(ref location, ref _rightMouseDownPos) < 3.0f) + { + var selectionCount = _editor.SelectionCount; + var underMouse = GetChildAt(location); + if (selectionCount == 0 && underMouse is KeyframePoint point) + { + // Select node + selectionCount = 1; + point.IsSelected = true; + _editor.UpdateTangents(); + } + + var viewRect = _editor._mainPanel.GetClientArea(); + _cmShowPos = PointToKeyframes(location, ref viewRect); + + var cm = new ContextMenu.ContextMenu(); + cm.AddButton("Add keyframe", () => _editor.AddKeyframe(_cmShowPos)).Enabled = _editor.KeyframesCount < _editor.MaxKeyframes; + if (selectionCount == 0) + { + } + else if (selectionCount == 1) + { + cm.AddButton("Edit keyframe", () => _editor.EditKeyframes(this, location)); + cm.AddButton("Remove keyframe", _editor.RemoveKeyframes); + } + else + { + cm.AddButton("Edit keyframes", () => _editor.EditKeyframes(this, location)); + cm.AddButton("Remove keyframes", _editor.RemoveKeyframes); + } + cm.AddButton("Edit all keyframes", () => _editor.EditAllKeyframes(this, location)); + if (_editor.EnableZoom != UseMode.Off || _editor.EnablePanning != UseMode.Off) + { + cm.AddSeparator(); + cm.AddButton("Show whole curve", _editor.ShowWholeCurve); + cm.AddButton("Reset view", _editor.ResetView); + } + _editor.OnShowContextMenu(cm, selectionCount); + cm.Show(this, location); + } + } + + if (base.OnMouseUp(location, button)) + { + // Clear flags + _rightMouseDown = false; + _leftMouseDown = false; + return true; + } + + return true; + } + + /// + public override bool OnMouseWheel(Vector2 location, float delta) + { + if (base.OnMouseWheel(location, delta)) + return true; + + // Zoom in/out + if (_editor.EnableZoom != UseMode.Off && IsMouseOver && !_leftMouseDown && RootWindow.GetKey(KeyboardKeys.Control)) + { + // TODO: preserve the view center point for easier zooming + _editor.ViewScale += GetUseModeMask(_editor.EnableZoom) * (delta * 0.1f); + return true; + } + + return false; + } + + /// + protected override void SetScaleInternal(ref Vector2 scale) + { + base.SetScaleInternal(ref scale); + + _editor.UpdateKeyframes(); + } + + /// + /// Converts the input point from curve editor contents control space into the keyframes time/value coordinates. + /// + /// The point. + /// The curve contents area bounds. + /// The result. + private Vector2 PointToKeyframes(Vector2 point, ref Rectangle curveContentAreaBounds) + { + // Contents -> Keyframes + return new Vector2( + (point.X + Location.X) / UnitsPerSecond, + (point.Y + Location.Y - curveContentAreaBounds.Height) / -UnitsPerSecond + ); + } + } + } +} diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index 57f40d583..d22721592 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -9,491 +9,15 @@ using FlaxEditor.GUI.ContextMenu; using FlaxEngine; using FlaxEngine.GUI; -// ReSharper disable RedundantAssignment - namespace FlaxEditor.GUI { - /// - /// The base class for editors. Allows to use generic curve editor without type information at compile-time. - /// - [HideInEditor] - public abstract class CurveEditorBase : ContainerControl - { - /// - /// The UI use mode flags. - /// - [Flags] - public enum UseMode - { - /// - /// Disable usage. - /// - Off = 0, - - /// - /// Allow only vertical usage. - /// - Vertical = 1, - - /// - /// Allow only horizontal usage. - /// - Horizontal = 2, - - /// - /// Allow both vertical and horizontal usage. - /// - On = Vertical | Horizontal, - } - - /// - /// Occurs when curve gets edited. - /// - public event Action Edited; - - /// - /// Occurs when curve data editing starts (via UI). - /// - public event Action EditingStart; - - /// - /// Occurs when curve data editing ends (via UI). - /// - public event Action EditingEnd; - - /// - /// The maximum amount of keyframes to use in a single curve. - /// - public int MaxKeyframes = ushort.MaxValue; - - /// - /// True if enable view zooming. Otherwise user won't be able to zoom in or out. - /// - public UseMode EnableZoom = UseMode.On; - - /// - /// True if enable view panning. Otherwise user won't be able to move the view area. - /// - public UseMode EnablePanning = UseMode.On; - - /// - /// Gets or sets the scroll bars usage. - /// - public abstract ScrollBars ScrollBars { get; set; } - - /// - /// Enables drawing start/end values continuous lines. - /// - public bool ShowStartEndLines; - - /// - /// Enables drawing background. - /// - public bool ShowBackground = true; - - /// - /// Enables drawing time and values axes (lines and labels). - /// - public bool ShowAxes = true; - - /// - /// Gets the type of the curves keyframes value. - /// - public abstract Type ValueType { get; } - - /// - /// The amount of frames per second of the curve animation (optional). Can be used to restrict the keyframes time values to the given time quantization rate. - /// - public abstract float? FPS { get; set; } - - /// - /// Gets or sets a value indicating whether show curve collapsed as a list of keyframe points rather than a full curve. - /// - public abstract bool ShowCollapsed { get; set; } - - /// - /// Gets or sets the view offset (via scroll bars). - /// - public abstract Vector2 ViewOffset { get; set; } - - /// - /// Gets or sets the view scale. - /// - public abstract Vector2 ViewScale { get; set; } - - /// - /// Gets the amount of keyframes added to the curve. - /// - public abstract int KeyframesCount { get; } - - /// - /// Called when curve gets edited. - /// - public void OnEdited() - { - Edited?.Invoke(); - } - - /// - /// Called when curve data editing starts (via UI). - /// - public void OnEditingStart() - { - EditingStart?.Invoke(); - } - - /// - /// Called when curve data editing ends (via UI). - /// - public void OnEditingEnd() - { - EditingEnd?.Invoke(); - } - - /// - /// Updates the keyframes positioning. - /// - public abstract void UpdateKeyframes(); - - /// - /// Updates the tangents positioning. - /// - public abstract void UpdateTangents(); - - /// - /// Evaluates the animation curve value at the specified time. - /// - /// The interpolated value from the curve at provided time. - /// The time to evaluate the curve at. - /// If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped. - public abstract void Evaluate(out object result, float time, bool loop = false); - - /// - /// Gets the keyframes collection (as boxed objects). - /// - /// The array of boxed keyframe values of type or . - public abstract object[] GetKeyframes(); - - /// - /// Sets the keyframes collection (as boxed objects). - /// - /// The array of boxed keyframe values of type or . - public abstract void SetKeyframes(object[] keyframes); - - /// - /// Adds the new keyframe (as boxed object). - /// - /// The keyframe time. - /// The keyframe value. - public abstract void AddKeyframe(float time, object value); - - /// - /// Gets the keyframe data (as boxed objects). - /// - /// The keyframe index. - /// The keyframe time. - /// The keyframe value (boxed). - /// The keyframe 'In' tangent value (boxed). - /// The keyframe 'Out' tangent value (boxed). - public abstract void GetKeyframe(int index, out float time, out object value, out object tangentIn, out object tangentOut); - - /// - /// Gets the existing keyframe value (as boxed object). - /// - /// The keyframe index. - /// The keyframe value. - public abstract object GetKeyframe(int index); - - /// - /// Sets the existing keyframe value (as boxed object). - /// - /// The keyframe index. - /// The keyframe value. - public abstract void SetKeyframeValue(int index, object value); - - /// - /// Converts the into the mask. - /// - /// The mode. - /// The mask. - protected static Vector2 GetUseModeMask(UseMode mode) - { - return new Vector2((mode & UseMode.Horizontal) == UseMode.Horizontal ? 1.0f : 0.0f, (mode & UseMode.Vertical) == UseMode.Vertical ? 1.0f : 0.0f); - } - - /// - /// Filters the given value using the . - /// - /// The mode. - /// The value to process. - /// The default value. - /// The combined value. - protected static Vector2 ApplyUseModeMask(UseMode mode, Vector2 value, Vector2 defaultValue) - { - return new Vector2( - (mode & UseMode.Horizontal) == UseMode.Horizontal ? value.X : defaultValue.X, - (mode & UseMode.Vertical) == UseMode.Vertical ? value.Y : defaultValue.Y - ); - } - } - /// /// The generic curve editor control. /// /// The keyframe value type. /// - public abstract class CurveEditor : CurveEditorBase where T : new() + public abstract partial class CurveEditor : CurveEditorBase where T : new() { - /// - /// The generic keyframe value accessor object for curve editor. - /// - /// The keyframe value type. - public interface IKeyframeAccess where U : new() - { - /// - /// Gets the default value. - /// - /// The value. - void GetDefaultValue(out U value); - - /// - /// Gets the curve components count. Vector types should return amount of component to use for value editing. - /// - /// The components count. - int GetCurveComponents(); - - /// - /// Gets the value of the component for the curve. - /// - /// The keyframe value. - /// The component index. - /// The curve value. - float GetCurveValue(ref U value, int component); - - /// - /// Sets the curve value of the component. - /// - /// The curve value to assign. - /// The keyframe value. - /// The component index. - void SetCurveValue(float curve, ref U value, int component); - } - - private class KeyframeAccess : - IKeyframeAccess, - IKeyframeAccess, - IKeyframeAccess, - IKeyframeAccess, - IKeyframeAccess, - IKeyframeAccess, - IKeyframeAccess, - IKeyframeAccess, - IKeyframeAccess, - IKeyframeAccess - { - void IKeyframeAccess.GetDefaultValue(out bool value) - { - value = false; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 1; - } - - float IKeyframeAccess.GetCurveValue(ref bool value, int component) - { - return value ? 1 : 0; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref bool value, int component) - { - value = curve >= 0.5f; - } - - void IKeyframeAccess.GetDefaultValue(out int value) - { - value = 0; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 1; - } - - float IKeyframeAccess.GetCurveValue(ref int value, int component) - { - return value; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref int value, int component) - { - value = (int)curve; - } - - void IKeyframeAccess.GetDefaultValue(out double value) - { - value = 0.0; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 1; - } - - float IKeyframeAccess.GetCurveValue(ref double value, int component) - { - return (float)value; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref double value, int component) - { - value = curve; - } - - void IKeyframeAccess.GetDefaultValue(out float value) - { - value = 0.0f; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 1; - } - - float IKeyframeAccess.GetCurveValue(ref float value, int component) - { - return value; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref float value, int component) - { - value = curve; - } - - void IKeyframeAccess.GetDefaultValue(out Vector2 value) - { - value = Vector2.Zero; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 2; - } - - float IKeyframeAccess.GetCurveValue(ref Vector2 value, int component) - { - return value[component]; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref Vector2 value, int component) - { - value[component] = curve; - } - - void IKeyframeAccess.GetDefaultValue(out Vector3 value) - { - value = Vector3.Zero; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 3; - } - - float IKeyframeAccess.GetCurveValue(ref Vector3 value, int component) - { - return value[component]; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref Vector3 value, int component) - { - value[component] = curve; - } - - void IKeyframeAccess.GetDefaultValue(out Vector4 value) - { - value = Vector4.Zero; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 4; - } - - float IKeyframeAccess.GetCurveValue(ref Vector4 value, int component) - { - return value[component]; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref Vector4 value, int component) - { - value[component] = curve; - } - - public void GetDefaultValue(out Quaternion value) - { - value = Quaternion.Identity; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 3; - } - - float IKeyframeAccess.GetCurveValue(ref Quaternion value, int component) - { - return value.EulerAngles[component]; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref Quaternion value, int component) - { - var euler = value.EulerAngles; - euler[component] = curve; - Quaternion.Euler(euler.X, euler.Y, euler.Z, out value); - } - - void IKeyframeAccess.GetDefaultValue(out Color value) - { - value = Color.Black; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 4; - } - - float IKeyframeAccess.GetCurveValue(ref Color value, int component) - { - return value[component]; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref Color value, int component) - { - value[component] = curve; - } - - void IKeyframeAccess.GetDefaultValue(out Color32 value) - { - value = Color32.Black; - } - - int IKeyframeAccess.GetCurveComponents() - { - return 4; - } - - float IKeyframeAccess.GetCurveValue(ref Color32 value, int component) - { - return value[component]; - } - - void IKeyframeAccess.SetCurveValue(float curve, ref Color32 value, int component) - { - value[component] = (byte)Mathf.Clamp(curve, 0, 255); - } - } - private class Popup : ContextMenuBase { private CustomEditorPresenter _presenter; @@ -600,454 +124,6 @@ namespace FlaxEditor.GUI } } - /// - /// The curve contents container control. - /// - /// - protected class ContentsBase : ContainerControl - { - private readonly CurveEditor _editor; - internal bool _leftMouseDown; - private bool _rightMouseDown; - internal Vector2 _leftMouseDownPos = Vector2.Minimum; - private Vector2 _rightMouseDownPos = Vector2.Minimum; - internal Vector2 _mousePos = Vector2.Minimum; - private float _mouseMoveAmount; - internal bool _isMovingSelection; - internal bool _isMovingTangent; - private TangentPoint _movingTangent; - private Vector2 _movingSelectionViewPos; - private Vector2 _cmShowPos; - - /// - /// Initializes a new instance of the class. - /// - /// The curve editor. - public ContentsBase(CurveEditor editor) - { - _editor = editor; - } - - private void UpdateSelectionRectangle() - { - var selectionRect = Rectangle.FromPoints(_leftMouseDownPos, _mousePos); - - // 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); - } - } - - _editor.UpdateTangents(); - } - - /// - public override bool IntersectsContent(ref Vector2 locationParent, out Vector2 location) - { - // Pass all events - location = PointFromParent(ref locationParent); - return true; - } - - /// - public override void OnMouseEnter(Vector2 location) - { - _mousePos = location; - - base.OnMouseEnter(location); - } - - /// - public override void OnMouseMove(Vector2 location) - { - _mousePos = location; - - // Moving view - if (_rightMouseDown) - { - // Calculate delta - Vector2 delta = location - _rightMouseDownPos; - delta *= GetUseModeMask(_editor.EnablePanning); - if (delta.LengthSquared > 0.01f) - { - // Move view - _mouseMoveAmount += delta.Length; - _editor.ViewOffset += delta * _editor.ViewScale; - _rightMouseDownPos = location; - Cursor = CursorType.SizeAll; - } - - return; - } - // Moving selection - else if (_isMovingSelection) - { - // Calculate delta (apply view offset) - Vector2 viewDelta = _editor.ViewOffset - _movingSelectionViewPos; - _movingSelectionViewPos = _editor.ViewOffset; - var viewRect = _editor._mainPanel.GetClientArea(); - var delta = location - _leftMouseDownPos - viewDelta; - _mouseMoveAmount += delta.Length; - if (delta.LengthSquared > 0.01f) - { - // Move selected keyframes - var keyframeDelta = PointToKeyframes(location, ref viewRect) - PointToKeyframes(_leftMouseDownPos - viewDelta, ref viewRect); - var accessor = _editor.Accessor; - var components = accessor.GetCurveComponents(); - for (var i = 0; i < _editor._points.Count; i++) - { - var p = _editor._points[i]; - if (p.IsSelected) - { - var k = _editor.GetKeyframe(p.Index); - float time = _editor.GetKeyframeTime(k); - float value = _editor.GetKeyframeValue(k, p.Component); - - float minTime = p.Index != 0 ? _editor.GetKeyframeTime(_editor.GetKeyframe(p.Index - 1)) + Mathf.Epsilon : float.MinValue; - float maxTime = p.Index != _editor.KeyframesCount - 1 ? _editor.GetKeyframeTime(_editor.GetKeyframe(p.Index + 1)) - Mathf.Epsilon : float.MaxValue; - - if (!_editor.ShowCollapsed) - { - // Move on value axis - value += keyframeDelta.Y; - } - - // Let the first selected point of this keyframe to edit time - bool isFirstSelected = false; - for (var j = 0; j < components; j++) - { - var idx = p.Index * components + j; - if (idx == i) - { - isFirstSelected = true; - break; - } - if (_editor._points[idx].IsSelected) - break; - } - if (isFirstSelected) - { - time += keyframeDelta.X; - - if (_editor.FPS.HasValue) - { - float fps = _editor.FPS.Value; - time = Mathf.Floor(time * fps) / fps; - } - - time = Mathf.Clamp(time, minTime, maxTime); - } - - // TODO: snapping keyframes to grid when moving - - _editor.SetKeyframeInternal(p.Index, time, value, p.Component); - } - } - _editor.UpdateKeyframes(); - _editor.UpdateTooltips(); - if (_editor.EnablePanning == UseMode.On) - { - _editor._mainPanel.ScrollViewTo(PointToParent(location)); - } - _leftMouseDownPos = location; - Cursor = CursorType.SizeAll; - } - - return; - } - // Moving tangent - else if (_isMovingTangent) - { - // Calculate delta (apply view offset) - Vector2 viewDelta = _editor.ViewOffset - _movingSelectionViewPos; - _movingSelectionViewPos = _editor.ViewOffset; - var viewRect = _editor._mainPanel.GetClientArea(); - var delta = location - _leftMouseDownPos - viewDelta; - _mouseMoveAmount += delta.Length; - if (delta.LengthSquared > 0.01f) - { - // Move selected tangent - var keyframeDelta = PointToKeyframes(location, ref viewRect) - PointToKeyframes(_leftMouseDownPos - viewDelta, ref viewRect); - var direction = _movingTangent.IsIn ? -1.0f : 1.0f; - _movingTangent.TangentValue += direction * keyframeDelta.Y; - _editor.UpdateTangents(); - _leftMouseDownPos = location; - Cursor = CursorType.SizeNS; - } - - return; - } - // Selecting - else if (_leftMouseDown) - { - UpdateSelectionRectangle(); - return; - } - - base.OnMouseMove(location); - } - - /// - public override void OnLostFocus() - { - // Clear flags and state - if (_leftMouseDown) - { - _leftMouseDown = false; - } - if (_rightMouseDown) - { - _rightMouseDown = false; - Cursor = CursorType.Default; - } - _isMovingSelection = false; - _isMovingTangent = false; - - base.OnLostFocus(); - } - - /// - public override bool OnMouseDown(Vector2 location, MouseButton button) - { - if (base.OnMouseDown(location, button)) - { - // Clear flags - _isMovingSelection = false; - _isMovingTangent = false; - _rightMouseDown = false; - _leftMouseDown = false; - return true; - } - - // Cache data - _isMovingSelection = false; - _isMovingTangent = false; - _mousePos = location; - if (button == MouseButton.Left) - { - _leftMouseDown = true; - _leftMouseDownPos = location; - } - if (button == MouseButton.Right) - { - _rightMouseDown = true; - _rightMouseDownPos = location; - } - - // Check if any node is under the mouse - var underMouse = GetChildAt(location); - if (underMouse is KeyframePoint keyframe) - { - if (_leftMouseDown) - { - // Check if user is pressing control - if (Root.GetKey(KeyboardKeys.Control)) - { - // Add to selection - keyframe.Select(); - _editor.UpdateTangents(); - } - // Check if node isn't selected - else if (!keyframe.IsSelected) - { - // Select node - _editor.ClearSelection(); - keyframe.Select(); - _editor.UpdateTangents(); - } - - // Start moving selected nodes - StartMouseCapture(); - _mouseMoveAmount = 0; - _isMovingSelection = true; - _movingSelectionViewPos = _editor.ViewOffset; - _editor.OnEditingStart(); - Focus(); - return true; - } - } - else if (underMouse is TangentPoint tangent && tangent.Visible) - { - if (_leftMouseDown) - { - // Start moving tangent - StartMouseCapture(); - _mouseMoveAmount = 0; - _isMovingTangent = true; - _movingTangent = tangent; - _movingSelectionViewPos = _editor.ViewOffset; - _editor.OnEditingStart(); - Focus(); - return true; - } - } - else - { - if (_leftMouseDown) - { - // Start selecting - StartMouseCapture(); - _editor.ClearSelection(); - _editor.UpdateTangents(); - Focus(); - return true; - } - if (_rightMouseDown) - { - // Start navigating - StartMouseCapture(); - Focus(); - return true; - } - } - - Focus(); - return true; - } - - /// - public override bool OnMouseUp(Vector2 location, MouseButton button) - { - _mousePos = location; - - if (_leftMouseDown && button == MouseButton.Left) - { - _leftMouseDown = false; - EndMouseCapture(); - Cursor = CursorType.Default; - - // Editing tangent - if (_isMovingTangent) - { - if (_mouseMoveAmount > 3.0f) - { - _editor.OnEdited(); - _editor.OnEditingEnd(); - } - } - // Moving keyframes - else if (_isMovingSelection) - { - if (_mouseMoveAmount > 3.0f) - { - _editor.OnEdited(); - _editor.OnEditingEnd(); - } - } - // Selecting - else - { - UpdateSelectionRectangle(); - } - - _isMovingSelection = false; - _isMovingTangent = false; - } - if (_rightMouseDown && button == MouseButton.Right) - { - _rightMouseDown = false; - EndMouseCapture(); - Cursor = CursorType.Default; - - // Check if no move has been made at all - if (_mouseMoveAmount < 3.0f) - { - var selectionCount = _editor.SelectionCount; - var underMouse = GetChildAt(location); - if (selectionCount == 0 && underMouse is KeyframePoint point) - { - // Select node - selectionCount = 1; - point.Select(); - _editor.UpdateTangents(); - } - - var viewRect = _editor._mainPanel.GetClientArea(); - _cmShowPos = PointToKeyframes(location, ref viewRect); - - var cm = new ContextMenu.ContextMenu(); - cm.AddButton("Add keyframe", () => _editor.AddKeyframe(_cmShowPos)).Enabled = _editor.KeyframesCount < _editor.MaxKeyframes; - if (selectionCount == 0) - { - } - else if (selectionCount == 1) - { - cm.AddButton("Edit keyframe", () => _editor.EditKeyframes(this, location)); - cm.AddButton("Remove keyframe", _editor.RemoveKeyframes); - } - else - { - cm.AddButton("Edit keyframes", () => _editor.EditKeyframes(this, location)); - cm.AddButton("Remove keyframes", _editor.RemoveKeyframes); - } - cm.AddButton("Edit all keyframes", () => _editor.EditAllKeyframes(this, location)); - if (_editor.EnableZoom != UseMode.Off || _editor.EnablePanning != UseMode.Off) - { - cm.AddSeparator(); - cm.AddButton("Show whole curve", _editor.ShowWholeCurve); - cm.AddButton("Reset view", _editor.ResetView); - } - _editor.OnShowContextMenu(cm, selectionCount); - cm.Show(this, location); - } - _mouseMoveAmount = 0; - } - - if (base.OnMouseUp(location, button)) - { - // Clear flags - _rightMouseDown = false; - _leftMouseDown = false; - return true; - } - - return true; - } - - /// - public override bool OnMouseWheel(Vector2 location, float delta) - { - if (base.OnMouseWheel(location, delta)) - return true; - - // Zoom in/out - if (_editor.EnableZoom != UseMode.Off && IsMouseOver && !_leftMouseDown && RootWindow.GetKey(KeyboardKeys.Control)) - { - // TODO: preserve the view center point for easier zooming - _editor.ViewScale += GetUseModeMask(_editor.EnableZoom) * (delta * 0.1f); - return true; - } - - return false; - } - - /// - protected override void SetScaleInternal(ref Vector2 scale) - { - base.SetScaleInternal(ref scale); - - _editor.UpdateKeyframes(); - } - - /// - /// Converts the input point from curve editor contents control space into the keyframes time/value coordinates. - /// - /// The point. - /// The curve contents area bounds. - /// The result. - private Vector2 PointToKeyframes(Vector2 point, ref Rectangle curveContentAreaBounds) - { - // Contents -> Keyframes - return new Vector2( - (point.X + Location.X) / UnitsPerSecond, - (point.Y + Location.Y - curveContentAreaBounds.Height) / -UnitsPerSecond - ); - } - } - /// /// The single keyframe control. /// @@ -1073,6 +149,11 @@ namespace FlaxEditor.GUI /// public bool IsSelected; + /// + /// Gets the point time and value on a curve. + /// + public Vector2 Point => Editor.GetKeyframePoint(Index, Component); + /// public override void Draw() { @@ -1100,21 +181,8 @@ namespace FlaxEditor.GUI return false; } - /// - /// Adds this keyframe to the selection. - /// - public void Select() - { - IsSelected = true; - } - - /// - /// Removes this keyframe from the selection. - /// - public void Deselect() - { - IsSelected = false; - } + /// + protected override bool ShowTooltip => base.ShowTooltip && !Editor._contents._isMovingSelection; /// /// Updates the tooltip. @@ -1260,7 +328,11 @@ namespace FlaxEditor.GUI public override Vector2 ViewOffset { get => _mainPanel.ViewOffset; - set => _mainPanel.ViewOffset = value; + set + { + _mainPanel.ViewOffset = value; + _mainPanel.FastScroll(); + } } /// @@ -1524,7 +596,7 @@ namespace FlaxEditor.GUI var p = _points[i]; if (p.IsSelected) { - p.Deselect(); + p.IsSelected = false; indicesToRemove.Add(p.Index); } } @@ -1538,26 +610,14 @@ namespace FlaxEditor.GUI OnEditingEnd(); } - /// - /// Shows the whole curve. - /// - public void ShowWholeCurve() + /// + public override void ShowWholeCurve() { ViewScale = ApplyUseModeMask(EnableZoom, _mainPanel.Size / _contents.Size, ViewScale); ViewOffset = ApplyUseModeMask(EnablePanning, -_mainPanel.ControlsBounds.Location, ViewOffset); UpdateKeyframes(); } - /// - /// Resets the view. - /// - public void ResetView() - { - ViewScale = ApplyUseModeMask(EnableZoom, Vector2.One, ViewScale); - ViewOffset = ApplyUseModeMask(EnablePanning, Vector2.Zero, ViewOffset); - UpdateKeyframes(); - } - /// public override void Evaluate(out object result, float time, bool loop = false) { @@ -1581,7 +641,7 @@ namespace FlaxEditor.GUI { for (int i = 0; i < _points.Count; i++) { - _points[i].Deselect(); + _points[i].IsSelected = false; } } @@ -1589,7 +649,7 @@ namespace FlaxEditor.GUI { for (int i = 0; i < _points.Count; i++) { - _points[i].Select(); + _points[i].IsSelected = true; } } @@ -2154,6 +1214,13 @@ namespace FlaxEditor.GUI OnEdited(); } + /// + public override Vector2 GetKeyframePoint(int index, int component) + { + var k = _keyframes[index]; + return new Vector2(k.Time, Accessor.GetCurveValue(ref k.Value, component)); + } + /// public override int KeyframesCount => _keyframes.Count; @@ -2820,6 +1887,13 @@ namespace FlaxEditor.GUI OnEdited(); } + /// + public override Vector2 GetKeyframePoint(int index, int component) + { + var k = _keyframes[index]; + return new Vector2(k.Time, Accessor.GetCurveValue(ref k.Value, component)); + } + /// public override int KeyframesCount => _keyframes.Count; @@ -2838,35 +1912,32 @@ namespace FlaxEditor.GUI // Place keyframes Rectangle curveContentAreaBounds = _mainPanel.GetClientArea(); var viewScale = ViewScale; + var pointsSize = _showCollapsed ? new Vector2(4.0f / viewScale.X, Height - 2.0f) : KeyframesSize / viewScale; for (int i = 0; i < _points.Count; i++) { var p = _points[i]; var k = _keyframes[p.Index]; - - var location = GetKeyframePoint(ref k, p.Component); - var point = new Vector2 + var point = GetKeyframePoint(ref k, p.Component); + var location = new Vector2 ( - location.X * UnitsPerSecond - p.Width * 0.5f, - location.Y * -UnitsPerSecond - p.Height * 0.5f + curveContentAreaBounds.Height + point.X * UnitsPerSecond - pointsSize.X * 0.5f, + point.Y * -UnitsPerSecond - pointsSize.Y * 0.5f + curveContentAreaBounds.Height ); - if (_showCollapsed) { - point.Y = 1.0f; - p.Size = new Vector2(4.0f / viewScale.X, Height - 2.0f); + location.Y = 1.0f; p.Visible = p.Component == 0; } else { - p.Size = KeyframesSize / viewScale; p.Visible = true; } - p.Location = point; + p.Bounds = new Rectangle(location, pointsSize); } // Calculate bounds var bounds = _points[0].Bounds; - for (var i = 1; i < _points.Count; i++) + for (int i = 1; i < _points.Count; i++) { bounds = Rectangle.Union(bounds, _points[i].Bounds); } @@ -2878,7 +1949,8 @@ namespace FlaxEditor.GUI bounds.Height = Mathf.Max(bounds.Height, 1.0f); bounds.Location = ApplyUseModeMask(EnablePanning, bounds.Location, _contents.Location); bounds.Size = ApplyUseModeMask(EnablePanning, bounds.Size, _contents.Size); - _contents.Bounds = bounds; + if (!_contents._isMovingSelection) + _contents.Bounds = bounds; } else if (_contents.Bounds == Rectangle.Empty) { diff --git a/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs b/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs index 783480c09..919d76f6b 100644 --- a/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs +++ b/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs @@ -72,9 +72,11 @@ namespace FlaxEditor.GUI internal Vector2 _leftMouseDownPos = Vector2.Minimum; private Vector2 _rightMouseDownPos = Vector2.Minimum; internal Vector2 _mousePos = Vector2.Minimum; - private float _mouseMoveAmount; + private Vector2 _movingViewLastPos; internal bool _isMovingSelection; - private Vector2 _movingSelectionViewPos; + internal bool _movedKeyframes; + private float _movingSelectionStart; + private float[] _movingSelectionOffsets; private Vector2 _cmShowPos; /// @@ -125,13 +127,12 @@ namespace FlaxEditor.GUI if (_rightMouseDown) { // Calculate delta - Vector2 delta = location - _rightMouseDownPos; + Vector2 delta = location - _movingViewLastPos; if (delta.LengthSquared > 0.01f && _editor.EnablePanning) { // Move view - _mouseMoveAmount += delta.Length; _editor.ViewOffset += delta * _editor.ViewScale; - _rightMouseDownPos = location; + _movingViewLastPos = location; Cursor = CursorType.SizeAll; } @@ -140,44 +141,38 @@ namespace FlaxEditor.GUI // Moving selection else if (_isMovingSelection) { - // Calculate delta (apply view offset) - Vector2 viewDelta = _editor.ViewOffset - _movingSelectionViewPos; - _movingSelectionViewPos = _editor.ViewOffset; var viewRect = _editor._mainPanel.GetClientArea(); - var delta = location - _leftMouseDownPos - viewDelta; - _mouseMoveAmount += delta.Length; - if (delta.LengthSquared > 0.01f) + var locationKeyframes = PointToKeyframes(location, ref viewRect); + for (var i = 0; i < _editor._points.Count; i++) { - // Move selected keyframes - var keyframeDelta = PointToKeyframes(location, ref viewRect) - PointToKeyframes(_leftMouseDownPos - viewDelta, ref viewRect); - for (var i = 0; i < _editor._points.Count; i++) + var p = _editor._points[i]; + if (p.IsSelected) { - var p = _editor._points[i]; - if (p.IsSelected) + var k = _editor._keyframes[p.Index]; + + float minTime = p.Index != 0 ? _editor._keyframes[p.Index - 1].Time : float.MinValue; + float maxTime = p.Index != _editor._keyframes.Count - 1 ? _editor._keyframes[p.Index + 1].Time : float.MaxValue; + + var offset = _movingSelectionOffsets[p.Index]; + k.Time = locationKeyframes.X + offset; + if (_editor.FPS.HasValue) { - var k = _editor._keyframes[p.Index]; - - float minTime = p.Index != 0 ? _editor._keyframes[p.Index - 1].Time : float.MinValue; - float maxTime = p.Index != _editor._keyframes.Count - 1 ? _editor._keyframes[p.Index + 1].Time : float.MaxValue; - - k.Time += keyframeDelta.X; - if (_editor.FPS.HasValue) - { - float fps = _editor.FPS.Value; - k.Time = Mathf.Floor(k.Time * fps) / fps; - } - k.Time = Mathf.Clamp(k.Time, minTime, maxTime); - - // TODO: snapping keyframes to grid when moving - - _editor._keyframes[p.Index] = k; + float fps = _editor.FPS.Value; + k.Time = Mathf.Floor(k.Time * fps) / fps; } + k.Time = Mathf.Clamp(k.Time, minTime, maxTime); + + // TODO: snapping keyframes to grid when moving + + _editor._keyframes[p.Index] = k; } _editor.UpdateKeyframes(); if (_editor.EnablePanning) - _editor._mainPanel.ScrollViewTo(PointToParent(location)); - _leftMouseDownPos = location; + { + //_editor._mainPanel.ScrollViewTo(PointToParent(_editor._mainPanel, location)); + } Cursor = CursorType.SizeAll; + _movedKeyframes = true; } return; @@ -234,6 +229,7 @@ namespace FlaxEditor.GUI { _rightMouseDown = true; _rightMouseDownPos = location; + _movingViewLastPos = location; } // Check if any node is under the mouse @@ -258,11 +254,17 @@ namespace FlaxEditor.GUI // Start moving selected nodes StartMouseCapture(); - _mouseMoveAmount = 0; _isMovingSelection = true; - _movingSelectionViewPos = _editor.ViewOffset; + _movedKeyframes = false; + var viewRect = _editor._mainPanel.GetClientArea(); + _movingSelectionStart = PointToKeyframes(location, ref viewRect).X; + if (_movingSelectionOffsets == null || _movingSelectionOffsets.Length != _editor._keyframes.Count) + _movingSelectionOffsets = new float[_editor._keyframes.Count]; + for (int i = 0; i < _movingSelectionOffsets.Length; i++) + _movingSelectionOffsets[i] = _editor._keyframes[i].Time - _movingSelectionStart; _editor.OnEditingStart(); Focus(); + Tooltip?.Hide(); return true; } } @@ -303,10 +305,11 @@ namespace FlaxEditor.GUI // Moving keyframes if (_isMovingSelection) { - if (_mouseMoveAmount > 3.0f) + if (_movedKeyframes) { _editor.OnEdited(); _editor.OnEditingEnd(); + _editor.UpdateKeyframes(); } } // Selecting @@ -316,6 +319,7 @@ namespace FlaxEditor.GUI } _isMovingSelection = false; + _movedKeyframes = false; } if (_rightMouseDown && button == MouseButton.Right) { @@ -324,7 +328,7 @@ namespace FlaxEditor.GUI Cursor = CursorType.Default; // Check if no move has been made at all - if (_mouseMoveAmount < 3.0f) + if (Vector2.Distance(ref location, ref _rightMouseDownPos) < 3.0f) { var selectionCount = _editor.SelectionCount; var underMouse = GetChildAt(location); @@ -362,7 +366,6 @@ namespace FlaxEditor.GUI } cm.Show(this, location); } - _mouseMoveAmount = 0; } if (base.OnMouseUp(location, button)) @@ -1045,8 +1048,10 @@ namespace FlaxEditor.GUI } // Adjust contents bounds to fill the keyframes area - if (EnablePanning) + if (EnablePanning && !_contents._isMovingSelection) + { _contents.Bounds = bounds; + } // Offset the keyframes (parent container changed its location) var posOffset = _contents.Location; diff --git a/Source/Editor/GUI/Timeline/Track.cs b/Source/Editor/GUI/Timeline/Track.cs index 297371159..66528efab 100644 --- a/Source/Editor/GUI/Timeline/Track.cs +++ b/Source/Editor/GUI/Timeline/Track.cs @@ -1276,24 +1276,24 @@ namespace FlaxEditor.GUI.Timeline case KeyboardKeys.ArrowUp: { int index = IndexInParent; - if (index > 0) + while (index != 0) { - do - { - toSelect = Parent.GetChild(--index) as Track; - } while (index != -1 && toSelect != null && !toSelect.HasParentsExpanded); + toSelect = Parent.GetChild(--index) as Track; + if (toSelect != null && toSelect.HasParentsExpanded) + break; + toSelect = null; } break; } case KeyboardKeys.ArrowDown: { int index = IndexInParent; - if (index < Parent.ChildrenCount - 1) + while (index < Parent.ChildrenCount - 1) { - do - { - toSelect = Parent.GetChild(++index) as Track; - } while (index != Parent.ChildrenCount && toSelect != null && !toSelect.HasParentsExpanded); + toSelect = Parent.GetChild(++index) as Track; + if (toSelect != null && toSelect.HasParentsExpanded) + break; + toSelect = null; } break; } diff --git a/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs b/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs index 05f0c42ae..751b695f6 100644 --- a/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/CameraCutTrack.cs @@ -24,7 +24,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks } } - private Image[] _thumbnails = new Image[2]; + private Image[] _thumbnails = new Image[3]; /// /// Initializes a new instance of the class. @@ -107,6 +107,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks float orthoScale = 1.0f; float fov = 60.0f; float customAspectRatio = 0.0f; + view.RenderLayersMask = new LayersMask(uint.MaxValue); // Try to evaluate camera properties based on the initial camera state if (cam) @@ -119,10 +120,15 @@ namespace FlaxEditor.GUI.Timeline.Tracks orthoScale = cam.OrthographicScale; fov = cam.FieldOfView; customAspectRatio = cam.CustomAspectRatio; + view.RenderLayersMask = cam.RenderLayersMask; } // Try to evaluate camera properties based on the animated tracks - float time = req.ThumbnailIndex == 0 ? Start : Start + Duration; + float time = Start; + if (req.ThumbnailIndex == 1) + time += Duration; + else if (req.ThumbnailIndex == 2) + time += Duration * 0.5f; foreach (var subTrack in track.SubTracks) { if (subTrack is MemberTrack memberTrack) @@ -176,7 +182,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks view.NonJitteredProjection = view.Projection; view.TemporalAAJitter = Vector4.Zero; view.ModelLODDistanceFactor = 100.0f; - view.Flags = ViewFlags.DefaultGame & ~(ViewFlags.MotionBlur | ViewFlags.EyeAdaptation); + view.Flags = ViewFlags.DefaultGame & ~(ViewFlags.MotionBlur); view.UpdateCachedData(); task.View = view; } @@ -202,7 +208,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks Bounds = new Rectangle(2, 2, CameraCutThumbnailRenderer.Width, CameraCutThumbnailRenderer.Height), }; } - else + else if (req.ThumbnailIndex == 1) { image = new Image { @@ -211,6 +217,15 @@ namespace FlaxEditor.GUI.Timeline.Tracks Bounds = new Rectangle(Width - 2 - CameraCutThumbnailRenderer.Width, 2, CameraCutThumbnailRenderer.Width, CameraCutThumbnailRenderer.Height), }; } + else + { + image = new Image + { + AnchorPreset = AnchorPresets.MiddleCenter, + Parent = this, + Bounds = new Rectangle(Width * 0.5f - 1 - CameraCutThumbnailRenderer.Width * 0.5f, 2, CameraCutThumbnailRenderer.Width, CameraCutThumbnailRenderer.Height), + }; + } image.UnlockChildrenRecursive(); _thumbnails[req.ThumbnailIndex] = image; UpdateUI(); @@ -226,14 +241,47 @@ namespace FlaxEditor.GUI.Timeline.Tracks private void UpdateUI() { - var width = Mathf.Min(CameraCutThumbnailRenderer.Width, (Width - 6.0f) * 0.5f); - for (int i = 0; i < _thumbnails.Length; i++) + if (_thumbnails == null) + return; + var width = Width - (_thumbnails.Length + 1) * 2; + if (width < 10.0f) + { + for (int i = 0; i < _thumbnails.Length; i++) + { + var image = _thumbnails[i]; + if (image != null) + image.Visible = false; + } + return; + } + var count = Mathf.Min(Mathf.FloorToInt(width / CameraCutThumbnailRenderer.Width), _thumbnails.Length); + if (count == 0 && _thumbnails.Length != 0) + { + var image = _thumbnails[0]; + if (image != null) + { + image.Width = Mathf.Min(CameraCutThumbnailRenderer.Width, width); + image.SetAnchorPreset(image.AnchorPreset, false); + image.Visible = true; + } + return; + } + for (int i = 0; i < count; i++) { var image = _thumbnails[i]; if (image != null) { - image.Width = width; - image.Visible = width >= 10.0f; + image.Width = CameraCutThumbnailRenderer.Width; + image.SetAnchorPreset(image.AnchorPreset, false); + image.Visible = true; + } + } + for (int i = count; i < _thumbnails.Length; i++) + { + var image = _thumbnails[i]; + if (image != null) + { + image.Visible = false; } } } @@ -260,7 +308,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks { base.OnDurationFramesChanged(); - UpdateThumbnails(new[] { 1 }); + UpdateThumbnails(new[] { 1, 2 }); } /// diff --git a/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs b/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs index 8e01d2706..080cdca67 100644 --- a/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs @@ -259,6 +259,8 @@ namespace FlaxEditor.GUI.Timeline.Tracks { Height = IsExpanded ? ExpandedHeight : CollapsedHeight; UpdateCurve(); + if (IsExpanded) + Curve.ShowWholeCurve(); base.OnExpandedChanged(); } diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Selection.cs b/Source/Editor/Windows/Assets/PrefabWindow.Selection.cs index 5b501d8cc..d1b088fe0 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Selection.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Selection.cs @@ -97,6 +97,7 @@ namespace FlaxEditor.Windows.Assets // For single node selected scroll view so user can see it if (nodes.Count == 1) { + nodes[0].ExpandAllParents(true); ScrollViewTo(nodes[0]); } } diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index abe67190e..6deb6d778 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -282,10 +282,10 @@ namespace FlaxEditor.Windows /// public override void OnInit() { - Editor.SceneEditing.SelectionChanged += OnOnSelectionChanged; + Editor.SceneEditing.SelectionChanged += OnSelectionChanged; } - private void OnOnSelectionChanged() + private void OnSelectionChanged() { _isUpdatingSelection = true; @@ -312,6 +312,7 @@ namespace FlaxEditor.Windows // For single node selected scroll view so user can see it if (nodes.Count == 1) { + nodes[0].ExpandAllParents(true); ScrollViewTo(nodes[0]); } } diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index 8963e9abf..98e2c241c 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -430,7 +430,7 @@ Asset* Content::LoadAsync(const StringView& path, const ScriptingTypeHandle& typ { // Ensure path is in a valid format String pathNorm(path); - FileSystem::NormalizePath(pathNorm); + StringUtils::PathRemoveRelativeParts(pathNorm); #if USE_EDITOR if (!FileSystem::FileExists(pathNorm)) diff --git a/Source/Engine/UI/GUI/Panels/Panel.cs b/Source/Engine/UI/GUI/Panels/Panel.cs index b2a011040..fb503affa 100644 --- a/Source/Engine/UI/GUI/Panels/Panel.cs +++ b/Source/Engine/UI/GUI/Panels/Panel.cs @@ -172,6 +172,15 @@ namespace FlaxEngine.GUI base.SetViewOffset(ref value); } + /// + /// Cuts the scroll bars value smoothing and imminently goes to the target scroll value. + /// + public void FastScroll() + { + HScrollBar?.FastScroll(); + VScrollBar?.FastScroll(); + } + /// /// Scrolls the view to the given control area. /// diff --git a/Source/Engine/UI/GUI/Panels/ScrollBar.cs b/Source/Engine/UI/GUI/Panels/ScrollBar.cs index 0175de5a5..bea692e19 100644 --- a/Source/Engine/UI/GUI/Panels/ScrollBar.cs +++ b/Source/Engine/UI/GUI/Panels/ScrollBar.cs @@ -182,6 +182,19 @@ namespace FlaxEngine.GUI _orientation = orientation; } + /// + /// Cuts the scroll bar value smoothing and imminently goes to the target scroll value. + /// + public void FastScroll() + { + if (!Mathf.NearEqual(_value, _targetValue)) + { + _value = _targetValue; + SetUpdate(ref _update, null); + OnValueChanged(); + } + } + /// /// Scrolls the view to the desire range (favors minimum value if cannot cover whole range in a bounds). ///