فهرست منبع

Line executor undo improvements

flabbet 8 ماه پیش
والد
کامیت
ba646a80ec

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs

@@ -11,8 +11,8 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnlyLineData
 {
-    public VecD Start { get; set; } = startPos; // Relative to the document top left
-    public VecD End { get; set; } = pos; // Relative to the document top left
+    public VecD Start { get; set; } = startPos;
+    public VecD End { get; set; } = pos;
 
     public VecD TransformedStart
     {

+ 11 - 4
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -446,7 +446,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         IMidChangeUndoableExecutor executor =
             Internals.ChangeController.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
-        if (executor is { CanRedo: true })
+        if (executor is { CanRedo: true }) 
         {
             executor.OnMidChangeRedo();
             return;
@@ -455,7 +455,11 @@ internal class DocumentOperationsModule : IDocumentOperations
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
 
-        Internals.ChangeController.TryStopActiveExecutor();
+        if (!Internals.ChangeController.IsChangeOfTypeActive<IMidChangeUndoableExecutor>())
+        {
+            Internals.ChangeController.TryStopActiveExecutor();
+        }
+
         Internals.ActionAccumulator.AddActions(new Redo_Action());
     }
 
@@ -807,12 +811,15 @@ internal class DocumentOperationsModule : IDocumentOperations
         Internals.ActionAccumulator.AddFinishedActions(new RasterizeMember_Action(memberId));
     }
 
-    public void InvokeCustomAction(Action action)
+    public void InvokeCustomAction(Action action, bool stopActiveExecutor = true)
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
 
-        Internals.ChangeController.TryStopActiveExecutor();
+        if (stopActiveExecutor)
+        {
+            Internals.ChangeController.TryStopActiveExecutor();
+        }
 
         IAction targetAction = new InvokeAction_PassthroughAction(action);
 

+ 59 - 11
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -9,8 +9,10 @@ using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.ViewModels.Document.TransformOverlays;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
@@ -20,6 +22,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
 
     protected Color StrokeColor => toolbar!.StrokeColor.ToColor();
     protected double StrokeWidth => toolViewModel!.ToolSize;
+    protected abstract bool UseGlobalUndo { get; }
+    protected abstract bool ShowApplyButton { get; }
 
     protected bool drawOnMask;
 
@@ -31,8 +35,10 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     private bool ignoreNextColorChange = false;
     private VecD lastStartPos;
 
-    public override bool CanUndo => document.LineToolOverlayHandler.HasUndo;
-    public override bool CanRedo => document.LineToolOverlayHandler.HasRedo;
+    private UndoStack<LineVectorData>? localUndoStack;
+
+    public override bool CanUndo => !UseGlobalUndo && localUndoStack is { UndoCount: > 0 };
+    public override bool CanRedo => !UseGlobalUndo && localUndoStack is { RedoCount: > 0 };
 
     public override ExecutionState Start()
     {
@@ -51,6 +57,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             return ExecutionState.Error;
         if (!drawOnMask && member is not ILayerHandler)
             return ExecutionState.Error;
+        
+        localUndoStack = new UndoStack<LineVectorData>();
 
         if (ActiveMode == ShapeToolMode.Drawing)
         {
@@ -61,7 +69,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             }
 
             document.LineToolOverlayHandler.Hide();
-            document.LineToolOverlayHandler.Show(startDrawingPos, startDrawingPos, false);
+            document.LineToolOverlayHandler.Show(startDrawingPos, startDrawingPos, false, AddToUndo);
             document.LineToolOverlayHandler.ShowHandles = false;
             document.LineToolOverlayHandler.IsSizeBoxEnabled = true;
 
@@ -85,6 +93,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             }
 
             ActiveMode = ShapeToolMode.Transform;
+
+            document.LineToolOverlayHandler.Show(data.Start, data.End, false, AddToUndo);
         }
         else
         {
@@ -97,7 +107,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     protected abstract bool InitShapeData(IReadOnlyLineData? data);
     protected abstract IAction DrawLine(VecD pos);
     protected abstract IAction TransformOverlayMoved(VecD start, VecD end);
-    protected abstract IAction SettingsChange();
+    protected abstract IAction[] SettingsChange();
     protected abstract IAction EndDraw();
 
     protected override void PrecisePositionChangeDrawingMode(VecD pos)
@@ -159,8 +169,9 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             return;
         }
 
