Sfoglia il codice sorgente

Merge branch 'master' into rasterize-points-performance

CPKreuz 10 mesi fa
parent
commit
2e85fdf677
73 ha cambiato i file con 1256 aggiunte e 363 eliminazioni
  1. 1 1
      src/PixiDocks
  2. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs
  3. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  4. 32 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  5. 7 4
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs
  6. 153 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs
  7. 68 51
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs
  8. 18 11
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  9. 87 7
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs
  10. 11 1
      src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs
  11. 9 2
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  12. 21 14
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  13. 25 5
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs
  14. 12 3
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs
  15. 99 13
      src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs
  16. 1 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs
  17. 67 0
      src/PixiEditor.ChangeableDocument/Changes/Vectors/ConvertToCurve_Change.cs
  18. 3 1
      src/PixiEditor/Data/Localization/Languages/en.json
  19. 9 6
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  20. 68 51
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  21. 18 2
      src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs
  22. 17 6
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  23. 14 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  24. 0 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs
  25. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  26. 9 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformDraggedEvent.cs
  27. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformStoppedEvent.cs
  28. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformableExecutor.cs
  29. 4 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  30. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs
  31. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs
  32. 0 74
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs
  33. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs
  34. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformReferenceLayerExecutor.cs
  35. 158 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  36. 2 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorLineToolExecutor.cs
  37. 1 1
      src/PixiEditor/Models/Handlers/IDocument.cs
  38. 3 1
      src/PixiEditor/Models/Handlers/IToolHandler.cs
  39. 2 0
      src/PixiEditor/Models/Handlers/IToolsHandler.cs
  40. 4 1
      src/PixiEditor/Models/Handlers/Tools/IMoveToolHandler.cs
  41. 29 7
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  42. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  43. 16 7
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  44. 18 0
      src/PixiEditor/ViewModels/Document/Nodes/FolderNodeViewModel.cs
  45. 11 2
      src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs
  46. 8 2
      src/PixiEditor/ViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs
  47. 19 0
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  48. 15 4
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  49. 10 0
      src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs
  50. 20 1
      src/PixiEditor/ViewModels/Tools/ToolViewModel.cs
  51. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs
  52. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/ColorPickerToolViewModel.cs
  53. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/FloodFillToolViewModel.cs
  54. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/LassoToolViewModel.cs
  55. 28 7
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs
  56. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs
  57. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs
  58. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs
  59. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs
  60. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/SelectToolViewModel.cs
  61. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  62. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs
  63. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  64. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  65. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/ZoomToolViewModel.cs
  66. 1 18
      src/PixiEditor/Views/Dock/ColorPickerDockView.axaml.cs
  67. 41 1
      src/PixiEditor/Views/Dock/ColorSlidersDockView.axaml.cs
  68. 1 0
      src/PixiEditor/Views/Layers/LayerControl.axaml
  69. 1 1
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml
  70. 6 0
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  71. 62 22
      src/PixiEditor/Views/Nodes/NodeGraphView.cs
  72. 14 0
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  73. 1 1
      src/PixiEditor/Views/Rendering/Scene.cs

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit b83ba013241e6d6b6d280eea8836e49c5c6b9f81
+Subproject commit 47107d7dc284e04ed92e4c470a6ed2f972e5d9cd

+ 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();
 }

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -54,12 +54,12 @@ public class MergeNode : RenderNode
         if (Bottom.Value != null && Top.Value != null)
         {
             int saved = target.Canvas.SaveLayer();
-            Bottom.Value.Paint(context, target);
+            Bottom.Value?.Paint(context, target);
 
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             target.Canvas.SaveLayer(paint);
             
-            Top.Value.Paint(context, target);
+            Top.Value?.Paint(context, target);
             target.Canvas.RestoreToCount(saved);
             return;
         }

+ 32 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -44,7 +44,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected internal bool IsDisposed => _isDisposed;
     private bool _isDisposed;
-    
+
     public void Execute(RenderContext context)
     {
         ExecuteInternal(context);
@@ -112,6 +112,36 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
     }
 
+    public void TraverseBackwards(Func<IReadOnlyNode, IReadOnlyNode?, IInputProperty, bool> action)
+    {
+        var visited = new HashSet<IReadOnlyNode>();
+        var queueNodes = new Queue<(IReadOnlyNode, IReadOnlyNode, IInputProperty)>();
+        queueNodes.Enqueue((this, null, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add((node.Item1)))
+            {
+                continue;
+            }
+
+            if (!action(node.Item1, node.Item2, node.Item3))
+            {
+                return;
+            }
+
+            foreach (var inputProperty in node.Item1.InputProperties)
+            {
+                if (inputProperty.Connection != null)
+                {
+                    queueNodes.Enqueue((inputProperty.Connection.Node, node.Item1, inputProperty));
+                }
+            }
+        }
+    }
+
     public void TraverseBackwards(Func<IReadOnlyNode, bool> action)
     {
         var visited = new HashSet<IReadOnlyNode>();
@@ -461,7 +491,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
         return new None();
     }
-    
+
     private void InvokeConnectionsChanged()
     {
         ConnectionsChanged?.Invoke();

+ 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;
     }
 }

+ 87 - 7
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -9,6 +9,7 @@ using PixiEditor.ChangeableDocument.Changes.Structure;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
@@ -29,7 +30,7 @@ public static class NodeOperations
             INodeFactory factory = (INodeFactory)Activator.CreateInstance(factoryType);
             allFactories.Add(factory.NodeType, factory);
         }
