Add ability to edit position curve in timeline with a gizmo in a viewport

This commit is contained in:
Wojtek Figat
2024-10-02 17:00:58 +02:00
parent 7f7549d2f7
commit e2462c8151
5 changed files with 325 additions and 16 deletions

View File

@@ -241,6 +241,15 @@ namespace FlaxEditor.GUI
/// <param name="value">The keyframe value.</param> /// <param name="value">The keyframe value.</param>
public abstract void SetKeyframeValue(int index, object value); public abstract void SetKeyframeValue(int index, object value);
/// <summary>
/// Sets the existing keyframe value (as boxed object).
/// </summary>
/// <param name="index">The keyframe index.</param>
/// <param name="value">The keyframe value.</param>
/// <param name="tangentIn">The keyframe 'In' tangent value (boxed).</param>
/// <param name="tangentOut">The keyframe 'Out' tangent value (boxed).</param>
public abstract void SetKeyframeValue(int index, object value, object tangentIn, object tangentOut);
/// <summary> /// <summary>
/// Gets the keyframe point (in keyframes space). /// Gets the keyframe point (in keyframes space).
/// </summary> /// </summary>

View File

@@ -1289,6 +1289,18 @@ namespace FlaxEditor.GUI
OnEdited(); OnEdited();
} }
/// <inheritdoc />
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();
}
/// <inheritdoc /> /// <inheritdoc />
public override Float2 GetKeyframePoint(int index, int component) public override Float2 GetKeyframePoint(int index, int component)
{ {
@@ -2011,6 +2023,20 @@ namespace FlaxEditor.GUI
OnEdited(); OnEdited();
} }
/// <inheritdoc />
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();
}
/// <inheritdoc /> /// <inheritdoc />
public override Float2 GetKeyframePoint(int index, int component) public override Float2 GetKeyframePoint(int index, int component)
{ {

View File

@@ -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
{
/// <summary>
/// Gizmo for editing position curve. Managed by the <see cref="SceneAnimationTimeline"/>.
/// </summary>
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<Vector3>)_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<Vector3>)_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<Vector3>)_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<Vector3>)_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<Vector3>)_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<TransformGizmo>();
}
public override void OnActivated()
{
base.OnActivated();
ActiveMode = Mode.Translate;
}
public override void OnDeactivated()
{
// Clear selection
_track = null;
_keyframe = -1;
_item = -1;
base.OnDeactivated();
}
}
}

View File