+        AddToUndo((lastStartPos, curPos));
         document!.LineToolOverlayHandler.Hide();
-        document!.LineToolOverlayHandler.Show(lastStartPos, curPos, true);
+        document!.LineToolOverlayHandler.Show(lastStartPos, curPos, ShowApplyButton, AddToUndo);
         base.OnLeftMouseButtonUp(argsPositionOnCanvas);
     }
 
@@ -200,28 +211,40 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             return;
 
         document!.LineToolOverlayHandler.Nudge(distance);
+        AddToUndo((document.LineToolOverlayHandler.LineStart, document.LineToolOverlayHandler.LineEnd));
     }
 
     public override void OnSettingsChanged(string name, object value)
     {
-        var colorChangedAction = SettingsChange();
-        internals!.ActionAccumulator.AddActions(colorChangedAction);
+        var colorChangedActions = SettingsChange();
+        if (ActiveMode == ShapeToolMode.Transform)
+        {
+            internals!.ActionAccumulator.AddFinishedActions(colorChangedActions);
+        }
     }
 
     public override void OnMidChangeUndo()
     {
-        if (ActiveMode != ShapeToolMode.Transform)
+        if (ActiveMode != ShapeToolMode.Transform || localUndoStack == null)
             return;
 
-        document!.LineToolOverlayHandler.Undo();
+        var undone = localUndoStack?.Undo();
+        if (undone is not null)
+        {
+            ApplyState(undone);
+        }
     }
 
     public override void OnMidChangeRedo()
     {
-        if (ActiveMode != ShapeToolMode.Transform)
+        if (ActiveMode != ShapeToolMode.Transform || localUndoStack == null)
             return;
 
-        document!.LineToolOverlayHandler.Redo();
+        var redone = localUndoStack?.Redo();
+        if (redone is not null)
+        {
+            ApplyState(redone);
+        }
     }
 
     public override void OnTransformApplied()
@@ -244,4 +267,29 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     {
         document!.LineToolOverlayHandler.Hide();
     }
+
+    private void AddToUndo((VecD, VecD) newPos)
+    {
+        if (UseGlobalUndo)
+        {
+            internals!.ActionAccumulator.AddFinishedActions(EndDraw(), TransformOverlayMoved(newPos.Item1, newPos.Item2), EndDraw());
+        }
+        else
+        {
+            localUndoStack!.AddState(ConstructLineData(newPos.Item1, newPos.Item2));
+        }
+    }
+
+    protected LineVectorData ConstructLineData(VecD start, VecD end)
+    {
+        return new LineVectorData(start, end) { StrokeWidth = (float)StrokeWidth, StrokeColor = StrokeColor };
+    }
+    
+    private void ApplyState(LineVectorData data)
+    {
+        toolbar!.StrokeColor = data.StrokeColor.ToColor();
+        toolbar!.ToolSize = data.StrokeWidth;
+        
+        document!.LineToolOverlayHandler.Show(data.Start, data.End, ShowApplyButton, AddToUndo);
+    }
 }

+ 6 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterLineToolExecutor.cs