-        
+
         nodeMap = new Dictionary<string, Type>();
         var nodeTypes = typeof(Node).Assembly.GetTypes().Where(x =>
                 x.IsSubclassOf(typeof(Node)) && x is { IsAbstract: false, IsInterface: false })
@@ -55,7 +56,7 @@ public static class NodeOperations
     {
         return nodeMap.TryGetValue(nodeUniqueName, out nodeType);
     }
-    
+
     public static Node CreateNode(Type nodeType, IReadOnlyDocument target, params object[] optionalParameters)
     {
         Node node = null;
@@ -71,12 +72,12 @@ public static class NodeOperations
         return node;
     }
 
-    public static List<ConnectProperty_ChangeInfo> AppendMember(
+    public static List<IChangeInfo> AppendMember(
         InputProperty<Painter?> parentInput,
         OutputProperty<Painter> toAddOutput,
         InputProperty<Painter> toAddInput, Guid memberId)
     {
-        List<ConnectProperty_ChangeInfo> changes = new();
+        List<IChangeInfo> changes = new();
         IOutputProperty? previouslyConnected = null;
         if (parentInput.Connection != null)
         {
@@ -94,7 +95,7 @@ public static class NodeOperations
 
         changes.Add(new ConnectProperty_ChangeInfo(memberId, parentInput.Node.Id,
             toAddOutput.InternalPropertyName, parentInput.InternalPropertyName));
-        
+
         return changes;
     }
 
@@ -113,6 +114,7 @@ public static class NodeOperations
             foreach (var input in connections)
             {
                 output.ConnectTo(input);
+
                 changes.Add(new ConnectProperty_ChangeInfo(output.Node.Id, input.Node.Id,
                     output.InternalPropertyName, input.InternalPropertyName));
             }
@@ -133,6 +135,84 @@ public static class NodeOperations
         return changes;
     }
 
+    public static List<IChangeInfo> AdjustPositionsAfterAppend(Node member, Node appendedTo, Node? previouslyConnected,
+        out Dictionary<Guid, VecD> originalPositions)
+    {
+        List<IChangeInfo> changes = new();
+        Dictionary<Guid, VecD> originalPositionDict = new();
+
+        member.Position = new VecD(appendedTo.Position.X - 250, appendedTo.Position.Y);
+
+        changes.Add(new NodePosition_ChangeInfo(member.Id, member.Position));
+
+        previouslyConnected?.TraverseBackwards((aNode, previousNode, _) =>
+        {
+            if (aNode is Node toMove)
+            {
+                originalPositionDict.Add(toMove.Id, toMove.Position);
+                var y = toMove.Position.Y;
+                toMove.Position = (previousNode?.Position ?? member.Position) - new VecD(250, 0);
+                toMove.Position = new VecD(toMove.Position.X, y);
+                changes.Add(new NodePosition_ChangeInfo(toMove.Id, toMove.Position));
+            }
+
+            return true;
+        });
+
+        originalPositions = originalPositionDict;
+        return changes;
+    }
+
+    public static List<IChangeInfo> AdjustPositionsBeforeAppend(Node member, Node appendedTo,
+        out Dictionary<Guid, VecD> originalPositions)
+    {
+        List<IChangeInfo> changes = new();
+        Dictionary<Guid, VecD> originalPositionDict = new();
+
+        member.TraverseBackwards((aNode, previousNode, _) =>
+        {
+            if (aNode is Node toMove)
+            {
+                originalPositionDict.Add(toMove.Id, toMove.Position);
+                var y = toMove.Position.Y;
+                VecD pos = member.Position + new VecD(250, 0);
+                if (previousNode != null)
+                {
+                    pos = previousNode.Position - new VecD(250, 0);
+                }
+
+                toMove.Position = pos;
+                toMove.Position = new VecD(toMove.Position.X, y);
+                changes.Add(new NodePosition_ChangeInfo(toMove.Id, toMove.Position));
+                
+                if(aNode == appendedTo) return false;
+            }
+
+            return true;
+        });
+
+        member.Position = new VecD(appendedTo.Position.X - 250, appendedTo.Position.Y);
+        changes.Add(new NodePosition_ChangeInfo(member.Id, member.Position));
+
+        originalPositions = originalPositionDict;
+        return changes;
+    }
+
+    public static List<IChangeInfo> RevertPositions(Dictionary<Guid, VecD> positions, IReadOnlyDocument target)
+    {
+        List<IChangeInfo> changes = new();
+        foreach (var (guid, position) in positions)
+        {
+            var node = target.FindNode(guid) as Node;
+            if (node == null) continue;
+
+            node.Position = position;
+            changes.Add(new NodePosition_ChangeInfo(guid, position));
+        }
+
+        return changes;
+    }
+
     public static ConnectionsData CreateConnectionsData(IReadOnlyNode node)
     {
         var originalOutputConnections = new Dictionary<PropertyConnection, List<PropertyConnection>>();
@@ -152,7 +232,7 @@ public static class NodeOperations
                 (new PropertyConnection(x.Node.Id, x.InternalPropertyName),
                     new PropertyConnection(x.Connection?.Node.Id, x.Connection?.InternalPropertyName)))
             .ToList();
-        
+
         return new ConnectionsData(originalOutputConnections, originalInputConnections);
     }
 
