Browse Source

Move tool, various changes

Equbuxu 3 years ago
parent
commit
c3aa0ea3d4
37 changed files with 770 additions and 109 deletions
  1. 66 12
      src/ChunkyImageLib/ChunkyImage.cs
  2. 1 0
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  3. 53 0
      src/ChunkyImageLib/Operations/ClearPathOperation.cs
  4. 1 1
      src/ChunkyImageLib/Operations/ClearRegionOperation.cs
  5. 24 0
      src/ChunkyImageLib/Operations/ImageOperation.cs
  6. 2 0
      src/Custom.ruleset
  7. 1 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs
  8. 7 3
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs
  9. 183 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs
  10. 22 0
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectionChangeHelper.cs
  11. 34 0
      src/PixiEditor.ChangeableDocument/Changes/Selection/SetSelection_Change.cs
  12. 5 22
      src/PixiEditor.ChangeableDocument/Changes/Selection/TransformSelectionPath_UpdateableChange.cs
  13. 15 11
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  14. 19 8
      src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs
  15. 22 10
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  16. 25 10
      src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs
  17. 1 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EllipseToolExecutor.cs
  18. 1 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineToolExecutor.cs
  19. 2 7
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs
  20. 2 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RectangleToolExecutor.cs
  21. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShapeToolExecutor.cs
  22. 48 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs
  23. 62 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs
  24. 1 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs
  25. 7 0
      src/PixiEditor/Models/Enums/ExecutorType.cs
  26. 10 2
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentTransformViewModel.cs
  27. 30 7
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  28. 6 2
      src/PixiEditor/ViewModels/SubViewModels/Main/SelectionViewModel.cs
  29. 2 0
      src/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs
  30. 5 0
      src/PixiEditor/ViewModels/SubViewModels/Tools/ShapeTool.cs
  31. 2 0
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tool.cs
  32. 12 0
      src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/MoveToolToolbar.cs
  33. 15 5
      src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/MoveToolViewModel.cs
  34. 15 2
      src/PixiEditor/Views/UserControls/SelectionOverlay.cs
  35. 61 3
      src/PixiEditor/Views/UserControls/TransformOverlay/TransformOverlay.cs
  36. 6 0
      src/PixiEditor/Views/UserControls/Viewport.xaml
  37. 1 1
      src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

+ 66 - 12
src/ChunkyImageLib/ChunkyImage.cs

@@ -90,18 +90,50 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         LatestSize = size;
         LatestSize = size;
         committedChunks = new()
         committedChunks = new()
         {
         {
-            [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new(),
+            [ChunkResolution.Full] = new(),
+            [ChunkResolution.Half] = new(),
+            [ChunkResolution.Quarter] = new(),
+            [ChunkResolution.Eighth] = new(),
         };
         };
         latestChunks = new()
         latestChunks = new()
         {
         {
-            [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new(),
+            [ChunkResolution.Full] = new(),
+            [ChunkResolution.Half] = new(),
+            [ChunkResolution.Quarter] = new(),
+            [ChunkResolution.Eighth] = new(),
         };
         };
         latestChunksData = new()
         latestChunksData = new()
         {
         {
-            [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new(),
+            [ChunkResolution.Full] = new(),
+            [ChunkResolution.Half] = new(),
+            [ChunkResolution.Quarter] = new(),
+            [ChunkResolution.Eighth] = new(),
         };
         };
     }
     }
 
 
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public RectI? FindLatestBounds()
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            RectI? rect = null;
+            foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
+            {
+                RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
+                rect ??= chunkBounds;
+                rect = rect.Value.Union(chunkBounds);
+            }
+            foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
+            {
+                RectI chunkBounds = new RectI(pos * FullChunkSize, new VecI(FullChunkSize));
+                rect ??= chunkBounds;
+                rect = rect.Value.Union(chunkBounds);
+            }
+            return rect;
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public ChunkyImage CloneFromCommitted()
     public ChunkyImage CloneFromCommitted()
     {
     {
@@ -138,7 +170,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             };
             };
         }
         }
     }
     }
-    
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public SKColor GetMostUpToDatePixel(VecI posOnImage)
     public SKColor GetMostUpToDatePixel(VecI posOnImage)
     {
     {
@@ -174,11 +206,11 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             {
             {
                 Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
                 Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
                 Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
                 Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
-                SKColor committedColor = committedChunk is null ? 
-                    SKColors.Transparent :  
+                SKColor committedColor = committedChunk is null ?
+                    SKColors.Transparent :
                     committedChunk.Surface.GetSRGBPixel(posInChunk);
                     committedChunk.Surface.GetSRGBPixel(posInChunk);
                 SKColor latestColor = latestChunk is null ?
                 SKColor latestColor = latestChunk is null ?
-                    SKColors.Transparent : 
+                    SKColors.Transparent :
                     latestChunk.Surface.GetSRGBPixel(posInChunk);
                     latestChunk.Surface.GetSRGBPixel(posInChunk);
                 // using a whole chunk just to draw 1 pixel is kinda dumb,
                 // using a whole chunk just to draw 1 pixel is kinda dumb,
                 // but this should be faster than any approach that requires allocations
                 // but this should be faster than any approach that requires allocations
@@ -191,7 +223,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             }
             }
         }
         }
     }
     }
-    
+
     /// <returns>
     /// <returns>
     /// True if the chunk existed and was drawn, otherwise false
     /// True if the chunk existed and was drawn, otherwise false
     /// </returns>
     /// </returns>
@@ -441,6 +473,20 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
     /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
     /// </summary>
     /// </summary>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public void EnqueueDrawImage(SKMatrix transformMatrix, Surface image, SKPaint? paint = null, bool copyImage = true)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            ImageOperation operation = new(transformMatrix, image, paint, copyImage);
+            EnqueueOperation(operation);
+        }
+    }
+
+    /// <summary>
+    /// Be careful about the copyImage argument, see other overload for details
+    /// </summary>
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawImage(ShapeCorners corners, Surface image, SKPaint? paint = null, bool copyImage = true)
     public void EnqueueDrawImage(ShapeCorners corners, Surface image, SKPaint? paint = null, bool copyImage = true)
     {
     {
         lock (lockObject)
         lock (lockObject)
@@ -452,10 +498,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     }
     }
 
 
     /// <summary>
     /// <summary>
-    /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects. 
-    /// It will however copy the surface right away which can be slow (in updateable changes especially). 
-    /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
-    /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
+    /// Be careful about the copyImage argument, see other overload for details
     /// </summary>
     /// </summary>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawImage(VecI pos, Surface image, SKPaint? paint = null, bool copyImage = true)
     public void EnqueueDrawImage(VecI pos, Surface image, SKPaint? paint = null, bool copyImage = true)
