From e08722897ef95288ff4990b4b77d497dc1662843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=91=E5=88=9A?= <1219318552@qq.com> Date: Thu, 4 Jul 2024 21:37:36 +0800 Subject: [PATCH] =?UTF-8?q?=E9=94=9A=E7=82=B9=E6=B7=BB=E5=8A=A0=E4=B8=8E?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TuneLab/Data/IPiecewiseCurve.cs | 17 +- TuneLab/Data/PiecewiseCurve.cs | 296 +++++++++++++++++++++- TuneLab/Views/PianoScrollView.cs | 1 + TuneLab/Views/PianoScrollViewOperation.cs | 182 ++++++++++--- 4 files changed, 459 insertions(+), 37 deletions(-) diff --git a/TuneLab/Data/IPiecewiseCurve.cs b/TuneLab/Data/IPiecewiseCurve.cs index 62706cd..ed46327 100644 --- a/TuneLab/Data/IPiecewiseCurve.cs +++ b/TuneLab/Data/IPiecewiseCurve.cs @@ -17,10 +17,13 @@ internal interface IPiecewiseCurve : IDataObject ticks); void AddLine(IReadOnlyList points, double extend); void Clear(double start, double end); - void DeleteAnchors(double start, double end); + void InsertPoint(AnchorPoint point); + void DeletePoints(double start, double end); void DeletePoints(IReadOnlyList points); void DeleteAllSelectedAnchors(); - void RemoveAnchorGroupAt(int index); + void DeleteAnchorGroupAt(int index); + void ConnectAnchorGroup(int leftIndex); + void MoveSelectedPoints(double offsetPos, double offsetValue); new List> GetInfo(); @@ -97,11 +100,19 @@ public static AreaID GetAreaID(this IPiecewiseCurve piecewiseCurve, double pos) } if (anchorGroup.IsEmpty()) - piecewiseCurve.RemoveAnchorGroupAt(gi); + piecewiseCurve.DeleteAnchorGroupAt(gi); } return result; } + public static void SelectAllAnchors(this IPiecewiseCurve piecewiseCurve) + { + foreach (var anchorGroup in piecewiseCurve.AnchorGroups) + { + anchorGroup.SelectAllItems(); + } + } + public static void DeselectAllAnchors(this IPiecewiseCurve piecewiseCurve) { foreach (var anchorGroup in piecewiseCurve.AnchorGroups) diff --git a/TuneLab/Data/PiecewiseCurve.cs b/TuneLab/Data/PiecewiseCurve.cs index 096c213..3089fb1 100644 --- a/TuneLab/Data/PiecewiseCurve.cs +++ b/TuneLab/Data/PiecewiseCurve.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Net.Security; using System.Reflection; using System.Text; using TuneLab.Base.Data; @@ -166,7 +168,7 @@ public void Clear(double start, double end) Push(new RedoOnlyCommand(NotifyRangeModified)); } - public void RemoveAnchorGroupAt(int index) + public void DeleteAnchorGroupAt(int index) { var anchorGroup = mAnchorGroups[index]; var start = anchorGroup.Start; @@ -178,7 +180,7 @@ public void RemoveAnchorGroupAt(int index) Push(new RedoOnlyCommand(NotifyRangeModified)); } - public void DeleteAnchors(double s, double e) + public void DeletePoints(double s, double e) { for (int gi = AnchorGroups.Count - 1; gi >= 0; gi--) { @@ -301,6 +303,294 @@ public void DeletePoints(IReadOnlyList points) } } + public void InsertPoint(AnchorPoint point) + { + double start = point.Pos; + double end = point.Pos; + void NotifyRangeModified() => mRangeModified.Invoke(start, end); + Push(new UndoOnlyCommand(NotifyRangeModified)); + var areaID = this.GetAreaID(point.Pos); + if (areaID.IsInGroup) + { + var anchorGroup = mAnchorGroups[areaID.Index]; + + int pi = anchorGroup.Count - 1; + for (; pi >= 0; pi--) + { + var anchor = anchorGroup[pi]; + if (anchor.Pos > point.Pos) + continue; + + if (anchor.Pos == point.Pos) + anchorGroup.RemoveAt(pi); + else + pi++; + + break; + } + + anchorGroup.Insert(pi, point); + start = anchorGroup[(pi - 1).Limit(0, anchorGroup.Count - 1)].Pos; + end = anchorGroup[(pi + 1).Limit(0, anchorGroup.Count - 1)].Pos; + } + else + { + var leftHasSelectedAnchor = (uint)areaID.LeftIndex < mAnchorGroups.Count && mAnchorGroups[areaID.LeftIndex].HasSelectedItem(); + var rightHasSelectedAnchor = (uint)areaID.RightIndex < mAnchorGroups.Count && mAnchorGroups[areaID.RightIndex].HasSelectedItem(); + if (leftHasSelectedAnchor) + { + var anchorGroup = mAnchorGroups[areaID.LeftIndex]; + anchorGroup.Add(point); + start = anchorGroup[(anchorGroup.Count - 2).Limit(0, anchorGroup.Count - 1)].Pos; + end = anchorGroup[(anchorGroup.Count - 1).Limit(0, anchorGroup.Count - 1)].Pos; + if (rightHasSelectedAnchor) + { + ConnectAnchorGroup(areaID.LeftIndex); + } + } + else + { + if (rightHasSelectedAnchor) + { + var anchorGroup = mAnchorGroups[areaID.RightIndex]; + anchorGroup.Insert(0, point); + start = anchorGroup[0.Limit(0, anchorGroup.Count - 1)].Pos; + end = anchorGroup[1.Limit(0, anchorGroup.Count - 1)].Pos; + } + else + { + var anchorGroup = new T(); + anchorGroup.Add(point); + mAnchorGroups.Insert(areaID.RightIndex, anchorGroup); + } + } + } + + NotifyRangeModified(); + Push(new RedoOnlyCommand(NotifyRangeModified)); + } + + void InsertPointsToGroup(T anchorGroup, IReadOnlyList points) + { + if (points.IsEmpty()) + return; + + double start = anchorGroup.IsEmpty() ? double.PositiveInfinity : anchorGroup.End; + double end = anchorGroup.IsEmpty() ? double.NegativeInfinity : anchorGroup.Start; + bool hasDeleteAnchor = false; + void NotifyRangeModified() => mRangeModified.Invoke(start, end); + + if (anchorGroup.IsEmpty()) + { + start = points.ConstFirst().Pos; + start = points.ConstLast().Pos; + Push(new UndoOnlyCommand(NotifyRangeModified)); + anchorGroup.Set(points); + NotifyRangeModified(); + Push(new RedoOnlyCommand(NotifyRangeModified)); + return; + } + + int pi = anchorGroup.Count - 1; + for (int i = points.Count - 1; i >= 0; i--) + { + var anchor = points[i]; + while (pi >= 0 && anchor.Pos < anchorGroup[pi].Pos) + { + pi--; + } + + void Insert(int index) + { + if (!hasDeleteAnchor) + { + Push(new UndoOnlyCommand(NotifyRangeModified)); + hasDeleteAnchor = true; + } + + anchorGroup.Insert(index, anchor); + end = Math.Max(end, anchorGroup[(index + 1).Limit(0, anchorGroup.Count - 1)].Pos); + start = Math.Min(start, anchorGroup[(index - 1).Limit(0, anchorGroup.Count - 1)].Pos); + } + + if (pi >= 0 && anchorGroup[pi].Pos == anchor.Pos) + { + anchorGroup.RemoveAt(pi); + Insert(pi); + } + else + { + Insert(pi + 1); + } + } + if (hasDeleteAnchor) + { + NotifyRangeModified(); + Push(new RedoOnlyCommand(NotifyRangeModified)); + } + } + + public void ConnectAnchorGroup(int leftIndex) + { + if ((uint)leftIndex >= mAnchorGroups.Count - 1) + return; + + var leftAnchorGroup = mAnchorGroups[leftIndex]; + var rightAnchorGroup = mAnchorGroups[leftIndex + 1]; + + double start = leftAnchorGroup[(leftAnchorGroup.Count - 2).Limit(0, leftAnchorGroup.Count - 1)].Pos; + double end = rightAnchorGroup[1.Limit(0, rightAnchorGroup.Count - 1)].Pos; + void NotifyRangeModified() => mRangeModified.Invoke(start, end); + Push(new UndoOnlyCommand(NotifyRangeModified)); + + for (int i = rightAnchorGroup.Count - 1; i >= 0; i--) + { + var anchor = rightAnchorGroup[0]; + rightAnchorGroup.RemoveAt(0); + leftAnchorGroup.Add(anchor); + } + mAnchorGroups.RemoveAt(leftIndex + 1); + + NotifyRangeModified(); + Push(new RedoOnlyCommand(NotifyRangeModified)); + } + + public void MoveSelectedPoints(double offsetPos, double offsetValue) + { + void MoveSelectedPointsFromAnchorGroupTo(int gi, double offsetPos, double offsetValue) + { + double leftSide = gi == 0 ? double.NegativeInfinity : mAnchorGroups[gi - 1].End; + double rightSide = gi == mAnchorGroups.Count - 1 ? double.PositiveInfinity : mAnchorGroups[gi + 1].Start; + var anchorGroup = mAnchorGroups[gi]; + var selectedAnchors = new System.Collections.Generic.LinkedList(); + double start = anchorGroup.End; + double end = anchorGroup.Start; + bool hasSelectedAnchor = false; + void NotifyRangeModified() => mRangeModified.Invoke(start, end); + for (int pi = anchorGroup.Count - 1; pi >= 0; pi--) + { + if (anchorGroup[pi].IsSelected) + { + if (!hasSelectedAnchor) + { + Push(new UndoOnlyCommand(NotifyRangeModified)); + hasSelectedAnchor = true; + } + + end = Math.Max(end, anchorGroup[(pi + 1).Limit(0, anchorGroup.Count - 1)].Pos); + start = Math.Min(start, anchorGroup[(pi - 1).Limit(0, anchorGroup.Count - 1)].Pos); + selectedAnchors.AddFirst(anchorGroup[pi]); + anchorGroup.RemoveAt(pi); + } + } + if (anchorGroup.IsEmpty()) + mAnchorGroups.RemoveAt(gi); + + if (hasSelectedAnchor) + { + NotifyRangeModified(); + Push(new RedoOnlyCommand(NotifyRangeModified)); + + var selectionStart = selectedAnchors.First().Pos; + var selectionEnd = selectedAnchors.Last().Pos; + var movedStart = selectionStart + offsetPos; + var movedEnd = selectionEnd + offsetPos; + int startIndex = mAnchorGroups.Count - 1; + int endIndex = 0; + bool hasIntersect = false; + bool Intersect(T ag) + { + var start = ag == anchorGroup ? leftSide : ag.Start; + var end = ag == anchorGroup ? rightSide : ag.End; + return start < movedEnd && end > movedStart; + } + for (int i = 0; i < mAnchorGroups.Count; i++) + { + if (Intersect(mAnchorGroups[i])) + { + hasIntersect = true; + startIndex = Math.Min(startIndex, i); + endIndex = Math.Max(endIndex, i); + } + } + T insertAnchorGroup = null!; + if (hasIntersect) + { + for (int i = startIndex; i < endIndex; i++) + { + ConnectAnchorGroup(startIndex); + } + insertAnchorGroup = mAnchorGroups[startIndex]; + } + else + { + int insertIndex = 0; + for (; insertIndex < mAnchorGroups.Count; insertIndex++) + { + if (mAnchorGroups[insertIndex].Start > movedEnd) + break; + } + + insertAnchorGroup = new T(); + mAnchorGroups.Insert(insertIndex, insertAnchorGroup); + } + InsertPointsToGroup(insertAnchorGroup, selectedAnchors.Select(point => new AnchorPoint(point.Pos + offsetPos, point.Value + offsetValue) { IsSelected = true }).ToList()); + } + } + + if (offsetPos > 0) + { + var anchorGroups = mAnchorGroups.ToList(); + for (int i = anchorGroups.Count - 1; i >= 0; i--) + { + MoveSelectedPointsFromAnchorGroupTo(mAnchorGroups.IndexOf(anchorGroups[i]), offsetPos, offsetValue); + } + } + else if (offsetPos < 0) + { + var anchorGroups = mAnchorGroups.ToList(); + for (int i = 0; i < anchorGroups.Count; i++) + { + MoveSelectedPointsFromAnchorGroupTo(mAnchorGroups.IndexOf(anchorGroups[i]), offsetPos, offsetValue); + } + } + else + { + if (offsetValue == 0) + return; + + foreach (var anchorGroup in mAnchorGroups) + { + double start = anchorGroup.End; + double end = anchorGroup.Start; + bool hasSelectedAnchor = false; + void NotifyRangeModified() => mRangeModified.Invoke(start, end); + for (int pi = anchorGroup.Count - 1; pi >= 0; pi--) + { + var anchorPoint = anchorGroup[pi]; + if (anchorPoint.IsSelected) + { + if (!hasSelectedAnchor) + { + Push(new UndoOnlyCommand(NotifyRangeModified)); + hasSelectedAnchor = true; + } + + end = Math.Max(end, anchorGroup[(pi + 1).Limit(0, anchorGroup.Count - 1)].Pos); + start = Math.Min(start, anchorGroup[(pi - 1).Limit(0, anchorGroup.Count - 1)].Pos); + anchorGroup[pi] = new AnchorPoint(anchorPoint.Pos, anchorPoint.Value + offsetValue) { IsSelected = true }; + } + } + + if (hasSelectedAnchor) + { + NotifyRangeModified(); + Push(new RedoOnlyCommand(NotifyRangeModified)); + } + } + } + } + public List> GetInfo() { return mAnchorGroups.GetInfo().Select(anchorGroup => anchorGroup.GetInfo().Select(p => p.ToPoint()).ToList()).ToList(); @@ -349,7 +639,7 @@ public double[] GetValues(IReadOnlyList ticks) void IDataObject>>.SetInfo(IEnumerable> info) { - IDataObject>>.SetInfo(mAnchorGroups, info.Where(points => points.Count > 1).Convert(points => { var t = new T(); t.Set(points.Select(point => new AnchorPoint(point))); return t; }).ToArray()); + IDataObject>>.SetInfo(mAnchorGroups, info.Where(points => points.Count > 0).Convert(points => { var t = new T(); t.Set(points.Select(point => new AnchorPoint(point))); return t; }).ToArray()); } readonly DataObjectList mAnchorGroups = new(); diff --git a/TuneLab/Views/PianoScrollView.cs b/TuneLab/Views/PianoScrollView.cs index 83b8045..a49927b 100644 --- a/TuneLab/Views/PianoScrollView.cs +++ b/TuneLab/Views/PianoScrollView.cs @@ -74,6 +74,7 @@ public PianoScrollView(IDependency dependency) mSelectionOperation = new(this); mAnchorSelectOperation = new(this); mAnchorDeleteOperation = new(this); + mAnchorMoveOperation = new(this); mDependency.PartProvider.ObjectChanged.Subscribe(Update, s); mDependency.PartProvider.When(p => p.Modified).Subscribe(Update, s); diff --git a/TuneLab/Views/PianoScrollViewOperation.cs b/TuneLab/Views/PianoScrollViewOperation.cs index 2f53324..9a6d7d3 100644 --- a/TuneLab/Views/PianoScrollViewOperation.cs +++ b/TuneLab/Views/PianoScrollViewOperation.cs @@ -345,13 +345,27 @@ bool DetectWaveformPrimaryButton() case MouseButtonType.PrimaryButton: if (!DetectWaveformPrimaryButton()) { + if (Part == null) + break; + if (item is AnchorItem anchorItem) { - // Move + mAnchorMoveOperation.Down(e.Position, ctrl, anchorItem.AnchorPoint); } else { - mAnchorSelectOperation.Down(e.Position, ctrl); + if (e.IsDoubleClick || mPreviewPitch != null) + { + var anchor = new AnchorPoint(TickAxis.X2Tick(e.Position.X) - Part.Pos, PitchAxis.Y2Pitch(e.Position.Y) - 0.5) { IsSelected = true }; + Part.Pitch.InsertPoint(anchor); + Part.Pitch.DeselectAllAnchors(); + anchor.Select(); + mAnchorMoveOperation.Down(e.Position, ctrl, anchor); + } + else + { + mAnchorSelectOperation.Down(e.Position, ctrl); + } } } break; @@ -592,6 +606,9 @@ protected override void OnMouseRelativeMoveToView(MouseMoveEventArgs e) case State.AnchorDeleting: mAnchorDeleteOperation.Move(e.Position.X); break; + case State.AnchorMoving: + mAnchorMoveOperation.Move(e.Position); + break; default: var item = ItemAt(e.Position); if (item is WaveformNoteResizeItem || item is WaveformPhonemeResizeItem) @@ -739,6 +756,10 @@ protected override void OnMouseUp(MouseUpEventArgs e) if (e.MouseButtonType == MouseButtonType.SecondaryButton) mAnchorDeleteOperation.Up(); break; + case State.AnchorMoving: + if (e.MouseButtonType == MouseButtonType.PrimaryButton) + mAnchorMoveOperation.Up(); + break; default: break; } @@ -875,6 +896,7 @@ protected override void OnKeyUpEvent(KeyEventArgs e) protected override void UpdateItems(IItemCollection items) { + mPreviewPitch = null; if (Part == null) return; @@ -950,34 +972,32 @@ protected override void UpdateItems(IItemCollection items) if (mState != State.None) break; - var areaID = Part.Pitch.GetAreaID(TickAxis.X2Tick(MousePosition.X) - Part.Pos); - var previewInfo = new List>(); - if (areaID.IsInGroup) - { - previewInfo.Add(Part.Pitch.AnchorGroups[areaID.Index].GetInfo().Select(p => p.ToPoint()).ToList()); - } - else - { - if (areaID.LeftIndex >= 0) - { - var anchorGroup = Part.Pitch.AnchorGroups[areaID.LeftIndex]; - if (anchorGroup.HasSelectedItem()) - previewInfo.Add(anchorGroup.GetInfo().Select(p => p.ToPoint()).ToList()); - } - if (areaID.RightIndex < Part.Pitch.AnchorGroups.Count) - { - var anchorGroup = Part.Pitch.AnchorGroups[areaID.RightIndex]; - if (anchorGroup.HasSelectedItem()) - previewInfo.Add(anchorGroup.GetInfo().Select(p => p.ToPoint()).ToList()); - } - } + if (!IsHover) + break; + + if (HoverItem() != null) + break; + + var pos = TickAxis.X2Tick(MousePosition.X) - Part.Pos; + var areaID = Part.Pitch.GetAreaID(pos); + int[] previewIndex = areaID.IsInGroup ? [areaID.Index] : [areaID.LeftIndex, areaID.RightIndex]; + var previewInfo = previewIndex + .Where(index => (uint)index < Part.Pitch.AnchorGroups.Count) + .Select(index => Part.Pitch.AnchorGroups[index]) + .Where(anchorGroup => anchorGroup.HasSelectedItem()) + .Select(anchorGroup => anchorGroup.GetInfo().Select(p => p.ToPoint()).ToList()).ToList(); if (previewInfo.Count == 0) break; - var previewAnchorGroups = new PiecewiseCurve(); - previewAnchorGroups.Set(previewInfo); - items.Add(new PreviewAnchorGroupItem(this) { PiecewiseCurve = previewAnchorGroups }); + mPreviewPitch = new PiecewiseCurve(); + mPreviewPitch.Set(previewInfo); + foreach (var anchorGroup in mPreviewPitch.AnchorGroups) + { + anchorGroup[0].Select(); + } + mPreviewPitch.InsertPoint(new AnchorPoint(pos, PitchAxis.Y2Pitch(MousePosition.Y) - 0.5)); + items.Add(new PreviewAnchorGroupItem(this) { PiecewiseCurve = mPreviewPitch }); break; default: @@ -1032,6 +1052,16 @@ protected override void UpdateItems(IItemCollection items) } } + protected override void OnMouseEnter(MouseEnterEventArgs e) + { + InvalidateVisual(); + } + + protected override void OnMouseLeave() + { + InvalidateVisual(); + } + class Operation(PianoScrollView pianoScrollView) { public PianoScrollView PianoScrollView => pianoScrollView; @@ -2288,7 +2318,6 @@ public void Move(Avalonia.Point point, bool alt) mLastPosOffset = posOffset; mMoved = true; part.DiscardTo(mHead); - part.BeginMergeReSegment(); foreach (var vibrato in mMoveVibratos) { vibrato.Pos.Set(vibrato.Pos.Value + posOffset); @@ -2299,7 +2328,6 @@ public void Move(Avalonia.Point point, bool alt) { part.InsertVibrato(vibrato); } - part.EndMergeReSegment(); } public void Up() @@ -2374,7 +2402,7 @@ public void Down(double x) double tick = PianoScrollView.TickAxis.X2Tick(x) - PianoScrollView.Part.Pos; mStart = tick; mEnd = tick; - PianoScrollView.Part.Pitch.DeleteAnchors(mStart, mEnd); + PianoScrollView.Part.Pitch.DeletePoints(mStart, mEnd); } public void Move(double x) @@ -2389,7 +2417,7 @@ public void Move(double x) double tick = PianoScrollView.TickAxis.X2Tick(x) - PianoScrollView.Part.Pos; mStart = Math.Min(mStart, tick); mEnd = Math.Max(mEnd, tick); - PianoScrollView.Part.Pitch.DeleteAnchors(mStart, mEnd); + PianoScrollView.Part.Pitch.DeletePoints(mStart, mEnd); } public void Up() @@ -2403,7 +2431,7 @@ public void Up() return; PianoScrollView.Part.Pitch.DiscardTo(mHead); - PianoScrollView.Part.Pitch.DeleteAnchors(mStart, mEnd); + PianoScrollView.Part.Pitch.DeletePoints(mStart, mEnd); PianoScrollView.Part.EndMergeDirty(); PianoScrollView.Part.Pitch.Commit(); } @@ -2415,6 +2443,96 @@ public void Up() readonly AnchorDeleteOperation mAnchorDeleteOperation; + class AnchorMoveOperation(PianoScrollView pianoScrollView) : Operation(pianoScrollView) + { + public void Down(Avalonia.Point point, bool ctrl, AnchorPoint anchor) + { + if (PianoScrollView.Part == null) + return; + + mCtrl = ctrl; + mIsSelected = anchor.IsSelected; + if (!mCtrl && !mIsSelected) + { + PianoScrollView.Part.Pitch.DeselectAllAnchors(); + } + anchor.Select(); + + State = State.AnchorMoving; + PianoScrollView.Part.DisableAutoPrepare(); + mHead = PianoScrollView.Part.Head; + mAnchor = anchor; + mXOffset = point.X - PianoScrollView.TickAxis.Tick2X(PianoScrollView.Part.Pos + anchor.Pos); + mYOffset = point.Y - PianoScrollView.PitchAxis.Pitch2Y(anchor.Value + 0.5); + } + + public void Move(Avalonia.Point point) + { + var part = PianoScrollView.Part; + if (part == null) + return; + + if (mAnchor == null) + return; + + double pos = PianoScrollView.TickAxis.X2Tick(point.X - mXOffset) - part.Pos; + double posOffset = pos - mAnchor.Pos; + double pitch = PianoScrollView.PitchAxis.Y2Pitch(point.Y - mYOffset) - 0.5; + double pitchOffset = pitch - mAnchor.Value; + + mMoved = true; + part.DiscardTo(mHead); + part.Pitch.MoveSelectedPoints(posOffset, pitchOffset); + } + + public void Up() + { + State = State.None; + + if (mAnchor == null) + return; + + if (PianoScrollView.Part == null) + return; + + PianoScrollView.Part.EnableAutoPrepare(); + if (mMoved) + { + PianoScrollView.Part.Commit(); + } + else + { + PianoScrollView.Part.Discard(); + if (mCtrl) + { + if (mIsSelected) + { + mAnchor.Inselect(); + } + } + else + { + PianoScrollView.Part.Pitch.DeselectAllAnchors(); + mAnchor.Select(); + } + } + mMoved = false; + mAnchor = null; + } + + AnchorPoint? mAnchor; + + bool mCtrl; + bool mIsSelected; + bool mMoved = false; + double mXOffset; + double mYOffset; + + Head mHead; + } + + readonly AnchorMoveOperation mAnchorMoveOperation; + class AnchorSelectOperation(PianoScrollView pianoScrollView) : SelectOperation(pianoScrollView) { protected override State SelectState => State.AnchorSelecting; @@ -2666,6 +2784,8 @@ public void Up() readonly SelectionOperation mSelectionOperation; + IPiecewiseCurve? mPreviewPitch = null; + public enum State { None,