@@ -246,7 +326,7 @@ public static class NodeOperations
                     value = expressionVariable.GetConstant();
                 }
             }
-            
+
             changes.Add(new PropertyValueUpdated_ChangeInfo(copy.Id, input.InternalPropertyName, value));
         }
 

+ 11 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs

@@ -6,6 +6,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
@@ -111,7 +112,16 @@ internal sealed class FlipImage_Change : Change
                         new LayerImageArea_ChangeInfo(member.Id, image.FindAffectedArea()));
                     image.CommitChanges();
                 }
-                // TODO: Add support for non-raster layers
+                else if (member is ITransformableObject transformableObject)
+                {
+                    RectD? tightBounds = member.GetTightBounds(frame);
+                    if(tightBounds == null) return;
+                    transformableObject.TransformationMatrix = transformableObject.TransformationMatrix.PostConcat(
+                        Matrix3X3.CreateScale(
+                            flipType == FlipType.Horizontal ? -1 : 1,
+                            flipType == FlipType.Vertical ? -1 : 1, 
+                            (float)tightBounds.Value.Center.X, (float)tightBounds.Value.Center.Y));
+                }
 
                 if (member.EmbeddedMask is not null)
                 {

+ 9 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -6,6 +6,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
@@ -172,8 +173,14 @@ internal sealed class RotateImage_Change : Change
                         });
                     }
                 }
-
-                // TODO: Add support for different Layer types
+                else if (member is ITransformableObject transformableObject)
+                {
+                    RectD? tightBounds = member.GetTightBounds(frame.Value);
+                    transformableObject.TransformationMatrix = transformableObject.TransformationMatrix.PostConcat(
+                        Matrix3X3.CreateRotation(
+                            RotationAngleToRadians(rotation),
+                            (float?)tightBounds?.Center.X ?? 0, (float?)tightBounds?.Center.Y ?? 0));
+                }
 
                 if (member.EmbeddedMask is null)
                     return;

+ 21 - 14
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
@@ -13,8 +14,9 @@ internal class CreateStructureMember_Change : Change
 
     private Guid parentGuid;
     private Type structureMemberOfType;
-    
+
     private ConnectionsData? connectionsData;
+    private Dictionary<Guid, VecD> originalPositions;
 
     [GenerateMakeChangeAction]
     public CreateStructureMember_Change(Guid parent, Guid newGuid,
@@ -27,25 +29,28 @@ internal class CreateStructureMember_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        if(structureMemberOfType == null || structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface || !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
+        if (structureMemberOfType == null || structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface ||
+            !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
             return false;
-        
+
         return target.TryFindNode<Node>(parentGuid, out _);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, bool firstApply,
         out bool ignoreInUndo)
     {
-        StructureNode member = (StructureNode)NodeOperations.CreateNode(structureMemberOfType, document); 
+        StructureNode member = (StructureNode)NodeOperations.CreateNode(structureMemberOfType, document);
         member.Id = newMemberGuid;
 
         document.TryFindNode<Node>(parentGuid, out var parentNode);
 
         List<IChangeInfo> changes = new() { CreateChangeInfo(member) };
-        
-        InputProperty<Painter> targetInput = parentNode.InputProperties.FirstOrDefault(x => 
+
+        InputProperty<Painter> targetInput = parentNode.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
-        
+
+        var previouslyConnected = targetInput.Connection;
+
         if (member is FolderNode folder)
         {
             document.NodeGraph.AddNode(member);
@@ -54,11 +59,12 @@ internal class CreateStructureMember_Change : Change
         else
         {
             document.NodeGraph.AddNode(member);
-            List<ConnectProperty_ChangeInfo> connectPropertyChangeInfo =
+            var connectPropertyChangeInfo =
                 NodeOperations.AppendMember(targetInput, member.Output, member.Background, member.Id);
             changes.AddRange(connectPropertyChangeInfo);
         }
-
+        
+        changes.AddRange(NodeOperations.AdjustPositionsAfterAppend(member, targetInput.Node, previouslyConnected?.Node as Node, out originalPositions));
 
         ignoreInUndo = false;
 
@@ -79,7 +85,7 @@ internal class CreateStructureMember_Change : Change
     {
         var container = document.FindNodeOrThrow<Node>(parentGuid);
 
-       InputProperty<Painter> backgroundInput = container.InputProperties.FirstOrDefault(x => 
+        InputProperty<Painter> backgroundInput = container.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
 
         StructureNode child = document.FindMemberOrThrow(newMemberGuid);
@@ -98,15 +104,16 @@ internal class CreateStructureMember_Change : Change
                 backgroundInput.InternalPropertyName);
             changes.Add(change);
         }
+        
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, document));
 
         return changes;
     }
 
-    private static void AppendFolder(InputProperty<Painter> backgroundInput, FolderNode folder, List<IChangeInfo> changes)
+    private static void AppendFolder(InputProperty<Painter> backgroundInput, FolderNode folder,
+        List<IChangeInfo> changes)
     {
         var appened = NodeOperations.AppendMember(backgroundInput, folder.Output, folder.Background, folder.Id);
         changes.AddRange(appened);
     }
-
-    
 }

+ 25 - 5
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs

@@ -1,4 +1,7 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
@@ -13,14 +16,18 @@ internal class DuplicateFolder_Change : Change
     private Guid[] contentGuids;
     private Guid[] contentDuplicateGuids;
 
+    private Guid[]? childGuidsToUse;
+
     private ConnectionsData? connectionsData;
     private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
+    private Dictionary<Guid, VecD> originalPositions;
 
     [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)
@@ -58,11 +65,14 @@ internal class DuplicateFolder_Change : Change
 
         target.NodeGraph.AddNode(clone);
         
+        var previousConnection = targetInput.Connection;
+
         operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node, previousConnection?.Node as Node, out originalPositions));
 
         DuplicateContent(target, clone, existingLayer, operations);