@@ -546,6 +589,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         }
         }
     }
     }
 
 
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public void EnqueueClearPath(SKPath path, RectI? pathTightBounds = null)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            ClearPathOperation operation = new(path, pathTightBounds);
+            EnqueueOperation(operation);
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueClear()
     public void EnqueueClear()
     {
     {

+ 1 - 0
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -6,6 +6,7 @@ namespace ChunkyImageLib;
 public interface IReadOnlyChunkyImage
 public interface IReadOnlyChunkyImage
 {
 {
     bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, SKSurface surface, VecI pos, SKPaint? paint = null);
     bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, SKSurface surface, VecI pos, SKPaint? paint = null);
+    RectI? FindLatestBounds();
     SKColor GetCommittedPixel(VecI posOnImage);
     SKColor GetCommittedPixel(VecI posOnImage);
     SKColor GetMostUpToDatePixel(VecI posOnImage);
     SKColor GetMostUpToDatePixel(VecI posOnImage);
     bool LatestOrCommittedChunkExists(VecI chunkPos);
     bool LatestOrCommittedChunkExists(VecI chunkPos);

+ 53 - 0
src/ChunkyImageLib/Operations/ClearPathOperation.cs

@@ -0,0 +1,53 @@
+using ChunkyImageLib.DataHolders;
+using SkiaSharp;
+
+namespace ChunkyImageLib.Operations;
+internal class ClearPathOperation : IDrawOperation
+{
+    private SKPath path;
+    private RectI pathTightBounds;
+
+    public bool IgnoreEmptyChunks => true;
+
+    public ClearPathOperation(SKPath path, RectI? pathTightBounds = null)
+    {
+        this.path = new SKPath(path);
+        this.pathTightBounds = (RectI)(pathTightBounds ?? path.TightBounds);
+    }
+
+    public void DrawOnChunk(Chunk chunk, VecI chunkPos)
+    {
+        chunk.Surface.SkiaSurface.Canvas.Save();
+
+        using SKPath transformedPath = new(path);
+        float scale = (float)chunk.Resolution.Multiplier();
+        VecD trans = -chunkPos * ChunkyImage.FullChunkSize * scale;
+        transformedPath.Transform(SKMatrix.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
+        chunk.Surface.SkiaSurface.Canvas.ClipPath(transformedPath);
+        chunk.Surface.SkiaSurface.Canvas.Clear();
+        chunk.Surface.SkiaSurface.Canvas.Restore();
+    }
+
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    {
+        return OperationHelper.FindChunksTouchingRectangle(pathTightBounds, ChunkPool.FullChunkSize);
+    }
+    public void Dispose()
+    {
+        path.Dispose();
+    }
+
+    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    {
+        var matrix = SKMatrix.CreateScale(verAxisX is not null ? -1 : 1, horAxisY is not null ? -1 : 1, verAxisX ?? 0, horAxisY ?? 0);
+        using var copy = new SKPath(path);
+        copy.Transform(matrix);
+
+        var newRect = pathTightBounds;
+        if (verAxisX is not null)
+            newRect = newRect.ReflectX((int)verAxisX);
+        if (horAxisY is not null)
+            newRect = newRect.ReflectY((int)horAxisY);
+        return new ClearPathOperation(copy, newRect);
+    }
+}

+ 1 - 1
src/ChunkyImageLib/Operations/ClearRegionOperation.cs

@@ -5,7 +5,7 @@ namespace ChunkyImageLib.Operations;
 
 
 internal class ClearRegionOperation : IDrawOperation
 internal class ClearRegionOperation : IDrawOperation
 {
 {
-    RectI rect;
+    private RectI rect;
 
 
     public bool IgnoreEmptyChunks => true;
     public bool IgnoreEmptyChunks => true;
 
 

+ 24 - 0
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -53,6 +53,30 @@ internal class ImageOperation : IDrawOperation
         imageWasCopied = copyImage;
         imageWasCopied = copyImage;
     }
     }
 
 
+    public ImageOperation(SKMatrix transformMatrix, Surface image, SKPaint? paint = null, bool copyImage = true)
+    {
+        if (paint is not null)
+            customPaint = paint.Clone();
+
+        this.corners = new ShapeCorners()
+        {
+            TopLeft = transformMatrix.MapPoint(0, 0),
+            TopRight = transformMatrix.MapPoint(image.Size.X, 0),
+            BottomLeft = transformMatrix.MapPoint(0, image.Size.Y),
+            BottomRight = transformMatrix.MapPoint(image.Size),
+        };
+        this.transformMatrix = transformMatrix;
+
+        // copying is needed for thread safety
+        if (copyImage)
+            toPaint = new Surface(image);
+        else
+            toPaint = image;
+        imageWasCopied = copyImage;
+    }
+
+
+
     public void DrawOnChunk(Chunk chunk, VecI chunkPos)
     public void DrawOnChunk(Chunk chunk, VecI chunkPos)
     {
     {
         //customPaint.FilterQuality = chunk.Resolution != ChunkResolution.Full;
         //customPaint.FilterQuality = chunk.Resolution != ChunkResolution.Full;

+ 2 - 0
src/Custom.ruleset

@@ -19,6 +19,7 @@
     <Rule Id="SA1005" Action="None" />
     <Rule Id="SA1005" Action="None" />
     <Rule Id="SA1008" Action="None" />
     <Rule Id="SA1008" Action="None" />
     <Rule Id="SA1009" Action="None" />
     <Rule Id="SA1009" Action="None" />
+    <Rule Id="SA1011" Action="None" />
     <Rule Id="SA1023" Action="None" />
     <Rule Id="SA1023" Action="None" />
     <Rule Id="SA1028" Action="None" />
     <Rule Id="SA1028" Action="None" />
     <Rule Id="SA1101" Action="None" />
     <Rule Id="SA1101" Action="None" />
@@ -62,6 +63,7 @@
     <Rule Id="SA1405" Action="None" />
     <Rule Id="SA1405" Action="None" />
     <Rule Id="SA1406" Action="None" />
     <Rule Id="SA1406" Action="None" />
     <Rule Id="SA1407" Action="None" />
     <Rule Id="SA1407" Action="None" />
+    <Rule Id="SA1408" Action="None" />
     <Rule Id="SA1410" Action="None" />
     <Rule Id="SA1410" Action="None" />
     <Rule Id="SA1411" Action="None" />
     <Rule Id="SA1411" Action="None" />
     <Rule Id="SA1413" Action="None" />
     <Rule Id="SA1413" Action="None" />

+ 1 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs

@@ -33,8 +33,7 @@ internal class ClearSelectedArea_Change : Change
         RectD bounds = target.Selection.SelectionPath.Bounds;
         RectD bounds = target.Selection.SelectionPath.Bounds;
         RectI intBounds = (RectI)bounds.Intersect(SKRect.Create(0, 0, target.Size.X, target.Size.Y)).RoundOutwards();
         RectI intBounds = (RectI)bounds.Intersect(SKRect.Create(0, 0, target.Size.X, target.Size.Y)).RoundOutwards();
 
 
-        image.SetClippingPath(target.Selection.SelectionPath);
-        image.EnqueueClearRegion(intBounds);
+        image.EnqueueClearPath(target.Selection.SelectionPath, intBounds);
         var affChunks = image.FindAffectedChunks();
         var affChunks = image.FindAffectedChunks();
         savedChunks = new(image, affChunks);
         savedChunks = new(image, affChunks);
         image.CommitChanges();
         image.CommitChanges();

+ 7 - 3
src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs

@@ -2,14 +2,16 @@
 internal class ShiftLayer_UpdateableChange : UpdateableChange
 internal class ShiftLayer_UpdateableChange : UpdateableChange
 {
 {
     private readonly Guid layerGuid;
     private readonly Guid layerGuid;
+    private bool keepOriginal;
     private VecI delta;
     private VecI delta;
     private CommittedChunkStorage? originalLayerChunks;
     private CommittedChunkStorage? originalLayerChunks;
 
 
     [GenerateUpdateableChangeActions]
     [GenerateUpdateableChangeActions]
-    public ShiftLayer_UpdateableChange(Guid layerGuid, VecI delta)
+    public ShiftLayer_UpdateableChange(Guid layerGuid, VecI delta, bool keepOriginal)
     {
     {
         this.delta = delta;
         this.delta = delta;
         this.layerGuid = layerGuid;
         this.layerGuid = layerGuid;
+        this.keepOriginal = keepOriginal;
     }
     }
 
 
     public override OneOf<Success, Error> InitializeAndValidate(Document target)
     public override OneOf<Success, Error> InitializeAndValidate(Document target)
@@ -20,9 +22,10 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
     }
     }
 
 
     [UpdateChangeMethod]
     [UpdateChangeMethod]
-    public void Update(VecI delta)
+    public void Update(VecI delta, bool keepOriginal)
     {
     {
         this.delta = delta;
         this.delta = delta;
+        this.keepOriginal = keepOriginal;
     }
     }
 
 
     private HashSet<VecI> DrawShiftedLayer(Document target)
     private HashSet<VecI> DrawShiftedLayer(Document target)
@@ -30,7 +33,8 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         var targetImage = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
         var targetImage = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
         var prevChunks = targetImage.FindAffectedChunks();
         var prevChunks = targetImage.FindAffectedChunks();
         targetImage.CancelChanges();
         targetImage.CancelChanges();
-        targetImage.EnqueueClear();
+        if (!keepOriginal)
+            targetImage.EnqueueClear();
         targetImage.EnqueueDrawChunkyImage(delta, targetImage, false, false);
         targetImage.EnqueueDrawChunkyImage(delta, targetImage, false, false);
         var curChunks = targetImage.FindAffectedChunks();
         var curChunks = targetImage.FindAffectedChunks();
         curChunks.UnionWith(prevChunks);
         curChunks.UnionWith(prevChunks);

+ 183 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs

@@ -0,0 +1,183 @@
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.Changes.Selection;
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+internal class TransformSelectedArea_UpdateableChange : UpdateableChange
+{
+    private readonly Guid[] membersToTransform;
+    private readonly bool drawOnMask;
+    private bool keepOriginal;
+    private ShapeCorners corners;
+
+    private Dictionary<Guid, (Surface surface, VecI pos)>? images;
+    private SKMatrix globalMatrix;
+    private RectI originalTightBounds;
+    private Dictionary<Guid, CommittedChunkStorage>? savedChunks;
+
+    private SKPath? originalPath;
+
+    private bool hasEnqueudImages = false;
+
+    private static SKPaint RegularPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+
+    [GenerateUpdateableChangeActions]
+    public TransformSelectedArea_UpdateableChange(
+        IEnumerable<Guid> membersToTransform,
+        ShapeCorners corners,
+        bool keepOriginal,
+        bool transformMask)
+    {
+        this.membersToTransform = membersToTransform.Select(static a => a).ToArray();
+        this.corners = corners;
+        this.keepOriginal = keepOriginal;
+        this.drawOnMask = transformMask;
+    }
+
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        if (membersToTransform.Length == 0 || target.Selection.SelectionPath.IsEmpty)
+            return new Error();
+
+        foreach (var guid in membersToTransform)
+        {
+            if (!DrawingChangeHelper.IsValidForDrawing(target, guid, drawOnMask))
+                return new Error();
+        }
+
+        originalPath = new SKPath(target.Selection.SelectionPath) { FillType = SKPathFillType.EvenOdd };
+        RectI bounds = (RectI)originalPath.TightBounds;
+
+        images = new();
+        foreach (var guid in membersToTransform)
+        {
+            ChunkyImage image = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
+            var extracted = ExtractArea(image, originalPath, bounds);
+            if (extracted.IsT0)
+                continue;
+            images.Add(guid, (extracted.AsT1.image, extracted.AsT1.extractedRect.Pos));
+        }
+        if (images.Count == 0)
+            return new Error();
+        originalTightBounds = bounds;
+        globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
+        return new Success();
+    }
+
+    public OneOf<None, (Surface image, RectI extractedRect)> ExtractArea(ChunkyImage image, SKPath path, RectI pathBounds)
+    {
+        // get rid of transparent areas on edges
+        var memberImageBounds = image.FindLatestBounds();
+        if (memberImageBounds is null)
+            return new None();
+        pathBounds = pathBounds.Intersect(memberImageBounds.Value);
+        pathBounds = pathBounds.Intersect(new RectI(VecI.Zero, image.LatestSize));
+        if (pathBounds.IsZeroOrNegativeArea)
+            return new None();
+
+        // shift the clip to account for the image being smaller than the selection
+        SKPath clipPath = new SKPath(path) { FillType = SKPathFillType.EvenOdd };
+        clipPath.Transform(SKMatrix.CreateTranslation(-pathBounds.X, -pathBounds.Y));
+
+        // draw
+        Surface output = new(pathBounds.Size);
+        output.SkiaSurface.Canvas.Save();
+        output.SkiaSurface.Canvas.ClipPath(clipPath);
+        image.DrawMostUpToDateRegionOn(pathBounds, ChunkResolution.Full, output.SkiaSurface, VecI.Zero);
+        output.SkiaSurface.Canvas.Restore();
+
+        return (output, pathBounds);
+    }
+
+    [UpdateChangeMethod]
+    public void Update(ShapeCorners corners, bool keepOriginal)
+    {
+        this.keepOriginal = keepOriginal;
+        this.corners = corners;
+        globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
+    }
+
+    private HashSet<VecI> DrawImage(Document doc, Guid memberGuid, Surface image, VecI originalPos, ChunkyImage memberImage)
+    {
+        var prevChunks = memberImage.FindAffectedChunks();
+
+        memberImage.CancelChanges();
+
+        if (!keepOriginal)
+            memberImage.EnqueueClearPath(originalPath!, originalTightBounds);
+        SKMatrix localMatrix = SKMatrix.CreateTranslation(originalPos.X - originalTightBounds.Left, originalPos.Y - originalTightBounds.Top);
+        localMatrix = localMatrix.PostConcat(globalMatrix);
+        memberImage.EnqueueDrawImage(localMatrix, image, RegularPaint, false);
+        hasEnqueudImages = true;
+
+        var affectedChunks = memberImage.FindAffectedChunks();
+        affectedChunks.UnionWith(prevChunks);
+        return affectedChunks;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        if (savedChunks is not null)
+            throw new InvalidOperationException("Apply called twice");
+        savedChunks = new();
+
+        List<IChangeInfo> infos = new();
+        foreach (var (guid, (image, pos)) in images!)
+        {
+            ChunkyImage memberImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
+            var chunks = DrawImage(target, guid, image, pos, memberImage);
+            savedChunks[guid] = new(memberImage, memberImage.FindAffectedChunks());
+            memberImage.CommitChanges();
+            infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, chunks, drawOnMask).AsT1);
+        }
+
+        infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
+
+        hasEnqueudImages = false;
+        ignoreInUndo = false;
+        return infos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        List<IChangeInfo> infos = new();
+        foreach (var (guid, (image, pos)) in images!)
+        {
+            ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
+            infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, DrawImage(target, guid, image, pos, targetImage), drawOnMask).AsT1);
+        }
+        infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
+        return infos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        List<IChangeInfo> infos = new();
+        foreach (var (guid, storage) in savedChunks!)
+        {
+            var storageCopy = storage;
+            var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, guid, drawOnMask, ref storageCopy);
+            infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, chunks, drawOnMask).AsT1);
+        }
+        savedChunks = null;
+        return infos;
+    }
+
+    public override void Dispose()
+    {
+        if (hasEnqueudImages)
+            throw new InvalidOperationException("Attempted to dispose the change while it's internally stored image is still used enqueued in some ChunkyImage. Most likely someone tried to dispose a change after ApplyTemporarily was called but before the subsequent call to Apply. Don't do that.");
+        foreach (var (_, (image, _)) in images!)
+        {
+            image.Dispose();
+        }
+
+        if (savedChunks is not null)
+        {
+            foreach (var (_, chunks) in savedChunks)
+            {
+                chunks.Dispose();
+            }
+        }
+    }
+}

