Jelajahi Sumber

Merge pull request #734 from PixiEditor/duplicate-layer-ctrt

Duplicate layer with ctrl
Krzysztof Krysiński 7 bulan lalu
induk
melakukan
2504a17328
52 mengubah file dengan 722 tambahan dan 259 penghapusan
  1. 1 1
      src/Drawie
  2. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs
  3. 7 4
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs
  4. 153 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs
  5. 68 51
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs
  6. 18 11
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  7. 19 6
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs
  8. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs
  9. 68 51
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  10. 18 2
      src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs
  11. 7 6
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  12. 14 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  13. 0 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs
  14. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  15. 9 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformDraggedEvent.cs
  16. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformStoppedEvent.cs
  17. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformableExecutor.cs
  18. 3 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  19. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs
  20. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs
  21. 0 74
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs
  22. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs
  23. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformReferenceLayerExecutor.cs
  24. 158 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  25. 1 1
      src/PixiEditor/Models/Handlers/IDocument.cs
  26. 3 1
      src/PixiEditor/Models/Handlers/IToolHandler.cs
  27. 2 0
      src/PixiEditor/Models/Handlers/IToolsHandler.cs
  28. 4 1
      src/PixiEditor/Models/Handlers/Tools/IMoveToolHandler.cs
  29. 1 1
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  30. 16 7
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  31. 18 0
      src/PixiEditor/ViewModels/Document/Nodes/FolderNodeViewModel.cs
  32. 11 2
      src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs
  33. 15 4
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  34. 10 0
      src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs
  35. 20 1
      src/PixiEditor/ViewModels/Tools/ToolViewModel.cs
  36. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs
  37. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/ColorPickerToolViewModel.cs
  38. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/FloodFillToolViewModel.cs
  39. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/LassoToolViewModel.cs
  40. 28 7
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs
  41. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs
  42. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs
  43. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs
  44. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs
  45. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/SelectToolViewModel.cs
  46. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  47. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs
  48. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  49. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  50. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/ZoomToolViewModel.cs
  51. 6 0
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  52. 14 0
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 0f4cbd35e77d52f89c944d5fcc7d732ccd22bd82
+Subproject commit 50007fb51e74bde8cfbb1a30f8d040ef0804e43e

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs

@@ -1,5 +1,6 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -14,4 +15,5 @@ public interface IReadOnlyShapeVectorData
     public RectD GeometryAABB { get; }
     public RectD TransformedAABB { get; }
     public ShapeCorners TransformationCorners { get; }
+    public VectorPath ToPath();
 }

+ 7 - 4
src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs

@@ -1,25 +1,28 @@
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 internal class ClearSelectedArea_Change : Change
 {
+    private VectorPath clearArea;
     private readonly Guid memberGuid;
     private readonly bool drawOnMask;
     private CommittedChunkStorage? savedChunks;
     private int frame;
 
     [GenerateMakeChangeAction]
-    public ClearSelectedArea_Change(Guid memberGuid, bool drawOnMask, int frame)
+    public ClearSelectedArea_Change(Guid memberGuid, VectorPath clearArea, bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.drawOnMask = drawOnMask;
+        this.clearArea = clearArea;
         this.frame = frame;
     }
 
     public override bool InitializeAndValidate(Document target)
     {
-        return !target.Selection.SelectionPath.IsEmpty && DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask);
+        return clearArea is { IsEmpty: false } && DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -29,10 +32,10 @@ internal class ClearSelectedArea_Change : Change
 
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
 
-        RectD bounds = target.Selection.SelectionPath.Bounds;
+        RectD bounds = clearArea.Bounds;
         RectI intBounds = (RectI)bounds.Intersect(new RectD(0, 0, target.Size.X, target.Size.Y)).RoundOutwards();
 
-        image.EnqueueClearPath(target.Selection.SelectionPath, intBounds);
+        image.EnqueueClearPath(clearArea, intBounds);
         var affArea = image.FindAffectedArea();
         savedChunks = new(image, affArea.Chunks);
         image.CommitChanges();

+ 153 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs

@@ -0,0 +1,153 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
+internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChange
+{
+    private List<Guid> layerGuids;
+    private VecD delta;
+    private Dictionary<Guid, ShapeVectorData> originalShapes;
+
+    private int frame;
+
+    [GenerateUpdateableChangeActions]
+    public PreviewShiftLayers_UpdateableChange(List<Guid> layerGuids, VecD delta, int frame)
+    {
+        this.delta = delta;
+        this.layerGuids = layerGuids;
+        this.frame = frame;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (layerGuids.Count == 0)
+        {
+            return false;
+        }
+
+        layerGuids = target.ExtractLayers(layerGuids);
+
+        foreach (var layer in layerGuids)
+        {
+            if (!target.HasMember(layer)) return false;
+        }
+
+        originalShapes = new Dictionary<Guid, ShapeVectorData>();
+
+        foreach (var layerGuid in layerGuids)
+        {
+            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+
+            if (layer is VectorLayerNode transformableObject)
+            {
+                originalShapes[layerGuid] = transformableObject.ShapeData;
+                transformableObject.ShapeData = null;
+            }
+        }
+
+        return true;
+    }
+
+    [UpdateChangeMethod]
+    public void Update(VecD delta)
+    {
+        this.delta = delta;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+        foreach (var layerGuid in layerGuids)
+        {
+            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+
+            if (layer is ImageLayerNode)
+            {
+                var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, true, (VecI)delta, frame);
+                changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
+            }
+            else if (layer is VectorLayerNode vectorLayer)
+            {
+                StrokeJoin join = StrokeJoin.Miter;
+                StrokeCap cap = StrokeCap.Butt;
+                
+                (vectorLayer.ShapeData as PathVectorData)?.Path.Dispose();
+
+                var originalShape = originalShapes[layerGuid];
+
+                var path = originalShape.ToPath();
+
+                if (originalShape is PathVectorData shape)
+                {
+                    join = shape.StrokeLineJoin;
+                    cap = shape.StrokeLineCap;
+                }
+
+                VecD mappedDelta = originalShape.TransformationMatrix.Invert().MapVector((float)delta.X, (float)delta.Y);
+                
+                var finalMatrix = Matrix3X3.CreateTranslation((float)mappedDelta.X, (float)mappedDelta.Y);
+
+                path.AddPath(path, finalMatrix, AddPathMode.Append);
+
+                var newShapeData = new PathVectorData(path)
+                {
+                    StrokeWidth = originalShape.StrokeWidth,
+                    StrokeColor = originalShape.StrokeColor,
+                    FillColor = originalShape.FillColor,
+                    Fill = originalShape.Fill,
+                    TransformationMatrix = originalShape.TransformationMatrix,
+                    StrokeLineJoin = join,
+                    StrokeLineCap = cap
+                };
+                
+                vectorLayer.ShapeData = newShapeData;
+                changes.Add(new VectorShape_ChangeInfo(layerGuid, ShiftLayer_UpdateableChange.AffectedAreaFromBounds(target, layerGuid, frame)));
+            }
+        }
+
+        return changes;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        ignoreInUndo = true;
+        return RevertPreview(target);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        return RevertPreview(target);
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> RevertPreview(Document target)
+    {
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+        foreach (var layerGuid in layerGuids)
+        {
+            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+
+            if (layer is ImageLayerNode imgLayer)
+            {
+                var image = imgLayer.GetLayerImageAtFrame(frame);
+                var affected = image.FindAffectedArea();
+                image.CancelChanges();
+                changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
+            }
+            else if (layer is VectorLayerNode transformableObject)
+            {
+                (transformableObject.ShapeData as PathVectorData)?.Path.Dispose();
+                transformableObject.ShapeData = originalShapes[layerGuid];
+            }
+        }
+
+        return changes;
+    }
+}

+ 68 - 51
src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs

@@ -2,25 +2,25 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
-internal class ShiftLayer_UpdateableChange : UpdateableChange
+internal class ShiftLayer_UpdateableChange : Change
 {
     private List<Guid> layerGuids;
-    private bool keepOriginal;
-    private VecI delta;
+    private VecD delta;
     private Dictionary<Guid, CommittedChunkStorage?> originalLayerChunks = new();
+    private Dictionary<Guid, Matrix3X3> originalTransformations = new();
 
-    private List<IChangeInfo> _tempChanges = new();
     private int frame;
 
-    [GenerateUpdateableChangeActions]
-    public ShiftLayer_UpdateableChange(List<Guid> layerGuids, VecI delta, bool keepOriginal, int frame)
+    [GenerateMakeChangeAction]
+    public ShiftLayer_UpdateableChange(List<Guid> layerGuids, VecD delta, int frame)
     {
         this.delta = delta;
         this.layerGuids = layerGuids;
-        this.keepOriginal = keepOriginal;
         this.frame = frame;
     }
 
@@ -41,59 +41,38 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         return true;
     }
 
-    [UpdateChangeMethod]
-    public void Update(VecI delta, bool keepOriginal)
-    {
-        this.delta = delta;
-        this.keepOriginal = keepOriginal;
-    }
-
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
     {
         originalLayerChunks = new Dictionary<Guid, CommittedChunkStorage?>();
+        originalTransformations = new Dictionary<Guid, Matrix3X3>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
         foreach (var layerGuid in layerGuids)
         {
             var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
 
-            // TODO: This now does't crash, but ignores other layer types. Think how to handle this.
-            if (layer is not ImageLayerNode)
+            if (layer is ImageLayerNode)
             {
-                continue;
-            }
-
-            var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta, frame);
-            var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
-
-            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
-
-            originalLayerChunks[layerGuid] = new(image, image.FindAffectedArea().Chunks);
-            image.CommitChanges();
-        }
+                var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, false, (VecI)delta, frame);
+                var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
 
