#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; /// /// Editing of a set of curves and keyframes independent of the actual visualization. /// /// This provides basic curve editing functionality outside a timeline context, e.g. for CurveParameters internal abstract class CurveEditing { /// /// 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. /// protected readonly VersionedKeyframeSet SelectedKeyframes; protected CurveEditing() : this(null) { } protected CurveEditing(VersionedKeyframeSet? sharedSelection) { SelectedKeyframes = sharedSelection ?? new VersionedKeyframeSet(); } protected abstract IEnumerable GetAllCurves(); protected abstract IEnumerable 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); /// /// Helper function to extract vDefs from all or selected UI controls across all curves in CurveEditor /// /// a list of curves with a list of vDefs protected IEnumerable 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; } } } } /// /// Expose the editor's curves to undo/redo consumers (e.g. tangent-drag command construction /// in ). The base is protected; this is /// the safe cross-class hook. /// internal IEnumerable GetCurvesForUndo() => GetAllCurves(); /// /// 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; overrides to /// surface the shared TimeLineCanvas state. /// 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(); 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 GetSelectedKeyframeInterpolationTypes() { var checkedInterpolationTypes = new HashSet(); foreach (var point in GetSelectedOrAllPoints()) { checkedInterpolationTypes.Add(point.OutInterpolation); checkedInterpolationTypes.Add(point.InInterpolation); } return checkedInterpolationTypes; } protected IEnumerable GetAllKeyframes() { return from curve in GetAllCurves() from keyframe in curve.GetVDefinitions() select keyframe; } protected void DuplicateSelectedKeyframes() { CopySelectedKeyframes(); PasteKeyframes(); } /// /// Align curve table-structure aligned with position stored in key definitions. /// 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 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; } }