+ 22 - 0
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectionChangeHelper.cs

@@ -0,0 +1,22 @@
+using ChunkyImageLib.Operations;
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Selection;
+internal class SelectionChangeHelper
+{
+    public static Selection_ChangeInfo DoSelectionTransform(
+        Document target, SKPath originalPath, RectI originalPathTightBounds, ShapeCorners to)
+    {
+        SKPath newPath = new(originalPath);
+
+        var matrix = SKMatrix.CreateTranslation((float)-originalPathTightBounds.X, (float)-originalPathTightBounds.Y).PostConcat(
+            OperationHelper.CreateMatrixFromPoints(to, originalPathTightBounds.Size));
+        newPath.Transform(matrix);
+
+        var toDispose = target.Selection.SelectionPath;
+        target.Selection.SelectionPath = newPath;
+        toDispose.Dispose();
+
+        return new Selection_ChangeInfo(new SKPath(target.Selection.SelectionPath));
+    }
+}

+ 34 - 0
src/PixiEditor.ChangeableDocument/Changes/Selection/SetSelection_Change.cs

@@ -0,0 +1,34 @@
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Selection;
+internal class SetSelection_Change : Change
+{
+    private readonly SKPath selection;
+    private SKPath? originalSelection;
+
+    [GenerateMakeChangeAction]
+    public SetSelection_Change(SKPath selection)
+    {
+        this.selection = new SKPath(selection) { FillType = SKPathFillType.EvenOdd };
+    }
+
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        originalSelection = ((IReadOnlySelection)target.Selection).SelectionPath;
+        return new Success();
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        target.Selection.SelectionPath = new SKPath(selection) { FillType = SKPathFillType.EvenOdd };
+        ignoreInUndo = false;
+        return new Selection_ChangeInfo(new SKPath(selection) { FillType = SKPathFillType.EvenOdd });
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        target.Selection.SelectionPath = new SKPath(originalSelection) { FillType = SKPathFillType.EvenOdd };
+        return new Selection_ChangeInfo(new SKPath(originalSelection) { FillType = SKPathFillType.EvenOdd });
+    }
+}

+ 5 - 22
src/PixiEditor.ChangeableDocument/Changes/Selection/TransformSelectionPath_UpdateableChange.cs

@@ -1,11 +1,10 @@
-using ChunkyImageLib.Operations;
-using SkiaSharp;
+using SkiaSharp;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 internal class TransformSelectionPath_UpdateableChange : UpdateableChange
 internal class TransformSelectionPath_UpdateableChange : UpdateableChange
 {
 {
     private SKPath? originalPath;
     private SKPath? originalPath;
-    private ShapeCorners originalCorners;
+    private RectI originalTightBounds;
     private ShapeCorners newCorners;
     private ShapeCorners newCorners;
 
 
     [GenerateUpdateableChangeActions]
     [GenerateUpdateableChangeActions]
@@ -25,35 +24,19 @@ internal class TransformSelectionPath_UpdateableChange : UpdateableChange
         if (target.Selection.SelectionPath.IsEmpty)
         if (target.Selection.SelectionPath.IsEmpty)
             return new Error();
             return new Error();
         originalPath = new(target.Selection.SelectionPath);
         originalPath = new(target.Selection.SelectionPath);
-        var bounds = originalPath.TightBounds;
-        originalCorners = new(bounds);
+        originalTightBounds = (RectI)originalPath.TightBounds;
         return new Success();
         return new Success();
     }
     }
 
 