-        ignoreInUndo = delta.TaxicabLength == 0;
-        return changes;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
-    {
-        _tempChanges.Clear();
-
-        foreach (var layerGuid in layerGuids)
-        {
-            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+                changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
 
-            if (layer is not ImageLayerNode)
+                originalLayerChunks[layerGuid] = new(image, image.FindAffectedArea().Chunks);
+                image.CommitChanges();
+            }
+            else if (layer is ITransformableObject transformableObject)
             {
-                continue;
+                originalTransformations[layerGuid] = transformableObject.TransformationMatrix;
+                AffectedArea affected = AffectedAreaFromBounds(target, layerGuid, frame);
+                transformableObject.TransformationMatrix = transformableObject.TransformationMatrix.PostConcat(
+                Matrix3X3.CreateTranslation((float)delta.X, (float)delta.Y));
+                changes.Add(new VectorShape_ChangeInfo(layerGuid, affected));
             }
-
-            var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta, frame);
-            _tempChanges.Add(new LayerImageArea_ChangeInfo(layerGuid, chunks));
         }
 
-        return _tempChanges;
+        ignoreInUndo = delta.TaxicabLength == 0;
+        return changes;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
@@ -103,15 +82,17 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         {
             var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
 
-            if (layer is not ImageLayerNode)
+            if (layer is ImageLayerNode)
             {
-                continue;
+                var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
+                CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
+                var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
+                changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
+            }
+            else if (layer is ITransformableObject transformableObject)
+            {
+                transformableObject.TransformationMatrix = originalTransformations[layerGuid];
             }
-
-            var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
-            CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
-            var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
-            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
         }
 
         return changes;
@@ -124,4 +105,40 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
             value?.Dispose();
         }
     }
+    
+    internal static AffectedArea AffectedAreaFromBounds(Document target, Guid layerGuid, int frame)
+    {
+        HashSet<VecI> chunks = new HashSet<VecI>();
+        
+        var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+        if (layer is not VectorLayerNode vectorLayer)
+        {
+            return new AffectedArea();
+        }
+
+        RectD? bounds = vectorLayer.GetTightBounds(frame);
+        if (bounds is null)
+        {
+            return new AffectedArea();
+        }
+        
+        RectD boundsValue = bounds.Value;
+
+        int chunkSize = ChunkyImage.FullChunkSize;
+        
+        VecI start = new VecI((int)boundsValue.X / chunkSize, (int)boundsValue.Y / chunkSize);
+        VecI end = new VecI((int)(boundsValue.X + boundsValue.Width) / chunkSize, (int)(boundsValue.Y + boundsValue.Height) / chunkSize);
+        
+        HashSet<VecI> affectedChunks = new HashSet<VecI>();
+        
+        for (int x = start.X; x <= end.X; x++)
+        {
+            for (int y = start.Y; y <= end.Y; y++)
+            {
+                affectedChunks.Add(new VecI(x, y));
+            }
+        }
+        
+        return new AffectedArea(affectedChunks);
+    }
 }

+ 18 - 11
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -9,6 +9,7 @@ using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
@@ -118,21 +119,22 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             }
             else if (layer is ITransformableObject transformable)
             {
-                SetTransformableMember(layer, member, transformable, tightBounds);
+                SetTransformableMember(layer.Id, member, transformable, tightBounds);
             }
         }
 
         return true;
     }
 
-    private void SetTransformableMember(StructureNode layer, MemberTransformationData member,
+    private void SetTransformableMember(Guid transformableId,
+        MemberTransformationData member,
         ITransformableObject transformable, RectD tightBounds)
     {
         member.OriginalBounds = tightBounds; 
         VecD posRelativeToMaster = member.OriginalBounds.Value.TopLeft - masterCorners.TopLeft;
 
         member.OriginalPos = (VecI)posRelativeToMaster;
-        member.AddTransformableObject(transformable, transformable.TransformationMatrix);
+        member.AddTransformableObject(transformableId, transformable.TransformationMatrix);
     }
 
     private void SetImageMember(Document target, MemberTransformationData member, RectD originalTightBounds,
@@ -255,7 +257,8 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             }
             else if (member.IsTransformable)
             {
-                member.TransformableObject.TransformationMatrix = member.LocalMatrix;
+                var transformable = target.FindMemberOrThrow(member.MemberId) as ITransformableObject;
+                transformable.TransformationMatrix = member.LocalMatrix;
 
                 AffectedArea area = GetTranslationAffectedArea();
                 infos.Add(new TransformObject_ChangeInfo(member.MemberId, area));
@@ -290,7 +293,8 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             }
             else if (member.IsTransformable)
             {
-                member.TransformableObject.TransformationMatrix = member.LocalMatrix;
+                var transformable = target.FindMemberOrThrow(member.MemberId) as ITransformableObject;
+                transformable.TransformationMatrix = member.LocalMatrix;
 
                 AffectedArea translationAffectedArea = GetTranslationAffectedArea();
                 var tmp = new AffectedArea(translationAffectedArea);
@@ -331,7 +335,8 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             }
             else if (member.IsTransformable)
             {
-                member.TransformableObject.TransformationMatrix = member.OriginalMatrix!.Value;
+                var transformable = target.FindMemberOrThrow(member.MemberId) as ITransformableObject;
+                transformable.TransformationMatrix = member.OriginalMatrix!.Value;
 
                 //TODO this is probably wrong
                 AffectedArea area = GetTranslationAffectedArea();
@@ -409,8 +414,8 @@ class MemberTransformationData : IDisposable
     public Guid MemberId { get; }
     public ShapeCorners MemberCorners { get; init; }
 
-    public ITransformableObject? TransformableObject { get; private set; }
-    public Matrix3X3? OriginalMatrix { get; private set; }
+    public Guid TransformableObjectId { get; private set; }
+    public Matrix3X3? OriginalMatrix { get; set; }
 
     public CommittedChunkStorage? SavedChunks { get; set; }
     public VectorPath? OriginalPath { get; set; }
@@ -418,18 +423,19 @@ class MemberTransformationData : IDisposable
     public RectD? OriginalBounds { get; set; }
     public VecD? OriginalPos { get; set; }
     public bool IsImage => Image != null;
-    public bool IsTransformable => TransformableObject != null;
+    public bool IsTransformable => TransformableObjectId != default;
     public RectI? RoundedOriginalBounds => (RectI)OriginalBounds?.RoundOutwards();
     public Matrix3X3 LocalMatrix { get; set; }
+    public ShapeVectorData? OriginalShapeData { get; set; }
 
     public MemberTransformationData(Guid memberId)
     {
         MemberId = memberId;
     }
 
-    public void AddTransformableObject(ITransformableObject transformableObject, Matrix3X3 originalMatrix)
+    public void AddTransformableObject(Guid transformableObject, Matrix3X3 originalMatrix)
     {
-        TransformableObject = transformableObject;
+        TransformableObjectId = transformableObject;
         OriginalMatrix = new Matrix3X3?(originalMatrix);
     }
 
@@ -446,5 +452,6 @@ class MemberTransformationData : IDisposable
         OriginalPath?.Dispose();
         OriginalPath = null;
         SavedChunks?.Dispose();
+        OriginalShapeData = null;
     }
 }