-        
+
         ignoreInUndo = false;
 
         return operations;
@@ -87,7 +97,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();
             }
@@ -100,6 +110,8 @@ internal class DuplicateFolder_Change : Change
                 NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
         }
 
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
         return changes;
     }
 
@@ -109,6 +121,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 +130,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 +153,7 @@ internal class DuplicateFolder_Change : Change
             operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
                 target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
         }
-        
+
         contentDuplicateGuids = contentGuidList.ToArray();
     }
 }

+ 12 - 3
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
@@ -12,18 +13,19 @@ internal class DuplicateLayer_Change : Change
     private Guid duplicateGuid;
     
     private ConnectionsData? connectionsData;
+    private Dictionary<Guid, VecD> originalPositions;
 
     [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);
         
@@ -42,6 +44,8 @@ internal class DuplicateLayer_Change : Change
         InputProperty<Painter?> targetInput = parent.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter) &&
             x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
+        
+        var previousConnection = targetInput.Connection;
 
         List<IChangeInfo> operations = new();
 
@@ -51,6 +55,9 @@ internal class DuplicateLayer_Change : Change
         
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
 
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node,
+            previousConnection?.Node as Node, out originalPositions));
+
         ignoreInUndo = false;
 
         return operations;
@@ -74,6 +81,8 @@ internal class DuplicateLayer_Change : Change
             changes.AddRange(NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
         }
         
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+        
         return changes;
     }
 }

+ 99 - 13
src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

@@ -1,6 +1,8 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 
@@ -14,8 +16,9 @@ internal class MoveStructureMember_Change : Change
 
     private Guid originalFolderGuid;
 
-    private ConnectionsData originalConnections; 
-    
+    private ConnectionsData originalConnections;
+    private Dictionary<Guid, VecD> originalPositions;
+
     private bool putInsideFolder;
 
 
@@ -34,20 +37,22 @@ internal class MoveStructureMember_Change : Change
         if (member is null || targetFolder is null)
             return false;
 
-        originalConnections = NodeOperations.CreateConnectionsData(member); 
-          
+        originalConnections = NodeOperations.CreateConnectionsData(member);
+
         return true;
     }
 
-    private static List<IChangeInfo> Move(Document document, Guid sourceNodeGuid, Guid targetNodeGuid, bool putInsideFolder)
+    private static List<IChangeInfo> Move(Document document, Guid sourceNodeGuid, Guid targetNodeGuid,
+        bool putInsideFolder, out Dictionary<Guid, VecD> originalPositions)
     {
         var sourceNode = document.FindMember(sourceNodeGuid);
         var targetNode = document.FindNode(targetNodeGuid);
+        originalPositions = null;
         if (sourceNode is null || targetNode is not IRenderInput backgroundInput)
             return [];
 
         List<IChangeInfo> changes = new();
-        
+
         Guid oldBackgroundId = sourceNode.Background.Node.Id;
 
         InputProperty<Painter?> inputProperty = backgroundInput.Background;
@@ -58,12 +63,43 @@ internal class MoveStructureMember_Change : Change
         }
 
         MoveStructureMember_ChangeInfo changeInfo = new(sourceNodeGuid, oldBackgroundId, targetNodeGuid);
+
+        var previouslyConnected = inputProperty.Connection;
+
+        bool isMovingBelow = false;
         
+        inputProperty.Node.TraverseForwards(x =>
+        {
+            if (x.Id == sourceNodeGuid)
+            {
+                isMovingBelow = true;
+                return false;
+            }
+            
+            return true;
+        });
+
+        if (isMovingBelow)
+        {
+            changes.AddRange(NodeOperations.AdjustPositionsBeforeAppend(sourceNode, inputProperty.Node, out originalPositions));
+        }
+
         changes.AddRange(NodeOperations.DetachStructureNode(sourceNode));
         changes.AddRange(NodeOperations.AppendMember(inputProperty, sourceNode.Output,
             sourceNode.Background,
             sourceNode.Id));
-        
+
+        if (!isMovingBelow)
+        {
+            changes.AddRange(NodeOperations.AdjustPositionsAfterAppend(sourceNode, inputProperty.Node,
+                previouslyConnected?.Node as Node, out originalPositions));
+        }
+
+        if (targetNode is FolderNode)
+        {
+            changes.AddRange(AdjustPutIntoFolderPositions(targetNode, originalPositions));
+        }
+
         changes.Add(changeInfo);
 
         return changes;
@@ -72,7 +108,7 @@ internal class MoveStructureMember_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
     {
-        var changes = Move(target, memberGuid, targetNodeGuid, putInsideFolder);
+        var changes = Move(target, memberGuid, targetNodeGuid, putInsideFolder, out originalPositions);
         ignoreInUndo = false;
         return changes;
     }
@@ -82,14 +118,64 @@ internal class MoveStructureMember_Change : Change
         StructureNode member = target.FindMember(memberGuid);
 
         List<IChangeInfo> changes = new List<IChangeInfo>();