-    private Selection_ChangeInfo CommonApply(Document target)
-    {
-        SKPath newPath = new(originalPath);
-
-        var matrix = SKMatrix.CreateTranslation((float)-originalCorners.TopLeft.X, (float)-originalCorners.TopLeft.Y).PostConcat(
-            OperationHelper.CreateMatrixFromPoints(newCorners, originalCorners.RectSize));
-        newPath.Transform(matrix);
-
-        var toDispose = target.Selection.SelectionPath;
-        target.Selection.SelectionPath = newPath;
-        toDispose.Dispose();
-
-        return new Selection_ChangeInfo(new SKPath(target.Selection.SelectionPath));
-    }
-
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
     {
         ignoreInUndo = false;
         ignoreInUndo = false;
-        return CommonApply(target);
+        return SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, newCorners);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
     {
-        return CommonApply(target);
+        return SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, newCorners);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)

+ 15 - 11
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -7,9 +7,8 @@ using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
-using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
 
@@ -22,7 +21,7 @@ internal static class ClipboardController
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
         "PixiEditor",
         "PixiEditor",
         "Copied.png");
         "Copied.png");
-    
+
     /// <summary>
     /// <summary>
     ///     Copies the selection to clipboard in PNG, Bitmap and DIB formats.
     ///     Copies the selection to clipboard in PNG, Bitmap and DIB formats.
     /// </summary>
     /// </summary>
@@ -30,14 +29,19 @@ internal static class ClipboardController
     {
     {
         if (!ClipboardHelper.TryClear())
         if (!ClipboardHelper.TryClear())
             return;
             return;
-        
-        using Surface? surface = document.MaybeExtractSelectedArea();
-        if (surface is null)
-            return;
 
 
+        var surface = document.MaybeExtractSelectedArea();
+        if (surface.IsT0)
+            return;
+        if (surface.IsT1)
+        {
+            NoticeDialog.Show("Selected area is empty", "Nothing to copy");
+            return;
+        }
+        var (actuallySurface, _) = surface.AsT2;
         DataObject data = new DataObject();
         DataObject data = new DataObject();
 
 
-        using (SKData pngData = surface.SkiaSurface.Snapshot().Encode())
+        using (SKData pngData = actuallySurface.SkiaSurface.Snapshot().Encode())
         {
         {
             // Stream should not be disposed
             // Stream should not be disposed
             MemoryStream pngStream = new MemoryStream();
             MemoryStream pngStream = new MemoryStream();
@@ -52,7 +56,7 @@ internal static class ClipboardController
             data.SetFileDropList(new StringCollection() { TempCopyFilePath });
             data.SetFileDropList(new StringCollection() { TempCopyFilePath });
         }
         }
 
 
-        WriteableBitmap finalBitmap = surface.ToWriteableBitmap();
+        WriteableBitmap finalBitmap = actuallySurface.ToWriteableBitmap();
         data.SetData(DataFormats.Bitmap, finalBitmap, true); // Bitmap, no transparency
         data.SetData(DataFormats.Bitmap, finalBitmap, true); // Bitmap, no transparency
         data.SetImage(finalBitmap); // DIB format, no transparency
         data.SetImage(finalBitmap); // DIB format, no transparency
 
 
@@ -77,7 +81,7 @@ internal static class ClipboardController
         document.Operations.PasteImagesAsLayers(images);
         document.Operations.PasteImagesAsLayers(images);
         return true;
         return true;
     }
     }
-    
+
     /// <summary>
     /// <summary>
     /// Gets images from clipboard, supported PNG, Dib and Bitmap.
     /// Gets images from clipboard, supported PNG, Dib and Bitmap.
     /// </summary>
     /// </summary>
@@ -114,7 +118,7 @@ internal static class ClipboardController
         }
         }
         return surfaces;
         return surfaces;
     }
     }
-    
+
     public static bool IsImageInClipboard()
     public static bool IsImageInClipboard()
     {
     {
         DataObject dao = ClipboardHelper.TryGetDataObject();
         DataObject dao = ClipboardHelper.TryGetDataObject();

+ 19 - 8
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -29,13 +29,22 @@ internal class ChangeExecutionController
         this.internals = internals;
         this.internals = internals;
     }
     }
 
 