+ 19 - 6
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs

@@ -1,4 +1,6 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
@@ -13,14 +15,17 @@ internal class DuplicateFolder_Change : Change
     private Guid[] contentGuids;
     private Guid[] contentDuplicateGuids;
 
+    private Guid[]? childGuidsToUse;
+
     private ConnectionsData? connectionsData;
     private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
 
     [GenerateMakeChangeAction]
-    public DuplicateFolder_Change(Guid folderGuid, Guid newGuid)
+    public DuplicateFolder_Change(Guid folderGuid, Guid newGuid, ImmutableList<Guid>? childGuids)
     {
         this.folderGuid = folderGuid;
         duplicateGuid = newGuid;
+        childGuidsToUse = childGuids?.ToArray();
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -57,12 +62,12 @@ internal class DuplicateFolder_Change : Change
         List<IChangeInfo> operations = new();
 
         target.NodeGraph.AddNode(clone);
-        
+
         operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
 
         DuplicateContent(target, clone, existingLayer, operations);
-        
+
         ignoreInUndo = false;
 
         return operations;
@@ -87,7 +92,7 @@ internal class DuplicateFolder_Change : Change
                 Node contentNode = target.FindNodeOrThrow<Node>(contentGuid);
                 changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
                 changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
-                
+
                 target.NodeGraph.RemoveNode(contentNode);
                 contentNode.Dispose();
             }
@@ -109,6 +114,7 @@ internal class DuplicateFolder_Change : Change
         Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
 
         nodeMap[existingLayer.Id] = clone.Id;
+        int counter = 0;
         List<Guid> contentGuidList = new();
 
         existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
@@ -117,6 +123,13 @@ internal class DuplicateFolder_Change : Change
                 return false;
 
             Node? node = targetNode.Clone();
+            
+            if(node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+            {
+                node.Id = childGuidsToUse[counter];
+                counter++;
+            }
+            
             nodeMap[x.Id] = node.Id;
             contentGuidList.Add(node.Id);
 
@@ -133,7 +146,7 @@ internal class DuplicateFolder_Change : Change
             operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
                 target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
         }
-        
+
         contentDuplicateGuids = contentGuidList.ToArray();
     }
 }

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs

@@ -14,16 +14,16 @@ internal class DuplicateLayer_Change : Change
     private ConnectionsData? connectionsData;
 
     [GenerateMakeChangeAction]
-    public DuplicateLayer_Change(Guid layerGuid)
+    public DuplicateLayer_Change(Guid layerGuid, Guid newGuid)
     {
         this.layerGuid = layerGuid;
+        this.duplicateGuid = newGuid;
     }
 
     public override bool InitializeAndValidate(Document target)
     {
         if (!target.TryFindMember<LayerNode>(layerGuid, out LayerNode? layer))
             return false;
-        duplicateGuid = Guid.NewGuid();
         
         connectionsData = NodeOperations.CreateConnectionsData(layer);
         

+ 68 - 51
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -95,61 +95,78 @@ internal class ActionAccumulator
         };
         busyTimer.Start();
 
-        while (queuedActions.Count > 0)
+        try
         {
-            var toExecute = queuedActions;
-            queuedActions = new();
-
-            List<IChangeInfo?> changes;
-            if (AreAllPassthrough(toExecute))
-            {
-                changes = toExecute.Select(a => (IChangeInfo?)a.action).ToList();
-            }
-            else
-            {
-                changes = await internals.Tracker.ProcessActions(toExecute);
-            }
-
-            List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
-            bool undoBoundaryPassed =
-                toExecute.Any(static action => action.action is ChangeBoundary_Action or Redo_Action or Undo_Action);
-            bool viewportRefreshRequest =
-                toExecute.Any(static action => action.action is RefreshViewport_PassthroughAction);
-            bool changeFrameRequest =
-                toExecute.Any(static action => action.action is SetActiveFrame_PassthroughAction);
-            foreach (IChangeInfo info in optimizedChanges)
-            {
-                internals.Updater.ApplyChangeFromChangeInfo(info);
-            }
-
-            if (undoBoundaryPassed)
-                internals.Updater.AfterUndoBoundaryPassed();
-
-            // update the contents of the bitmaps
-            var affectedAreas = new AffectedAreasGatherer(document.AnimationHandler.ActiveFrameTime, internals.Tracker,
-                optimizedChanges);
-            if (DrawingBackendApi.Current.IsHardwareAccelerated)
+            while (queuedActions.Count > 0)
             {
-                canvasUpdater.UpdateGatheredChunksSync(affectedAreas,
-                    undoBoundaryPassed || viewportRefreshRequest);
-            }
-            else
-            {
-                await canvasUpdater.UpdateGatheredChunks(affectedAreas,
-                    undoBoundaryPassed || viewportRefreshRequest);
-            }
-
-            previewUpdater.UpdatePreviews(undoBoundaryPassed || changeFrameRequest || viewportRefreshRequest, affectedAreas.ImagePreviewAreas.Keys,
-                affectedAreas.MaskPreviewAreas.Keys,
-                affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames);
-
-            // force refresh viewports for better responsiveness
-            foreach (var (_, value) in internals.State.Viewports)
-            {
-                if (!value.Delayed)
-                    value.InvalidateVisual();
+                var toExecute = queuedActions;
+                queuedActions = new();
+
+                List<IChangeInfo?> changes;
+                if (AreAllPassthrough(toExecute))
+                {
+                    changes = toExecute.Select(a => (IChangeInfo?)a.action).ToList();
+                }
+                else
+                {
+                    changes = await internals.Tracker.ProcessActions(toExecute);
+                }
+
+                List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
+                bool undoBoundaryPassed =
+                    toExecute.Any(static action =>
+                        action.action is ChangeBoundary_Action or Redo_Action or Undo_Action);
+                bool viewportRefreshRequest =
+                    toExecute.Any(static action => action.action is RefreshViewport_PassthroughAction);
+                bool changeFrameRequest =
+                    toExecute.Any(static action => action.action is SetActiveFrame_PassthroughAction);
+                foreach (IChangeInfo info in optimizedChanges)
+                {
+                    internals.Updater.ApplyChangeFromChangeInfo(info);
+                }
+
+                if (undoBoundaryPassed)
+                    internals.Updater.AfterUndoBoundaryPassed();
+
+                // update the contents of the bitmaps
+                var affectedAreas = new AffectedAreasGatherer(document.AnimationHandler.ActiveFrameTime,
+                    internals.Tracker,
+                    optimizedChanges);
+                if (DrawingBackendApi.Current.IsHardwareAccelerated)
+                {
+                    canvasUpdater.UpdateGatheredChunksSync(affectedAreas,
+                        undoBoundaryPassed || viewportRefreshRequest);
+                }
+                else
+                {
+                    await canvasUpdater.UpdateGatheredChunks(affectedAreas,
+                        undoBoundaryPassed || viewportRefreshRequest);
+                }
+
+                previewUpdater.UpdatePreviews(undoBoundaryPassed || changeFrameRequest || viewportRefreshRequest,
+                    affectedAreas.ImagePreviewAreas.Keys,
+                    affectedAreas.MaskPreviewAreas.Keys,
+                    affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames);
+
+                // force refresh viewports for better responsiveness
+                foreach (var (_, value) in internals.State.Viewports)
+                {
+                    if (!value.Delayed)
+                        value.InvalidateVisual();
+                }
             }
         }