-        
+
         MoveStructureMember_ChangeInfo changeInfo = new(memberGuid, targetNodeGuid, originalFolderGuid);
-        
+
         changes.AddRange(NodeOperations.DetachStructureNode(member));
         changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, member, target.NodeGraph));
-        
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
         changes.Add(changeInfo);
-        
+
+        return changes;
+    }
+    
+    private static List<IChangeInfo> AdjustPutIntoFolderPositions(Node targetNode, Dictionary<Guid, VecD> originalPositions)
+    {
+        List<IChangeInfo> changes = new();
+
+        if (targetNode is FolderNode folder)
+        {
+            folder.Content.Connection.Node.TraverseBackwards(contentNode =>
+            {
+                if (contentNode is Node node)
+                {
+                    if (!originalPositions.ContainsKey(node.Id))
+                    {
+                        originalPositions[node.Id] = node.Position;
+                    }
+                    
+                    node.Position = new VecD(node.Position.X, folder.Position.Y + 250);
+                    changes.Add(new NodePosition_ChangeInfo(node.Id, node.Position));
+                }
+                
+                return true;
+            });
+            
+            folder.Background.Connection?.Node.TraverseBackwards(bgNode =>
+            {
+                if (bgNode is Node node)
+                {
+                    if (!originalPositions.ContainsKey(node.Id))
+                    {
+                        originalPositions[node.Id] = node.Position;
+                    }
+
+                    double pos = folder.Position.Y;
+
+                    if (folder.Content.Connection != null)
+                    {
+                        pos -= 250;
+                    }
+                    
+                    node.Position = new VecD(node.Position.X, pos);
+                    changes.Add(new NodePosition_ChangeInfo(node.Id, node.Position));
+                }
+                
+                return true;
+            });
+        }
+
         return changes;
     }
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs

@@ -46,6 +46,7 @@ internal class RasterizeMember_Change : Change
         
         ImageLayerNode imageLayer = new ImageLayerNode(target.Size, target.ProcessingColorSpace);
         imageLayer.MemberName = node.DisplayName;
+        imageLayer.Position = node.Position;
 
         target.NodeGraph.AddNode(imageLayer);
         

+ 67 - 0
src/PixiEditor.ChangeableDocument/Changes/Vectors/ConvertToCurve_Change.cs

@@ -0,0 +1,67 @@
+using ChunkyImageLib.Operations;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
+
+namespace PixiEditor.ChangeableDocument.Changes.Vectors;
+
+internal class ConvertToCurve_Change : Change
+{
+    public readonly Guid memberId;
+
+    private ShapeVectorData originalData;
+
+    [GenerateMakeChangeAction]
+    public ConvertToCurve_Change(Guid memberId)
+    {
+        this.memberId = memberId;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (target.TryFindNode(memberId, out VectorLayerNode? node))
+        {
+            return node.ShapeData != null && node.ShapeData is not PathVectorData;
+        }
+
+        return false;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
+        originalData = node.ShapeData;
+
+        node.ShapeData = new PathVectorData(originalData.ToPath())
+        {
+            Fill = originalData.Fill,
+            FillColor = originalData.FillColor,
+            StrokeColor = originalData.StrokeColor,
+            StrokeWidth = originalData.StrokeWidth,
+            TransformationMatrix = originalData.TransformationMatrix
+        };
+
+        ignoreInUndo = false;
+
+        var aabb = node.ShapeData.TransformedVisualAABB;
+        var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+            (RectI)aabb, ChunkyImage.FullChunkSize));
+
+        return new VectorShape_ChangeInfo(memberId, affected);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
+        node.ShapeData = originalData;
+
+        var aabb = node.ShapeData.TransformedVisualAABB;
+        var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+            (RectI)aabb, ChunkyImage.FullChunkSize));
+
+        return new VectorShape_ChangeInfo(memberId, affected);
+    }
+}

+ 3 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -834,5 +834,7 @@
   "BLUR_FILTER_NODE": "Gaussian Blur Filter",
   "LENGTH": "Length",
   "GREATER_THAN_OR_EQUAL": "Greater than or equal",
-  "COLOR_NODE": "Color"
+  "COLOR_NODE": "Color",
+  "CONVERT_TO_CURVE": "Convert to curve",
+  "CONVERT_TO_CURVE_DESCRIPTIVE": "Convert selected vector layer to a curve/path"
 }

+ 9 - 6
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -148,11 +148,12 @@ internal class DocumentViewModelBuilder
 
             data?.Add(builder);
         }
-        
+
         TryAddMissingKeyFrames(root, data, documentGraph);
     }
 
-    private static void TryAddMissingKeyFrames(List<KeyFrameGroup> groups, List<KeyFrameBuilder>? data, NodeGraph documentGraph)
+    private static void TryAddMissingKeyFrames(List<KeyFrameGroup> groups, List<KeyFrameBuilder>? data,
+        NodeGraph documentGraph)
     {
         if (data == null)
         {
@@ -164,15 +165,15 @@ internal class DocumentViewModelBuilder
             if (node.KeyFrames.Length > 1 && data.All(x => x.NodeId != node.Id))
             {
                 GroupKeyFrameBuilder builder = new GroupKeyFrameBuilder()
-                .WithNodeId(node.Id);
-                
+                    .WithNodeId(node.Id);
+
                 foreach (var keyFrame in node.KeyFrames)
                 {
                     builder.WithChild<KeyFrameBuilder>(x => x
                         .WithKeyFrameId(keyFrame.Id)
                         .WithNodeId(node.Id));
-                }   
-                
+                }
+
                 data.Add(builder);
             }
         }