@@ -9,6 +9,9 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
 internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
 {
+    protected override bool UseGlobalUndo => false;
+    protected override bool ShowApplyButton => true;
+
     protected override bool InitShapeData(IReadOnlyLineData? data)
     {
         return false;
@@ -30,12 +33,12 @@ internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
             (float)StrokeWidth, StrokeColor, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
-    protected override IAction SettingsChange()
+    protected override IAction[] SettingsChange()
     {
         VecD dir = GetSignedDirection(startDrawingPos, curPos);
         VecD oppositeDir = new VecD(-dir.X, -dir.Y);
-        return new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos, oppositeDir), ToPixelPos(curPos, dir), (float)StrokeWidth,
-            StrokeColor, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+        return [new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos, oppositeDir), ToPixelPos(curPos, dir), (float)StrokeWidth,
+            StrokeColor, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)];
     }
 
     private VecI ToPixelPos(VecD pos, VecD dir)

+ 15 - 12
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorLineToolExecutor.cs

@@ -5,6 +5,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Models.Handlers.Tools;
 using Drawie.Numerics;
+using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
@@ -15,6 +16,9 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
 
     protected override bool AlignToPixels => false;
 
+    protected override bool UseGlobalUndo => true;
+    protected override bool ShowApplyButton => false;
+
     protected override bool InitShapeData(IReadOnlyLineData? data)
     {
         if (data is null)
@@ -28,11 +32,7 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
 
     protected override IAction DrawLine(VecD pos)
     {
-        LineVectorData data = new LineVectorData(startDrawingPos, pos)
-        {
-            StrokeColor = StrokeColor,
-            StrokeWidth = (float)StrokeWidth,
-        };
+        LineVectorData data = ConstructLineData(startDrawingPos, pos);
         
         startPoint = startDrawingPos;
         endPoint = pos;
@@ -42,11 +42,7 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
 
     protected override IAction TransformOverlayMoved(VecD start, VecD end)
     {
-        LineVectorData data = new LineVectorData(start, end)
-        {
-            StrokeColor = StrokeColor,
-            StrokeWidth = (float)StrokeWidth,
-        };
+        var data = ConstructLineData(start, end);
         
         startPoint = start;
         endPoint = end;
@@ -54,13 +50,20 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
         return new SetShapeGeometry_Action(memberId, data);
     }
 
-    protected override IAction SettingsChange()
+    protected override IAction[] SettingsChange()
     {
-        return TransformOverlayMoved(startPoint, endPoint);
+        return [TransformOverlayMoved(startPoint, endPoint), new EndSetShapeGeometry_Action()];
     }
 
     protected override IAction EndDraw()
     {
         return new EndSetShapeGeometry_Action();
     }
+
+    public override bool IsFeatureEnabled(IExecutorFeature feature)
+    {
+        if(feature is IMidChangeUndoableExecutor) return false;
+        
+        return base.IsFeatureEnabled(feature);
+    }
 }

+ 1 - 5
src/PixiEditor/Models/Handlers/ILineOverlayHandler.cs

@@ -7,11 +7,7 @@ internal interface ILineOverlayHandler
 {
     public void Hide();
     public bool Nudge(VecD distance);
-    public bool Undo();
-    public bool Redo();
-    public void Show(VecD startPos, VecD endPos, bool showApplyButton);
-    public bool HasUndo { get; }
-    public bool HasRedo { get; }
+    public void Show(VecD startPos, VecD endPos, bool showApplyButton, Action<(VecD, VecD)> addToUndo);
     public VecD LineStart { get; set; }
     public VecD LineEnd { get; set; }
     public bool ShowHandles { get; set; }

+ 3 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -160,6 +160,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
     public bool IsChangeFeatureActive<T>() where T : IExecutorFeature =>
         Internals.ChangeController.IsChangeOfTypeActive<T>();
+    
+    public T? TryGetExecutorFeature<T>() where T : IExecutorFeature =>
+        Internals.ChangeController.TryGetExecutorFeature<T>();
 
     public bool PointerDragChangeInProgress =>
         Internals.ChangeController.IsBlockingChangeActive && Internals.ChangeController.LeftMousePressed;

+ 10 - 46
src/PixiEditor/ViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs

@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 namespace PixiEditor.ViewModels.Document.TransformOverlays;
 
@@ -10,8 +11,6 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
 {
     public event EventHandler<(VecD, VecD)>? LineMoved;
 
-    private TransformOverlayUndoStack<(VecD, VecD)>? undoStack = null;
-
     private VecD lineStart;
 
     public VecD LineStart
@@ -66,6 +65,13 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
         set => SetProperty(ref actionCompletedCommand, value);
     }
 
+    private ICommand addToUndoCommand = null;
+    public ICommand AddToUndoCommand
+    {
+        get => addToUndoCommand;
+        set => SetProperty(ref addToUndoCommand, value);
+    }
+    
     private bool showApplyButton;
 
     public bool ShowApplyButton
@@ -76,34 +82,21 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
 
     public LineToolOverlayViewModel()
     {
-        ActionCompletedCommand =
-            new RelayCommand(() => undoStack?.AddState((LineStart, LineEnd), TransformOverlayStateType.Move));
     }
 
-    public void Show(VecD lineStart, VecD endPos, bool showApplyButton)
+    public void Show(VecD lineStart, VecD endPos, bool showApplyButton, Action<(VecD, VecD)> addToUndo)
     {
-        if (undoStack is not null)
-            return;
-        undoStack = new();
-        
-        undoStack.AddState((lineStart, endPos), TransformOverlayStateType.Initial);
-
         LineStart = lineStart;
         LineEnd = endPos; 
         IsEnabled = true;
         ShowApplyButton = showApplyButton;
         ShowHandles = true;
         IsSizeBoxEnabled = false;
+        AddToUndoCommand = new RelayCommand(() => addToUndo((LineStart, LineEnd)));
     }
 
-    public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0;
-    public bool HasRedo => undoStack is not null && undoStack.RedoCount > 0; 
-
     public void Hide()
     {
-        if (undoStack is null)
-            return;
-        undoStack = null;
         IsEnabled = false;
         ShowApplyButton = false;
         IsSizeBoxEnabled = false;
@@ -111,37 +104,8 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
 
     public bool Nudge(VecD distance)
     {
-        if (undoStack is null)
-            return false;
         LineStart = LineStart + distance;
         LineEnd = LineEnd + distance;
-        undoStack.AddState((lineStart, lineEnd), TransformOverlayStateType.Nudge);
-        return true;
-    }
-
-    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;
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/Document/TransformOverlays/PathOverlayUndoStack.cs → src/PixiEditor/ViewModels/Document/TransformOverlays/UndoStack.cs

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.ViewModels.Document.TransformOverlays;
 
-internal class PathOverlayUndoStack<TState> : IDisposable where TState : class
+internal class UndoStack<TState> : IDisposable where TState : class
 {
     private struct StackItem<TState>
     {

+ 14 - 6
src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs

@@ -25,10 +25,11 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedRedo))
             return;
         doc.Operations.Redo();
-        doc.Operations.InvokeCustomAction(() =>
+        doc.Operations.InvokeCustomAction(
+            () =>
         {
             Owner.ToolsSubViewModel.OnPostRedoInlet();
-        });
+        }, false);
     }
 
     /// <summary>
@@ -42,10 +43,11 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedUndo))
             return;
         doc.Operations.Undo();
