Browse Source

Implement undo/redo for the line tool, fix line tool overlay not showing up in the right place

Equbuxu 2 years ago
parent
commit
87ec0afb10

+ 1 - 1
src/PixiEditor.DrawingApi.Core/Bridge/DrawingBackendApi.cs

@@ -5,7 +5,7 @@ namespace PixiEditor.DrawingApi.Core.Bridge
 {
 {
     public static class DrawingBackendApi
     public static class DrawingBackendApi
     {
     {
-        private static IDrawingBackend _current;
+        private static IDrawingBackend? _current;
 
 
         public static IDrawingBackend Current
         public static IDrawingBackend Current
         {
         {

+ 3 - 0
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -89,6 +89,9 @@ internal class ChangeExecutionController
         return true;
         return true;
     }
     }
 
 
+    public void MidChangeUndoInlet() => currentSession?.OnMidChangeUndo();
+    public void MidChangeRedoInlet() => currentSession?.OnMidChangeRedo();
+
     public void ConvertedKeyDownInlet(Key key)
     public void ConvertedKeyDownInlet(Key key)
     {
     {
         currentSession?.OnConvertedKeyDown(key);
         currentSession?.OnConvertedKeyDown(key);

+ 6 - 0
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -210,14 +210,20 @@ internal class DocumentOperationsModule
     public void Undo()
     public void Undo()
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
+        {
+            Internals.ChangeController.MidChangeUndoInlet();
             return;
             return;
+        }
         Internals.ActionAccumulator.AddActions(new Undo_Action());
         Internals.ActionAccumulator.AddActions(new Undo_Action());
     }
     }
 
 
     public void Redo()
     public void Redo()
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
+        {
+            Internals.ChangeController.MidChangeRedoInlet();
             return;
             return;
+        }
         Internals.ActionAccumulator.AddActions(new Redo_Action());
         Internals.ActionAccumulator.AddActions(new Redo_Action());
     }
     }
 
 

+ 22 - 10
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineToolExecutor.cs

@@ -55,11 +55,11 @@ internal class LineToolExecutor : UpdateableChangeExecutor
         if (transforming)
         if (transforming)
             return;
             return;
         started = true;
         started = true;
-        curPos = pos;
-        VecI targetPos = pos;
+
         if (toolViewModel!.Snap)
         if (toolViewModel!.Snap)
-            targetPos = ShapeToolExecutor<ShapeTool>.Get45IncrementedPosition(startPos, curPos);
-        internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, startPos, targetPos, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask));
+            pos = ShapeToolExecutor<ShapeTool>.Get45IncrementedPosition(startPos, pos);
+        curPos = pos;
+        internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, startPos, pos, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask));
     }
     }
 
 
     public override void OnLeftMouseButtonUp()
     public override void OnLeftMouseButtonUp()
@@ -69,10 +69,8 @@ internal class LineToolExecutor : UpdateableChangeExecutor
             onEnded!(this);
             onEnded!(this);
             return;
             return;
         }
         }
-        
-        document!.LineToolOverlayViewModel.LineStart = startPos + new VecD(0.5);
-        document!.LineToolOverlayViewModel.LineEnd = curPos + new VecD(0.5);
-        document!.LineToolOverlayViewModel.IsEnabled = true;
+
+        document!.LineToolOverlayViewModel.Show(startPos + new VecD(0.5), curPos + new VecD(0.5));
         transforming = true;
         transforming = true;
     }
     }
 
 
@@ -83,12 +81,26 @@ internal class LineToolExecutor : UpdateableChangeExecutor
         internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, (VecI)start, (VecI)end, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask));
         internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, (VecI)start, (VecI)end, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask));
     }
     }
 
 