-    public bool TryStartUpdateableChange<T>()
+    public ExecutorType GetCurrentExecutorType()
+    {
+        if (currentSession is null)
+            return ExecutorType.None;
+        return currentSession.Type;
+    }
+
+    public bool TryStartExecutor<T>(bool force = false)
         where T : UpdateableChangeExecutor, new()
         where T : UpdateableChangeExecutor, new()
     {
     {
-        if (currentSession is not null)
+        if (currentSession is not null && !force)
             return false;
             return false;
+        if (force)
+            currentSession?.ForceStop();
         T executor = new T();
         T executor = new T();
-        executor.Initialize(document, internals, this, EndChange);
+        executor.Initialize(document, internals, this, EndExecutor);
         if (executor.Start() == ExecutionState.Success)
         if (executor.Start() == ExecutionState.Success)
         {
         {
             currentSession = executor;
             currentSession = executor;
@@ -44,11 +53,13 @@ internal class ChangeExecutionController
         return false;
         return false;
     }
     }
 
 
-    public bool TryStartUpdateableChange(UpdateableChangeExecutor brandNewExecutor)
+    public bool TryStartExecutor(UpdateableChangeExecutor brandNewExecutor, bool force = false)
     {
     {
-        if (currentSession is not null)
+        if (currentSession is not null && !force)
             return false;
             return false;
-        brandNewExecutor.Initialize(document, internals, this, EndChange);
+        if (force)
+            currentSession?.ForceStop();
+        brandNewExecutor.Initialize(document, internals, this, EndExecutor);
         if (brandNewExecutor.Start() == ExecutionState.Success)
         if (brandNewExecutor.Start() == ExecutionState.Success)
         {
         {
             currentSession = brandNewExecutor;
             currentSession = brandNewExecutor;
@@ -57,14 +68,14 @@ internal class ChangeExecutionController
         return false;
         return false;
     }
     }
 
 
-    private void EndChange(UpdateableChangeExecutor executor)
+    private void EndExecutor(UpdateableChangeExecutor executor)
     {
     {
         if (executor != currentSession)
         if (executor != currentSession)
             throw new InvalidOperationException();
             throw new InvalidOperationException();
         currentSession = null;
         currentSession = null;
     }
     }
 
 
-    public bool TryStopActiveUpdateableChange()
+    public bool TryStopActiveExecutor()
     {
     {
         if (currentSession is null)
         if (currentSession is null)
             return false;
             return false;

+ 22 - 10
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using ChunkyImageLib;
+using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
@@ -80,7 +75,7 @@ internal class DocumentOperationsModule
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
             return;
             return;
-        
+
         RectI maxSize = new RectI(VecI.Zero, Document.SizeBindable);
         RectI maxSize = new RectI(VecI.Zero, Document.SizeBindable);
         foreach (var imageWithName in images)
         foreach (var imageWithName in images)
         {
         {
@@ -214,7 +209,24 @@ internal class DocumentOperationsModule
 
 
     public void PasteImageWithTransform(Surface image, VecI startPos)
     public void PasteImageWithTransform(Surface image, VecI startPos)
     {
     {
-        Internals.ChangeController.TryStartUpdateableChange(new PasteImageExecutor(image, startPos));
+        if (Document.SelectedStructureMember is null)
+            return;
+        Internals.ChangeController.TryStartExecutor(new PasteImageExecutor(image, startPos));
+    }
+
+    public void TransformSelectedArea(bool toolLinked)
+    {
+        if (Document.SelectedStructureMember is null ||
+            Internals.ChangeController.IsChangeActive && !toolLinked ||
+            Document.SelectionPathBindable.IsEmpty)
+            return;
+        Internals.ChangeController.TryStartExecutor(new TransformSelectedAreaExecutor(toolLinked));
+    }
+
+    public void TryStopToolLinkedExecutor()
+    {
+        if (Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked)
+            Internals.ChangeController.TryStopActiveExecutor();
     }
     }
 
 
     public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask) =>
     public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask) =>
@@ -222,10 +234,10 @@ internal class DocumentOperationsModule
 
 
     private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, bool finish)
     private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, bool finish)
     {
     {
-        if (Internals.ChangeController.IsChangeActive)
+        if (Internals.ChangeController.IsChangeActive || Document.SelectedStructureMember is null)
             return;
             return;
         Internals.ActionAccumulator.AddActions(
         Internals.ActionAccumulator.AddActions(
-            new PasteImage_Action(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask),
+            new PasteImage_Action(image, corners, Document.SelectedStructureMember.GuidValue, ignoreClipSymmetriesEtc, drawOnMask),
             new EndPasteImage_Action()
             new EndPasteImage_Action()
             );
             );
         if (finish)
         if (finish)

+ 25 - 10
src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
 
 namespace Models.DocumentModels.Public;
 namespace Models.DocumentModels.Public;
@@ -14,23 +15,37 @@ internal class DocumentToolsModule
         this.Internals = internals;
         this.Internals = internals;
     }
     }
 
 
-    public void UseOpacitySlider() => Internals.ChangeController.TryStartUpdateableChange<StructureMemberOpacityExecutor>();
+    public void UseOpacitySlider() => Internals.ChangeController.TryStartExecutor<StructureMemberOpacityExecutor>();
 
 
-    public void UsePenTool() => Internals.ChangeController.TryStartUpdateableChange<PenToolExecutor>();
+    public void UseShiftLayerTool() => Internals.ChangeController.TryStartExecutor<ShiftLayerExecutor>();
 
 
-    public void UseEraserTool() => Internals.ChangeController.TryStartUpdateableChange<EraserToolExecutor>();
+    public void UsePenTool() => Internals.ChangeController.TryStartExecutor<PenToolExecutor>();
 
 
-    public void UseColorPickerTool() => Internals.ChangeController.TryStartUpdateableChange<ColorPickerToolExecutor>();
+    public void UseEraserTool() => Internals.ChangeController.TryStartExecutor<EraserToolExecutor>();
 
 
-    public void UseRectangleTool() => Internals.ChangeController.TryStartUpdateableChange<RectangleToolExecutor>();
+    public void UseColorPickerTool() => Internals.ChangeController.TryStartExecutor<ColorPickerToolExecutor>();
 
 
-    public void UseEllipseTool() => Internals.ChangeController.TryStartUpdateableChange<EllipseToolExecutor>();
+    public void UseRectangleTool()
+    {
+        bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
+        Internals.ChangeController.TryStartExecutor<RectangleToolExecutor>(force);
+    }
 
 
-    public void UseLineTool() => Internals.ChangeController.TryStartUpdateableChange<LineToolExecutor>();
+    public void UseEllipseTool()
+    {
+        bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
+        Internals.ChangeController.TryStartExecutor<EllipseToolExecutor>(force);
+    }
+
+    public void UseLineTool()
+    {
+        bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
+        Internals.ChangeController.TryStartExecutor<LineToolExecutor>(force);
+    }
 
 
-    public void UseSelectTool() => Internals.ChangeController.TryStartUpdateableChange<SelectToolExecutor>();
+    public void UseSelectTool() => Internals.ChangeController.TryStartExecutor<SelectToolExecutor>();
 
 
-    public void UseBrightnessTool() => Internals.ChangeController.TryStartUpdateableChange<BrightnessToolExecutor>();
+    public void UseBrightnessTool() => Internals.ChangeController.TryStartExecutor<BrightnessToolExecutor>();
 
 
-    public void UseFloodFillTool() => Internals.ChangeController.TryStartUpdateableChange<FloodFillToolExecutor>();
+    public void UseFloodFillTool() => Internals.ChangeController.TryStartExecutor<FloodFillToolExecutor>();
 }
 }

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EllipseToolExecutor.cs

@@ -20,6 +20,7 @@ internal class EllipseToolExecutor : ShapeToolExecutor<EllipseToolViewModel>
         internals!.ActionAccumulator.AddActions(new DrawEllipse_Action(memberGuid, rect, strokeColor, fillColor, strokeWidth, drawOnMask));
         internals!.ActionAccumulator.AddActions(new DrawEllipse_Action(memberGuid, rect, strokeColor, fillColor, strokeWidth, drawOnMask));
     }
     }
 
 
+    public override ExecutorType Type => ExecutorType.ToolLinked;
     protected override DocumentTransformMode TransformMode => DocumentTransformMode.NoRotation;
     protected override DocumentTransformMode TransformMode => DocumentTransformMode.NoRotation;
     protected override void DrawShape(VecI currentPos) => DrawEllipseOrCircle(currentPos);
     protected override void DrawShape(VecI currentPos) => DrawEllipseOrCircle(currentPos);
 
 

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineToolExecutor.cs

@@ -9,6 +9,7 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
 #nullable enable
 internal class LineToolExecutor : ShapeToolExecutor<LineToolViewModel>
 internal class LineToolExecutor : ShapeToolExecutor<LineToolViewModel>
 {
 {
+    public override ExecutorType Type => ExecutorType.ToolLinked;
     public override ExecutionState Start()
     public override ExecutionState Start()
     {
     {
         ColorsViewModel? colorsVM = ViewModelMain.Current?.ColorsSubViewModel;
         ColorsViewModel? colorsVM = ViewModelMain.Current?.ColorsSubViewModel;

+ 2 - 7
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using ChunkyImageLib;
+using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -39,7 +34,7 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
 
 
         ShapeCorners corners = new(new RectD(pos, image.Size));
         ShapeCorners corners = new(new RectD(pos, image.Size));
         internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid, false, drawOnMask));
         internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid, false, drawOnMask));
-        document.TransformViewModel.ShowTransform(DocumentTransformMode.Freeform, corners);
+        document.TransformViewModel.ShowTransform(DocumentTransformMode.Freeform, true, corners);
 
 
         return ExecutionState.Success;
         return ExecutionState.Success;
     }
     }

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

