Browse Source

Undo/Redo for transform overlay

Equbuxu 2 years ago
parent
commit
a40505b7e1

+ 19 - 0
src/ChunkyImageLib/DataHolders/ShapeCorners.cs

@@ -149,4 +149,23 @@ public struct ShapeCorners
         TopLeft = TopLeft.Rotate(angle, around),
         TopRight = TopRight.Rotate(angle, around)
     };
+
+    public static bool operator !=(ShapeCorners left, ShapeCorners right) => !(left == right);
+    public static bool operator == (ShapeCorners left, ShapeCorners right)
+    {
+        return 
+           left.TopLeft == right.TopLeft &&
+           left.TopRight == right.TopRight &&
+           left.BottomLeft == right.BottomLeft &&
+           left.BottomRight == right.BottomRight;
+    }
+
+    public bool AlmostEquals(ShapeCorners other, double epsilon = 0.001)
+    {
+        return
+            TopLeft.AlmostEquals(other.TopLeft, epsilon) &&
+            TopRight.AlmostEquals(other.TopRight, epsilon) &&
+            BottomLeft.AlmostEquals(other.BottomLeft, epsilon) &&
+            BottomRight.AlmostEquals(other.BottomRight, epsilon);
+    }
 }

+ 7 - 0
src/PixiEditor.DrawingApi.Core/Numerics/VecD.cs

@@ -224,4 +224,11 @@ public struct VecD : IEquatable<VecD>
     {
         return other.X == X && other.Y == Y;
     }
+
+    public bool AlmostEquals(VecD other, double axisEpsilon = 0.001)
+    {
+        double dX = Math.Abs(X - other.X);
+        double dY = Math.Abs(Y - other.Y);
+        return dX < axisEpsilon && dY < axisEpsilon;
+    }
 }

+ 4 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -45,6 +45,10 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
         internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid, false, drawOnMask));
     }
 
+    public override void OnMidChangeUndo() => document!.TransformViewModel.Undo();
+
+    public override void OnMidChangeRedo() => document!.TransformViewModel.Redo();
+
     public override void OnTransformApplied()
     {
         internals!.ActionAccumulator.AddFinishedActions(new EndPasteImage_Action());

+ 14 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShapeToolExecutor.cs

@@ -114,6 +114,20 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
         onEnded?.Invoke(this);
     }
 
+    public override void OnMidChangeUndo()
+    {
+        if (!transforming)
+            return;
+        document!.TransformViewModel.Undo();
+    }
+
+    public override void OnMidChangeRedo()
+    {
+        if (!transforming)
+            return;
+        document!.TransformViewModel.Redo();
+    }
+
     public override void OnPixelPositionChange(VecI pos)
     {
         if (transforming)

+ 4 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformReferenceLayerExecutor.cs

@@ -25,6 +25,10 @@ internal class TransformReferenceLayerExecutor : UpdateableChangeExecutor
         internals!.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(corners));
     }
 
+    public override void OnMidChangeUndo() => document!.TransformViewModel.Undo();
+
+    public override void OnMidChangeRedo() => document!.TransformViewModel.Redo();
+
     public override void OnTransformApplied()
     {
         internals!.ActionAccumulator.AddFinishedActions(new EndTransformReferenceLayer_Action());

+ 4 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs

@@ -45,6 +45,10 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
             new TransformSelectedArea_Action(membersToTransform!, corners, tool!.KeepOriginalImage, false));
     }
 
+    public override void OnMidChangeUndo() => document!.TransformViewModel.Undo();
+
+    public override void OnMidChangeRedo() => document!.TransformViewModel.Redo();
+
     public override void OnTransformApplied()
     {
         if (Type == ExecutorType.ToolLinked)

+ 59 - 1
src/PixiEditor/ViewModels/SubViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs

@@ -1,5 +1,7 @@
-using ChunkyImageLib.DataHolders;
+using System.Windows.Input;
+using ChunkyImageLib.DataHolders;
 using PixiEditor;
+using PixiEditor.Helpers;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels.SubViewModels;
@@ -11,6 +13,8 @@ namespace PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
 #nullable enable
 internal class DocumentTransformViewModel : NotifyableObject
 {
+    private TransformOverlayUndoStack<(ShapeCorners, TransformState)>? undoStack = null;
+
     private TransformState internalState;
     public TransformState InternalState
     {
@@ -90,18 +94,70 @@ internal class DocumentTransformViewModel : NotifyableObject
         }
     }
 
+    private ICommand? actionCompletedCommand = null;
+    public ICommand? ActionCompletedCommand
+    {
+        get => actionCompletedCommand;
+        set => SetProperty(ref actionCompletedCommand, value);
+    }
+
     public event EventHandler<ShapeCorners>? TransformMoved;
 
     private DocumentTransformMode activeTransformMode = DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective;
 
+    public DocumentTransformViewModel()
+    {
+        ActionCompletedCommand = new RelayCommand((_) =>
+        {
+            if (undoStack is null)
+                return;
+
+            var lastState = undoStack.PeekCurrent();
+            if (lastState is not null && lastState.Value.Item1.AlmostEquals(Corners) && lastState.Value.Item2.AlmostEquals(InternalState))
+                return;
+
+            undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Move);
+        });
+    }
+
+    public bool Undo()
+    {
+        if (undoStack is null)
+            return false;
+        var state = undoStack.Undo();
+        if (state is null)
+            return false;
+        (Corners, InternalState) = state.Value;
+        return true;
+    }
+
+    public bool Redo()
+    {
+        if (undoStack is null)
+            return false;
+        var state = undoStack.Redo();
+        if (state is null)
+            return false;
+        (Corners, InternalState) = state.Value;
+        return true;
+    }
+
     public void HideTransform()
     {
+        if (undoStack is null)
+            return;
+        undoStack = null;
+
         TransformActive = false;
         ShowTransformControls = false;
     }
 
     public void ShowTransform(DocumentTransformMode mode, bool coverWholeScreen, ShapeCorners initPos, bool showApplyButton)
     {
+        if (undoStack is not null)
+            return;
+        undoStack = new();
+
         activeTransformMode = mode;
         CornerFreedom = TransformCornerFreedom.Scale;
         SideFreedom = TransformSideFreedom.Stretch;
@@ -110,6 +166,8 @@ internal class DocumentTransformViewModel : NotifyableObject
         CoverWholeScreen = coverWholeScreen;
         TransformActive = true;
         ShowTransformControls = showApplyButton;
+
+        undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Initial);
     }
 
     public void ModifierKeysInlet(bool isShiftDown, bool isCtrlDown, bool isAltDown)