+    public override void OnMidChangeUndo()
+    {
+        if (!transforming)
+            return;
+        document!.LineToolOverlayViewModel.Undo();
+    }
+
+    public override void OnMidChangeRedo()
+    {
+        if (!transforming)
+            return;
+        document!.LineToolOverlayViewModel.Redo();
+    }
+
     public override void OnTransformApplied()
     public override void OnTransformApplied()
     {
     {
         if (!transforming)
         if (!transforming)
             return;
             return;
 
 
-        document!.LineToolOverlayViewModel.IsEnabled = false;
+        document!.LineToolOverlayViewModel.Hide();
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
         onEnded!(this);
         onEnded!(this);
     }
     }
@@ -96,7 +108,7 @@ internal class LineToolExecutor : UpdateableChangeExecutor
     public override void ForceStop()
     public override void ForceStop()
     {
     {
         if (transforming)
         if (transforming)
-            document!.LineToolOverlayViewModel.IsEnabled = false;
+            document!.LineToolOverlayViewModel.Hide();
 
 
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
     }
     }

+ 2 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs

@@ -47,4 +47,6 @@ internal abstract class UpdateableChangeExecutor
     public virtual void OnTransformMoved(ShapeCorners corners) { }
     public virtual void OnTransformMoved(ShapeCorners corners) { }
     public virtual void OnTransformApplied() { }
     public virtual void OnTransformApplied() { }
     public virtual void OnLineOverlayMoved(VecD start, VecD end) { }
     public virtual void OnLineOverlayMoved(VecD start, VecD end) { }
+    public virtual void OnMidChangeUndo() { }
+    public virtual void OnMidChangeRedo() { }
 }
 }

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -20,6 +20,7 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
 using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using Colors = PixiEditor.DrawingApi.Core.ColorsImpl.Colors;
 using Colors = PixiEditor.DrawingApi.Core.ColorsImpl.Colors;

+ 0 - 41
src/PixiEditor/ViewModels/SubViewModels/Document/LineToolOverlayViewModel.cs

@@ -1,41 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using PixiEditor.DrawingApi.Core.Numerics;
-
-namespace PixiEditor.ViewModels.SubViewModels.Document;
-internal class LineToolOverlayViewModel : NotifyableObject
-{
-    public event EventHandler<(VecD, VecD)> LineMoved;
-
-    private VecD lineStart;
-    public VecD LineStart
-    {
-        get => lineStart;
-        set 
-        {
-            if (SetProperty(ref lineStart, value))
-                LineMoved?.Invoke(this, (lineStart, lineEnd));
-        }
-    }
-
-    private VecD lineEnd;
-    public VecD LineEnd
-    {
-        get => lineEnd;
-        set
-        {
-            if (SetProperty(ref lineEnd, value))
-                LineMoved?.Invoke(this, (lineStart, lineEnd));
-        }
-    }
-
-    private bool isEnabled;
-    public bool IsEnabled
-    {
-        get => isEnabled;
-        set => SetProperty(ref isEnabled, value);
-    }
-}

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

@@ -1,8 +1,13 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using PixiEditor;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels;
+using PixiEditor.ViewModels.SubViewModels;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
 using PixiEditor.Views.UserControls.Overlays.TransformOverlay;
 using PixiEditor.Views.UserControls.Overlays.TransformOverlay;
 
 