@@ -351,6 +352,7 @@ internal class NodeGraphBuilder
     {
         this.WithNodeOfType(typeof(ImageLayerNode))
             .WithName(name)
+            .WithPosition(new Vector2 { X = -250, Y = 0 })
             .WithId(AllNodes.Count)
             .WithKeyFrames(
             [
@@ -372,6 +374,7 @@ internal class NodeGraphBuilder
     {
         this.WithNodeOfType(typeof(ImageLayerNode))
             .WithName(name)
+            .WithPosition(new Vector2 { X = -250, Y = 0 })
             .WithId(AllNodes.Count)
             .WithKeyFrames(
             [

+ 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();
         }
     }
 

+ 17 - 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,10 +900,20 @@ internal class DocumentOperationsModule : IDocumentOperations
             if (Document.StructureHelper.TryFindNode(node, out INodeHandler nodeHandler) &&
                 nodeHandler.InternalName == OutputNode.UniqueName)
                 return;
-            
+
             actions.Add(new DeleteNode_Action(node));
         }
 
         Internals.ActionAccumulator.AddFinishedActions(actions.ToArray());
     }
+
+    public void ConvertToCurve(Guid memberId)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+
+        Internals.ActionAccumulator.AddFinishedActions(new ConvertToCurve_Action(memberId));
+    }
 }

+ 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);

+ 4 - 1
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)
             {
@@ -94,7 +97,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
 
             ActiveMode = ShapeToolMode.Transform;
 
-            document.LineToolOverlayHandler.Show(data.Start, data.End, false, AddToUndo);
+            document.LineToolOverlayHandler.Show(data.TransformedStart, data.TransformedEnd, false, AddToUndo);
         }
         else
         {

+ 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;
     }
 }

+ 2 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorLineToolExecutor.cs

@@ -24,8 +24,8 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
         if (data is null)
             return false;
 
-        startPoint = data.Start;
-        endPoint = data.End;
+        startPoint = data.TransformedStart;
+        endPoint = data.TransformedEnd;
 
         return true;
     }

+ 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; }
 }

+ 29 - 7
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -89,7 +89,7 @@ public class PreviewPainter
     {
         painterInstances.Remove(requestId);
         dirtyTextures.Remove(requestId);
-        
+
         if (repaintingTextures.Contains(requestId))
         {
             pendingRemovals.Add(requestId);
@@ -126,13 +126,18 @@ public class PreviewPainter
                 continue;
             }
 
+            if (!painterInstances.TryGetValue(texture, out var painterInstance))
+            {
+                repaintingTextures.Remove(texture);
+                dirtyTextures.Remove(texture);
+                continue;
+            }
+
             repaintingTextures.Add(texture);
 
             renderTexture.DrawingSurface.Canvas.Clear();
             renderTexture.DrawingSurface.Canvas.Save();
 
-            PainterInstance painterInstance = painterInstances[texture];
-
             Matrix3X3? matrix = painterInstance.RequestMatrix?.Invoke();
 
             renderTexture.DrawingSurface.Canvas.SetMatrix(matrix ?? Matrix3X3.Identity);
@@ -146,9 +151,16 @@ public class PreviewPainter
                 {
                     Dispatcher.UIThread.Invoke(() =>
                     {
-                        if(pendingRemovals.Contains(texture))
+                        if (pendingRemovals.Contains(texture))
                         {
-                            renderTexture.Dispose();
+                            if (!renderTexture.IsDisposed)
+                            {
+                                try
+                                {
+                                    renderTexture.Dispose();
+                                } catch (Exception) { }
+                            }
+
                             renderTextures.Remove(texture);
                             pendingRemovals.Remove(texture);
                             pendingResizes.Remove(texture);
@@ -156,9 +168,19 @@ public class PreviewPainter
                             return;
                         }
                         
-                        if (renderTexture != null && !renderTexture.IsDisposed)
+                        if (renderTexture is { IsDisposed: false })
                         {
-                            renderTexture.DrawingSurface.Canvas.Restore();
+                            try
+                            {
+                                renderTexture.DrawingSurface.Canvas.Restore();
+                            }
+                            catch (Exception)
+                            {
+                                repaintingTextures.Remove(texture);
+                                dirtyTextures.Remove(texture);
+                                pendingResizes.Remove(texture);
+                                return;
+                            }
                         }
 
                         painterInstance.RequestRepaint?.Invoke();

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -41,5 +41,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.50")]
-[assembly: AssemblyFileVersion("2.0.0.50")]
+[assembly: AssemblyVersion("2.0.0.53")]
+[assembly: AssemblyFileVersion("2.0.0.53")]

+ 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)

+ 8 - 2
src/PixiEditor/ViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs

@@ -18,7 +18,7 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
         get => lineStart;
         set
         {
-            if (SetProperty(ref lineStart, value))
+            if (SetProperty(ref lineStart, value) && isInitialized)
                 LineMoved?.Invoke(this, (lineStart, lineEnd));
         }
     }
@@ -30,7 +30,7 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
         get => lineEnd;
         set
         {
-            if (SetProperty(ref lineEnd, value))
+            if (SetProperty(ref lineEnd, value) && isInitialized)
                 LineMoved?.Invoke(this, (lineStart, lineEnd));
         }
     }
@@ -80,12 +80,15 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
         set => SetProperty(ref showApplyButton, value);
     }
 