+ 3 - 1
src/PixiEditor/ViewModels/SubViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs

@@ -62,6 +62,8 @@ internal class LineToolOverlayViewModel : NotifyableObject
 
     public void Show(VecD lineStart, VecD lineEnd)
     {
+        if (undoStack is not null)
+            return;
         undoStack = new();
         undoStack.AddState((lineStart, lineEnd), TransformOverlayStateType.Initial);
 
@@ -74,7 +76,7 @@ internal class LineToolOverlayViewModel : NotifyableObject
     {
         if (undoStack is null)
             return;
-
+        undoStack = null;
         IsEnabled = false;
     }
 

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/TransformOverlays/TransformOverlayUndoStack.cs

@@ -33,6 +33,8 @@ internal class TransformOverlayUndoStack<TState> where TState : struct
         current = new(state, type);
     }
 
+    public TState? PeekCurrent() => current?.State;
+
     public TState? Undo()
     {
         if (current is null || undoStack.Count == 0)

+ 22 - 3
src/PixiEditor/Views/UserControls/Overlays/TransformOverlay/TransformOverlay.cs

@@ -47,6 +47,16 @@ internal class TransformOverlay : Decorator
         DependencyProperty.Register(nameof(CoverWholeScreen), typeof(bool), typeof(TransformOverlay),
             new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender));
 
+
+    public static readonly DependencyProperty ActionCompletedProperty =
+        DependencyProperty.Register(nameof(ActionCompleted), typeof(ICommand), typeof(TransformOverlay), new(null));
+
+    public ICommand? ActionCompleted
+    {
+        get => (ICommand?)GetValue(ActionCompletedProperty);
+        set => SetValue(ActionCompletedProperty, value);
+    }
+
     public bool CoverWholeScreen
     {
         get => (bool)GetValue(ConverWholeScreenProperty);
@@ -428,25 +438,34 @@ internal class TransformOverlay : Decorator
         base.OnMouseUp(e);
         if (e.ChangedButton != MouseButton.Left)
             return;
+
+        bool handled = false;
         if (ReleaseAnchor())
         {
-            e.Handled = true;
+            handled = true;
         }
         else if (isMoving)
         {
             isMoving = false;
-            e.Handled = true;
+            handled = true;
             ReleaseMouseCapture();
         }
         else if (isRotating)
         {
             isRotating = false;
-            e.Handled = true;
+            handled = true;
             ReleaseMouseCapture();
             Cursor = Cursors.Arrow;
             var pos = TransformHelper.ToVecD(e.GetPosition(this));
             UpdateRotationCursor(pos);
         }
+
+        if (handled)
+        {
+            e.Handled = true;
+            if (ActionCompleted is not null && ActionCompleted.CanExecute(null))
+                ActionCompleted.Execute(null);
+        }
     }
 
     private static void OnRequestedCorners(DependencyObject obj, DependencyPropertyChangedEventArgs args)

+ 11 - 1
src/PixiEditor/Views/UserControls/Overlays/TransformOverlay/TransformState.cs

@@ -1,4 +1,5 @@
-using ChunkyImageLib.DataHolders;
+using System.CodeDom;
+using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
 
 #nullable enable
@@ -9,4 +10,13 @@ internal struct TransformState
     public VecD Origin { get; set; }
     public double ProportionalAngle1 { get; set; }
     public double ProportionalAngle2 { get; set; }
+
+    public bool AlmostEquals(TransformState other, double epsilon = 0.001)
+    {
+        return
+            OriginWasManuallyDragged == other.OriginWasManuallyDragged &&
+            other.Origin.AlmostEquals(Origin, epsilon) &&
+            Math.Abs(ProportionalAngle1 - other.ProportionalAngle1) < epsilon &&
+            Math.Abs(ProportionalAngle2 - other.ProportionalAngle2) < epsilon;
+    }
 }

+ 1 - 0
src/PixiEditor/Views/UserControls/Viewport.xaml

@@ -200,6 +200,7 @@
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch"
                         Visibility="{Binding Document.TransformViewModel.TransformActive, Converter={converters:BoolToVisibilityConverter}}"
+                        ActionCompleted="{Binding Document.TransformViewModel.ActionCompletedCommand}"
                         Corners="{Binding Document.TransformViewModel.Corners, Mode=TwoWay}"
                         RequestedCorners="{Binding Document.TransformViewModel.RequestedCorners, Mode=TwoWay}"
                         CornerFreedom="{Binding Document.TransformViewModel.CornerFreedom}"