+        catch (Exception e)
+        {
+            busyTimer.Stop();
+            document.Busy = false;
+            executing = false;
+#if DEBUG
+            Console.WriteLine(e);
+#endif
+            await CrashHelper.SendExceptionInfoAsync(e);
+            throw;
+        }
 
         busyTimer.Stop();
         if (document.Busy)

+ 18 - 2
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -221,12 +221,28 @@ internal class ChangeExecutionController
         currentSession?.OnLeftMouseButtonUp(argsPositionOnCanvas);
     }
 
-    public void TransformMovedInlet(ShapeCorners corners)
+    public void TransformChangedInlet(ShapeCorners corners)
     {
         if (currentSession is ITransformableExecutor transformableExecutor)
         {
             LastTransformState = corners;
-            transformableExecutor.OnTransformMoved(corners);
+            transformableExecutor.OnTransformChanged(corners);
+        }
+    }
+    
+    public void TransformDraggedInlet(VecD from, VecD to)
+    {
+        if (currentSession is ITransformDraggedEvent transformableExecutor)
+        {
+            transformableExecutor.OnTransformDragged(from, to);
+        }
+    }
+
+    public void TransformStoppedInlet()
+    {
+        if(currentSession is ITransformStoppedEvent transformStoppedEvent)
+        {
+            transformStoppedEvent.OnTransformStopped();
         }
     }
 

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

@@ -89,7 +89,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         bool drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
         if (drawOnMask && !member.HasMaskBindable)
             return;
-        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.Id, drawOnMask, frame));
+        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.Id, Internals.Tracker.Document.Selection.SelectionPath, drawOnMask, frame));
         if (clearSelection)
             Internals.ActionAccumulator.AddActions(new ClearSelection_Action());
         Internals.ActionAccumulator.AddFinishedActions();
@@ -210,13 +210,13 @@ internal class DocumentOperationsModule : IDocumentOperations
         bool isFolder = Document.StructureHelper.Find(guidValue) is IFolderHandler;
         if (!isFolder)
         {
-            Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
+            Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue, Guid.NewGuid()));
         }
         else
         {
             Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
-                new DuplicateFolder_Action(guidValue, newGuid),
+                new DuplicateFolder_Action(guidValue, newGuid, null),
                 new SetSelectedMember_PassthroughAction(newGuid));
         }
     }
@@ -881,7 +881,8 @@ internal class DocumentOperationsModule : IDocumentOperations
 
         Internals.ChangeController.TryStopActiveExecutor();
 
-        Internals.ActionAccumulator.AddFinishedActions(new KeyFrameLength_Action(celId, startFrame, duration), new EndKeyFrameLength_Action());
+        Internals.ActionAccumulator.AddFinishedActions(new KeyFrameLength_Action(celId, startFrame, duration),
+            new EndKeyFrameLength_Action());
     }
 
     public void DeleteNodes(Guid[] nodes)
@@ -890,7 +891,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             return;
 
         Internals.ChangeController.TryStopActiveExecutor();
-        
+
         List<IAction> actions = new();
 
         for (var i = 0; i < nodes.Length; i++)
@@ -899,7 +900,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             if (Document.StructureHelper.TryFindNode(node, out INodeHandler nodeHandler) &&
                 nodeHandler.InternalName == OutputNode.UniqueName)
                 return;
-            
+
             actions.Add(new DeleteNode_Action(node));
         }
 

+ 14 - 1
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -146,7 +146,7 @@ internal class DocumentStructureModule
         return layers;
     }
 
-    public List<IStructureMemberHandler> GetAllMembers()
+    public List<IStructureMemberHandler> TraverseAllMembers()
     {
         List<IStructureMemberHandler> members = new List<IStructureMemberHandler>();
 
@@ -160,6 +160,19 @@ internal class DocumentStructureModule
         return members;
     }
 