+    private bool isInitialized;
+    
     public LineToolOverlayViewModel()
     {
     }
 
     public void Show(VecD lineStart, VecD endPos, bool showApplyButton, Action<(VecD, VecD)> addToUndo)
     {
+        isInitialized = false;
         LineStart = lineStart;
         LineEnd = endPos; 
         IsEnabled = true;
@@ -93,6 +96,8 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
         ShowHandles = true;
         IsSizeBoxEnabled = false;
         AddToUndoCommand = new RelayCommand(() => addToUndo((LineStart, LineEnd)));
+        
+        isInitialized = true;
     }
 
     public void Hide()
@@ -100,6 +105,7 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
         IsEnabled = false;
         ShowApplyButton = false;
         IsSizeBoxEnabled = false;
+        isInitialized = false;
     }
 
     public bool Nudge(VecD distance)

+ 19 - 0
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -206,6 +206,13 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
         return member is ILayerHandler && member is not IRasterLayerHandler;
     }
+    
+    [Evaluator.CanExecute("PixiEditor.Layer.SelectedMemberIsVectorLayer")]
+    public bool SelectedMemberIsVectorLayer(object property)
+    {
+        var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
+        return member is IVectorLayerHandler;
+    }
 
     private bool HasSelectedMember(bool above)
     {
@@ -500,6 +507,18 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
 
         doc!.Operations.Rasterize(member.Id);
     }
+    
+    [Command.Basic("PixiEditor.Layer.ConvertToCurve", "CONVERT_TO_CURVE", "CONVERT_TO_CURVE_DESCRIPTIVE",
+        CanExecute = "PixiEditor.Layer.SelectedMemberIsVectorLayer")]
+    public void ConvertActiveLayerToCurve()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        var member = doc?.SelectedStructureMember;
+        if (member is null)
+            return;
+
+        doc!.Operations.ConvertToCurve(member.Id);
+    }
 
     [Evaluator.Icon("PixiEditor.Layer.ToggleReferenceLayerTopMostIcon")]
     public IImage GetAboveEverythingReferenceLayerIcon()

+ 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)
         {

+ 1 - 18
src/PixiEditor/Views/Dock/ColorPickerDockView.axaml.cs

@@ -19,23 +19,6 @@ public partial class ColorPickerDockView : UserControl
         base.OnLoaded(e);
         var textBoxes = this.GetVisualDescendants().OfType<TextBox>().ToArray();
 
-        foreach (var textBox in textBoxes)
-        {
-            var existingBehaviors = Interaction.GetBehaviors(textBox);
-            if(existingBehaviors.Any(x => x is GlobalShortcutFocusBehavior)) continue;
-            bool attach = false;
-            if (existingBehaviors == null)
-            {
-                attach = true;
-                existingBehaviors = new BehaviorCollection();
-            }
-
-            existingBehaviors.Add(new GlobalShortcutFocusBehavior());
-
-            if (attach)
-            {
-                Interaction.SetBehaviors(textBox, existingBehaviors);
-            }
-        }
+        ColorSlidersDockView.AttachBehavioursToTextBoxes(textBoxes); 
     }
 }

+ 41 - 1
src/PixiEditor/Views/Dock/ColorSlidersDockView.axaml.cs

@@ -1,6 +1,10 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+using Avalonia.Xaml.Interactivity;
+using PixiEditor.Helpers.Behaviours;
 
 namespace PixiEditor.Views.Dock;
 
@@ -10,5 +14,41 @@ public partial class ColorSlidersDockView : UserControl
     {
         InitializeComponent();
     }
-}
 
+    protected override void OnLoaded(RoutedEventArgs e)
+    {
+        base.OnLoaded(e);
+        var textBoxes = this.GetVisualDescendants().OfType<TextBox>().ToArray();
+
+        AttachBehavioursToTextBoxes(textBoxes);
+    }
+
+    internal static void AttachBehavioursToTextBoxes(TextBox[] textBoxes)
+    {
+        foreach (var textBox in textBoxes)
+        {
+            var existingBehaviors = Interaction.GetBehaviors(textBox);
+            if (existingBehaviors.Any(x => x is GlobalShortcutFocusBehavior)) continue;
+            bool attach = false;
+            if (existingBehaviors == null)
+            {
+                attach = true;
+                existingBehaviors = new BehaviorCollection();
+            }
+
+            try
+            {
+                existingBehaviors.Add(new GlobalShortcutFocusBehavior());
+
+                if (attach)
+                {
+                    Interaction.SetBehaviors(textBox, existingBehaviors);
+                }
+            }
+            catch (ArgumentOutOfRangeException)
+            {
+                // avalonia's bug
+            }
+        }
+    }
+}

+ 1 - 0
src/PixiEditor/Views/Layers/LayerControl.axaml

@@ -200,6 +200,7 @@
                 <MenuItem ui:Translator.Key="MERGE_WITH_BELOW" Command="{xaml:Command PixiEditor.Layer.MergeWithBelow}" />
                 <Separator />
                 <MenuItem ui:Translator.Key="RASTERIZE" Command="{xaml:Command PixiEditor.Layer.Rasterize}" />