@@ -1,11 +1,13 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
 #nullable enable
 internal class RectangleToolExecutor : ShapeToolExecutor<RectangleToolViewModel>
 internal class RectangleToolExecutor : ShapeToolExecutor<RectangleToolViewModel>
 {
 {
+    public override ExecutorType Type => ExecutorType.ToolLinked;
     private void DrawRectangle(VecI curPos)
     private void DrawRectangle(VecI curPos)
     {
     {
         RectI rect;
         RectI rect;

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

@@ -95,7 +95,7 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
         if (transforming)
         if (transforming)
             return;
             return;
         transforming = true;
         transforming = true;
-        document!.TransformViewModel.ShowTransform(TransformMode, new ShapeCorners(lastRect));
+        document!.TransformViewModel.ShowTransform(TransformMode, false, new ShapeCorners(lastRect));
     }
     }
 
 
     public override void ForceStop()
     public override void ForceStop()

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

@@ -0,0 +1,48 @@
+using ChunkyImageLib.DataHolders;
+using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
+using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+#nullable enable
+internal class ShiftLayerExecutor : UpdateableChangeExecutor
+{
+    private Guid guidValue;
+    private VecI startPos;
+    private MoveToolToolbar? toolbar;
+
+    public override ExecutionState Start()
+    {
+        ViewModelMain? vm = ViewModelMain.Current;
+        StructureMemberViewModel? member = document!.SelectedStructureMember;
+        toolbar = (MoveToolToolbar?)(ViewModelMain.Current?.ToolsSubViewModel.GetTool<MoveToolViewModel>()?.Toolbar);
+        if (vm is null || member is not LayerViewModel layer || layer.ShouldDrawOnMask || toolbar is null)
+            return ExecutionState.Error;
+
+        guidValue = member.GuidValue;
+        startPos = controller!.LastPixelPosition;
+
+        ShiftLayer_Action action = new(guidValue, VecI.Zero, toolbar.KeepOriginalImage);
+        internals!.ActionAccumulator.AddActions(action);
+
+        return ExecutionState.Success;
+    }
+
+    public override void OnPixelPositionChange(VecI pos)
+    {
+        ShiftLayer_Action action = new(guidValue, pos - startPos, toolbar!.KeepOriginalImage);
+        internals!.ActionAccumulator.AddActions(action);
+    }
+
+    public override void OnLeftMouseButtonUp()
+    {
+        internals!.ActionAccumulator.AddFinishedActions(new EndShiftLayer_Action());
+        onEnded?.Invoke(this);
+    }
+
+    public override void ForceStop()
+    {
+        internals!.ActionAccumulator.AddFinishedActions(new EndShiftLayer_Action());
+    }
+}

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

@@ -0,0 +1,62 @@
+using ChunkyImageLib.DataHolders;
+using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
+using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+#nullable enable
+internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
+{
+    private Guid[]? membersToTransform;
+    private MoveToolToolbar? toolbar;
+
+    public override ExecutorType Type { get; }
+
+    public TransformSelectedAreaExecutor(bool toolLinked)
+    {
+        Type = toolLinked ? ExecutorType.ToolLinked : ExecutorType.Regular;
+    }
+
+    public override ExecutionState Start()
+    {
+        toolbar = (MoveToolToolbar?)(ViewModelMain.Current?.ToolsSubViewModel.GetTool<MoveToolViewModel>()?.Toolbar);
+        if (toolbar is null || document!.SelectedStructureMember is null || document!.SelectionPathBindable.IsEmpty)
+            return ExecutionState.Error;
+
+        var members = document.SoftSelectedStructureMembers
+            .Append(document.SelectedStructureMember)
+            .Where(static m => m is LayerViewModel);
+
+        if (!members.Any())
+            return ExecutionState.Error;
+
+        ShapeCorners corners = new(document.SelectionPathBindable.TightBounds);
+        document.TransformViewModel.ShowTransform(DocumentTransformMode.Freeform, true, corners);
+        membersToTransform = members.Select(static a => a.GuidValue).ToArray();
+        internals!.ActionAccumulator.AddActions(
+            new TransformSelectedArea_Action(membersToTransform, corners, toolbar.KeepOriginalImage, false));
+        return ExecutionState.Success;
+    }
+
+    public override void OnTransformMoved(ShapeCorners corners)
+    {
+        internals!.ActionAccumulator.AddActions(
+            new TransformSelectedArea_Action(membersToTransform!, corners, toolbar!.KeepOriginalImage, false));
+    }
+
+    public override void OnTransformApplied()
+    {
+        internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
+        internals!.ActionAccumulator.AddFinishedActions();
+        document!.TransformViewModel.HideTransform();
+        onEnded!.Invoke(this);
+    }
+
+    public override void ForceStop()
+    {
+        internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
+        internals!.ActionAccumulator.AddFinishedActions();
+        document!.TransformViewModel.HideTransform();
+    }
+}

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

@@ -13,6 +13,7 @@ internal abstract class UpdateableChangeExecutor
     private bool initialized = false;
     private bool initialized = false;
 
 
     protected Action<UpdateableChangeExecutor>? onEnded;
     protected Action<UpdateableChangeExecutor>? onEnded;
+    public virtual ExecutorType Type => ExecutorType.Regular;
 
 
     public void Initialize(DocumentViewModel document, DocumentInternalParts internals, ChangeExecutionController controller, Action<UpdateableChangeExecutor> onEnded)
     public void Initialize(DocumentViewModel document, DocumentInternalParts internals, ChangeExecutionController controller, Action<UpdateableChangeExecutor> onEnded)
     {
     {

+ 7 - 0
src/PixiEditor/Models/Enums/ExecutorType.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Models.Enums;
+internal enum ExecutorType
+{
+    None,
+    Regular,
+    ToolLinked,
+}

+ 10 - 2
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentTransformViewModel.cs

@@ -48,13 +48,20 @@ internal class DocumentTransformViewModel : NotifyableObject
         set => SetProperty(ref transformActive, value);
         set => SetProperty(ref transformActive, value);
     }
     }
 
 
+    private bool coverWholeScreen;
+    public bool CoverWholeScreen
+    {
+        get => coverWholeScreen;
+        set => SetProperty(ref coverWholeScreen, value);
+    }
+
     private ShapeCorners requestedCorners;
     private ShapeCorners requestedCorners;
     public ShapeCorners RequestedCorners
     public ShapeCorners RequestedCorners
     {
     {
         get => requestedCorners;
         get => requestedCorners;
         set
         set
         {
         {
-            // We must raise the event if if the value hasn't changed, so I'm not using SetProperty
+            // The event must be raised even if the value hasn't changed, so I'm not using SetProperty
             requestedCorners = value;
             requestedCorners = value;
             RaisePropertyChanged(nameof(RequestedCorners));
             RaisePropertyChanged(nameof(RequestedCorners));
         }
         }
@@ -80,13 +87,14 @@ internal class DocumentTransformViewModel : NotifyableObject
         TransformActive = false;
         TransformActive = false;
     }
     }
 
 
-    public void ShowTransform(DocumentTransformMode mode, ShapeCorners initPos)
+    public void ShowTransform(DocumentTransformMode mode, bool coverWholeScreen, ShapeCorners initPos)
     {
     {
         activeTransformMode = mode;
         activeTransformMode = mode;
         CornerFreedom = TransformCornerFreedom.Scale;
         CornerFreedom = TransformCornerFreedom.Scale;
         SideFreedom = TransformSideFreedom.Stretch;
         SideFreedom = TransformSideFreedom.Stretch;
         LockRotation = mode == DocumentTransformMode.NoRotation;
         LockRotation = mode == DocumentTransformMode.NoRotation;
         RequestedCorners = initPos;
         RequestedCorners = initPos;
+        CoverWholeScreen = coverWholeScreen;
         TransformActive = true;
         TransformActive = true;
     }
     }
 
 

+ 30 - 7
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -246,16 +246,39 @@ internal class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(AllChangesSaved));
         RaisePropertyChanged(nameof(AllChangesSaved));
     }
     }
 
 
-    public Surface? MaybeExtractSelectedArea()
+    /// <summary>
+    /// Takes the selected area and converts it into a surface
+    /// </summary>
+    /// <returns><see cref="Error"/> on error, <see cref="None"/> for empty <see cref="Surface"/>, <see cref="Surface"/> otherwise.</returns>
+    public OneOf<Error, None, (Surface, RectI)> MaybeExtractSelectedArea(StructureMemberViewModel? layerToExtractFrom = null)
     {
     {
-        if (SelectedStructureMember is null || SelectedStructureMember is not LayerViewModel layerVm || SelectionPathBindable.IsEmpty)
-            return null;
+        layerToExtractFrom ??= SelectedStructureMember;
+        if (layerToExtractFrom is null || layerToExtractFrom is not LayerViewModel layerVm)
+            return new Error();
+        if (SelectionPathBindable.IsEmpty)
+            return new None();
 
 
         IReadOnlyLayer? layer = (IReadOnlyLayer?)Internals.Tracker.Document.FindMember(layerVm.GuidValue);
         IReadOnlyLayer? layer = (IReadOnlyLayer?)Internals.Tracker.Document.FindMember(layerVm.GuidValue);
         if (layer is null)
         if (layer is null)
-            return null;
-            
+            return new Error();
+
         RectI bounds = (RectI)SelectionPathBindable.TightBounds;
         RectI bounds = (RectI)SelectionPathBindable.TightBounds;
+        RectI? memberImageBounds;
+        try
+        {
+            memberImageBounds = layer.LayerImage.FindLatestBounds();
+        }
+        catch (ObjectDisposedException)
+        {
+            return new Error();
+        }
+        if (memberImageBounds is null)
+            return new None();
+        bounds = bounds.Intersect(memberImageBounds.Value);
+        bounds = bounds.Intersect(new RectI(VecI.Zero, SizeBindable));
+        if (bounds.IsZeroOrNegativeArea)
+            return new None();
+
         Surface output = new(bounds.Size);
         Surface output = new(bounds.Size);
 
 
         SKPath clipPath = new SKPath(SelectionPathBindable) { FillType = SKPathFillType.EvenOdd };
         SKPath clipPath = new SKPath(SelectionPathBindable) { FillType = SKPathFillType.EvenOdd };
@@ -269,11 +292,11 @@ internal class DocumentViewModel : NotifyableObject
         catch (ObjectDisposedException)
         catch (ObjectDisposedException)
         {
         {
             output.Dispose();
             output.Dispose();
-            return null;
+            return new Error();
         }
         }
         output.SkiaSurface.Canvas.Restore();
         output.SkiaSurface.Canvas.Restore();
 
 
-        return output;
+        return (output, bounds);
     }
     }
 
 
     public SKColor PickColor(VecI pos, bool fromAllLayers)
     public SKColor PickColor(VecI pos, bool fromAllLayers)

+ 6 - 2
src/PixiEditor/ViewModels/SubViewModels/Main/SelectionViewModel.cs

@@ -1,7 +1,5 @@
 using System.Windows.Input;
 using System.Windows.Input;
-using PixiEditor.Models.Commands.Attributes;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
-using PixiEditor.Models.Commands.Attributes.Evaluators;
 
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
 