+    public List<IStructureMemberHandler> GetAllMembers()
+    {
+        List<IStructureMemberHandler> members = new List<IStructureMemberHandler>();
+
+        foreach (INodeHandler node in doc.NodeGraphHandler.AllNodes)
+        {
+            if (node is IStructureMemberHandler member)
+                members.Add(member);
+        }
+
+        return members;
+    }
+
     private void FillPath(INodeHandler node, List<INodeHandler> toFill)
     {
         node.TraverseForwards(newNode =>

+ 0 - 1
src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs

@@ -21,7 +21,6 @@ internal class DocumentToolsModule
 
     public void UseOpacitySlider() => Internals.ChangeController.TryStartExecutor<StructureMemberOpacityExecutor>();
 
-    public void UseShiftLayerTool() => Internals.ChangeController.TryStartExecutor<ShiftLayerExecutor>();
 
     public void UsePenTool() => Internals.ChangeController.TryStartExecutor<PenToolExecutor>();
 

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs

@@ -140,7 +140,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         return pos1;
     }
 
-    public override void OnTransformMoved(ShapeCorners corners)
+    public override void OnTransformChanged(ShapeCorners corners)
     {
         if (ActiveMode != ShapeToolMode.Transform)
             return;

+ 9 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformDraggedEvent.cs

@@ -0,0 +1,9 @@
+using ChunkyImageLib.DataHolders;
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+
+public interface ITransformDraggedEvent : IExecutorFeature
+{
+    public void OnTransformDragged(VecD from, VecD to);
+}

+ 6 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformStoppedEvent.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+
+public interface ITransformStoppedEvent : IExecutorFeature
+{
+    public void OnTransformStopped();
+}

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformableExecutor.cs

@@ -6,7 +6,7 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 public interface ITransformableExecutor : IExecutorFeature
 {
     public bool IsTransforming { get; }
-    public void OnTransformMoved(ShapeCorners corners); 
+    public void OnTransformChanged(ShapeCorners corners); 
     public void OnTransformApplied();
     public void OnLineOverlayMoved(VecD start, VecD end);
     public void OnSelectedObjectNudged(VecI distance);

+ 3 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -79,6 +79,9 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         if (member is IVectorLayerHandler)
         {
             var node = (VectorLayerNode)internals.Tracker.Document.FindMember(member.Id);
+            
+            if(node is null)
+                return ExecutionState.Error;
 
             if (node.ShapeData is not IReadOnlyLineData data)
             {

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs

@@ -20,7 +20,7 @@ internal class MagicWandToolExecutor : UpdateableChangeExecutor
     public override ExecutionState Start()
     {
         var magicWand = GetHandler<IMagicWandToolHandler>();
-        var members = document!.ExtractSelectedLayers(true);
+        var members = document!.ExtractSelectedLayers(true).ToList();
 
         if (magicWand is null || members.Count == 0)
             return ExecutionState.Error;

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -62,7 +62,7 @@ internal class PasteImageExecutor : UpdateableChangeExecutor, ITransformableExec
 
     public bool IsTransforming => true; 
 
-    public void OnTransformMoved(ShapeCorners corners)
+    public void OnTransformChanged(ShapeCorners corners)
     {
         internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, default));
     }

+ 0 - 74
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs

@@ -1,74 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using PixiEditor.ChangeableDocument.Actions.Generated;
-using Drawie.Backend.Core.Numerics;
-using PixiEditor.Models.Handlers;
-using PixiEditor.Models.Handlers.Tools;
-using PixiEditor.Models.Tools;
-using Drawie.Numerics;
-
-namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
-#nullable enable
-internal class ShiftLayerExecutor : UpdateableChangeExecutor
-{
-    private List<Guid> _affectedMemberGuids = new List<Guid>();
-    private VecI startPos;
-    private IMoveToolHandler? tool;
-
-    public override ExecutorStartMode StartMode => ExecutorStartMode.OnMouseLeftButtonDown;
-
-    public override ExecutionState Start()
-    {
-        IStructureMemberHandler? member = document!.SelectedStructureMember;
-
-        tool = GetHandler<IMoveToolHandler>();
-        if (tool is null)
-            return ExecutionState.Error;
-
-
-        if (member != null)
-            _affectedMemberGuids.Add(member.Id);
-        _affectedMemberGuids.AddRange(document!.SoftSelectedStructureMembers.Select(x => x.Id));
-
-        RemoveDrawOnMaskLayers(_affectedMemberGuids);
-
-        startPos = controller!.LastPixelPosition;
-
-        ShiftLayer_Action action = new(_affectedMemberGuids, VecI.Zero, tool.KeepOriginalImage,
-            document!.AnimationHandler.ActiveFrameBindable);
-        internals!.ActionAccumulator.AddActions(action);
-
-        return ExecutionState.Success;
-    }
-
-    private void RemoveDrawOnMaskLayers(List<Guid> affectedMemberGuids)
-    {
-        for (var i = 0; i < affectedMemberGuids.Count; i++)
-        {
-            var guid = affectedMemberGuids[i];
-            if (document!.StructureHelper.FindOrThrow(guid) is ILayerHandler { ShouldDrawOnMask: true })
-            {
-                _affectedMemberGuids.Remove(guid);
-                i--;
-            }
-        }
-    }
-
-    public override void OnPixelPositionChange(VecI pos)
-    {
-        ShiftLayer_Action action = new(_affectedMemberGuids, pos - startPos, tool!.KeepOriginalImage,
-            document!.AnimationHandler.ActiveFrameBindable);
-        internals!.ActionAccumulator.AddActions(action);
-    }
-
-    public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)
-    {
-        internals!.ActionAccumulator.AddFinishedActions(new EndShiftLayer_Action());
-        onEnded?.Invoke(this);
-    }
-
-    public override void ForceStop()
-    {
-        internals!.ActionAccumulator.AddFinishedActions(new EndShiftLayer_Action());
-    }
-}

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs

@@ -145,7 +145,7 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
 
     public bool IsTransforming => ActiveMode == ShapeToolMode.Transform; 
 
-    public virtual void OnTransformMoved(ShapeCorners corners)
+    public virtual void OnTransformChanged(ShapeCorners corners)
     {
         
     }

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformReferenceLayerExecutor.cs

@@ -22,7 +22,7 @@ internal class TransformReferenceLayerExecutor : UpdateableChangeExecutor, ITran
 
     public bool IsTransforming => true;
 
-    public void OnTransformMoved(ShapeCorners corners)
+    public void OnTransformChanged(ShapeCorners corners)
     {
         internals!.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(corners));
     }

+ 158 - 4
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -1,21 +1,28 @@
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Linq;
 using Avalonia.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.ViewModels.Document.Nodes;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
-internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransformableExecutor, IMidChangeUndoableExecutor
+internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransformableExecutor, ITransformDraggedEvent,
+    IMidChangeUndoableExecutor,
+    ITransformStoppedEvent
 {
     private Dictionary<Guid, ShapeCorners> memberCorners = new();
     private IMoveToolHandler? tool;
@@ -25,9 +32,12 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
     public override bool BlocksOtherActions => false;
 
     private List<Guid> selectedMembers = new();
+    private List<Guid> originalSelectedMembers = new();
 
+    private ShapeCorners cornersOnStartDuplicate;
     private ShapeCorners lastCorners = new();
     private bool movedOnce;
+    private bool duplicateOnStop = false;
 
     public TransformSelectedExecutor(bool toolLinked)
     {
@@ -42,7 +52,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
         tool.TransformingSelectedArea = true;
         List<IStructureMemberHandler> members = new();
-
+        originalSelectedMembers = document.SelectedMembers.ToList();
         var guids = document.ExtractSelectedLayers(false);
         members = guids.Select(g => document.StructureHelper.Find(g)).ToList();
 
@@ -50,6 +60,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             return ExecutionState.Error;
 
         document.TransformHandler.PassthroughPointerPressed += OnLeftMouseButtonDown;
+
         return SelectMembers(members);
     }
 
@@ -115,6 +126,8 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         selectedMembers = members.Select(m => m.Id).ToList();
 
         lastCorners = masterCorners;
+
+
         document.TransformHandler.ShowTransform(mode, true, masterCorners,
             Type == ExecutorType.Regular || tool.KeepOriginalImage);
 
@@ -122,6 +135,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
         movedOnce = false;
         isInProgress = true;
+
         return ExecutionState.Success;
     }
 
@@ -164,6 +178,11 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         }
     }
 
+    public void OnTransformStopped()
+    {
+        DuplicateIfRequired();
+    }
+
     private void Deselect(List<ILayerHandler> topMostWithinClick)
     {
         var topMost = topMostWithinClick.FirstOrDefault();
@@ -212,6 +231,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         if (isInProgress)
         {
             internals.ActionAccumulator.AddActions(new EndTransformSelected_Action());
+            internals!.ActionAccumulator.AddActions(new EndPreviewShiftLayers_Action());
             document!.TransformHandler.HideTransform();
             AddSnappingForMembers(selectedMembers);
 
@@ -223,22 +243,48 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
     public bool IsTransforming => isInProgress;
 
-    public void OnTransformMoved(ShapeCorners corners)
+    public void OnTransformChanged(ShapeCorners corners)
     {
         DoTransform(corners);
         lastCorners = corners;
     }
 
+    public void OnTransformDragged(VecD from, VecD to)
+    {
+        if (!isInProgress)
+            return;
+
+        if (tool.DuplicateOnMove)
+        {
+            if (!duplicateOnStop)
+            {
+                cornersOnStartDuplicate = lastCorners;
+                duplicateOnStop = true;
+                internals.ActionAccumulator.AddFinishedActions(new EndTransformSelected_Action());
+            }
+
+            VecD delta = new VecD(
+                to.X - from.X,
+                to.Y - from.Y);
+
+            internals.ActionAccumulator.AddActions(new PreviewShiftLayers_Action(selectedMembers, delta,
+                document!.AnimationHandler.ActiveFrameBindable));
+        }
+    }
+
     private void DoTransform(ShapeCorners corners)
     {
         if (!isInProgress)
             return;
 
+        if (duplicateOnStop) return;
+
         if (!movedOnce)
         {
             internals!.ActionAccumulator.AddActions(
                 new TransformSelected_Action(lastCorners, tool.KeepOriginalImage, memberCorners, false,
                     document.AnimationHandler.ActiveFrameBindable));
+
             movedOnce = true;
         }
 
@@ -247,6 +293,101 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
                 document!.AnimationHandler.ActiveFrameBindable));
     }
 