+                <MenuItem ui:Translator.Key="CONVERT_TO_CURVE" Command="{xaml:Command PixiEditor.Layer.ConvertToCurve}" />
             </ContextMenu>
         </Border.ContextMenu>
     </Border>

+ 1 - 1
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml

@@ -181,7 +181,7 @@
         <rendering:Scene
             Focusable="True" Name="scene"
             ZIndex="1"
-            SceneRenderer="{Binding Source={viewModels:MainVM DocumentManagerSVM}, Path=ActiveDocument.SceneRenderer}"
+            SceneRenderer="{Binding Document.SceneRenderer, ElementName=vpUc, Mode=OneWay}"
             Document="{Binding Document, ElementName=vpUc, Mode=OneWay}"
             UseTouchGestures="{Binding UseTouchGestures, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=OneWay}"
             Center="{Binding Center, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=OneWayToSource}"

+ 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()

+ 62 - 22
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -1,4 +1,5 @@
-using System.Collections.ObjectModel;
+using System.Collections;
+using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.ComponentModel;
 using System.Windows.Input;
@@ -234,31 +235,30 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
             nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged += NodeItems_CollectionChanged;
             nodeViewsCache = nodeItemsControl.ItemsPanelRoot.Children.ToList();
+            HandleNodesAdded(nodeViewsCache);
         });
     }
 
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged -= NodeItems_CollectionChanged;
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        if (nodeItemsControl is { ItemsPanelRoot: not null })
+        {
+            nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged += NodeItems_CollectionChanged;
+            nodeViewsCache = nodeItemsControl.ItemsPanelRoot.Children.ToList();
+            HandleNodesAdded(nodeViewsCache);
+        }
+    }
+
     private void NodeItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
     {
         if (e.Action == NotifyCollectionChangedAction.Add)
         {
-            foreach (Control control in e.NewItems)
-            {
-                if (control is not ContentPresenter presenter)
-                {
-                    continue;
-                }
-
-                nodeViewsCache.Add(presenter);
-                presenter.PropertyChanged += OnPresenterPropertyChanged;
-                if (presenter.Child == null)
-                {
-                    continue;
-                }
-
-                NodeView nodeView = (NodeView)presenter.Child;
-                nodeView.PropertyChanged += NodeView_PropertyChanged;
-                nodeView.Node.PropertyChanged += Node_PropertyChanged;
-            }
+            HandleNodesAdded(e.NewItems);
         }
         else if (e.Action == NotifyCollectionChangedAction.Remove)
         {
@@ -272,15 +272,18 @@ internal class NodeGraphView : Zoombox.Zoombox
                 nodeViewsCache.Remove(presenter);
 
                 presenter.PropertyChanged -= OnPresenterPropertyChanged;
+                if (presenter.Content is NodeViewModel nvm)
+                {
+                    nvm.PropertyChanged -= Node_PropertyChanged;
+                }
+
                 if (presenter.Child == null)
                 {
                     continue;
                 }
 
-
                 NodeView nodeView = (NodeView)presenter.Child;
                 nodeView.PropertyChanged -= NodeView_PropertyChanged;
-                nodeView.Node.PropertyChanged -= Node_PropertyChanged;
             }
         }
         else if (e.Action == NotifyCollectionChangedAction.Reset)
@@ -289,6 +292,37 @@ internal class NodeGraphView : Zoombox.Zoombox
         }
     }
 
+    private void HandleNodesAdded(IList? items)
+    {
+        foreach (Control control in items)
+        {
+            if (control is not ContentPresenter presenter)
+            {
+                continue;
+            }
+
+            if (!nodeViewsCache.Contains(presenter))
+            {
+                nodeViewsCache.Add(presenter);
+            }
+
+            presenter.PropertyChanged += OnPresenterPropertyChanged;
+
+            if (presenter.Content is NodeViewModel nvm)
+            {
+                nvm.PropertyChanged += Node_PropertyChanged;
+            }
+
+            if (presenter.Child == null)
+            {
+                continue;
+            }
+
+            NodeView nodeView = (NodeView)presenter.Child;
+            nodeView.PropertyChanged += NodeView_PropertyChanged;
+        }
+    }
+
     private void OnPresenterPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
     {
         if (e.Property == ContentPresenter.ChildProperty)
@@ -602,6 +636,11 @@ internal class NodeGraphView : Zoombox.Zoombox
             {
                 ConnectionView connectionView = (ConnectionView)contentPresenter.FindDescendantOfType<ConnectionView>();
 
+                if (connectionView == null)
+                {
+                    continue;
+                }
+
                 if (connectionView.InputProperty == propertyView || connectionView.OutputProperty == propertyView)
                 {
                     connectionView.UpdateSocketPoints();
@@ -639,7 +678,8 @@ internal class NodeGraphView : Zoombox.Zoombox
             return;
         }
 
-        (INodePropertyHandler, INodePropertyHandler, INodePropertyHandler?) connection = (startConnectionProperty, null, null);
+        (INodePropertyHandler, INodePropertyHandler, INodePropertyHandler?) connection = (startConnectionProperty, null,
+            null);
         if (socket != null)
         {
             endConnectionNode = socket.Node;

+ 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)

+ 1 - 1
src/PixiEditor/Views/Rendering/Scene.cs

@@ -638,7 +638,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     protected void RenderFrame(PixelSize size)
     {
-        if (resources != null)
+        if (resources != null && !resources.IsDisposed)
         {
             if (size.Width == 0 || size.Height == 0)
             {