@@ -36,4 +34,10 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
     {
     {
         return !Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectionPathBindable?.IsEmpty ?? false;
         return !Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectionPathBindable?.IsEmpty ?? false;
     }
     }
+
+    [Command.Basic("PixiEditor.Selection.TransformArea", "Transform selected area", "Transform selected area", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.T, Modifiers = ModifierKeys.Control)]
+    public void TransformSelectedArea()
+    {
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(false);
+    }
 }
 }

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs

@@ -120,8 +120,10 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>
 
 
         //update old tool
         //update old tool
         LastActionTool?.UpdateActionDisplay(false, false, false);
         LastActionTool?.UpdateActionDisplay(false, false, false);
+        LastActionTool?.OnDeselected();
         //update new tool
         //update new tool
         ActiveTool.UpdateActionDisplay(ctrlIsDown, shiftIsDown, altIsDown);
         ActiveTool.UpdateActionDisplay(ctrlIsDown, shiftIsDown, altIsDown);
+        ActiveTool.OnSelected();
 
 
         tool.IsActive = true;
         tool.IsActive = true;
         SetToolCursor(tool.GetType());
         SetToolCursor(tool.GetType());

+ 5 - 0
src/PixiEditor/ViewModels/SubViewModels/Tools/ShapeTool.cs

@@ -12,4 +12,9 @@ internal abstract class ShapeTool : ToolViewModel
         Cursor = Cursors.Cross;
         Cursor = Cursors.Cross;
         Toolbar = new BasicShapeToolbar();
         Toolbar = new BasicShapeToolbar();
     }
     }
+
+    public override void OnDeselected()
+    {
+        ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TryStopToolLinkedExecutor();
+    }
 }
 }

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Tools/Tool.cs

@@ -50,4 +50,6 @@ internal abstract class ToolViewModel : NotifyableObject
 
 
     public virtual void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) { }
     public virtual void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) { }
     public virtual void OnLeftMouseButtonDown(VecD pos) { }
     public virtual void OnLeftMouseButtonDown(VecD pos) { }
+    public virtual void OnSelected() { }
+    public virtual void OnDeselected() { }
 }
 }

+ 12 - 0
src/PixiEditor/ViewModels/SubViewModels/Tools/ToolSettings/Toolbars/MoveToolToolbar.cs

@@ -0,0 +1,12 @@
+using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Settings;
+
+namespace PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
+internal class MoveToolToolbar : Toolbar
+{
+    public bool KeepOriginalImage => GetSetting<BoolSetting>(nameof(KeepOriginalImage)).Value;
+
+    public MoveToolToolbar()
+    {
+        Settings.Add(new BoolSetting(nameof(KeepOriginalImage), "Kеep original image"));
+    }
+}

+ 15 - 5
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -1,5 +1,7 @@
 using System.Windows.Input;
 using System.Windows.Input;
+using ChunkyImageLib.DataHolders;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.UserControls.BrushShapeOverlay;
 using PixiEditor.Views.UserControls.BrushShapeOverlay;
 
 
 namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 namespace PixiEditor.ViewModels.SubViewModels.Tools.Tools;
@@ -12,6 +14,7 @@ internal class MoveToolViewModel : ToolViewModel
     public MoveToolViewModel()
     public MoveToolViewModel()
     {
     {
         ActionDisplay = defaultActionDisplay;
         ActionDisplay = defaultActionDisplay;
+        Toolbar = new MoveToolToolbar();
         Cursor = Cursors.Arrow;
         Cursor = Cursors.Arrow;
     }
     }
 
 
@@ -20,11 +23,18 @@ internal class MoveToolViewModel : ToolViewModel
     public override BrushShape BrushShape => BrushShape.Hidden;
     public override BrushShape BrushShape => BrushShape.Hidden;
     public override bool HideHighlight => true;
     public override bool HideHighlight => true;
 
 
-    public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    public override void OnLeftMouseButtonDown(VecD pos)
     {
     {
-        if (ctrlIsDown)
-            ActionDisplay = "Hold mouse to move all layers.";
-        else
-            ActionDisplay = defaultActionDisplay;
+        ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseShiftLayerTool();
+    }
+
+    public override void OnSelected()
+    {
+        ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(true);
+    }
+
+    public override void OnDeselected()
+    {
+        ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TryStopToolLinkedExecutor();
     }
     }
 }
 }

+ 15 - 2
src/PixiEditor/Views/UserControls/SelectionOverlay.cs

@@ -2,7 +2,6 @@
 using System.Windows.Controls;
 using System.Windows.Controls;
 using System.Windows.Media;
 using System.Windows.Media;
 using System.Windows.Media.Animation;
 using System.Windows.Media.Animation;
-using SkiaSharp;
 
 
 namespace PixiEditor.Views.UserControls;
 namespace PixiEditor.Views.UserControls;
 #nullable enable
 #nullable enable
@@ -12,10 +11,18 @@ internal class SelectionOverlay : Control
         DependencyProperty.Register(nameof(Path), typeof(SKPath), typeof(SelectionOverlay),
         DependencyProperty.Register(nameof(Path), typeof(SKPath), typeof(SelectionOverlay),
             new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
             new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
 
 
-
     public static readonly DependencyProperty ZoomboxScaleProperty =
     public static readonly DependencyProperty ZoomboxScaleProperty =
         DependencyProperty.Register(nameof(ZoomboxScale), typeof(double), typeof(SelectionOverlay), new(1.0, OnZoomboxScaleChanged));
         DependencyProperty.Register(nameof(ZoomboxScale), typeof(double), typeof(SelectionOverlay), new(1.0, OnZoomboxScaleChanged));
 
 
+    public static readonly DependencyProperty ShowFillProperty =
+        DependencyProperty.Register(nameof(ShowFill), typeof(bool), typeof(SelectionOverlay), new(true, OnShowFillChanged));
+
+    public bool ShowFill
+    {
+        get => (bool)GetValue(ShowFillProperty);
+        set => SetValue(ShowFillProperty, value);
+    }
+
     public double ZoomboxScale
     public double ZoomboxScale
     {
     {
         get => (double)GetValue(ZoomboxScaleProperty);
         get => (double)GetValue(ZoomboxScaleProperty);
@@ -86,4 +93,10 @@ internal class SelectionOverlay : Control
         self.whitePen.Thickness = 1.0 / newScale;
         self.whitePen.Thickness = 1.0 / newScale;
         self.blackDashedPen.Thickness = 1.0 / newScale;
         self.blackDashedPen.Thickness = 1.0 / newScale;
     }
     }
+
+    private static void OnShowFillChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
+    {
+        var self = (SelectionOverlay)obj;
+        self.fillBrush.Opacity = (bool)args.NewValue ? 1 : 0;
+    }
 }
 }

+ 61 - 3
src/PixiEditor/Views/UserControls/TransformOverlay/TransformOverlay.cs

@@ -42,6 +42,16 @@ internal class TransformOverlay : Decorator
     public static readonly DependencyProperty ZoomboxAngleProperty =
     public static readonly DependencyProperty ZoomboxAngleProperty =
         DependencyProperty.Register(nameof(ZoomboxAngle), typeof(double), typeof(TransformOverlay), new(0.0));
         DependencyProperty.Register(nameof(ZoomboxAngle), typeof(double), typeof(TransformOverlay), new(0.0));
 
 
+    public static readonly DependencyProperty ConverWholeScreenProperty =
+        DependencyProperty.Register(nameof(CoverWholeScreen), typeof(bool), typeof(TransformOverlay),
+            new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender));
+
+    public bool CoverWholeScreen
+    {
+        get => (bool)GetValue(ConverWholeScreenProperty);
+        set => SetValue(ConverWholeScreenProperty, value);
+    }
+
     public double ZoomboxAngle
     public double ZoomboxAngle
     {
     {
         get => (double)GetValue(ZoomboxAngleProperty);
         get => (double)GetValue(ZoomboxAngleProperty);
@@ -96,6 +106,8 @@ internal class TransformOverlay : Decorator
         set => SetValue(LockRotationProperty, value);
         set => SetValue(LockRotationProperty, value);
     }
     }
 
 
+    private const int anchorSizeMultiplierForRotation = 15;
+
     private bool isResettingRequestedCorners = false;
     private bool isResettingRequestedCorners = false;
     private bool isMoving = false;
     private bool isMoving = false;
     private VecD mousePosOnStartMove = new();
     private VecD mousePosOnStartMove = new();
@@ -142,11 +154,51 @@ internal class TransformOverlay : Decorator
             UpdateRotationCursor(TransformHelper.ToVecD(Mouse.GetPosition(this)));
             UpdateRotationCursor(TransformHelper.ToVecD(Mouse.GetPosition(this)));
     }
     }
 
 
