#nullable enable using T3.Core.Animation; using T3.Core.DataTypes; using T3.Core.Operator; using T3.Editor.UiModel.Commands; using T3.Editor.UiModel.Commands.Animation; namespace T3.Editor.Gui.Windows.TimeLine; /// /// Piecewise-linear time-warp drag engine. Powers three interactions: /// /// Dragging an inner TimeWarp handle (two-segment remap around it). /// Dragging an SRI edge when inner handles exist (single-segment remap against the nearest handle). /// Middle-translate when handles exist (pure translation of keys, clips, and handles). /// /// Builds a on completion containing the keyframe change, /// the clip-move, and optionally a so undo/redo /// restores handle positions along with the retimed elements. /// internal sealed class TimeWarpDrag { public TimeWarpDrag(TimeLineCanvas canvas, TimeWarpHandleSet handles) { _canvas = canvas; _handles = handles; } public bool IsActive { get; private set; } /// Start a drag. is the moving anchor; fixed boundaries pin the segment ends. public void Begin(Instance composition, double origHandleU, double prevBoundary, double nextBoundary, bool singleSegment, double segmentLeftU, double segmentRightU, bool pureTranslation, bool trackHandlePositions, bool useAllKeyframes) { Reset(); _origHandleU = origHandleU; _prev = prevBoundary; _next = nextBoundary; _singleSegment = singleSegment; _segLeftU = segmentLeftU; _segRightU = segmentRightU; _pureTranslation = pureTranslation; if (trackHandlePositions) { _handles.CopyTo(_origHandles); _handlesCommand = new SetTimeWarpHandlesCommand(_handles, _origHandles); } // Snapshot keyframes that are in the affected range (or all, for pure translation), // aggregated across every active keyframe editor (DopeSheet + CurveEditor when split). var editors = _canvas.KeyframeEditors; var source = useAllKeyframes ? editors.EnumerateAllKeyframes() : editors.EnumerateAllSelectedKeyframes(); foreach (var def in source) { if (!pureTranslation && !InsideAffectedRange(def.U)) continue; _keys.Add(def); _keyOrigU.Add(def.U); } if (_keys.Count > 0) { editors.CopyAllCurvesTo(_curves); _keyframesCommand = new ChangeKeyframesCommand(_keys, _curves); } // Clips: retime start/end of each selected clip. foreach (var clip in _canvas.ClipArea.EnumerateSelectedClips()) { _clips.Add(clip); _clipOrigStart.Add(clip.TimeRange.Start); _clipOrigEnd.Add(clip.TimeRange.End); } if (_clips.Count > 0) { _clipBuffer.Clear(); for (var i = 0; i < _clips.Count; i++) _clipBuffer.Add(_clips[i]); _clipsCommand = new MoveTimeClipsCommand(composition, _clipBuffer); } IsActive = true; } /// Apply this frame's remap. is the moving anchor's new U. public void Update(double currentU) { if (!IsActive) return; if (_pureTranslation) { var du = currentU - _origHandleU; for (var i = 0; i < _keys.Count; i++) _keys[i].U = _keyOrigU[i] + du; for (var i = 0; i < _clips.Count; i++) { var clip = _clips[i]; clip.TimeRange.Start = _clipOrigStart[i] + (float)du; clip.TimeRange.End = _clipOrigEnd[i] + (float)du; } if (_handlesCommand != null) { for (var i = 0; i < _origHandles.Count && i < _handles.Count; i++) _handles.SetValueUnsorted(i, _origHandles[i] + du); } AnimationParameterEditing.CurvesTablesNeedsRefresh = true; return; } for (var i = 0; i < _keys.Count; i++) _keys[i].U = RemapPiecewise(_keyOrigU[i], currentU); for (var i = 0; i < _clips.Count; i++) { var clip = _clips[i]; var newStart = (float)RemapPiecewise(_clipOrigStart[i], currentU); var newEnd = (float)RemapPiecewise(_clipOrigEnd[i], currentU); if (newEnd - newStart < 1 / 60f) newEnd = newStart + 1 / 60f; clip.TimeRange.Start = newStart; clip.TimeRange.End = newEnd; } AnimationParameterEditing.CurvesTablesNeedsRefresh = true; } /// Finalize and push the macro command onto the undo stack. public void Complete() { if (!IsActive) return; if (_keys.Count == 0 && _clips.Count == 0 && _handlesCommand == null) { Reset(); return; } var commands = new List(3); if (_keyframesCommand != null) { _keyframesCommand.StoreCurrentValues(); commands.Add(_keyframesCommand); } if (_clipsCommand != null) { _clipsCommand.StoreCurrentValues(); commands.Add(_clipsCommand); } if (_handlesCommand != null) { _handlesCommand.StoreCurrentValues(_handles); commands.Add(_handlesCommand); } if (commands.Count > 0) { var macro = new MacroCommand("TimeWarp", commands); UndoRedoStack.AddAndExecute(macro); } Reset(); } private bool InsideAffectedRange(double u) { if (_singleSegment) return u >= _segLeftU && u <= _segRightU; return u >= _prev && u <= _next; } private double RemapPiecewise(double t, double newU) { var origU = _origHandleU; if (_singleSegment) { // One-segment warp: either segLeft or segRight coincides with origU (the moving point). if (t < _segLeftU || t > _segRightU) return t; if (Math.Abs(_segLeftU - origU) < 1e-9) { // Left end is moving; right end fixed. var right = _segRightU; var lenOrig = right - origU; if (Math.Abs(lenOrig) < 1e-9) return t; var ratio = (t - origU) / lenOrig; return newU + ratio * (right - newU); } else { // Right end is moving; left end fixed. var left = _segLeftU; var lenOrig = origU - left; if (Math.Abs(lenOrig) < 1e-9) return t; var ratio = (t - left) / lenOrig; return left + ratio * (newU - left); } } // Two-segment warp: (prev, origU) and (origU, next). if (t < _prev || t > _next) return t; if (t <= origU) { var lenOrig = origU - _prev; if (Math.Abs(lenOrig) < 1e-9) return t; var ratio = (t - _prev) / lenOrig; return _prev + ratio * (newU - _prev); } else { var lenOrig = _next - origU; if (Math.Abs(lenOrig) < 1e-9) return t; var ratio = (t - origU) / lenOrig; return newU + ratio * (_next - newU); } } private void Reset() { _keys.Clear(); _keyOrigU.Clear(); _clips.Clear(); _clipOrigStart.Clear(); _clipOrigEnd.Clear(); _origHandles.Clear(); _keyframesCommand = null; _clipsCommand = null; _handlesCommand = null; _pureTranslation = false; _singleSegment = false; IsActive = false; } private readonly TimeLineCanvas _canvas; private readonly TimeWarpHandleSet _handles; private double _origHandleU; private double _prev; private double _next; private bool _singleSegment; private double _segLeftU; private double _segRightU; private bool _pureTranslation; private readonly List _keys = new(64); private readonly List _keyOrigU = new(64); private readonly List _clips = new(16); private readonly List _clipOrigStart = new(16); private readonly List _clipOrigEnd = new(16); private readonly List _origHandles = new(8); private readonly List _curves = new(32); private readonly List _clipBuffer = new(16); private ChangeKeyframesCommand? _keyframesCommand; private MoveTimeClipsCommand? _clipsCommand; private SetTimeWarpHandlesCommand? _handlesCommand; }