+    private void DuplicateSelected()
+    {
+        List<IAction> actions = new();
+
+        List<Guid> newLayerGuids = new();
+        List<Guid> newGuidsOfOriginal = new();
+
+        internals.ActionAccumulator.StartChangeBlock();
+
+        actions.Add(new EndPreviewShiftLayers_Action());
+
+        VectorPath? original = document.SelectionPathBindable != null
+            ? new VectorPath(document.SelectionPathBindable)
+            : null;
+
+        VectorPath? clearArea = null;
+        if (original != null)
+        {
+            var selection = document.SelectionPathBindable;
+            var inverse = new VectorPath();
+            inverse.AddRect(new RectD(new(0, 0), document.SizeBindable));
+
+            clearArea = inverse.Op(selection, VectorPathOp.Difference);
+        }
+
+        for (var i = 0; i < originalSelectedMembers.Count; i++)
+        {
+            var member = originalSelectedMembers[i];
+            Guid newGuid = Guid.NewGuid();
+            if (document.StructureHelper.Find(member) is not FolderNodeViewModel folder)
+            {
+                newLayerGuids.Add(newGuid);
+                actions.Add(new DuplicateLayer_Action(member, newGuid));
+                if (document.SelectionPathBindable is { IsEmpty: false })
+                {
+                    actions.Add(new ClearSelectedArea_Action(newGuid, clearArea, false,
+                        document.AnimationHandler.ActiveFrameBindable));
+                }
+            }
+            else
+            {
+                int childCount = folder.CountChildrenRecursive();
+                Guid[] newGuidsArray = new Guid[childCount];
+                for (var j = 0; j < childCount; j++)
+                {
+                    newGuidsArray[j] = Guid.NewGuid();
+                }
+
+                actions.Add(new DuplicateFolder_Action(member, newGuid, newGuidsArray.ToImmutableList()));
+
+                for (int j = 0; j < childCount; j++)
+                {
+                    if (document.SelectionPathBindable is { IsEmpty: false })
+                    {
+                        actions.Add(new ClearSelectedArea_Action(newGuidsArray[j], clearArea, false,
+                            document.AnimationHandler.ActiveFrameBindable));
+                    }
+                }
+
+                newLayerGuids.AddRange(newGuidsArray);
+            }
+
+            newGuidsOfOriginal.Add(newGuid);
+        }
+
+        internals!.ActionAccumulator.AddFinishedActions(actions.ToArray());
+
+        actions.Clear();
+
+        VecD delta = new VecD(
+            lastCorners.AABBBounds.TopLeft.X - cornersOnStartDuplicate.AABBBounds.TopLeft.X,
+            lastCorners.AABBBounds.TopLeft.Y - cornersOnStartDuplicate.AABBBounds.TopLeft.Y);
+
+        actions.Add(new ShiftLayer_Action(newLayerGuids, delta, document!.AnimationHandler.ActiveFrameBindable));
+
+        internals!.ActionAccumulator.AddFinishedActions(actions.ToArray());
+
+        actions.Clear();
+
+        actions.Add(new ClearSoftSelectedMembers_PassthroughAction());
+        foreach (var newGuid in newGuidsOfOriginal)
+        {
+            actions.Add(new AddSoftSelectedMember_PassthroughAction(newGuid));
+        }
+
+        actions.Add(new SetSelectedMember_PassthroughAction(newGuidsOfOriginal.Last()));
+
+        internals!.ActionAccumulator.AddFinishedActions(actions.ToArray());
+
+
+        internals.ActionAccumulator.EndChangeBlock();
+
+        tool!.DuplicateOnMove = false;
+    }
+
     public void OnLineOverlayMoved(VecD start, VecD end) { }
 
     public void OnSelectedObjectNudged(VecI distance) => document!.TransformHandler.Nudge(distance);
@@ -264,6 +405,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             tool.TransformingSelectedArea = false;
         }
 
+        internals!.ActionAccumulator.AddActions(new EndPreviewShiftLayers_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
@@ -286,6 +428,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             tool.TransformingSelectedArea = false;
         }
 
+        internals!.ActionAccumulator.AddActions(new EndPreviewShiftLayers_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
@@ -293,6 +436,16 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
         isInProgress = false;
         document.TransformHandler.PassthroughPointerPressed -= OnLeftMouseButtonDown;
+        DuplicateIfRequired();
+    }
+
+    private void DuplicateIfRequired()
+    {
+        if (duplicateOnStop)
+        {
+            DuplicateSelected();
+            duplicateOnStop = false;
+        }
     }
 
     private void AddSnappingForMembers(List<Guid> memberGuids)
@@ -314,6 +467,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
     public bool IsFeatureEnabled(IExecutorFeature feature)
     {
-        return feature is ITransformableExecutor && IsTransforming;
+        return feature is ITransformableExecutor && IsTransforming || feature is IMidChangeUndoableExecutor ||
+               feature is ITransformStoppedEvent || feature is ITransformDraggedEvent;
     }
 }

+ 1 - 1
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -58,7 +58,7 @@ internal interface IDocument : IHandler
     public void SetProcessingColorSpace(ColorSpace infoColorSpace);
     public void SetSize(VecI infoSize);
     public Color PickColor(VecD controllerLastPrecisePosition, DocumentScope scope, bool includeReference, bool includeCanvas, int frame, bool isTopMost);
-    public List<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false);
+    public HashSet<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false);
     public void UpdateSavedState();
 
     internal void InternalRaiseLayersChanged(LayersChangedEventArgs e);

+ 3 - 1
src/PixiEditor/Models/Handlers/IToolHandler.cs

@@ -56,7 +56,7 @@ internal interface IToolHandler : IHandler
 
     public virtual string? DefaultNewLayerName => null;
 
-    public void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown);
+    public void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey);
     public void UseTool(VecD pos);
     public void OnToolSelected(bool restoring);
 
@@ -66,4 +66,6 @@ internal interface IToolHandler : IHandler
     public void OnPostUndo();
     public void OnPostRedo();
     public void OnActiveFrameChanged(int newFrame);
+    public void OnPreUndoInlet();
+    public void OnPreRedoInlet();
 }

+ 2 - 0
src/PixiEditor/Models/Handlers/IToolsHandler.cs

@@ -33,4 +33,6 @@ internal interface IToolsHandler : IHandler
     public void AddPropertyChangedCallback(string propertyName, Action callback);
     public void OnPostUndoInlet();
     public void OnPostRedoInlet();
+    public void OnPreRedoInlet();
+    public void OnPreUndoInlet();
 }

+ 4 - 1
src/PixiEditor/Models/Handlers/Tools/IMoveToolHandler.cs

@@ -1,7 +1,10 @@
-namespace PixiEditor.Models.Handlers.Tools;
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.Handlers.Tools;
 
 internal interface IMoveToolHandler : IToolHandler
 {
     public bool KeepOriginalImage { get; }
     public bool TransformingSelectedArea { get; set; }
+    public bool DuplicateOnMove { get; set; }
 }

+ 1 - 1
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -156,7 +156,7 @@ public class PreviewPainter
                             return;
                         }
                         
-                        if (renderTexture != null && !renderTexture.IsDisposed)
+                        if (renderTexture is { IsDisposed: false })
                         {
                             renderTexture.DrawingSurface.Canvas.Restore();
                         }

+ 16 - 7
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -236,7 +236,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         NodeGraph = new NodeGraphViewModel(this, Internals);
 
         TransformViewModel = new(this);