-namespace PixiEditor.ViewModels.SubViewModels.Document;
+namespace PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
 #nullable enable
 #nullable enable
 internal class DocumentTransformViewModel : NotifyableObject
 internal class DocumentTransformViewModel : NotifyableObject
 {
 {

+ 106 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs

@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using PixiEditor;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Helpers;
+using PixiEditor.ViewModels;
+using PixiEditor.ViewModels.SubViewModels;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
+
+namespace PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
+internal class LineToolOverlayViewModel : NotifyableObject
+{
+    public event EventHandler<(VecD, VecD)>? LineMoved;
+
+    private TransformOverlayUndoStack<(VecD, VecD)>? undoStack = null;
+
+    private VecD lineStart;
+    public VecD LineStart
+    {
+        get => lineStart;
+        set
+        {
+            if (SetProperty(ref lineStart, value))
+                LineMoved?.Invoke(this, (lineStart, lineEnd));
+        }
+    }
+
+    private VecD lineEnd;
+    public VecD LineEnd
+    {
+        get => lineEnd;
+        set
+        {
+            if (SetProperty(ref lineEnd, value))
+                LineMoved?.Invoke(this, (lineStart, lineEnd));
+        }
+    }
+
+    private bool isEnabled;
+    public bool IsEnabled
+    {
+        get => isEnabled;
+        set => SetProperty(ref isEnabled, value);
+    }
+
+    private ICommand? actionCompletedCommand = null;
+    public ICommand? ActionCompletedCommand
+    {
+        get => actionCompletedCommand;
+        set => SetProperty(ref actionCompletedCommand, value);
+    }
+
+    public LineToolOverlayViewModel()
+    {
+        ActionCompletedCommand = new RelayCommand((_) => undoStack?.AddState((LineStart, LineEnd), TransformOverlayStateType.Move));
+    }
+
+    public void Show(VecD lineStart, VecD lineEnd)
+    {
+        undoStack = new();
+        undoStack.AddState((lineStart, lineEnd), TransformOverlayStateType.Initial);
+
+        LineStart = lineStart;
+        LineEnd = lineEnd;
+        IsEnabled = true;
+    }
+
+    public void Hide()
+    {
+        if (undoStack is null)
+            return;
+
+        IsEnabled = false;
+    }
+
+    public bool Undo()
+    {
+        if (undoStack is null)
+            return false;
+
+        var newState = undoStack.Undo();
+        if (newState is null)
+            return false;
+        LineStart = newState.Value.Item1;
+        LineEnd = newState.Value.Item2;
+        return true;
+    }
+
+    public bool Redo()
+    {
+        if (undoStack is null)
+            return false;
+
+        var newState = undoStack.Redo();
+        if (newState is null)
+            return false;
+        LineStart = newState.Value.Item1;
+        LineEnd = newState.Value.Item2;
+        return true;
+    }
+}

+ 28 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/TransformOverlays/TransformOverlayActionType.cs

@@ -0,0 +1,28 @@
+namespace PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
+internal enum TransformOverlayStateType
+{
+    /// <summary>
+    /// The overlay was properly moved via mouse
+    /// </summary>
+    Move,
+
+    /// <summary>
+    /// The overlay was nudged using arrows keys
+    /// </summary>
+    Nudge,
+
+    /// <summary>
+    /// The overlay was set to this state when it was enabled
+    /// </summary>
+    Initial
+}
+
+internal static class TransformOverlayStateTypeEx
+{
+    public static bool IsMergeable(this TransformOverlayStateType type) => type switch
+    {
+        TransformOverlayStateType.Move => false,
+        TransformOverlayStateType.Nudge => true,
+        TransformOverlayStateType.Initial => false
+    };
+}

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

@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Documents;
+
+namespace PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
+internal class TransformOverlayUndoStack<TState> where TState : struct
+{
+    private struct StackItem<TState>
+    {
+        public TState State { get; set; }
+        public TransformOverlayStateType Type { get; set; }
+
+        public StackItem(TState state, TransformOverlayStateType type)
+        {
+            State = state;
+            Type = type;
+        }
+    }
+
+    private Stack<StackItem<TState>> undoStack = new();
+    private Stack<StackItem<TState>> redoStack = new();
+    private StackItem<TState>? current;
+
+    public void AddState(TState state, TransformOverlayStateType type)
+    {
+        redoStack.Clear();
+        if (current is not null)
+            undoStack.Push(current.Value);
+
+        current = new(state, type);
+    }
+
+    public TState? Undo()
+    {
+        if (current is null || undoStack.Count == 0)
+            return null;
+
+        while (true)
+        {
+            TransformOverlayStateType oldType = current.Value.Type;
+            DoUndoStep();
+            TransformOverlayStateType newType = current.Value.Type;
+            if (oldType != newType || !oldType.IsMergeable() || undoStack.Count == 0)
+                break;
+        }
+        return current.Value.State;
+    }
+
+    public TState? Redo()
+    {
+        if (current is null || redoStack.Count == 0)
+            return null;
+
+        while (true)
+        {
+            TransformOverlayStateType oldType = current.Value.Type;
+            DoRedoStep();
+            TransformOverlayStateType newType = current.Value.Type;
+            if (oldType != newType || !oldType.IsMergeable() || redoStack.Count == 0)
+                break;
+        }
+        return current.Value.State;
+    }
+
+    private void DoUndoStep()
+    {
+        redoStack.Push(current.Value);
+        current = undoStack.Pop();
+    }
+
+    private void DoRedoStep()
+    {
+        undoStack.Push(current.Value);
+        current = redoStack.Pop();
+    }
+}

+ 4 - 4
src/PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs

@@ -21,7 +21,7 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
     public void Redo()
     public void Redo()
     {
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
-        if (doc is null || doc.UpdateableChangeActive || !doc.HasSavedRedo)
+        if (doc is null || (!doc.UpdateableChangeActive && !doc.HasSavedRedo))
             return;
             return;
         doc.Operations.Redo();
         doc.Operations.Redo();
     }
     }
@@ -34,7 +34,7 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
     public void Undo()
     public void Undo()
     {
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
-        if (doc is null || doc.UpdateableChangeActive || !doc.HasSavedUndo)
+        if (doc is null || (!doc.UpdateableChangeActive && !doc.HasSavedUndo))
             return;
             return;
         doc.Operations.Undo();
         doc.Operations.Undo();
     }
     }