+    private void DrawMouseInputArea(DrawingContext context, VecD size)
+    {
+        if (CoverWholeScreen)
+        {
+            context.DrawRectangle(Brushes.Transparent, null, new Rect(new Point(-size.X * 50, -size.Y * 50), new Size(size.X * 101, size.Y * 101)));
+            return;
+        }
+
+        StreamGeometry geometry = new();
+        using (StreamGeometryContext ctx = geometry.Open())
+        {
+            ctx.BeginFigure(TransformHelper.ToPoint(Corners.TopLeft), true, true);
+            ctx.LineTo(TransformHelper.ToPoint(Corners.TopRight), true, true);
+            ctx.LineTo(TransformHelper.ToPoint(Corners.BottomRight), true, true);
+            ctx.LineTo(TransformHelper.ToPoint(Corners.BottomLeft), true, true);
+            ctx.Close();
+        }
+
+        if (LockRotation)
+            return;
+
+        context.DrawGeometry(Brushes.Transparent, null, geometry);
+        Span<Point> points = stackalloc Point[]
+        {
+            TransformHelper.ToPoint(Corners.TopLeft),
+            TransformHelper.ToPoint(Corners.TopRight),
+            TransformHelper.ToPoint(Corners.BottomLeft),
+            TransformHelper.ToPoint(Corners.BottomRight),
+            TransformHelper.ToPoint((Corners.TopLeft + Corners.TopRight) / 2),
+            TransformHelper.ToPoint((Corners.TopLeft + Corners.BottomLeft) / 2),
+            TransformHelper.ToPoint((Corners.BottomRight + Corners.TopRight) / 2),
+            TransformHelper.ToPoint((Corners.BottomRight + Corners.BottomLeft) / 2),
+        };
+        double ellipseSize = (TransformHelper.AnchorSize * anchorSizeMultiplierForRotation - 2) / (ZoomboxScale * 2);
+        foreach (var point in points)
+        {
+            context.DrawEllipse(Brushes.Transparent, null, point, ellipseSize, ellipseSize);
+        }
+    }
+
     private void DrawOverlay
     private void DrawOverlay
         (DrawingContext context, VecD size, ShapeCorners corners, VecD origin, double zoomboxScale)
         (DrawingContext context, VecD size, ShapeCorners corners, VecD origin, double zoomboxScale)
     {
     {
-        // draw transparent background to enable mouse input everywhere
-        context.DrawRectangle(Brushes.Transparent, null, new Rect(new Point(-size.X * 50, -size.Y * 50), new Size(size.X * 101, size.Y * 101)));
+        // draw transparent background to enable mouse input
+        DrawMouseInputArea(context, size);
 
 
         blackPen.Thickness = 1 / zoomboxScale;
         blackPen.Thickness = 1 / zoomboxScale;
         blackDashedPen.Thickness = 1 / zoomboxScale;
         blackDashedPen.Thickness = 1 / zoomboxScale;
@@ -201,6 +253,12 @@ internal class TransformOverlay : Decorator
         context.DrawGeometry(Brushes.White, blackPen, rotateCursorGeometry);
         context.DrawGeometry(Brushes.White, blackPen, rotateCursorGeometry);
     }
     }
 
 
+    protected override void OnMouseLeave(MouseEventArgs e)
+    {
+        base.OnMouseLeave(e);
+        rotateCursorGeometry.Transform = new ScaleTransform(0, 0);
+    }
+
     protected override void OnMouseDown(MouseButtonEventArgs e)
     protected override void OnMouseDown(MouseButtonEventArgs e)
     {
     {
         base.OnMouseDown(e);
         base.OnMouseDown(e);
@@ -245,7 +303,7 @@ internal class TransformOverlay : Decorator
             TransformHelper.GetAnchorInPosition(mousePos, Corners, InternalState.Origin, ZoomboxScale) is not null ||
             TransformHelper.GetAnchorInPosition(mousePos, Corners, InternalState.Origin, ZoomboxScale) is not null ||
             TransformHelper.IsWithinTransformHandle(TransformHelper.GetDragHandlePos(Corners, ZoomboxScale), mousePos, ZoomboxScale))
             TransformHelper.IsWithinTransformHandle(TransformHelper.GetDragHandlePos(Corners, ZoomboxScale), mousePos, ZoomboxScale))
             return false;
             return false;
-        return TransformHelper.GetAnchorInPosition(mousePos, Corners, InternalState.Origin, ZoomboxScale, 15) is not null;
+        return TransformHelper.GetAnchorInPosition(mousePos, Corners, InternalState.Origin, ZoomboxScale, anchorSizeMultiplierForRotation) is not null;
     }
     }
 
 
     private bool UpdateRotationCursor(VecD mousePos)
     private bool UpdateRotationCursor(VecD mousePos)

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

@@ -16,6 +16,7 @@
     xmlns:vm="clr-namespace:PixiEditor.ViewModels"
     xmlns:vm="clr-namespace:PixiEditor.ViewModels"
     xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
     xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
     xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML"
     xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML"
+    xmlns:tools ="clr-namespace:PixiEditor.ViewModels.SubViewModels.Tools.Tools"
     mc:Ignorable="d"
     mc:Ignorable="d"
     x:Name="vpUc"
     x:Name="vpUc"
     d:DesignHeight="450"
     d:DesignHeight="450"
@@ -76,6 +77,9 @@
                     </ImageBrush>
                     </ImageBrush>
                 </Border.Background>
                 </Border.Background>
                 <Grid>
                 <Grid>
+                    <Grid.Resources>
+                        <converters:IsSpecifiedTypeConverter x:Key="IsSelectToolConverter" SpecifiedType="{x:Type tools:SelectToolViewModel}"/>
+                    </Grid.Resources>
                     <Canvas>
                     <Canvas>
                         <Image
                         <Image
                             Width="{Binding Document.ReferenceBitmap.Width}"
                             Width="{Binding Document.ReferenceBitmap.Width}"
@@ -107,6 +111,7 @@
                         DragCommand="{Binding Document.DragSymmetryCommand}"
                         DragCommand="{Binding Document.DragSymmetryCommand}"
                         DragEndCommand="{Binding Document.EndDragSymmetryCommand}" />
                         DragEndCommand="{Binding Document.EndDragSymmetryCommand}" />
                     <uc:SelectionOverlay
                     <uc:SelectionOverlay
+                        ShowFill="{Binding ToolsSubViewModel.ActiveTool, Source={vm:MainVM}, Converter={StaticResource IsSelectToolConverter}}"
                         Path="{Binding Document.SelectionPathBindable}"
                         Path="{Binding Document.SelectionPathBindable}"
                         ZoomboxScale="{Binding Zoombox.Scale}" />
                         ZoomboxScale="{Binding Zoombox.Scale}" />
                     <brush:BrushShapeOverlay
                     <brush:BrushShapeOverlay
@@ -129,6 +134,7 @@
                         CornerFreedom="{Binding Document.TransformViewModel.CornerFreedom}"
                         CornerFreedom="{Binding Document.TransformViewModel.CornerFreedom}"
                         SideFreedom="{Binding Document.TransformViewModel.SideFreedom}"
                         SideFreedom="{Binding Document.TransformViewModel.SideFreedom}"
                         LockRotation="{Binding Document.TransformViewModel.LockRotation}"
                         LockRotation="{Binding Document.TransformViewModel.LockRotation}"
+                        CoverWholeScreen="{Binding Document.TransformViewModel.CoverWholeScreen}"
                         SnapToAngles="{Binding Document.TransformViewModel.SnapToAngles}"
                         SnapToAngles="{Binding Document.TransformViewModel.SnapToAngles}"
                         InternalState="{Binding Document.TransformViewModel.InternalState, Mode=TwoWay}"
                         InternalState="{Binding Document.TransformViewModel.InternalState, Mode=TwoWay}"
                         ZoomboxScale="{Binding Zoombox.Scale}"
                         ZoomboxScale="{Binding Zoombox.Scale}"

+ 1 - 1
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -563,7 +563,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
             return;
             return;
         updateableChangeActive = true;
         updateableChangeActive = true;
         shiftingLayer = true;
         shiftingLayer = true;
-        Helpers.ActionAccumulator.AddActions(new ShiftLayer_Action(layer.GuidValue, delta));
+        Helpers.ActionAccumulator.AddActions(new ShiftLayer_Action(layer.GuidValue, delta, false));
     }
     }
 
 
     public void EndShiftLayer()
     public void EndShiftLayer()