-        TransformViewModel.TransformMoved += (_, args) => Internals.ChangeController.TransformMovedInlet(args);
+        TransformViewModel.TransformChanged += (args) => Internals.ChangeController.TransformChangedInlet(args);
+        TransformViewModel.TransformDragged += (from, to) => Internals.ChangeController.TransformDraggedInlet(from, to);
+        TransformViewModel.TransformStopped += () => Internals.ChangeController.TransformStoppedInlet();
 
         PathOverlayViewModel = new(this, Internals);
         PathOverlayViewModel.PathChanged += path =>
@@ -547,7 +549,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
         for (int i = 0; i < selectedLayers.Count; i++)
         {
-            var memberVm = StructureHelper.Find(selectedLayers[i]);
+            var memberVm = StructureHelper.Find(selectedLayers.ElementAt(i));
             IReadOnlyStructureNode? layer = Internals.Tracker.Document.FindMember(memberVm.Id);
             if (layer is null)
                 return new Error();
@@ -679,7 +681,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             if (scope == DocumentScope.AllLayers)
             {
                 using Surface tmpSurface = new Surface(SizeBindable);
-                HashSet<Guid> layers = StructureHelper.GetAllMembers().Select(x => x.Id).ToHashSet();
+                HashSet<Guid> layers = StructureHelper.TraverseAllMembers().Select(x => x.Id).ToHashSet();
                 Renderer.RenderLayers(tmpSurface.DrawingSurface, layers, frameTime.Frame, ChunkResolution.Full,
                     SizeBindable);
 
@@ -814,8 +816,15 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         List<Guid> layerGuids = new List<Guid>();
         if (SelectedStructureMember is not null)
             layerGuids.Add(SelectedStructureMember.Id);
+        
+        foreach (var member in softSelectedStructureMembers)
+        {
+            if (member.Id != SelectedStructureMember?.Id)
+            {
+                layerGuids.Add(member.Id);
+            }
+        }
 
-        layerGuids.AddRange(softSelectedStructureMembers.Select(x => x.Id));
         return layerGuids;
     }
 
@@ -824,9 +833,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     /// </summary>
     /// <param name="includeFoldersWithMask">Should folders with mask be included</param>
     /// <returns>A list of GUIDs of selected layers</returns>
-    public List<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false)
+    public HashSet<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false)
     {
-        var result = new List<Guid>();
+        var result = new HashSet<Guid>();
         List<Guid> selectedMembers = GetSelectedMembers();
         var allLayers = StructureHelper.GetAllMembers();
         foreach (var member in allLayers)
@@ -855,7 +864,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         OnPropertyChanged(nameof(AllChangesSaved));
     }
 
-    private void ExtractSelectedLayers(IFolderHandler folder, List<Guid> list,
+    private void ExtractSelectedLayers(IFolderHandler folder, HashSet<Guid> list,
         bool includeFoldersWithMask)
     {
         foreach (var member in folder.Children)

+ 18 - 0
src/PixiEditor/ViewModels/Document/Nodes/FolderNodeViewModel.cs

@@ -10,4 +10,22 @@ namespace PixiEditor.ViewModels.Document.Nodes;
 internal class FolderNodeViewModel : StructureMemberViewModel<FolderNode>, IFolderHandler
 {
     public ObservableCollection<IStructureMemberHandler> Children { get; } = new();
+
+    public int CountChildrenRecursive()
+    {
+        int count = 0;
+        foreach (var child in Children)
+        {
+            if (child is FolderNodeViewModel folder)
+            {
+                count += folder.CountChildrenRecursive();
+            }
+            else
+            {
+                count++;
+            }
+        }
+
+        return count;
+    }
 }

+ 11 - 2
src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs

@@ -135,7 +135,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         set
         {
             SetProperty(ref corners, value);
-            TransformMoved?.Invoke(this, value);
+            TransformChanged?.Invoke(value);
         }
     }
 
@@ -190,8 +190,16 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
                 new RelayCommand<MouseOnCanvasEventArgs>(x => PassthroughPointerPressed?.Invoke(x));
         }
     }
+    
+    private RelayCommand<(VecD, VecD)>? transformDraggedCommand;
+    public RelayCommand<(VecD, VecD)> TransformDraggedCommand
+    {
+        get => transformDraggedCommand ??= new RelayCommand<(VecD from, VecD to)>(x => TransformDragged?.Invoke(x.from, x.to));
+    }
 
-    public event EventHandler<ShapeCorners>? TransformMoved;
+    public event Action<ShapeCorners>? TransformChanged;
+    public event Action<VecD, VecD> TransformDragged;
+    public event Action TransformStopped; 
 
     private DocumentTransformMode activeTransformMode = DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective;
 
@@ -200,6 +208,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         this.document = document;
         ActionCompletedCommand = new RelayCommand(() =>
         {
+            TransformStopped?.Invoke();
             AddToUndoCommand?.Execute(Corners);
 
             if (undoStack is null)

+ 15 - 4
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -101,6 +101,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     private bool shiftIsDown;
     private bool ctrlIsDown;
     private bool altIsDown;
+    private Key lastKey;
 
     private ToolViewModel _preTransientTool;
 
@@ -262,9 +263,9 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         }
 
         //update old tool
-        LastActionTool?.ModifierKeyChanged(false, false, false);
+        LastActionTool?.KeyChanged(false, false, false, Key.None);
         //update new tool
-        ActiveTool.ModifierKeyChanged(ctrlIsDown, shiftIsDown, altIsDown);
+        ActiveTool.KeyChanged(ctrlIsDown, shiftIsDown, altIsDown, lastKey);
         ActiveTool.OnToolSelected(wasTransient);
 
         tool.IsActive = true;
@@ -415,12 +416,12 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
 
     public void ConvertedKeyDownInlet(FilteredKeyEventArgs args)
     {
-        ActiveTool?.ModifierKeyChanged(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown);
+        ActiveTool?.KeyChanged(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown, args.Key);
     }
 
     public void ConvertedKeyUpInlet(FilteredKeyEventArgs args)
     {
-        ActiveTool?.ModifierKeyChanged(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown);
+        ActiveTool?.KeyChanged(args.IsCtrlDown, args.IsShiftDown, args.IsAltDown, args.Key);
     }
     
     public void OnPostUndoInlet()
@@ -432,6 +433,16 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     {
         ActiveTool?.OnPostRedo();
     }
+    
+    public void OnPreUndoInlet()
+    {
+        ActiveTool?.OnPreUndoInlet();
+    }
+    
+    public void OnPreRedoInlet()
+    {
+        ActiveTool?.OnPreRedoInlet();
+    }
 
     private void ToolbarSettingChanged(string settingName, object value)
     {

+ 10 - 0
src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs

@@ -28,6 +28,11 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedRedo))
             return;
         
+        doc.Operations.InvokeCustomAction(
+            () =>
+        {
+            Owner.ToolsSubViewModel.OnPreRedoInlet();
+        }, false);
         doc.Operations.Redo();
         doc.Operations.InvokeCustomAction(
             () =>
@@ -48,6 +53,11 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedUndo))
             return;
+        doc.Operations.InvokeCustomAction(
+            () =>
+        {
+            Owner.ToolsSubViewModel.OnPreUndoInlet();
+        }, false);
         doc.Operations.Undo();
         doc.Operations.InvokeCustomAction(
             () =>

+ 20 - 1
src/PixiEditor/ViewModels/Tools/ToolViewModel.cs

@@ -153,7 +153,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
         ActionDisplay = new LocalizedString(ActionDisplay.Key);
     }
 
-    public virtual void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) { }
+    public virtual void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey) { }
 
     public virtual void UseTool(VecD pos) { }
 
@@ -188,7 +188,9 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
     public virtual void OnPostUndo() { }
     public virtual void OnPostRedo() { }
     public virtual void OnActiveFrameChanged(int newFrame) { }
+    public virtual void OnPreUndoInlet() { }
 