-        doc.Operations.InvokeCustomAction(() =>
+        doc.Operations.InvokeCustomAction(
+            () =>
         {
             Owner.ToolsSubViewModel.OnPostUndoInlet();
-        });
+        }, false);
     }
 
     /// <summary>
@@ -59,7 +61,10 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return false;
-        return doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() || doc.HasSavedUndo;
+        
+        IMidChangeUndoableExecutor executor = doc.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
+        
+        return executor is { CanUndo: true } || doc.HasSavedUndo;
     }
 
     /// <summary>
@@ -73,6 +78,9 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return false;
-        return doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() || doc.HasSavedRedo;
+        
+        IMidChangeUndoableExecutor executor = doc.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
+        
+        return executor is { CanRedo: true } || doc.HasSavedRedo;
     }
 }

+ 22 - 9
src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs

@@ -23,6 +23,7 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
 
     public override bool IsErasable => false;
+    private bool isActivated;
 
     public VectorLineToolViewModel()
     {
@@ -69,18 +70,24 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
         if (restoring) return;
 
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
-        var layer = document.SelectedStructureMember;
-        if (layer is IVectorLayerHandler vectorLayer)
+        document.Tools.UseVectorLineTool();
+        isActivated = true;
+    }
+
+    public override void OnPostUndo()
+    {
+        if (isActivated)
         {
-            if (vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyLineData
-                lineVectorData)
-            {
-                document.LineToolOverlayViewModel.Show(lineVectorData.TransformedStart, lineVectorData.TransformedEnd,
-                    false);
-            }
+            OnSelected(false);
         }
+    }
 
