548 lines
20 KiB
C#
548 lines
20 KiB
C#
#nullable enable
|
|
using ImGuiNET;
|
|
using T3.Core.Animation;
|
|
using T3.Core.DataTypes;
|
|
using T3.Core.Operator;
|
|
using T3.Core.Utils;
|
|
using T3.Editor.Gui.Interaction.Keyboard;
|
|
using T3.Editor.Gui.Styling;
|
|
using T3.Editor.Gui.UiHelpers;
|
|
using T3.Editor.Gui.Windows.TimeLine;
|
|
using T3.Editor.UiModel;
|
|
using T3.Editor.UiModel.Commands;
|
|
using T3.Editor.UiModel.Commands.Animation;
|
|
|
|
namespace T3.Editor.Gui.Interaction.WithCurves;
|
|
|
|
/// <summary>
|
|
/// Editing of a set of curves and keyframes independent of the actual visualization.
|
|
/// </summary>
|
|
/// <remarks>This provides basic curve editing functionality outside a timeline context, e.g. for CurveParameters</remarks>
|
|
internal abstract class CurveEditing
|
|
{
|
|
/// <summary>
|
|
/// Selection of keyframes driven by this editor. May be a set private to this instance
|
|
/// (default constructor) or one shared with sibling editors that should see each other's
|
|
/// selection changes — e.g. DopeSheetArea and TimelineCurveEditor on the same timeline.
|
|
/// </summary>
|
|
protected readonly VersionedKeyframeSet SelectedKeyframes;
|
|
|
|
protected CurveEditing() : this(null) { }
|
|
|
|
protected CurveEditing(VersionedKeyframeSet? sharedSelection)
|
|
{
|
|
SelectedKeyframes = sharedSelection ?? new VersionedKeyframeSet();
|
|
}
|
|
|
|
protected abstract IEnumerable<Curve> GetAllCurves();
|
|
protected abstract IEnumerable<KeyframeCopyAndPasting.CurveWithDetails> GetAllCurvesWithDetails();
|
|
protected abstract void ViewAllOrSelectedKeys(bool alsoChangeTimeRange = false);
|
|
protected abstract void DeleteSelectedKeyframes(Instance composition);
|
|
protected abstract void PasteKeyframes();
|
|
protected internal abstract void HandleCurvePointDragging(in Guid compositionSymbolId, VDefinition vDef, bool isSelected);
|
|
|
|
/// <summary>
|
|
/// Helper function to extract vDefs from all or selected UI controls across all curves in CurveEditor
|
|
/// </summary>
|
|
/// <returns>a list of curves with a list of vDefs</returns>
|
|
protected IEnumerable<VDefinition> GetSelectedOrAllPoints()
|
|
{
|
|
if (SelectedKeyframes.Count > 0)
|
|
{
|
|
foreach (var x in SelectedKeyframes)
|
|
{
|
|
yield return x;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var curve in GetAllCurves())
|
|
{
|
|
foreach (var x in curve.GetVDefinitions())
|
|
{
|
|
yield return x;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expose the editor's curves to undo/redo consumers (e.g. tangent-drag command construction
|
|
/// in <see cref="CurvePoint"/>). The base <see cref="GetAllCurves"/> is protected; this is
|
|
/// the safe cross-class hook.
|
|
/// </summary>
|
|
internal IEnumerable<Curve> GetCurvesForUndo() => GetAllCurves();
|
|
|
|
/// <summary>
|
|
/// UniqueId of the keyframe that is currently hovered across the timeline's views (e.g.
|
|
/// hovering a CEA keyframe should outline the matching DSA row keyframe, and vice versa).
|
|
/// Default null for standalone editors; <see cref="TimelineCurveEditor"/> overrides to
|
|
/// surface the shared TimeLineCanvas state.
|
|
/// </summary>
|
|
internal virtual int? GetHoveredKeyframeUniqueId() => null;
|
|
|
|
internal bool TrySelectKeyFrame(Curve curve, VDefinition vDef)
|
|
{
|
|
foreach (var c in GetAllCurves())
|
|
{
|
|
if (c == curve)
|
|
{
|
|
SelectedKeyframes.Add(vDef);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected void DrawContextMenu(Instance composition)
|
|
{
|
|
CustomComponents.DrawContextMenuForScrollCanvas(ContextMenuContentAction, ref _contextMenuIsOpen);
|
|
return;
|
|
|
|
void ContextMenuContentAction()
|
|
{
|
|
var selectedInterpolations = GetSelectedKeyframeInterpolationTypes();
|
|
|
|
var modes = selectedInterpolations as VDefinition.KeyInterpolation[] ?? selectedInterpolations.ToArray();
|
|
|
|
bool changed = false;
|
|
if (SelectedKeyframes.Count > 0)
|
|
{
|
|
CustomComponents.HintLabel("Interpolation...");
|
|
|
|
if (ImGui.MenuItem("Linear", null, modes.Contains(VDefinition.KeyInterpolation.Linear)))
|
|
{
|
|
OnLinear();
|
|
UpdateAllTangents();
|
|
changed = true;
|
|
}
|
|
|
|
|
|
if (ImGui.MenuItem("Smooth (Clamped)", null, modes.Contains(VDefinition.KeyInterpolation.Smooth)))
|
|
{
|
|
OnSmooth();
|
|
UpdateAllTangents();
|
|
changed = true;
|
|
}
|
|
|
|
if (ImGui.MenuItem("Smooth", null, modes.Contains(VDefinition.KeyInterpolation.Cubic)))
|
|
{
|
|
OnCubic();
|
|
UpdateAllTangents();
|
|
changed = true;
|
|
}
|
|
|
|
if (ImGui.MenuItem("Horizontal", null, modes.Contains(VDefinition.KeyInterpolation.Horizontal)))
|
|
{
|
|
OnHorizontal();
|
|
UpdateAllTangents();
|
|
changed = true;
|
|
}
|
|
|
|
if (ImGui.MenuItem("Constant", null, modes.Contains(VDefinition.KeyInterpolation.Constant)))
|
|
{
|
|
OnConstant();
|
|
UpdateAllTangents();
|
|
changed = true;
|
|
}
|
|
|
|
|
|
ImGui.Separator();
|
|
|
|
{
|
|
var allMirrored = GetSelectedOrAllPoints().All(v => !v.BrokenTangents);
|
|
if (ImGui.MenuItem("Mirror Tangents", null, allMirrored))
|
|
{
|
|
ForSelectedOrAllPointsDo((vDef) =>
|
|
{
|
|
vDef.BrokenTangents = allMirrored; // Toggle
|
|
if (!vDef.BrokenTangents)
|
|
{
|
|
// Sync out angle to mirror in
|
|
vDef.OutTangentAngle = vDef.InTangentAngle + Math.PI;
|
|
}
|
|
});
|
|
UpdateAllTangents();
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
{
|
|
var anyWeighted = GetSelectedOrAllPoints().Any(v => v.Weighted);
|
|
if (ImGui.MenuItem("Weighted Tensions", null, anyWeighted))
|
|
{
|
|
ForSelectedOrAllPointsDo((vDef) =>
|
|
{
|
|
vDef.Weighted = !anyWeighted; // Toggle
|
|
if (!vDef.Weighted)
|
|
{
|
|
vDef.TensionIn = 1.0f;
|
|
vDef.TensionOut = 1.0f;
|
|
}
|
|
});
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
ImGui.Separator();
|
|
|
|
if (ImGui.BeginMenu("Before curve...", SelectedKeyframes.Count > 0))
|
|
{
|
|
foreach (CurveUtils.OutsideCurveBehavior mapping in Enum.GetValues(typeof(CurveUtils.OutsideCurveBehavior)))
|
|
{
|
|
if (ImGui.MenuItem(mapping.ToString(), null))
|
|
{
|
|
ApplyPreCurveMapping(mapping);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
ImGui.EndMenu();
|
|
}
|
|
|
|
if (ImGui.BeginMenu("After curve...", SelectedKeyframes.Count > 0))
|
|
{
|
|
foreach (CurveUtils.OutsideCurveBehavior mapping in Enum.GetValues(typeof(CurveUtils.OutsideCurveBehavior)))
|
|
{
|
|
if (ImGui.MenuItem(mapping.ToString(), null))
|
|
{
|
|
ApplyPostCurveMapping(mapping);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
ImGui.EndMenu();
|
|
}
|
|
|
|
if (ImGui.MenuItem("Copy keyframes", SelectedKeyframes.Count > 0))
|
|
{
|
|
CopySelectedKeyframes();
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (ImGui.MenuItem("Paste keyframes", "", false,KeyframeCopyAndPasting.HasValidClipboard))
|
|
{
|
|
PasteKeyframes();
|
|
changed = true;
|
|
}
|
|
|
|
if (SelectedKeyframes.Count > 0)
|
|
{
|
|
if (ImGui.MenuItem("Delete keyframes", SelectedKeyframes.Count > 0))
|
|
{
|
|
DeleteSelectedKeyframes(composition);
|
|
changed = true;
|
|
}
|
|
|
|
if (ImGui.MenuItem("Space Evenly", SelectedKeyframes.Count > 2))
|
|
{
|
|
EvenlySpaceKeyframes();
|
|
changed = true;
|
|
}
|
|
|
|
if (ImGui.BeginMenu("Set Sequential Values", enabled:SelectedKeyframes.Count>1))
|
|
{
|
|
if (ImGui.MenuItem("Start at 0"))
|
|
{
|
|
var value = 0;
|
|
ForSelectedOrAllPointsDo((vDef) =>
|
|
{
|
|
vDef.Value = value;
|
|
value++;
|
|
});
|
|
|
|
changed = true;
|
|
}
|
|
|
|
if (ImGui.MenuItem("Start at First"))
|
|
{
|
|
var selectedOrAllPoints = GetSelectedOrAllPoints().OrderBy(v => v.U).ToList();
|
|
if (selectedOrAllPoints.Count > 0)
|
|
{
|
|
var value = selectedOrAllPoints[0].Value;
|
|
ForSelectedOrAllPointsDo((vDef) =>
|
|
{
|
|
vDef.Value = value;
|
|
value++;
|
|
});
|
|
}
|
|
changed = true;
|
|
}
|
|
|
|
ImGui.EndMenu();
|
|
}
|
|
|
|
|
|
if (TimeLineCanvas.Current != null && ImGui.MenuItem("Duplicate keyframes", SelectedKeyframes.Count > 0))
|
|
{
|
|
DuplicateSelectedKeyframes();
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (ImGui.MenuItem(SelectedKeyframes.Count > 0 ? "View Selected" : "View All", UserActions.FocusSelection.ListShortcuts()))
|
|
ViewAllOrSelectedKeys();
|
|
|
|
if (changed)
|
|
{
|
|
composition.GetSymbolUi().FlagAsModified();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateAllTangents()
|
|
{
|
|
foreach (var curve in GetAllCurves())
|
|
{
|
|
curve.UpdateTangents();
|
|
}
|
|
}
|
|
|
|
private bool _contextMenuIsOpen;
|
|
|
|
private delegate void DoSomethingWithKeyframeDelegate(VDefinition v);
|
|
|
|
private void CopySelectedKeyframes()
|
|
{
|
|
var newCurves = new List<KeyframeCopyAndPasting.CurveWithDetails>();
|
|
foreach (var curveWithDetails in GetAllCurvesWithDetails())
|
|
{
|
|
Curve? newCurve = null;
|
|
|
|
foreach (var keyframe in curveWithDetails.Curve.GetVDefinitions())
|
|
{
|
|
if (!SelectedKeyframes.Contains(keyframe))
|
|
continue;
|
|
|
|
newCurve ??= new Curve();
|
|
|
|
newCurve.AddOrUpdateV(keyframe.U, keyframe.Clone());
|
|
}
|
|
|
|
if (newCurve == null)
|
|
continue;
|
|
|
|
var newCurveWithDetails = curveWithDetails; // Copies struct
|
|
newCurveWithDetails.Curve = newCurve;
|
|
newCurves.Add(newCurveWithDetails);
|
|
}
|
|
|
|
KeyframeCopyAndPasting.SetClipboard(newCurves);
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EvenlySpaceKeyframes()
|
|
{
|
|
var selectedOrAllPoints = GetSelectedOrAllPoints().OrderBy(v => v.U).ToList();
|
|
if (selectedOrAllPoints.Count <= 2)
|
|
{
|
|
Log.Debug("You need to select 3 or more keyframes to distribute timing");
|
|
return;
|
|
}
|
|
|
|
var startTime = selectedOrAllPoints[0].U;
|
|
var endTime = selectedOrAllPoints[^1].U;
|
|
var duration = endTime - startTime;
|
|
if (duration < 0.00001)
|
|
{
|
|
Log.Warning("Keyframes overlap");
|
|
return;
|
|
}
|
|
|
|
var cmd = new ChangeKeyframesCommand(selectedOrAllPoints, GetAllCurves());
|
|
|
|
var groups = selectedOrAllPoints
|
|
.GroupBy(k => k.U)
|
|
.OrderBy(g => g.Key)
|
|
.ToList();
|
|
|
|
for (var i = 0; i < groups.Count; i++)
|
|
{
|
|
var g = groups[i];
|
|
var f = groups.Count > 1
|
|
? i / (double)(groups.Count - 1)
|
|
: 0.0;
|
|
|
|
foreach (var key in g)
|
|
{
|
|
key.U = MathUtils.Lerp(startTime, endTime, f);
|
|
}
|
|
}
|
|
|
|
cmd.StoreCurrentValues();
|
|
UndoRedoStack.Add(cmd);
|
|
}
|
|
|
|
|
|
private void ForSelectedOrAllPointsDo(DoSomethingWithKeyframeDelegate doFunc)
|
|
{
|
|
var selectedOrAllPoints = GetSelectedOrAllPoints().OrderBy(v => v.U).ToList();
|
|
var cmd = new ChangeKeyframesCommand(selectedOrAllPoints, GetAllCurves());
|
|
|
|
foreach (var keyframe in selectedOrAllPoints)
|
|
{
|
|
doFunc(keyframe);
|
|
}
|
|
|
|
cmd.StoreCurrentValues();
|
|
UndoRedoStack.Add(cmd);
|
|
}
|
|
|
|
private void OnSmooth()
|
|
{
|
|
ForSelectedOrAllPointsDo((vDef) =>
|
|
{
|
|
vDef.BrokenTangents = false;
|
|
vDef.InInterpolation = VDefinition.KeyInterpolation.Smooth;
|
|
vDef.OutInterpolation = VDefinition.KeyInterpolation.Smooth;
|
|
vDef.TensionIn = 1.0f;
|
|
vDef.TensionOut = 1.0f;
|
|
vDef.Weighted = false;
|
|
});
|
|
|
|
}
|
|
|
|
private void OnCubic()
|
|
{
|
|
ForSelectedOrAllPointsDo((vDef) =>
|
|
{
|
|
vDef.BrokenTangents = false;
|
|
vDef.InInterpolation = VDefinition.KeyInterpolation.Cubic;
|
|
vDef.OutInterpolation = VDefinition.KeyInterpolation.Cubic;
|
|
vDef.TensionIn = 1.0f;
|
|
vDef.TensionOut = 1.0f;
|
|
vDef.Weighted = false;
|
|
});
|
|
}
|
|
|
|
private void OnHorizontal()
|
|
{
|
|
ForSelectedOrAllPointsDo((vDef) =>
|
|
{
|
|
vDef.BrokenTangents = false;
|
|
vDef.InInterpolation = VDefinition.KeyInterpolation.Horizontal;
|
|
vDef.InTangentAngle = 0;
|
|
vDef.OutInterpolation = VDefinition.KeyInterpolation.Horizontal;
|
|
vDef.OutTangentAngle = Math.PI;
|
|
vDef.TensionIn = 1.0f;
|
|
vDef.TensionOut = 1.0f;
|
|
vDef.Weighted = false;
|
|
});
|
|
}
|
|
|
|
private void OnConstant()
|
|
{
|
|
ForSelectedOrAllPointsDo((vDef) =>
|
|
{
|
|
vDef.BrokenTangents = true;
|
|
vDef.OutInterpolation = VDefinition.KeyInterpolation.Constant;
|
|
vDef.TensionIn = 1.0f;
|
|
vDef.TensionOut = 1.0f;
|
|
vDef.Weighted = false;
|
|
});
|
|
}
|
|
|
|
private void OnLinear()
|
|
{
|
|
ForSelectedOrAllPointsDo((vDef) =>
|
|
{
|
|
vDef.BrokenTangents = true;
|
|
vDef.InInterpolation = VDefinition.KeyInterpolation.Linear;
|
|
vDef.OutInterpolation = VDefinition.KeyInterpolation.Linear;
|
|
vDef.TensionIn = 1.0f;
|
|
vDef.TensionOut = 1.0f;
|
|
vDef.Weighted = false;
|
|
});
|
|
}
|
|
|
|
private void ApplyPostCurveMapping(CurveUtils.OutsideCurveBehavior mapping)
|
|
{
|
|
foreach (var curve in GetAllCurves())
|
|
{
|
|
curve.PostCurveMapping = mapping;
|
|
}
|
|
}
|
|
|
|
private void ApplyPreCurveMapping(CurveUtils.OutsideCurveBehavior mapping)
|
|
{
|
|
foreach (var curve in GetAllCurves())
|
|
{
|
|
curve.PreCurveMapping = mapping;
|
|
}
|
|
}
|
|
|
|
private IEnumerable<VDefinition.KeyInterpolation> GetSelectedKeyframeInterpolationTypes()
|
|
{
|
|
var checkedInterpolationTypes = new HashSet<VDefinition.KeyInterpolation>();
|
|
foreach (var point in GetSelectedOrAllPoints())
|
|
{
|
|
checkedInterpolationTypes.Add(point.OutInterpolation);
|
|
checkedInterpolationTypes.Add(point.InInterpolation);
|
|
}
|
|
|
|
return checkedInterpolationTypes;
|
|
}
|
|
|
|
protected IEnumerable<VDefinition> GetAllKeyframes()
|
|
{
|
|
return from curve in GetAllCurves()
|
|
from keyframe in curve.GetVDefinitions()
|
|
select keyframe;
|
|
}
|
|
|
|
protected void DuplicateSelectedKeyframes()
|
|
{
|
|
CopySelectedKeyframes();
|
|
PasteKeyframes();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Align curve table-structure aligned with position stored in key definitions.
|
|
/// </summary>
|
|
protected void RebuildCurveTables()
|
|
{
|
|
foreach (var curve in GetAllCurves())
|
|
{
|
|
curve.BeginBatchEdit();
|
|
foreach (var (u, vDef) in curve.Table.ToList()) // Copy before modifications
|
|
{
|
|
if (Math.Abs(u - vDef.U) > 0.001f)
|
|
{
|
|
curve.MoveKey(u, vDef.U);
|
|
}
|
|
}
|
|
curve.EndBatchEdit();
|
|
}
|
|
}
|
|
|
|
protected static bool TryGetBoundsOnCanvas(IEnumerable<VDefinition> keyframes, out ImRect bounds)
|
|
{
|
|
var foundOneOrMoreKeys = false;
|
|
|
|
bounds = new ImRect(-Vector2.One, Vector2.One);
|
|
var isFirst = true;
|
|
foreach (var k in keyframes)
|
|
{
|
|
foundOneOrMoreKeys = true;
|
|
var p = new Vector2((float)k.U, (float)k.Value);
|
|
|
|
if (isFirst)
|
|
{
|
|
bounds = new ImRect(p, p);
|
|
isFirst = false;
|
|
}
|
|
else
|
|
{
|
|
bounds.Add(p);
|
|
}
|
|
}
|
|
|
|
return foundOneOrMoreKeys;
|
|
}
|
|
} |