+    public virtual void OnPreRedoInlet() { }
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings)
     {
         if (settings == null || settings.Count == 0 || toolset == null)
@@ -299,6 +301,23 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
         }
     }
 
+    protected void SetValue<T>(T value, [CallerMemberName] string name = null)
+    {
+        var setting = Toolbar.GetSetting(name);
+        if(setting is null)
+        {
+            throw new InvalidOperationException($"Setting {name} not found in toolbar {Toolbar.GetType().Name}");
+        }
+
+        if (setting.GetSettingType() != typeof(T))
+        {
+            throw new InvalidCastException($"Setting {name} is not of type {typeof(T).Name}");
+        }
+        
+        setting.Value = value;
+    }
+    
+
     private bool IsExposeSetting(KeyValuePair<string, object> settingConfig, out bool expose)
     {
         bool isExpose = settingConfig.Key.StartsWith("Expose", StringComparison.InvariantCultureIgnoreCase);

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs

@@ -58,7 +58,7 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         if (!ctrlIsDown)
         {

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/ColorPickerToolViewModel.cs

@@ -168,6 +168,6 @@ internal class ColorPickerToolViewModel : ToolViewModel, IColorPickerHandler
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseColorPickerTool();
     }
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) =>
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey) =>
         UpdateActionDisplay(ctrlIsDown, shiftIsDown);
 }

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/FloodFillToolViewModel.cs

@@ -42,7 +42,7 @@ internal class FloodFillToolViewModel : ToolViewModel, IFloodFillToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         if (ctrlIsDown)
         {

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/LassoToolViewModel.cs

@@ -27,7 +27,7 @@ internal class LassoToolViewModel : ToolViewModel, ILassoToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         if (shiftIsDown)
         {

+ 28 - 7
src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -42,6 +42,13 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
         }
     }
 
+    [Settings.Bool("_duplicate_on_move", ExposedByDefault = false)]
+    public bool DuplicateOnMove
+    {
+        get => GetValue<bool>();
+        set => SetValue(value);
+    }
+
     public override BrushShape BrushShape => BrushShape.Hidden;
     public override Type[]? SupportedLayerTypes { get; } = null;
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
@@ -57,23 +64,25 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
         }
     }
 
+
     public override void UseTool(VecD pos)
     {
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(true);
     }
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
-        
+        DuplicateOnMove = ctrlIsDown && argsKey is Key.None or Key.LeftCtrl or Key.RightCtrl && !shiftIsDown && !altIsDown;
     }
-
+    
     protected override void OnSelected(bool restoring)
     {
         if (TransformingSelectedArea)
         {
             return;
         }
-        
+
+        DuplicateOnMove = false;
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(true);
     }
 
@@ -84,6 +93,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
         {
             vm.DocumentManagerSubViewModel.ActiveDocument?.Operations.TryStopToolLinkedExecutor();
             TransformingSelectedArea = false;
+            DuplicateOnMove = false;
         }
     }
 
@@ -91,7 +101,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
     {
         if (IsActive)
         {
-           OnToolSelected(false);
+            OnToolSelected(false);
         }
     }
 
@@ -99,15 +109,26 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
     {
         if (IsActive)
         {
+            TransformingSelectedArea = false;
             OnToolSelected(false);
         }
     }
 
+    public override void OnPreUndoInlet()
+    {
+        DuplicateOnMove = false;
+    }
+    
+    public override void OnPreRedoInlet()
+    {
+        DuplicateOnMove = false;
+    }
+
     protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
     {
         UpdateSelection();
     }
-    
+
     public override void OnActiveFrameChanged(int newFrame)
     {
         UpdateSelection();
@@ -126,7 +147,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
         {
             return;
         }
-        
+
         activeDocument.TransformViewModel.ShowTransformControls = KeepOriginalImage;
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs

@@ -47,7 +47,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
 
         public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
 
-        public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+        public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
         {
             ActionDisplay = new LocalizedString("PEN_TOOL_ACTION_DISPLAY", Shortcut);
         }

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs

@@ -28,7 +28,7 @@ internal class RasterEllipseToolViewModel : ShapeTool, IRasterEllipseToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         DrawFromCenter = ctrlIsDown;
         

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs

@@ -35,7 +35,7 @@ internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         DrawFromCenter = ctrlIsDown;
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs

@@ -30,7 +30,7 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         DrawFromCenter = ctrlIsDown;
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/SelectToolViewModel.cs

@@ -32,7 +32,7 @@ internal class SelectToolViewModel : ToolViewModel, ISelectToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         if (shiftIsDown)
         {

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs

@@ -42,7 +42,7 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorEllipseTool();
     }
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         DrawFromCenter = ctrlIsDown;
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs

@@ -43,7 +43,7 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         DrawFromCenter = ctrlIsDown;
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs

@@ -84,7 +84,7 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
         }
     }
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         if (ctrlIsDown)
         {

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs

@@ -34,7 +34,7 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
     public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         DrawFromCenter = ctrlIsDown;
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/ZoomToolViewModel.cs

@@ -36,7 +36,7 @@ internal class ZoomToolViewModel : ToolViewModel
 
     public override LocalizedString Tooltip => new LocalizedString("ZOOM_TOOL_TOOLTIP", Shortcut);
 
-    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         if (ctrlIsDown)
         {

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

@@ -347,6 +347,11 @@ internal class ViewportOverlays
         {
             Source = Viewport, Path = "Document.TransformViewModel.LockShear", Mode = BindingMode.OneWay
         };
+        
+        Binding transformDraggedBinding = new()
+        {
+            Source = Viewport, Path = "Document.TransformViewModel.TransformDraggedCommand", Mode = BindingMode.OneWay
+        };
 
         transformOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         transformOverlay.Bind(TransformOverlay.ActionCompletedProperty, actionCompletedBinding);
@@ -366,6 +371,7 @@ internal class ViewportOverlays
         transformOverlay.Bind(TransformOverlay.ScaleFromCenterProperty, scaleFromCenterBinding);
         transformOverlay.Bind(TransformOverlay.CanAlignToPixelsProperty, canAlignToPixelsBinding);
         transformOverlay.Bind(TransformOverlay.LockShearProperty, lockShearBinding);
+        transformOverlay.Bind(TransformOverlay.TransformDraggedCommandProperty, transformDraggedBinding);
     }
     
     private void BindVectorPathOverlay()

+ 14 - 0
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -179,6 +179,15 @@ internal class TransformOverlay : Overlay
         get => GetValue(LockShearProperty);
         set => SetValue(LockShearProperty, value);
     }
+
+    public static readonly StyledProperty<ICommand> TransformDraggedCommandProperty = AvaloniaProperty.Register<TransformOverlay, ICommand>(
+        nameof(TransformDraggedCommand));
+
+    public ICommand TransformDraggedCommand
+    {
+        get => GetValue(TransformDraggedCommandProperty);
+        set => SetValue(TransformDraggedCommandProperty, value);
+    }
     
     static TransformOverlay()
     {
@@ -770,9 +779,14 @@ internal class TransformOverlay : Overlay
         SnappingController.HighlightedXAxis = snapDeltaResult.SnapAxisXName;
         SnappingController.HighlightedYAxis = snapDeltaResult.SnapAxisYName;
 
+        VecD from = originOnStartMove;
+        
         Corners = ApplyCornersWithDelta(cornersOnStartMove, delta, snapDelta);
 
         InternalState = InternalState with { Origin = originOnStartMove + delta + snapDelta };
+        
+        VecD to = InternalState.Origin;
+        TransformDraggedCommand?.Execute((from, to));
     }
 
     private ShapeCorners ApplyCornersWithDelta(ShapeCorners corners, VecD delta, VecD snapDelta)