Files
2026-07-13 13:13:17 +08:00

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