@@ -1,11 +1,12 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. // Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System; using System;
using FlaxEngine;
using FlaxEditor.Content; using FlaxEditor.Content;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI.Drag; using FlaxEditor.GUI.Drag;
using FlaxEditor.GUI.Timeline.Tracks; using FlaxEditor.GUI.Timeline.Tracks;
using FlaxEditor.SceneGraph; using FlaxEditor.SceneGraph;
using FlaxEngine;
namespace FlaxEditor.GUI.Timeline namespace FlaxEditor.GUI.Timeline
{ {
@@ -25,6 +26,7 @@ namespace FlaxEditor.GUI.Timeline
} }
private SceneAnimationPlayer _player; private SceneAnimationPlayer _player;
private EditCurveTrackGizmo _curveTrackGizmo;
private bool _showSelected3dTrack = true; private bool _showSelected3dTrack = true;
internal Guid _id; 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();
}
/// <inheritdoc /> /// <inheritdoc />
public override void OnPlay() public override void OnPlay()
{ {
@@ -289,8 +304,17 @@ namespace FlaxEditor.GUI.Timeline
UpdatePlaybackState(); UpdatePlaybackState();
// Draw all selected 3D position tracks as Bezier curve in editor viewport // 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<TransformGizmo>().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) foreach (var track in SelectedTracks)
{ {
if ( if (
@@ -302,31 +326,44 @@ namespace FlaxEditor.GUI.Timeline
{ {
var curve = (BezierCurveEditor<Vector3>)curveTrack.Curve; var curve = (BezierCurveEditor<Vector3>)curveTrack.Curve;
var keyframes = curve.Keyframes; var keyframes = curve.Keyframes;
var selectedKeyframe = _curveTrackGizmo?.Keyframe ?? -1;
var selectedItem = _curveTrackGizmo?.Item ?? -1;
for (var i = 0; i < keyframes.Count; i++) for (var i = 0; i < keyframes.Count; i++)
{ {
var k = keyframes[i]; var k = keyframes[i];
DebugDraw.DrawSphere(new BoundingSphere(k.Value, 10.0f), Color.Red); var selected = selectedKeyframe == i && selectedItem == 0;
DebugDraw.DrawSphere(new BoundingSphere(k.Value, 9.5f), new Color(1, 0, 0, 0.1f), 0, false); 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) if (!k.TangentIn.IsZero)
{ {
selected = selectedKeyframe == i && selectedItem == 1;
var t = k.Value + k.TangentIn; var t = k.Value + k.TangentIn;
DebugDraw.DrawLine(k.Value, t, Color.Yellow); DebugDraw.DrawLine(k.Value, t, Color.Yellow);
DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(0.1f), 0, false); DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(coveredAlpha), 0, false);
var box = BoundingBox.FromSphere(new BoundingSphere(t, 6.0f)); var box = BoundingBox.FromSphere(new BoundingSphere(t, EditCurveTrackGizmo.TangentSize));
DebugDraw.DrawBox(box, Color.AliceBlue); DebugDraw.DrawBox(box, selected ? Color.Yellow : Color.AliceBlue);
DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(0.1f), 0, false); DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(coveredAlpha), 0, false);
if (select && box.Intersects(ref selectRay))
SelectKeyframeGizmo(curveTrack, i, 2);
} }
if (!k.TangentOut.IsZero) if (!k.TangentOut.IsZero)
{ {
selected = selectedKeyframe == i && selectedItem == 2;
var t = k.Value + k.TangentOut; var t = k.Value + k.TangentOut;
DebugDraw.DrawLine(k.Value, t, Color.Yellow); DebugDraw.DrawLine(k.Value, t, Color.Yellow);
DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(0.1f), 0, false); DebugDraw.DrawLine(k.Value, t, Color.Yellow.AlphaMultiplied(coveredAlpha), 0, false);
var box = BoundingBox.FromSphere(new BoundingSphere(t, 6.0f)); var box = BoundingBox.FromSphere(new BoundingSphere(t, EditCurveTrackGizmo.TangentSize));
DebugDraw.DrawBox(box, Color.AliceBlue); DebugDraw.DrawBox(box, selected ? Color.Yellow : Color.AliceBlue);
DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(0.1f), 0, false); DebugDraw.DrawBox(box, Color.AliceBlue.AlphaMultiplied(coveredAlpha), 0, false);
if (select && box.Intersects(ref selectRay))
SelectKeyframeGizmo(curveTrack, i, 2);
} }
if (i != 0) if (i != 0)
@@ -341,6 +378,18 @@ namespace FlaxEditor.GUI.Timeline
} }
} }
/// <inheritdoc />
public override void OnDestroy()
{
if (_curveTrackGizmo != null)
{
_curveTrackGizmo.Destroy();
_curveTrackGizmo = null;
}
base.OnDestroy();
}
/// <inheritdoc /> /// <inheritdoc />
protected override void OnShowViewContextMenu(ContextMenu.ContextMenu menu) protected override void OnShowViewContextMenu(ContextMenu.ContextMenu menu)
{ {

View File

@@ -12,15 +12,17 @@ namespace FlaxEditor.Gizmo
[HideInEditor] [HideInEditor]
public abstract class GizmoBase public abstract class GizmoBase
{ {
private IGizmoOwner _owner;
/// <summary> /// <summary>
/// Gets the gizmo owner. /// Gets the gizmo owner.
/// </summary> /// </summary>
public IGizmoOwner Owner { get; } public IGizmoOwner Owner => _owner;
/// <summary> /// <summary>
/// Gets a value indicating whether this gizmo is active. /// Gets a value indicating whether this gizmo is active.
/// </summary> /// </summary>
public bool IsActive => Owner.Gizmos.Active == this; public bool IsActive => _owner.Gizmos.Active == this;
/// <summary> /// <summary>
/// Gets a value indicating whether this gizmo is using mouse currently (eg. user moving objects). /// 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) protected GizmoBase(IGizmoOwner owner)
{ {
// Link // Link
Owner = owner; _owner = owner;
Owner.Gizmos.Add(this); _owner.Gizmos.Add(this);
} }
/// <summary> /// <summary>
@@ -94,5 +96,25 @@ namespace FlaxEditor.Gizmo
public virtual void Draw(ref RenderContext renderContext) public virtual void Draw(ref RenderContext renderContext)
{ {
} }
/// <summary>
/// Activates thi gizmo mode.
/// </summary>
public void Activate()
{
_owner.Gizmos.Active = this;
}
/// <summary>
/// Removes the gizmo from the owner.
/// </summary>
public void Destroy()
{
if (_owner != null)
{
_owner.Gizmos.Remove(this);
_owner = null;
}
}
} }
} }