@@ -50,7 +50,7 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
         if (doc is null)
             return false;
             return false;
-        return !doc.UpdateableChangeActive && doc.HasSavedUndo;
+        return doc.UpdateableChangeActive || doc.HasSavedUndo;
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -64,6 +64,6 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
         if (doc is null)
             return false;
             return false;
-        return !doc.UpdateableChangeActive && doc.HasSavedRedo;
+        return doc.UpdateableChangeActive || doc.HasSavedRedo;
     }
     }
 }
 }

+ 16 - 3
src/PixiEditor/Views/UserControls/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -22,6 +22,15 @@ internal class LineToolOverlay : Control
         DependencyProperty.Register(nameof(LineEnd), typeof(VecD), typeof(LineToolOverlay),
         DependencyProperty.Register(nameof(LineEnd), typeof(VecD), typeof(LineToolOverlay),
             new FrameworkPropertyMetadata(VecD.Zero, FrameworkPropertyMetadataOptions.AffectsRender));
             new FrameworkPropertyMetadata(VecD.Zero, FrameworkPropertyMetadataOptions.AffectsRender));
 
 
+    public static readonly DependencyProperty ActionCompletedProperty =
+        DependencyProperty.Register(nameof(ActionCompleted), typeof(ICommand), typeof(LineToolOverlay), new(null));
+
+    public ICommand? ActionCompleted
+    {
+        get => (ICommand)GetValue(ActionCompletedProperty);
+        set => SetValue(ActionCompletedProperty, value);
+    }
+
     public VecD LineEnd
     public VecD LineEnd
     {
     {
         get => (VecD)GetValue(LineEndProperty);
         get => (VecD)GetValue(LineEndProperty);
@@ -48,6 +57,7 @@ internal class LineToolOverlay : Control
 
 
     private LineToolOverlayAnchor? capturedAnchor = null;
     private LineToolOverlayAnchor? capturedAnchor = null;
     private bool dragging = false;
     private bool dragging = false;
+    private bool movedWhileMouseDown = false;
 
 
     private PathGeometry handleGeometry = new()
     private PathGeometry handleGeometry = new()
     {
     {
@@ -109,6 +119,7 @@ internal class LineToolOverlay : Control
             capturedAnchor = LineToolOverlayAnchor.End;
             capturedAnchor = LineToolOverlayAnchor.End;
         else if (TransformHelper.IsWithinTransformHandle(handlePos, pos, ZoomboxScale))
         else if (TransformHelper.IsWithinTransformHandle(handlePos, pos, ZoomboxScale))
             dragging = true;
             dragging = true;
+        movedWhileMouseDown = false;
 
 
         mouseDownPos = pos;
         mouseDownPos = pos;
         lineStartOnMouseDown = LineStart;
         lineStartOnMouseDown = LineStart;
@@ -119,19 +130,18 @@ internal class LineToolOverlay : Control
 
 
     protected void MouseMoved(object sender, MouseEventArgs e)
     protected void MouseMoved(object sender, MouseEventArgs e)
     {
     {
-        /*base.OnMouseMove(e);
-        e.Handled = true;*/
-
         VecD pos = TransformHelper.ToVecD(e.GetPosition(this));
         VecD pos = TransformHelper.ToVecD(e.GetPosition(this));
         if (capturedAnchor == LineToolOverlayAnchor.Start)
         if (capturedAnchor == LineToolOverlayAnchor.Start)
         {
         {
             LineStart = pos;
             LineStart = pos;
+            movedWhileMouseDown = true;
             return;
             return;
         }
         }
 
 
         if (capturedAnchor == LineToolOverlayAnchor.End)
         if (capturedAnchor == LineToolOverlayAnchor.End)
         {
         {
             LineEnd = pos;
             LineEnd = pos;
+            movedWhileMouseDown = true;
             return;
             return;
         }
         }
 
 
@@ -140,6 +150,7 @@ internal class LineToolOverlay : Control
             var delta = pos - mouseDownPos;
             var delta = pos - mouseDownPos;
             LineStart = lineStartOnMouseDown + delta;
             LineStart = lineStartOnMouseDown + delta;
             LineEnd = lineEndOnMouseDown + delta;
             LineEnd = lineEndOnMouseDown + delta;
+            movedWhileMouseDown = true;
             return;
             return;
         }
         }
     }
     }
@@ -153,6 +164,8 @@ internal class LineToolOverlay : Control
         e.Handled = true;
         e.Handled = true;
         capturedAnchor = null;
         capturedAnchor = null;
         dragging = false;
         dragging = false;
+        if (movedWhileMouseDown && ActionCompleted is not null && ActionCompleted.CanExecute(null))
+            ActionCompleted.Execute(null);
 
 
         ReleaseMouseCapture();
         ReleaseMouseCapture();
     }
     }

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

@@ -212,6 +212,7 @@
                         ZoomboxAngle="{Binding Zoombox.Angle}"/>
                         ZoomboxAngle="{Binding Zoombox.Angle}"/>
                     <lineOverlay:LineToolOverlay
                     <lineOverlay:LineToolOverlay
                         Visibility="{Binding Document.LineToolOverlayViewModel.IsEnabled, Converter={converters:BoolToVisibilityConverter}}"
                         Visibility="{Binding Document.LineToolOverlayViewModel.IsEnabled, Converter={converters:BoolToVisibilityConverter}}"
+                        ActionCompleted="{Binding Document.LineToolOverlayViewModel.ActionCompletedCommand}"
                         LineStart="{Binding Document.LineToolOverlayViewModel.LineStart, Mode=TwoWay}"
                         LineStart="{Binding Document.LineToolOverlayViewModel.LineStart, Mode=TwoWay}"
                         LineEnd="{Binding Document.LineToolOverlayViewModel.LineEnd, Mode=TwoWay}"
                         LineEnd="{Binding Document.LineToolOverlayViewModel.LineEnd, Mode=TwoWay}"
                         ZoomboxScale="{Binding Zoombox.Scale}"/>
                         ZoomboxScale="{Binding Zoombox.Scale}"/>