-        document.Tools.UseVectorLineTool();
+    public override void OnPostRedo()
+    {
+        if (isActivated)
+        {
+            OnSelected(false);
+        }
     }
 
     protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
@@ -88,4 +95,10 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
         OnDeselecting(false);
         OnSelected(false);
     }
+
+    public override void OnDeselecting(bool transient)
+    {
+        base.OnDeselecting(transient);
+        isActivated = false;
+    }
 }

+ 6 - 0
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -237,6 +237,11 @@ internal class ViewportOverlays
             Source = Viewport, Path = "Document.LineToolOverlayViewModel.IsSizeBoxEnabled", Mode = BindingMode.TwoWay
         };
         
+        Binding addToUndoCommandBinding = new()
+        {
+            Source = Viewport, Path = "Document.LineToolOverlayViewModel.AddToUndoCommand", Mode = BindingMode.OneWay
+        };
+        
         lineToolOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         lineToolOverlay.Bind(LineToolOverlay.SnappingControllerProperty, snappingBinding);
         lineToolOverlay.Bind(LineToolOverlay.ActionCompletedProperty, actionCompletedBinding);
@@ -247,6 +252,7 @@ internal class ViewportOverlays
         lineToolOverlay.Bind(LineToolOverlay.ShowHandlesProperty, showHandlesBinding);
         lineToolOverlay.Bind(LineToolOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
         lineToolOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
+        lineToolOverlay.Bind(LineToolOverlay.AddToUndoCommandProperty, addToUndoCommandBinding);
     }
 
     private void BindTransformOverlay()

+ 12 - 0
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -75,6 +75,15 @@ internal class LineToolOverlay : Overlay
         set => SetValue(IsSizeBoxEnabledProperty, value);
     }
 
+    public static readonly StyledProperty<ICommand> AddToUndoCommandProperty = AvaloniaProperty.Register<LineToolOverlay, ICommand>(
+        nameof(AddToUndoCommand));
+
+    public ICommand AddToUndoCommand
+    {
+        get => GetValue(AddToUndoCommandProperty);
+        set => SetValue(AddToUndoCommandProperty, value);
+    }
+
     static LineToolOverlay()
     {
         LineStartProperty.Changed.Subscribe(RenderAffectingPropertyChanged);
@@ -131,6 +140,7 @@ internal class LineToolOverlay : Overlay
         moveHandle.StrokePaint = blackPaint;
         moveHandle.OnPress += OnHandlePress;
         moveHandle.OnDrag += MoveHandleOnDrag;
+        moveHandle.OnRelease += OnHandleRelease;
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         moveHandle.OnHover += (handle, _) => Refresh();
         moveHandle.OnRelease += OnHandleRelease;
@@ -162,6 +172,8 @@ internal class LineToolOverlay : Overlay
 
         isDraggingHandle = false;
         IsSizeBoxEnabled = false;
+        
+        AddToUndoCommand.Execute((LineStart, LineEnd));
     }
 
     protected override void ZoomChanged(double newZoom)