From e2462c81517b18d1eb8e5dff8b607d085cddd5b6 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 2 Oct 2024 17:00:58 +0200 Subject: [PATCH] Add ability to edit position curve in timeline with a gizmo in a viewport --- Source/Editor/GUI/CurveEditor.Base.cs | 9 + Source/Editor/GUI/CurveEditor.cs | 26 +++ .../GUI/Timeline/EditCurveTrackGizmo.cs | 203 ++++++++++++++++++ .../GUI/Timeline/SceneAnimationTimeline.cs | 73 +++++-- Source/Editor/Gizmo/GizmoBase.cs | 30 ++- 5 files changed, 325 insertions(+), 16 deletions(-) create mode 100644 Source/Editor/GUI/Timeline/EditCurveTrackGizmo.cs diff --git a/Source/Editor/GUI/CurveEditor.Base.cs b/Source/Editor/GUI/CurveEditor.Base.cs index 6a919f0c2..25de316fa 100644 --- a/Source/Editor/GUI/CurveEditor.Base.cs +++ b/Source/Editor/GUI/CurveEditor.Base.cs @@ -241,6 +241,15 @@ namespace FlaxEditor.GUI /// The keyframe value. public abstract void SetKeyframeValue(int index, object value); + /// + /// Sets the existing keyframe value (as boxed object). + /// + /// The keyframe index. + /// The keyframe value. + /// The keyframe 'In' tangent value (boxed). + /// The keyframe 'Out' tangent value (boxed). + public abstract void SetKeyframeValue(int index, object value, object tangentIn, object tangentOut); + /// /// Gets the keyframe point (in keyframes space). /// diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index bc96cf3bc..3499ef5d3 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -1289,6 +1289,18 @@ namespace FlaxEditor.GUI OnEdited(); } + /// + public override void SetKeyframeValue(int index, object value, object tangentIn, object tangentOut) + { + var k = _keyframes[index]; + k.Value = (T)value; + _keyframes[index] = k; + + UpdateKeyframes(); + UpdateTooltips(); + OnEdited(); + } + /// public override Float2 GetKeyframePoint(int index, int component) { @@ -2011,6 +2023,20 @@ namespace FlaxEditor.GUI OnEdited(); } + /// + public override void SetKeyframeValue(int index, object value, object tangentIn, object tangentOut) + { + var k = _keyframes[index]; + k.Value = (T)value; + k.TangentIn = (T)tangentIn; + k.TangentOut = (T)tangentOut; + _keyframes[index] = k; + + UpdateKeyframes(); + UpdateTooltips(); + OnEdited(); + } + /// public override Float2 GetKeyframePoint(int index, int component) { diff --git a/Source/Editor/GUI/Timeline/EditCurveTrackGizmo.cs b/Source/Editor/GUI/Timeline/EditCurveTrackGizmo.cs new file mode 100644 index 000000000..7cb1e7c5b --- /dev/null +++ b/Source/Editor/GUI/Timeline/EditCurveTrackGizmo.cs @@ -0,0 +1,203 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +using FlaxEditor.Gizmo; +using FlaxEditor.GUI.Timeline.Tracks; +using FlaxEditor.GUI.Timeline.Undo; +using FlaxEditor.SceneGraph; +using FlaxEngine; + +namespace FlaxEditor.GUI.Timeline +{ + /// + /// Gizmo for editing position curve. Managed by the . + /// + sealed class EditCurveTrackGizmo : TransformGizmoBase + { + public const float KeyframeSize = 10.0f; + public const float TangentSize = 6.0f; + + private readonly SceneAnimationTimeline _timeline; + private CurvePropertyTrack _track; + private int _keyframe = -1; + private int _item = -1; + private byte[] _curveEditingStartData; + + public int Keyframe => _keyframe; + public int Item => _item; + + public EditCurveTrackGizmo(IGizmoOwner owner, SceneAnimationTimeline timeline) + : base(owner) + { + _timeline = timeline; + } + + public void SelectKeyframe(CurvePropertyTrack track, int keyframe, int item) + { + _track = track; + _keyframe = keyframe; + _item = item; + } + + public override BoundingSphere FocusBounds + { + get + { + if (_track == null) + return BoundingSphere.Empty; + var curve = (BezierCurveEditor)_track.Curve; + var keyframes = curve.Keyframes; + var k = keyframes[_keyframe]; + if (_item == 1) + k.Value += k.TangentIn; + else if (_item == 2) + k.Value += k.TangentOut; + return new BoundingSphere(k.Value, KeyframeSize); + } + } + + protected override int SelectionCount => _track != null ? 1 : 0; + + protected override SceneGraphNode GetSelectedObject(int index) + { + return null; + } + + protected override Transform GetSelectedTransform(int index) + { + if (_track == null) + return Transform.Identity; + var curve = (BezierCurveEditor)_track.Curve; + var keyframes = curve.Keyframes; + var k = keyframes[_keyframe]; + if (_item == 1) + k.Value += k.TangentIn; + else if (_item == 2) + k.Value += k.TangentOut; + return new Transform(k.Value); + } + + protected override void GetSelectedObjectsBounds(out BoundingBox bounds, out bool navigationDirty) + { + bounds = BoundingBox.Empty; + navigationDirty = false; + if (_track == null) + return; + var curve = (BezierCurveEditor)_track.Curve; + var keyframes = curve.Keyframes; + var k = keyframes[_keyframe]; + if (_item == 1) + k.Value += k.TangentIn; + else if (_item == 2) + k.Value += k.TangentOut; + bounds = new BoundingBox(k.Value - KeyframeSize, k.Value + KeyframeSize); + } + + protected override bool IsSelected(SceneGraphNode obj) + { + return false; + } + + protected override void OnStartTransforming() + { + base.OnStartTransforming(); + + // Start undo + _curveEditingStartData = EditTrackAction.CaptureData(_track); + } + + protected override void OnEndTransforming() + { + base.OnEndTransforming(); + + // End undo + var after = EditTrackAction.CaptureData(_track); + if (!Utils.ArraysEqual(_curveEditingStartData, after)) + _track.Timeline.AddBatchedUndoAction(new EditTrackAction(_track.Timeline, _track, _curveEditingStartData, after)); + _curveEditingStartData = null; + } + + protected override void OnApplyTransformation(ref Vector3 translationDelta, ref Quaternion rotationDelta, ref Vector3 scaleDelta) + { + base.OnApplyTransformation(ref translationDelta, ref rotationDelta, ref scaleDelta); + + var curve = (BezierCurveEditor)_track.Curve; + var keyframes = curve.Keyframes; + var k = keyframes[_keyframe]; + if (_item == 0) + k.Value += translationDelta; + else if (_item == 1) + k.TangentIn += translationDelta; + else if (_item == 2) + k.TangentOut += translationDelta; + curve.SetKeyframeValue(_keyframe, k.Value, k.TangentIn, k.TangentOut); + } + + public override void Pick() + { + if (_track == null) + return; + var selectRay = Owner.MouseRay; + var curve = (BezierCurveEditor)_track.Curve; + var keyframes = curve.Keyframes; + for (var i = 0; i < keyframes.Count; i++) + { + var k = keyframes[i]; + + var sphere = new BoundingSphere(k.Value, KeyframeSize); + if (sphere.Intersects(ref selectRay)) + { + SelectKeyframe(_track, i, 0); + return; + } + + if (!k.TangentIn.IsZero) + { + var t = k.Value + k.TangentIn; + var box = BoundingBox.FromSphere(new BoundingSphere(t, TangentSize)); + if (box.Intersects(ref selectRay)) + { + SelectKeyframe(_track, i, 1); + return; + } + } + + if (!k.TangentOut.IsZero) + { + var t = k.Value + k.TangentOut; + var box = BoundingBox.FromSphere(new BoundingSphere(t, TangentSize)); + if (box.Intersects(ref selectRay)) + { + SelectKeyframe(_track, i, 2); + return; + } + } + } + } + + public override void Update(float dt) + { + base.Update(dt); + + // Deactivate when track gets deselected + if (_track != null && !_timeline.SelectedTracks.Contains(_track)) + Owner.Gizmos.Active = Owner.Gizmos.Get(); + } + + public override void OnActivated() + { + base.OnActivated(); + + ActiveMode = Mode.Translate; + } + + public override void OnDeactivated() + { + // Clear selection + _track = null; + _keyframe = -1; + _item = -1; + + base.OnDeactivated(); + } + } +} diff --git a/Source/Editor/GUI/Timeline/SceneAnimationTimeline.cs b/Source/Editor/GUI/Timeline/SceneAnimationTimeline.cs index ceac9931b..9f3f49e34 100644 --- a/Source/Editor/GUI/Timeline/SceneAnimationTimeline.cs +++ b/Source/Editor/GUI/Timeline/SceneAnimationTimeline.cs @@ -1,11 +1,12 @@ // Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; +using FlaxEngine; using FlaxEditor.Content; +using FlaxEditor.Gizmo; using FlaxEditor.GUI.Drag; using FlaxEditor.GUI.Timeline.Tracks; using FlaxEditor.SceneGraph; -using FlaxEngine; namespace FlaxEditor.GUI.Timeline { @@ -25,6 +26,7 @@ namespace FlaxEditor.GUI.Timeline } private SceneAnimationPlayer _player; + private EditCurveTrackGizmo _curveTrackGizmo; private bool _showSelected3dTrack = true; internal Guid _id; @@ -239,6 +241,19 @@ namespace FlaxEditor.GUI.Timeline } } + private void SelectKeyframeGizmo(CurvePropertyTrack track, int keyframe, int item) + { + var mainGizmo = Editor.Instance.MainTransformGizmo; + if (!mainGizmo.IsActive) + return; // Skip when using vertex painting or terrain or foliage tools + if (_curveTrackGizmo == null) + { + _curveTrackGizmo = new EditCurveTrackGizmo(mainGizmo.Owner, this); + } + _curveTrackGizmo.SelectKeyframe(track, keyframe, item); + _curveTrackGizmo.Activate(); + } + /// public override void OnPlay() { @@ -289,8 +304,17 @@ namespace FlaxEditor.GUI.Timeline UpdatePlaybackState(); // Draw all selected 3D position tracks as Bezier curve in editor viewport - if (ShowSelected3dTrack) + if (!VisibleInHierarchy || !EnabledInHierarchy) { + // Disable curve transform gizmo to normal gizmo + if (_curveTrackGizmo != null && _curveTrackGizmo.IsActive) + _curveTrackGizmo.Owner.Gizmos.Get().Activate(); + } + else if (ShowSelected3dTrack) + { + bool select = FlaxEngine.Input.GetMouseButtonDown(MouseButton.Left); + Ray selectRay = Editor.Instance.MainTransformGizmo.Owner.MouseRay; + const float coveredAlpha = 0.1f; foreach (var track in SelectedTracks) { if ( @@ -302,31 +326,44 @@ namespace FlaxEditor.GUI.Timeline { var curve = (BezierCurveEditor)curveTrack.Curve; var keyframes = curve.Keyframes; + var selectedKeyframe = _curveTrackGizmo?.Keyframe ?? -1; + var selectedItem = _curveTrackGizmo?.Item ?? -1; for (var i = 0; i < keyframes.Count; i++) { var k = keyframes[i]; - DebugDraw.DrawSphere(new BoundingSphere(k.Value, 10.0f), Color.Red); - DebugDraw.DrawSphere(new BoundingSphere(k.Value, 9.5f), new Color(1, 0, 0, 0.1f), 0, false); + var selected = selectedKeyframe == i && selectedItem == 0; + var sphere = new BoundingSphere(k.Value, EditCurveTrackGizmo.KeyframeSize); + DebugDraw.DrawSphere(sphere, selected ? Color.Yellow : Color.Red); + sphere.Radius *= 0.95f; + DebugDraw.DrawSphere(sphere, new Color(1, 0, 0, coveredAlpha), 0, false); + if (select && sphere.Intersects(ref selectRay)) + SelectKeyframeGizmo(curveTrack, i, 0); if (!k.TangentIn.IsZero) { + selected = selectedKeyframe == i && selectedItem == 1; var t = k.Value + k.TangentIn; DebugDraw.DrawLine(k.Value, t, Color.Yellow); - DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(0.1f), 0, false); - var box = BoundingBox.FromSphere(new BoundingSphere(t, 6.0f)); - DebugDraw.DrawBox(box, Color.AliceBlue); - DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(0.1f), 0, false); + DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(coveredAlpha), 0, false); + var box = BoundingBox.FromSphere(new BoundingSphere(t, EditCurveTrackGizmo.TangentSize)); + DebugDraw.DrawBox(box, selected ? Color.Yellow : Color.AliceBlue); + DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(coveredAlpha), 0, false); + if (select && box.Intersects(ref selectRay)) + SelectKeyframeGizmo(curveTrack, i, 2); } if (!k.TangentOut.IsZero) { + selected = selectedKeyframe == i && selectedItem == 2; var t = k.Value + k.TangentOut; DebugDraw.DrawLine(k.Value, t, Color.Yellow); - DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(0.1f), 0, false); - var box = BoundingBox.FromSphere(new BoundingSphere(t, 6.0f)); - DebugDraw.DrawBox(box, Color.AliceBlue); - DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(0.1f), 0, false); + DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(coveredAlpha), 0, false); + var box = BoundingBox.FromSphere(new BoundingSphere(t, EditCurveTrackGizmo.TangentSize)); + DebugDraw.DrawBox(box, selected ? Color.Yellow : Color.AliceBlue); + DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(coveredAlpha), 0, false); + if (select && box.Intersects(ref selectRay)) + SelectKeyframeGizmo(curveTrack, i, 2); } if (i != 0) @@ -341,6 +378,18 @@ namespace FlaxEditor.GUI.Timeline } } + /// + public override void OnDestroy() + { + if (_curveTrackGizmo != null) + { + _curveTrackGizmo.Destroy(); + _curveTrackGizmo = null; + } + + base.OnDestroy(); + } + /// protected override void OnShowViewContextMenu(ContextMenu.ContextMenu menu) { diff --git a/Source/Editor/Gizmo/GizmoBase.cs b/Source/Editor/Gizmo/GizmoBase.cs index af119444d..72990ea1f 100644 --- a/Source/Editor/Gizmo/GizmoBase.cs +++ b/Source/Editor/Gizmo/GizmoBase.cs @@ -12,15 +12,17 @@ namespace FlaxEditor.Gizmo [HideInEditor] public abstract class GizmoBase { + private IGizmoOwner _owner; + /// /// Gets the gizmo owner. /// - public IGizmoOwner Owner { get; } + public IGizmoOwner Owner => _owner; /// /// Gets a value indicating whether this gizmo is active. /// - public bool IsActive => Owner.Gizmos.Active == this; + public bool IsActive => _owner.Gizmos.Active == this; /// /// Gets a value indicating whether this gizmo is using mouse currently (eg. user moving objects). @@ -39,8 +41,8 @@ namespace FlaxEditor.Gizmo protected GizmoBase(IGizmoOwner owner) { // Link - Owner = owner; - Owner.Gizmos.Add(this); + _owner = owner; + _owner.Gizmos.Add(this); } /// @@ -94,5 +96,25 @@ namespace FlaxEditor.Gizmo public virtual void Draw(ref RenderContext renderContext) { } + + /// + /// Activates thi gizmo mode. + /// + public void Activate() + { + _owner.Gizmos.Active = this; + } + + /// + /// Removes the gizmo from the owner. + /// + public void Destroy() + { + if (_owner != null) + { + _owner.Gizmos.Remove(this); + _owner = null; + } + } } }