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;
+ }
+ }
}
}