浏览代码

Merge branch 'master' into development

Krzysztof Krysiński 2 年之前
父节点
当前提交
269869345f
共有 45 个文件被更改,包括 814 次插入101 次删除
  1. 21 8
      src/ChunkyImageLib/ChunkyImage.cs
  2. 34 0
      src/ChunkyImageLib/Operations/ApplyMaskOperation.cs
  3. 1 1
      src/ChunkyImageLib/Operations/BresenhamLineOperation.cs
  4. 1 1
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  5. 1 1
      src/ChunkyImageLib/Operations/ClearPathOperation.cs
  6. 1 1
      src/ChunkyImageLib/Operations/ClearRegionOperation.cs
  7. 1 1
      src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs
  8. 1 1
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  9. 0 1
      src/ChunkyImageLib/Operations/IDrawOperation.cs
  10. 6 0
      src/ChunkyImageLib/Operations/IMirroredDrawOperation.cs
  11. 1 1
      src/ChunkyImageLib/Operations/ImageOperation.cs
  12. 1 1
      src/ChunkyImageLib/Operations/PathOperation.cs
  13. 1 1
      src/ChunkyImageLib/Operations/PixelOperation.cs
  14. 1 1
      src/ChunkyImageLib/Operations/PixelsOperation.cs
  15. 1 1
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  16. 0 5
      src/ChunkyImageLib/Operations/ReplaceColorOperation.cs
  17. 2 3
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  18. 8 8
      src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs
  19. 117 0
      src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs
  20. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  21. 260 0
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  22. 45 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/ApplyMask_Change.cs
  23. 17 0
      src/PixiEditor.ChangeableDocument/Enums/FlipType.cs
  24. 1 1
      src/PixiEditor.DrawingApi.Core/Numerics/Matrix3X3.cs
  25. 5 2
      src/PixiEditor/Helpers/Behaviours/MouseBehavior.cs
  26. 48 0
      src/PixiEditor/Models/Controllers/MouseUpdateController.cs
  27. 28 6
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  28. 0 7
      src/PixiEditor/Models/Enums/FlipType.cs
  29. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  30. 65 4
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs
  31. 7 0
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  32. 11 0
      src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  33. 1 0
      src/PixiEditor/Views/Dialogs/PalettesBrowser.xaml.cs
  34. 30 21
      src/PixiEditor/Views/MainWindow.xaml
  35. 18 0
      src/PixiEditor/Views/UserControls/Layers/FolderControl.xaml.cs
  36. 1 0
      src/PixiEditor/Views/UserControls/Layers/LayerControl.xaml
  37. 25 6
      src/PixiEditor/Views/UserControls/Layers/LayerControl.xaml.cs
  38. 2 2
      src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml
  39. 3 3
      src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml.cs
  40. 7 2
      src/PixiEditor/Views/UserControls/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs
  41. 12 3
      src/PixiEditor/Views/UserControls/Overlays/LineToolOverlay/LineToolOverlay.cs
  42. 15 2
      src/PixiEditor/Views/UserControls/Overlays/SymmetryOverlay/SymmetryOverlay.cs
  43. 6 2
      src/PixiEditor/Views/UserControls/PreviewWindow.xaml.cs
  44. 0 1
      src/PixiEditor/Views/UserControls/Viewport.xaml
  45. 5 0
      src/PixiEditor/Views/UserControls/Viewport.xaml.cs

+ 21 - 8
src/ChunkyImageLib/ChunkyImage.cs

@@ -156,7 +156,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
                 preciseBounds ??= globalChunkBounds;
                 preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
             }
-            preciseBounds = preciseBounds?.Intersect(new RectI(VecI.Zero, CommittedSize));
+            preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
 
             return preciseBounds;
         }
@@ -538,6 +538,16 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             EnqueueOperation(operation);
         }
     }
+    
+    public void EnqueueApplyMask(ChunkyImage mask)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            ApplyMaskOperation operation = new(mask);
+            EnqueueOperation(operation);
+        }
+    }
 
     /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
@@ -655,12 +665,15 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
         List<IDrawOperation> operations = new(4) { operation };
 
-        if (horizontalSymmetryAxis is not null && verticalSymmetryAxis is not null)
-            operations.Add(operation.AsMirrored(verticalSymmetryAxis, horizontalSymmetryAxis));
-        if (horizontalSymmetryAxis is not null)
-            operations.Add(operation.AsMirrored(null, horizontalSymmetryAxis));
-        if (verticalSymmetryAxis is not null)
-            operations.Add(operation.AsMirrored(verticalSymmetryAxis, null));
+        if (operation is IMirroredDrawOperation mirroredOperation)
+        {
+            if (horizontalSymmetryAxis is not null && verticalSymmetryAxis is not null)
+                operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, horizontalSymmetryAxis));
+            if (horizontalSymmetryAxis is not null)
+                operations.Add(mirroredOperation.AsMirrored(null, horizontalSymmetryAxis));
+            if (verticalSymmetryAxis is not null)
+                operations.Add(mirroredOperation.AsMirrored(verticalSymmetryAxis, null));
+        }
 
         foreach (var op in operations)
         {
@@ -854,7 +867,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     }
 
     /// <returns>
-    /// All chunks that have something in them, including latest (uncommitted) ones
+    ///     All chunks that have something in them, including latest (uncommitted) ones
     /// </returns>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public HashSet<VecI> FindAllChunks()

+ 34 - 0
src/ChunkyImageLib/Operations/ApplyMaskOperation.cs

@@ -0,0 +1,34 @@
+using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+
+namespace ChunkyImageLib.Operations;
+
+internal class ApplyMaskOperation : IDrawOperation
+{
+    private ChunkyImage mask;
+    private Paint clippingPaint = new Paint() { BlendMode = BlendMode.DstIn };
+
+    public bool IgnoreEmptyChunks => true;
+
+    public ApplyMaskOperation(ChunkyImage maskToApply)
+    {
+        mask = maskToApply;
+    }
+    
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    {
+        return mask.FindCommittedChunks();
+    }
+    
+    public void DrawOnChunk(Chunk chunk, VecI chunkPos)
+    {
+        mask.DrawCommittedChunkOn(chunkPos, chunk.Resolution, chunk.Surface.DrawingSurface, VecI.Zero, clippingPaint);
+    }
+
+    public void Dispose()
+    {
+        clippingPaint.Dispose();
+    }
+}

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

@@ -5,7 +5,7 @@ using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
 namespace ChunkyImageLib.Operations;
-internal class BresenhamLineOperation : IDrawOperation
+internal class BresenhamLineOperation : IMirroredDrawOperation
 {
     public bool IgnoreEmptyChunks => false;
     private readonly VecI from;

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

@@ -2,7 +2,7 @@
 using PixiEditor.DrawingApi.Core.Numerics;
 
 namespace ChunkyImageLib.Operations;
-internal class ChunkyImageOperation : IDrawOperation
+internal class ChunkyImageOperation : IMirroredDrawOperation
 {
     private readonly ChunkyImage imageToDraw;
     private readonly VecI targetPos;

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

@@ -3,7 +3,7 @@ using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.Vector;
 
 namespace ChunkyImageLib.Operations;
-internal class ClearPathOperation : IDrawOperation
+internal class ClearPathOperation : IMirroredDrawOperation
 {
     private VectorPath path;
     private RectI pathTightBounds;

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

@@ -2,7 +2,7 @@
 
 namespace ChunkyImageLib.Operations;
 
-internal class ClearRegionOperation : IDrawOperation
+internal class ClearRegionOperation : IMirroredDrawOperation
 {
     private RectI rect;
 

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

@@ -5,7 +5,7 @@ using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
 namespace ChunkyImageLib.Operations;
-internal class DrawingSurfaceLineOperation : IDrawOperation
+internal class DrawingSurfaceLineOperation : IMirroredDrawOperation
 {
     public bool IgnoreEmptyChunks => false;
 

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

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 using PixiEditor.DrawingApi.Core.Surface.Vector;
 
 namespace ChunkyImageLib.Operations;
-internal class EllipseOperation : IDrawOperation
+internal class EllipseOperation : IMirroredDrawOperation
 {
     public bool IgnoreEmptyChunks => false;
 

+ 0 - 1
src/ChunkyImageLib/Operations/IDrawOperation.cs

@@ -8,5 +8,4 @@ internal interface IDrawOperation : IOperation
     bool IgnoreEmptyChunks { get; }
     void DrawOnChunk(Chunk chunk, VecI chunkPos);
     HashSet<VecI> FindAffectedChunks(VecI imageSize);
-    IDrawOperation AsMirrored(int? verAxisX, int? horAxisY);
 }

+ 6 - 0
src/ChunkyImageLib/Operations/IMirroredDrawOperation.cs

@@ -0,0 +1,6 @@
+namespace ChunkyImageLib.Operations;
+
+internal interface IMirroredDrawOperation : IDrawOperation
+{
+    IDrawOperation AsMirrored(int? verAxisX, int? horAxisY);
+}

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

@@ -5,7 +5,7 @@ using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
 namespace ChunkyImageLib.Operations;
 
-internal class ImageOperation : IDrawOperation
+internal class ImageOperation : IMirroredDrawOperation
 {
     private Matrix3X3 transformMatrix;
     private ShapeCorners corners;

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

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 using PixiEditor.DrawingApi.Core.Surface.Vector;
 
 namespace ChunkyImageLib.Operations;
-internal class PathOperation : IDrawOperation
+internal class PathOperation : IMirroredDrawOperation
 {
     private readonly VectorPath path;
 

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

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
 namespace ChunkyImageLib.Operations;
 
-internal class PixelOperation : IDrawOperation
+internal class PixelOperation : IMirroredDrawOperation
 {
     public bool IgnoreEmptyChunks => false;
     private readonly VecI pixel;

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

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
 namespace ChunkyImageLib.Operations;
 
-internal class PixelsOperation : IDrawOperation
+internal class PixelsOperation : IMirroredDrawOperation
 {
     public bool IgnoreEmptyChunks => false;
     private readonly Point[] pixels;

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

@@ -4,7 +4,7 @@ using PixiEditor.DrawingApi.Core.Surface;
 
 namespace ChunkyImageLib.Operations;
 
-internal class RectangleOperation : IDrawOperation
+internal class RectangleOperation : IMirroredDrawOperation
 {
     public RectangleOperation(ShapeData rect)
     {

+ 0 - 5
src/ChunkyImageLib/Operations/ReplaceColorOperation.cs

@@ -51,11 +51,6 @@ internal class ReplaceColorOperation : IDrawOperation
         return OperationHelper.FindChunksTouchingRectangle(new RectI(VecI.Zero, imageSize), ChunkyImage.FullChunkSize);
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
-    {
-        return new ReplaceColorOperation(this.oldColor, this.newColor);
-    }
-
     public void Dispose()
     {
     }

+ 2 - 3
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -262,7 +262,7 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         return false;
     }
     
-    public List<Guid> ExtractLayers(List<Guid> members)
+    public List<Guid> ExtractLayers(IList<Guid> members)
     {
         var result = new List<Guid>();
         foreach (var member in members)
@@ -282,7 +282,7 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         return result;
     }
 
-    private List<Guid> ExtractLayers(Folder folder, List<Guid> list)
+    private void ExtractLayers(Folder folder, List<Guid> list)
     {
         foreach (var member in folder.Children)
         {
@@ -295,6 +295,5 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
                 ExtractLayers(childFolder, list);
             }
         }
-        return list;
     }
 }

+ 8 - 8
src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs

@@ -6,25 +6,25 @@ namespace PixiEditor.ChangeableDocument.Changes.Root;
 internal class CenterContent_Change : Change
 {
     private VecI _oldOffset;
-    private List<Guid> _affectedLayers;
+    private List<Guid> affectedLayers;
     private Dictionary<Guid, CommittedChunkStorage>? originalLayerChunks;
 
     [GenerateMakeChangeAction]
     public CenterContent_Change(List<Guid> layers)
     {
-        _affectedLayers = layers;
+        affectedLayers = layers;
     }
     
     public override bool InitializeAndValidate(Document target)
     {
-        if (_affectedLayers.Count == 0)
+        if (affectedLayers.Count == 0)
         {
             return false;
         }
 
-        _affectedLayers = target.ExtractLayers(_affectedLayers);
+        affectedLayers = target.ExtractLayers(affectedLayers);
 
-        foreach (var layer in _affectedLayers)
+        foreach (var layer in affectedLayers)
         {
             if (!target.HasMember(layer)) return false;
         }
@@ -38,7 +38,7 @@ internal class CenterContent_Change : Change
     {
         VecI currentCenter = new VecI(0, 0);
         RectI? currentBounds = null;
-        foreach (var layerGuid in _affectedLayers)
+        foreach (var layerGuid in affectedLayers)
         {
             Layer layer = document.FindMemberOrThrow<Layer>(layerGuid);
             RectI? tightBounds = layer.LayerImage.FindPreciseCommittedBounds();
@@ -64,7 +64,7 @@ internal class CenterContent_Change : Change
         List<IChangeInfo> changes = new List<IChangeInfo>();
         originalLayerChunks = new Dictionary<Guid, CommittedChunkStorage>();
         
-        foreach (var layerGuid in _affectedLayers)
+        foreach (var layerGuid in affectedLayers)
         {
             Layer layer = target.FindMemberOrThrow<Layer>(layerGuid);
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, false, shift);
@@ -81,7 +81,7 @@ internal class CenterContent_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         List<IChangeInfo> changes = new List<IChangeInfo>();
-        foreach (var layerGuid in _affectedLayers)
+        foreach (var layerGuid in affectedLayers)
         {
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             CommittedChunkStorage? originalChunks = originalLayerChunks?[layerGuid];

+ 117 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs

@@ -0,0 +1,117 @@
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
+
+namespace PixiEditor.ChangeableDocument.Changes.Root;
+
+internal sealed class FlipImage_Change : Change
+{
+    private readonly FlipType flipType;
+    private List<Guid> membersToFlip;
+
+    [GenerateMakeChangeAction]
+    public FlipImage_Change(FlipType flipType, List<Guid>? membersToFlip = null)
+    {
+        this.flipType = flipType;
+        membersToFlip ??= new List<Guid>();
+        this.membersToFlip = membersToFlip;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (membersToFlip.Count > 0)
+        {
+            membersToFlip = target.ExtractLayers(membersToFlip);
+            
+            foreach (var layer in membersToFlip)
+            {
+                if (!target.HasMember(layer)) return false;
+            }  
+        }
+        
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        var changes = Flip(target);
+        
+        ignoreInUndo = false;
+        return changes;
+    }
+
+    private void FlipImage(ChunkyImage img)
+    {
+        using Paint paint = new()
+        {
+            BlendMode = DrawingApi.Core.Surface.BlendMode.Src
+        };
+
+        RectI bounds = new RectI(VecI.Zero, img.LatestSize);
+        if (membersToFlip.Count > 0)
+        {
+            var preciseBounds = img.FindPreciseCommittedBounds();
+            if (preciseBounds.HasValue)
+            {
+                bounds = preciseBounds.Value;
+            }
+        }
+
+        using Surface originalSurface = new(img.LatestSize);
+        img.DrawMostUpToDateRegionOn(
+            new RectI(VecI.Zero, img.LatestSize), 
+            ChunkResolution.Full,
+            originalSurface.DrawingSurface,
+            VecI.Zero);
+
+        using Surface flipped = new Surface(img.LatestSize);
+
+        bool flipX = flipType == FlipType.Horizontal;
+        bool flipY = flipType == FlipType.Vertical;
+        
+        flipped.DrawingSurface.Canvas.Save();
+                flipped.DrawingSurface.Canvas.Scale(flipX ? -1 : 1, flipY ? -1 : 1, flipX ? bounds.X + (bounds.Width / 2f) : 0,
+            flipY ? bounds.Y + (bounds.Height / 2f) : 0f);
+        flipped.DrawingSurface.Canvas.DrawSurface(originalSurface.DrawingSurface, 0, 0, paint);
+        flipped.DrawingSurface.Canvas.Restore();
+        
+        img.EnqueueClear();
+        img.EnqueueDrawImage(VecI.Zero, flipped);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        return Flip(target);
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> Flip(Document target)
+    {
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+
+        target.ForEveryMember(member =>
+        {
+            if (membersToFlip.Count == 0 || membersToFlip.Contains(member.GuidValue))
+            {
+                if (member is Layer layer)
+                {
+                    FlipImage(layer.LayerImage);
+                    changes.Add(
+                        new LayerImageChunks_ChangeInfo(member.GuidValue, layer.LayerImage.FindAffectedChunks()));
+                    layer.LayerImage.CommitChanges();
+                }
+
+                if (member.Mask is not null)
+                {
+                    FlipImage(member.Mask);
+                    member.Mask.CommitChanges();
+                }
+            }
+        });
+
+        return changes;
+    }
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -19,7 +19,7 @@ internal abstract class ResizeBasedChangeBase : Change
         return true;
     }
     
-    protected void Resize(ChunkyImage img, Guid memberGuid, VecI size, VecI offset, Dictionary<Guid, CommittedChunkStorage> deletedChunksDict)
+    protected virtual void Resize(ChunkyImage img, Guid memberGuid, VecI size, VecI offset, Dictionary<Guid, CommittedChunkStorage> deletedChunksDict)
     {
         img.EnqueueResize(size);
         img.EnqueueClear();

+ 260 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -0,0 +1,260 @@
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
+
+namespace PixiEditor.ChangeableDocument.Changes.Root;
+
+internal sealed class RotateImage_Change : Change
+{
+    private readonly RotationAngle rotation;
+    private List<Guid> membersToRotate;
+    
+    private VecI originalSize;
+    private int originalHorAxisY;
+    private int originalVerAxisX;
+    private Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
+    private Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
+
+    [GenerateMakeChangeAction]
+    public RotateImage_Change(RotationAngle rotation, List<Guid>? membersToRotate)
+    {
+        this.rotation = rotation;
+        membersToRotate ??= new List<Guid>();
+        this.membersToRotate = membersToRotate;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (membersToRotate.Count > 0)
+        {
+            membersToRotate = target.ExtractLayers(membersToRotate);
+            
+            foreach (var layer in membersToRotate)
+            {
+                if (!target.HasMember(layer)) return false;
+            }  
+        }
+        
+        originalSize = target.Size;
+        originalHorAxisY = target.HorizontalSymmetryAxisY;
+        originalVerAxisX = target.VerticalSymmetryAxisX;
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        var changes = Rotate(target);
+        
+        ignoreInUndo = false;
+        return changes;
+    }
+
+    private void Resize(ChunkyImage img, Guid memberGuid,
+        Dictionary<Guid, CommittedChunkStorage> deletedChunksDict, List<IChangeInfo>? changes)
+    {
+        RectI bounds = new RectI(VecI.Zero, img.CommittedSize);
+        if (membersToRotate.Count > 0)
+        {
+            var preciseBounds = img.FindPreciseCommittedBounds();
+            if (preciseBounds.HasValue)
+            {
+                bounds = preciseBounds.Value;
+            }
+        }
+
+        int originalWidth = bounds.Width;
+        int originalHeight = bounds.Height;
+        
+        int newWidth = rotation == RotationAngle.D180 ? originalWidth : originalHeight;
+        int newHeight = rotation == RotationAngle.D180 ? originalHeight : originalWidth;
+
+        VecI oldSize = new VecI(originalWidth, originalHeight);
+        VecI newSize = new VecI(newWidth, newHeight);
+        
+        using Paint paint = new()
+        {
+            BlendMode = DrawingApi.Core.Surface.BlendMode.Src
+        };
+        
+        using Surface originalSurface = new(oldSize);
+        img.DrawMostUpToDateRegionOn(
+            bounds, 
+            ChunkResolution.Full,
+            originalSurface.DrawingSurface,
+            VecI.Zero);
+
+        using Surface flipped = new Surface(newSize);
+
+        float translationX = newSize.X;
+        float translationY = newSize.Y;
+        switch (rotation)
+        {
+            case RotationAngle.D90:
+                translationY = 0;
+                break;
+            case RotationAngle.D270:
+                translationX = 0;
+                break;
+        }
+        
+        flipped.DrawingSurface.Canvas.Save();
+        flipped.DrawingSurface.Canvas.Translate(translationX, translationY);
+        flipped.DrawingSurface.Canvas.RotateRadians(RotationAngleToRadians(rotation), 0, 0);
+        flipped.DrawingSurface.Canvas.DrawSurface(originalSurface.DrawingSurface, 0, 0, paint);
+        flipped.DrawingSurface.Canvas.Restore();
+
+        if (membersToRotate.Count == 0) 
+        {
+            img.EnqueueResize(newSize);
+        }
+
+        img.EnqueueClear();
+        img.EnqueueDrawImage(bounds.Pos, flipped);
+
+        var affectedChunks = img.FindAffectedChunks();
+        deletedChunksDict.Add(memberGuid, new CommittedChunkStorage(img, affectedChunks));
+        changes?.Add(new LayerImageChunks_ChangeInfo(memberGuid, affectedChunks));
+        img.CommitChanges();
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> Rotate(Document target)
+    {
+        if (membersToRotate.Count == 0)
+        {
+            return RotateWholeImage(target);
+        }
+
+        return RotateMembers(target, membersToRotate);
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> RotateMembers(Document target, List<Guid> guids)
+    {
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+
+        target.ForEveryMember((member) =>
+        {
+            if (guids.Contains(member.GuidValue))
+            {
+                if (member is Layer layer)
+                {
+                    Resize(layer.LayerImage, layer.GuidValue, deletedChunks, changes);
+                }
+
+                if (member.Mask is null)
+                    return;
+
+                Resize(member.Mask, member.GuidValue, deletedMaskChunks, null);
+            }
+        });
+
+        return changes;
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> RotateWholeImage(Document target)
+    {
+        int newWidth = rotation == RotationAngle.D180 ? target.Size.X : target.Size.Y;
+        int newHeight = rotation == RotationAngle.D180 ? target.Size.Y : target.Size.X;
+
+        VecI newSize = new VecI(newWidth, newHeight);
+
+        float normalizedSymmX = originalVerAxisX / Math.Max(target.Size.X, 0.1f);
+        float normalizedSymmY = originalHorAxisY / Math.Max(target.Size.Y, 0.1f);
+
+        target.Size = newSize;
+        target.VerticalSymmetryAxisX = (int)(newSize.X * normalizedSymmX);
+        target.HorizontalSymmetryAxisY = (int)(newSize.Y * normalizedSymmY);
+
+        target.ForEveryMember((member) =>
+        {
+            if (member is Layer layer)
+            {
+                Resize(layer.LayerImage, layer.GuidValue, deletedChunks, null);
+            }
+
+            if (member.Mask is null)
+                return;
+
+            Resize(member.Mask, member.GuidValue, deletedMaskChunks, null);
+        });
+
+        return new Size_ChangeInfo(newSize, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
+    }
+    
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        if (membersToRotate.Count == 0)
+        {
+            return RevertRotateWholeImage(target);
+        }
+
+        return RevertRotateMembers(target);
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> RevertRotateWholeImage(Document target)
+    {
+        target.Size = originalSize;
+        RevertRotateMembers(target);
+
+        target.HorizontalSymmetryAxisY = originalHorAxisY;
+        target.VerticalSymmetryAxisX = originalVerAxisX;
+
+        return new Size_ChangeInfo(originalSize, originalVerAxisX, originalHorAxisY);
+    }
+
+    private List<IChangeInfo> RevertRotateMembers(Document target)
+    {
+        List<IChangeInfo> revertChanges = new List<IChangeInfo>();
+        target.ForEveryMember((member) =>
+        {
+            if(membersToRotate.Count > 0 && !membersToRotate.Contains(member.GuidValue)) return;
+            if (member is Layer layer)
+            {
+                layer.LayerImage.EnqueueResize(originalSize);
+                deletedChunks[layer.GuidValue].ApplyChunksToImage(layer.LayerImage);
+                revertChanges.Add(new LayerImageChunks_ChangeInfo(layer.GuidValue, layer.LayerImage.FindAffectedChunks()));
+                layer.LayerImage.CommitChanges();
+            }
+
+            if (member.Mask is null)
+                return;
+            member.Mask.EnqueueResize(originalSize);
+            deletedMaskChunks[member.GuidValue].ApplyChunksToImage(member.Mask);
+            revertChanges.Add(new LayerImageChunks_ChangeInfo(member.GuidValue, member.Mask.FindAffectedChunks()));
+            member.Mask.CommitChanges();
+        });
+
+        DisposeDeletedChunks();
+        return revertChanges;
+    }
+
+    private void DisposeDeletedChunks()
+    {
+        foreach (var stored in deletedChunks)
+            stored.Value.Dispose();
+        deletedChunks = new();
+
+        foreach (var stored in deletedMaskChunks)
+            stored.Value.Dispose();
+        deletedMaskChunks = new();
+    }
+
+    public override void Dispose()
+    {
+        DisposeDeletedChunks();
+    }
+
+    private float RotationAngleToRadians(RotationAngle rotationAngle)
+    {
+        return rotationAngle switch
+        {
+            RotationAngle.D90 => 90f * Matrix3X3.DegreesToRadians,
+            RotationAngle.D180 => 180f * Matrix3X3.DegreesToRadians,
+            RotationAngle.D270 => 270f * Matrix3X3.DegreesToRadians,
+            _ => throw new ArgumentOutOfRangeException(nameof(rotationAngle), rotationAngle, null)
+        };
+    }
+}

+ 45 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/ApplyMask_Change.cs

@@ -0,0 +1,45 @@
+using PixiEditor.ChangeableDocument.Changes.Drawing;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal sealed class ApplyMask_Change : Change
+{
+    private Guid structureMemberGuid;
+
+    private CommittedChunkStorage? savedChunks;
+
+    [GenerateMakeChangeAction]
+    public ApplyMask_Change(Guid structureMemberGuid)
+    {
+        this.structureMemberGuid = structureMemberGuid;
+    }
+        
+    public override bool InitializeAndValidate(Document target)
+    {
+        var member = target.FindMember(structureMemberGuid);
+        bool isValid = member is not (null or Folder) && member.Mask is not null;
+
+        return isValid;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        var layer = (Layer)target.FindMember(structureMemberGuid)!;
+        layer!.LayerImage.EnqueueApplyMask(layer.Mask!);
+        ignoreInUndo = false;
+        var layerInfo = new LayerImageChunks_ChangeInfo(structureMemberGuid, layer.LayerImage.FindAffectedChunks());
+        savedChunks = new CommittedChunkStorage(layer.LayerImage, layerInfo.Chunks);
+        
+        layer.LayerImage.CommitChanges();
+        return layerInfo;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, structureMemberGuid, false, ref savedChunks);
+        return new LayerImageChunks_ChangeInfo(structureMemberGuid, affected);
+    }
+}

+ 17 - 0
src/PixiEditor.ChangeableDocument/Enums/FlipType.cs

@@ -0,0 +1,17 @@
+namespace PixiEditor.ChangeableDocument.Enums;
+
+public enum FlipType
+{
+    Horizontal,
+    Vertical
+}
+
+/// <summary>
+///     Rotation specified in degrees
+/// </summary>
+public enum RotationAngle
+{
+    D90,
+    D180,
+    D270
+}

+ 1 - 1
src/PixiEditor.DrawingApi.Core/Numerics/Matrix3X3.cs

@@ -12,7 +12,7 @@ namespace PixiEditor.DrawingApi.Core.Numerics;
 /// </remarks>
 public struct Matrix3X3 : IEquatable<Matrix3X3>
 {
-    internal const float DegreesToRadians = 0.017453292f;
+    public const float DegreesToRadians = 0.017453292f;
 
     public static readonly Matrix3X3 Identity = new() { ScaleX = 1f, ScaleY = 1f, Persp2 = 1f };
     

+ 5 - 2
src/PixiEditor/Helpers/Behaviours/MouseBehavior.cs

@@ -1,6 +1,7 @@
 using System.Windows;
 using System.Windows.Input;
 using Microsoft.Xaml.Behaviors;
+using PixiEditor.Models.Controllers;
 
 namespace PixiEditor.Helpers.Behaviours
 {
@@ -34,15 +35,17 @@ namespace PixiEditor.Helpers.Behaviours
             get => (FrameworkElement)GetValue(RelativeToProperty);
             set => SetValue(RelativeToProperty, value);
         }
+        
+        private MouseUpdateController mouseUpdateController;
 
         protected override void OnAttached()
         {
-            AssociatedObject.MouseMove += AssociatedObjectOnMouseMove;
+            mouseUpdateController = new MouseUpdateController(AssociatedObject, AssociatedObjectOnMouseMove);
         }
 
         protected override void OnDetaching()
         {
-            AssociatedObject.MouseMove -= AssociatedObjectOnMouseMove;
+            mouseUpdateController.Dispose();
         }
 
         private void AssociatedObjectOnMouseMove(object sender, MouseEventArgs mouseEventArgs)

+ 48 - 0
src/PixiEditor/Models/Controllers/MouseUpdateController.cs

@@ -0,0 +1,48 @@
+using System.Timers;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Threading;
+
+namespace PixiEditor.Models.Controllers;
+
+public class MouseUpdateController : IDisposable
+{
+    private const int MouseUpdateIntervalMs = 7;  // 7ms ~= 142 Hz
+    
+    private readonly System.Timers.Timer _timer;
+    
+    private UIElement element;
+    
+    private MouseEventHandler mouseMoveHandler;
+    
+    public MouseUpdateController(UIElement uiElement, MouseEventHandler onMouseMove)
+    {
+        mouseMoveHandler = onMouseMove;
+        element = uiElement;
+        
+        _timer = new System.Timers.Timer(MouseUpdateIntervalMs);
+        _timer.AutoReset = true;
+        _timer.Elapsed += TimerOnElapsed;
+        
+        element.MouseMove += OnMouseMove;
+    }
+
+    private void TimerOnElapsed(object sender, ElapsedEventArgs e)
+    {
+        _timer.Stop();
+        element.MouseMove += OnMouseMove;
+    }
+
+    private void OnMouseMove(object sender, MouseEventArgs e)
+    {
+        element.MouseMove -= OnMouseMove;
+        _timer.Start();
+        mouseMoveHandler(sender, e);
+    }
+
+    public void Dispose()
+    {
+        _timer.Dispose();
+        element.MouseMove -= OnMouseMove;
+    }
+}

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

@@ -1,11 +1,7 @@
 using System.Collections.Immutable;
-using System.Windows.Interop;
-using System.Windows.Shapes;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
-using PixiEditor.ChangeableDocument.ChangeInfos.Root.ReferenceLayerChangeInfos;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
@@ -13,9 +9,7 @@ using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Position;
-using PixiEditor.Parser;
 using PixiEditor.ViewModels.SubViewModels.Document;
-using PixiEditor.Views.UserControls.Overlays.TransformOverlay;
 
 namespace PixiEditor.Models.DocumentModels.Public;
 #nullable enable
@@ -196,6 +190,14 @@ internal class DocumentOperationsModule
             return;
         Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMemberMask_Action(member.GuidValue));
     }
+    
+    public void ApplyMask(StructureMemberViewModel member)
+    {
+        if (Internals.ChangeController.IsChangeActive)
+            return;
+        
+        Internals.ActionAccumulator.AddFinishedActions(new ApplyMask_Action(member.GuidValue), new DeleteStructureMemberMask_Action(member.GuidValue));
+    }
 
     public void SetSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new SetSelectedMember_PassthroughAction(memberGuid));
 
@@ -289,6 +291,26 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action());
     }
 
+    public void FlipImage(FlipType flipType) => FlipImage(flipType, null);
+
+    public void FlipImage(FlipType flipType, List<Guid> membersToFlip)
+    {
+        if (Internals.ChangeController.IsChangeActive)
+            return;
+        
+        Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, membersToFlip));
+    }
+
+    public void RotateImage(RotationAngle rotation) => RotateImage(rotation, null);
+
+    public void RotateImage(RotationAngle rotation, List<Guid> membersToRotate)
+    {
+        if (Internals.ChangeController.IsChangeActive)
+            return;
+        
+        Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate));
+    }
+    
     public void CenterContent(IReadOnlyList<Guid> structureMembers)
     {
         if (Internals.ChangeController.IsChangeActive)

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

@@ -1,7 +0,0 @@
-namespace PixiEditor.Models.Enums;
-
-public enum FlipType
-{
-    Horizontal,
-    Vertical
-}

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

@@ -50,5 +50,5 @@ using System.Windows;
 // 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("0.1.9.4")]
-[assembly: AssemblyFileVersion("0.1.9.4")]
+[assembly: AssemblyVersion("0.1.9.5")]
+[assembly: AssemblyFileVersion("0.1.9.5")]

+ 65 - 4
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs

@@ -1,8 +1,10 @@
 using System.Collections.ObjectModel;
 using System.Windows.Input;
+using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.Events;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
@@ -55,6 +57,67 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         
         ActiveDocument?.Operations.ClipCanvas();
     }
+    
+    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", "Flip Image Horizontally", "Flip Image Horizontally", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipImageHorizontally()
+    {
+        if (ActiveDocument is null)
+            return;
+        
+        ActiveDocument?.Operations.FlipImage(FlipType.Horizontal);
+    }
+    
+    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", "Flip Selected Layers Horizontally", "Flip Selected Layers Horizontally", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipLayersHorizontally()
+    {
+        if (ActiveDocument?.SelectedStructureMember == null)
+            return;
+        
+        ActiveDocument?.Operations.FlipImage(FlipType.Horizontal, ActiveDocument.GetSelectedMembers());
+    }
+    
+    [Command.Basic("PixiEditor.Document.FlipImageVertical", "Flip Image Vertically", "Flip Image Vertically", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipImageVertically()
+    {
+        if (ActiveDocument is null)
+            return;
+        
+        ActiveDocument?.Operations.FlipImage(FlipType.Vertical);
+    }
+    
+    [Command.Basic("PixiEditor.Document.FlipLayersVertical", "Flip Selected Layers Vertically", "Flip Selected Layers Vertically", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipLayersVertically()
+    {
+        if (ActiveDocument?.SelectedStructureMember == null)
+            return;
+        
+        ActiveDocument?.Operations.FlipImage(FlipType.Vertical, ActiveDocument.GetSelectedMembers());
+    }
+    
+    [Command.Basic("PixiEditor.Document.Rotate90Deg", "Rotate Image 90 degrees", 
+        "Rotate Image 90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]
+    [Command.Basic("PixiEditor.Document.Rotate180Deg", "Rotate Image 180 degrees", 
+        "Rotate Image 180 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
+    [Command.Basic("PixiEditor.Document.Rotate270Deg", "Rotate Image -90 degrees", 
+        "Rotate Image -90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
+    public void RotateImage(RotationAngle angle)
+    {
+        ActiveDocument?.Operations.RotateImage(angle);
+    }
+
+    [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "Rotate Selected Layers 90 degrees", 
+        "Rotate Selected Layers 90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]
+    [Command.Basic("PixiEditor.Document.Rotate180DegLayers", "Rotate Selected Layers 180 degrees", 
+        "Rotate Selected Layers 180 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
+    [Command.Basic("PixiEditor.Document.Rotate270DegLayers", "Rotate Selected Layers -90 degrees", 
+        "Rotate Selected Layers -90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
+    public void RotateLayers(RotationAngle angle)
+    {
+        if (ActiveDocument?.SelectedStructureMember == null)
+            return;
+        
+        ActiveDocument?.Operations.RotateImage(angle, ActiveDocument.GetSelectedMembers());
+    }
 
     [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "Toggle vertical symmetry axis", "Toggle vertical symmetry axis", CanExecute = "PixiEditor.HasDocument", IconPath = "SymmetryVertical.png")]
     public void ToggleVerticalSymmetryAxis()
@@ -132,11 +195,9 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Document.CenterContent", "Center Content", "Center Content", CanExecute = "PixiEditor.HasDocument")]
     public void CenterContent()
     {
-        if(ActiveDocument is null || ActiveDocument.SelectedStructureMember == null)
+        if(ActiveDocument?.SelectedStructureMember == null)
             return;
         
-        List<Guid> layerGuids = new List<Guid>() { ActiveDocument.SelectedStructureMember.GuidValue };
-        layerGuids.AddRange(ActiveDocument.SoftSelectedStructureMembers.Select(x => x.GuidValue));
-        ActiveDocument.Operations.CenterContent(layerGuids);
+        ActiveDocument.Operations.CenterContent(ActiveDocument.GetSelectedMembers());
     }
 }

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

@@ -484,4 +484,11 @@ internal partial class DocumentViewModel : NotifyableObject
     public void InternalAddSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Add(member);
     public void InternalRemoveSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Remove(member);
     #endregion
+
+    public List<Guid> GetSelectedMembers()
+    {
+        List<Guid> layerGuids = new List<Guid>() { SelectedStructureMember.GuidValue };
+        layerGuids.AddRange( SoftSelectedStructureMembers.Select(x => x.GuidValue));
+        return layerGuids;
+    }
 }

+ 11 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -242,6 +242,17 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         
         member.MaskIsVisibleBindable = !member.MaskIsVisibleBindable;
     }
+    
+    [Command.Basic("PixiEditor.Layer.ApplyMask", "Apply mask", "Apply mask", CanExecute = "PixiEditor.Layer.ActiveLayerHasMask")]
+    public void ApplyMask()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        var member = doc?.SelectedStructureMember;
+        if (member is null || !member.HasMaskBindable)
+            return;
+        
+        doc!.Operations.ApplyMask(member);
+    }
 
     [Command.Basic("PixiEditor.Layer.ToggleVisible", "Toggle visibility", "Toggle visibility", CanExecute = "PixiEditor.HasDocument")]
     public void ToggleVisible()

+ 1 - 0
src/PixiEditor/Views/Dialogs/PalettesBrowser.xaml.cs

@@ -185,6 +185,7 @@ internal partial class PalettesBrowser : Window
     {
         await Dispatcher.InvokeAsync(async () =>
         {
+            SortedResults ??= new WpfObservableRangeCollection<Palette>();
             switch (refreshType)
             {
                 case RefreshType.All:

+ 30 - 21
src/PixiEditor/Views/MainWindow.xaml

@@ -256,21 +256,23 @@
                             IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
                             IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.VerticalSymmetryAxisEnabledBindable}"
                             Header="_Vertical Line Symmetry"/>
-                        <!--<Separator/>
-                    <MenuItem Header="_Rotate to right 90&#186;" Command="{Binding DocumentSubViewModel.RotateToRightCommand}">
-                        <MenuItem.CommandParameter>
-                            <sys:Double>90</sys:Double>
-                        </MenuItem.CommandParameter>
-                    </MenuItem>
-                    <MenuItem Header="_Rotate to left 90&#186;" Command="{Binding DocumentSubViewModel.RotateToRightCommand}">
-                        <MenuItem.CommandParameter>
-                            <sys:Double>-90</sys:Double>
-                        </MenuItem.CommandParameter>
-                    </MenuItem>
-                    <Separator/>
-                    <MenuItem Header="_Flip Horizontal" Command="{Binding DocumentSubViewModel.FlipCommand}" CommandParameter="Horizontal"/>
-                    <MenuItem Header="_Flip Vertical" Command="{Binding DocumentSubViewModel.FlipCommand}" CommandParameter="Vertical"/>
-                -->
+                        <Separator/>
+                        <MenuItem Header="_Rotation">
+                            <MenuItem Header="Rotate Image 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90Deg"/>
+                            <MenuItem Header="Rotate Image 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180Deg"/>
+                            <MenuItem Header="Rotate Image -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270Deg"/>
+                            
+                            <Separator/>
+                            <MenuItem Header="Rotate Selected Layers 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90DegLayers"/>
+                            <MenuItem Header="Rotate Selected Layers 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>
+                            <MenuItem Header="Rotate Selected Layers -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270DegLayers"/>
+                        </MenuItem>
+                        <MenuItem Header="_Flip">
+                            <MenuItem Header="Flip Image _Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipImageHorizontal"/>
+                            <MenuItem Header="Flip Image _Vertically" cmds:Menu.Command="PixiEditor.Document.FlipImageVertical"/>
+                            <MenuItem Header="Flip Selected Layers _Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
+                            <MenuItem Header="Flip Selected Layers _Vertically" cmds:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
+                        </MenuItem>
                     </MenuItem>
                     <MenuItem
                         Header="_View">
@@ -548,16 +550,16 @@
                                             Stylus.IsTouchFeedbackEnabled="False"
                                             Document="{Binding Document}">
                                             <usercontrols:Viewport.ContextMenu>
-                                                <ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
+                                                <ContextMenu DataContext="{Binding PlacementTarget.Document, RelativeSource={RelativeSource Self}}">
                                                     <ContextMenu.Template>
                                                         <ControlTemplate>
-                                                            <Border Height="120" Background="{StaticResource AccentColor}" BorderBrush="Black" BorderThickness="1" CornerRadius="5">
-                                                                <Grid>
+                                                            <Border Background="{StaticResource AccentColor}" BorderBrush="Black" BorderThickness="1" CornerRadius="5">
+                                                                <Grid Height="235">
                                                                     <Grid.ColumnDefinitions>
-                                                                        <ColumnDefinition Width="100"/>
                                                                         <ColumnDefinition Width="{Binding Palette, Converter={converters:PaletteItemsToWidthConverter}}"/>
+                                                                        <ColumnDefinition />
                                                                     </Grid.ColumnDefinitions>
-                                                                    <Border BorderThickness="0 0 1 0" BorderBrush="Black">
+                                                                    <Border Grid.Column="1" BorderThickness="0 0 1 0" BorderBrush="Black">
                                                                         <StackPanel Orientation="Vertical" Grid.Column="0">
                                                                             <MenuItem
 																		Header="_Select All"
@@ -575,9 +577,16 @@
                                                                             <MenuItem
 																		Header="_Paste"
 																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Paste" />
+                                                                            <Separator />
+                                                                            <MenuItem Header="Flip _Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
+                                                                            <MenuItem Header="Flip _Vertically" cmds:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
+                                                                            <Separator />
+                                                                            <MenuItem Header="Rotate 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90DegLayers"/>
+                                                                            <MenuItem Header="Rotate 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>
+                                                                            <MenuItem Header="Rotate -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270DegLayers"/>
                                                                         </StackPanel>
                                                                     </Border>
-                                                                    <ScrollViewer Margin="5" Grid.Column="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
+                                                                    <ScrollViewer Margin="5" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
                                                                         <ItemsControl ItemsSource="{Binding Palette}" AlternationCount="9999">
                                                                             <ItemsControl.ItemsPanel>
                                                                                 <ItemsPanelTemplate>

+ 18 - 0
src/PixiEditor/Views/UserControls/Layers/FolderControl.xaml.cs

@@ -1,6 +1,7 @@
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Media;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
@@ -20,12 +21,29 @@ internal partial class FolderControl : UserControl
     public static string? FolderControlDataName = typeof(FolderControl).FullName;
     public static string? LayerControlDataName = typeof(LayerControl).FullName;
 
+    public static readonly DependencyProperty ManagerProperty = DependencyProperty.Register(
+        nameof(Manager), typeof(LayersManager), typeof(FolderControl), new PropertyMetadata(default(LayersManager)));
+
+    public LayersManager Manager
+    {
+        get { return (LayersManager)GetValue(ManagerProperty); }
+        set { SetValue(ManagerProperty, value); }
+    }
+
     private readonly Brush? highlightColor;
+    
+    private MouseUpdateController mouseUpdateController;
 
     public FolderControl()
     {
         InitializeComponent();
         highlightColor = (Brush?)App.Current.Resources["SoftSelectedLayerColor"];
+        Loaded += OnLoaded;
+    }
+
+    private void OnLoaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController = new MouseUpdateController(this, Manager.FolderControl_MouseMove);
     }
 
     private void Grid_DragEnter(object sender, DragEventArgs e)

+ 1 - 0
src/PixiEditor/Views/UserControls/Layers/LayerControl.xaml

@@ -151,6 +151,7 @@
                     IsChecked="{Binding PlacementTarget.Tag.Layer.MaskIsVisibleBindable, RelativeSource={RelativeSource AncestorType=ContextMenu}}" 
                     IsEnabled="{Binding PlacementTarget.Tag.Layer.HasMaskBindable, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
                     Header="Enable mask"/>
+                <MenuItem Header="Apply mask" Command="{cmds:Command PixiEditor.Layer.ApplyMask}"/>
                 <Separator/>
                 <MenuItem Header="Merge selected" Command="{cmds:Command PixiEditor.Layer.MergeSelected}"/>
                 <MenuItem Header="Merge with above" Command="{cmds:Command PixiEditor.Layer.MergeWithAbove}"/>

+ 25 - 6
src/PixiEditor/Views/UserControls/Layers/LayerControl.xaml.cs

@@ -3,6 +3,7 @@ using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Media;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
@@ -21,12 +22,6 @@ internal partial class LayerControl : UserControl
         set => SetValue(LayerProperty, value);
     }
 
-    public LayerControl()
-    {
-        InitializeComponent();
-        highlightColor = (Brush?)App.Current.Resources["SoftSelectedLayerColor"];
-    }
-
     public static readonly DependencyProperty ControlButtonsVisibleProperty = DependencyProperty.Register(
         nameof(ControlButtonsVisible), typeof(Visibility), typeof(LayerControl), new PropertyMetadata(System.Windows.Visibility.Hidden));
 
@@ -39,6 +34,15 @@ internal partial class LayerControl : UserControl
     public static readonly DependencyProperty LayerColorProperty =
         DependencyProperty.Register(nameof(LayerColor), typeof(string), typeof(LayerControl), new PropertyMetadata("#00000000"));
 
+    public static readonly DependencyProperty ManagerProperty = DependencyProperty.Register(
+        nameof(Manager), typeof(LayersManager), typeof(LayerControl), new PropertyMetadata(default(LayersManager)));
+
+    public LayersManager Manager
+    {
+        get { return (LayersManager)GetValue(ManagerProperty); }
+        set { SetValue(ManagerProperty, value); }
+    }
+    
     public Visibility ControlButtonsVisible
     {
         get { return (Visibility)GetValue(ControlButtonsVisibleProperty); }
@@ -57,6 +61,21 @@ internal partial class LayerControl : UserControl
     public static readonly DependencyProperty MoveToFrontCommandProperty = DependencyProperty.Register(
         nameof(MoveToFrontCommand), typeof(RelayCommand), typeof(LayerControl), new PropertyMetadata(default(RelayCommand)));
 
+
+    private MouseUpdateController mouseUpdateController;
+    
+    public LayerControl()
+    {
+        InitializeComponent();
+        Loaded += LayerControl_Loaded;
+        highlightColor = (Brush?)App.Current.Resources["SoftSelectedLayerColor"];
+    }
+
+    private void LayerControl_Loaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController = new MouseUpdateController(this, Manager.LayerControl_MouseMove);
+    }
+
     public RelayCommand MoveToFrontCommand
     {
         get { return (RelayCommand)GetValue(MoveToFrontCommandProperty); }

+ 2 - 2
src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml

@@ -153,15 +153,15 @@
                     <HierarchicalDataTemplate DataType="{x:Type docVm:FolderViewModel}" ItemsSource="{Binding Children}">
                         <layerUserControls:FolderControl
                             Folder="{Binding}"
+                            Manager="{Binding ElementName=layersManager}"
                             MouseDown="FolderControl_MouseDown"
-                            MouseMove="FolderControl_MouseMove"
                             MouseUp="FolderControl_MouseUp"/>
                     </HierarchicalDataTemplate>
                     <DataTemplate DataType="{x:Type docVm:LayerViewModel}">
                         <layerUserControls:LayerControl
                             Layer="{Binding}"
+                            Manager="{Binding ElementName=layersManager}"
                             MouseDown="LayerControl_MouseDown"
-                            MouseMove="LayerControl_MouseMove"
                             MouseUp="LayerControl_MouseUp"/>
                     </DataTemplate>
                 </TreeView.Resources>

+ 3 - 3
src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml.cs

@@ -46,7 +46,7 @@ internal partial class LayersManager : UserControl
         }
     }
     
-    private void LayerControl_MouseMove(object? sender, System.Windows.Input.MouseEventArgs? e)
+    public void LayerControl_MouseMove(object? sender, System.Windows.Input.MouseEventArgs? e)
     {
         if (e is null)
             return;
@@ -82,8 +82,8 @@ internal partial class LayersManager : UserControl
             }
         }
     }
-    
-    private void FolderControl_MouseMove(object? sender, System.Windows.Input.MouseEventArgs? e)
+
+    public void FolderControl_MouseMove(object? sender, System.Windows.Input.MouseEventArgs? e)
     {
         if (e is null)
             return;

+ 7 - 2
src/PixiEditor/Views/UserControls/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs

@@ -6,6 +6,7 @@ using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using PixiEditor;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Views;
 using PixiEditor.Views.UserControls;
 using PixiEditor.Views.UserControls.Overlays.BrushShapeOverlay;
@@ -64,6 +65,8 @@ internal class BrushShapeOverlay : Control
     private Pen whitePen = new Pen(Brushes.LightGray, 1);
     private Point lastMousePos = new();
 
+    private MouseUpdateController mouseUpdateController;
+
     public BrushShapeOverlay()
     {
         Loaded += ControlLoaded;
@@ -74,14 +77,16 @@ internal class BrushShapeOverlay : Control
     {
         if (MouseEventSource is null)
             return;
-        MouseEventSource.MouseMove -= SourceMouseMove;
+        
+        mouseUpdateController.Dispose();
     }
 
     private void ControlLoaded(object sender, RoutedEventArgs e)
     {
         if (MouseEventSource is null)
             return;
-        MouseEventSource.MouseMove += SourceMouseMove;
+        
+        mouseUpdateController = new MouseUpdateController(MouseEventSource, SourceMouseMove);
     }
 
     private void SourceMouseMove(object sender, MouseEventArgs args)

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

@@ -4,6 +4,7 @@ using System.Windows.Input;
 using System.Windows.Media;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Views.UserControls.Overlays.TransformOverlay;
 
 namespace PixiEditor.Views.UserControls.Overlays.LineToolOverlay;
@@ -55,9 +56,17 @@ internal class LineToolOverlay : Control
             .ConvertFrom("M 0.50025839 0 0.4248062 0.12971572 0.34987079 0.25994821 h 0.1002584 V 0.45012906 H 0.25994831 V 0.34987066 L 0.12971577 0.42480604 0 0.5002582 0.12971577 0.57519373 0.25994831 0.65012926 V 0.5498709 H 0.45012919 V 0.74005175 H 0.34987079 L 0.42480619 0.87028439 0.50025839 1 0.57519399 0.87028439 0.65012959 0.74005175 H 0.54987119 V 0.5498709 H 0.74005211 V 0.65012926 L 0.87028423 0.57519358 1 0.5002582 0.87028423 0.42480604 0.74005169 0.34987066 v 0.1002584 H 0.54987077 V 0.25994821 h 0.1002584 L 0.5751938 0.12971572 Z"),
     };
 
+    private MouseUpdateController mouseUpdateController;
+
     public LineToolOverlay()
     {
         Cursor = Cursors.Arrow;
+        Loaded += OnLoaded;
+    }
+
+    private void OnLoaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController = new MouseUpdateController(this, MouseMoved);
     }
 
     private static void OnZoomboxScaleChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
@@ -108,10 +117,10 @@ internal class LineToolOverlay : Control
         CaptureMouse();
     }
 
-    protected override void OnMouseMove(MouseEventArgs e)
+    protected void MouseMoved(object sender, MouseEventArgs e)
     {
-        base.OnMouseMove(e);
-        e.Handled = true;
+        /*base.OnMouseMove(e);
+        e.Handled = true;*/
 
         VecD pos = TransformHelper.ToVecD(e.GetPosition(this));
         if (capturedAnchor == LineToolOverlayAnchor.Start)

+ 15 - 2
src/PixiEditor/Views/UserControls/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -7,6 +7,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Views;
 using PixiEditor.Views.UserControls;
 using PixiEditor.Views.UserControls.Overlays.SymmetryOverlay;
@@ -116,6 +117,18 @@ internal class SymmetryOverlay : Control
     private int horizontalAxisY;
     private int verticalAxisX;
 
+    private MouseUpdateController mouseUpdateController;
+
+    public SymmetryOverlay()
+    {
+        Loaded += OnLoaded;
+    }
+
+    private void OnLoaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController = new MouseUpdateController(this, MouseMoved);
+    }
+
     protected override void OnRender(DrawingContext drawingContext)
     {
         base.OnRender(drawingContext);
@@ -358,9 +371,9 @@ internal class SymmetryOverlay : Control
         e.Handled = true;
     }
 
-    protected override void OnMouseMove(MouseEventArgs e)
+    protected void MouseMoved(object sender, MouseEventArgs e)
     {
-        base.OnMouseMove(e);
+        /*base.OnMouseMove(e);*/
 
         var pos = ToVecD(e.GetPosition(this));
         UpdateHovered(IsTouchingHandle(pos));

+ 6 - 2
src/PixiEditor/Views/UserControls/PreviewWindow.xaml.cs

@@ -4,6 +4,7 @@ using System.Windows.Input;
 using System.Windows.Media;
 using BackendColor = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Models.Controllers;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.Models.Enums;
 
@@ -46,12 +47,15 @@ internal partial class PreviewWindow : UserControl
         get => (Color)GetValue(PrimaryColorProperty);
         set => SetValue(PrimaryColorProperty, value);
     }
+    
+    private MouseUpdateController mouseUpdateController;
 
     public PreviewWindow()
     {
         InitializeComponent();
-
-        imageGrid.MouseMove += ImageGrid_MouseMove;
+        
+        mouseUpdateController = new MouseUpdateController(imageGrid, ImageGrid_MouseMove);
+        
         imageGrid.MouseRightButtonDown += ImageGrid_MouseRightButtonDown;
         imageGrid.MouseEnter += ImageGrid_MouseEnter;
         imageGrid.MouseLeave += ImageGrid_MouseLeave;

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

@@ -27,7 +27,6 @@
     <Grid 
         x:Name="mainGrid"
         MouseDown="Image_MouseDown"
-        MouseMove="Image_MouseMove"
         MouseUp="Image_MouseUp"
         PreviewMouseDown="Grid_PreviewMouseDown">
         <i:Interaction.Triggers>

+ 5 - 0
src/PixiEditor/Views/UserControls/Viewport.xaml.cs

@@ -7,6 +7,7 @@ using System.Windows.Media.Imaging;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Events;
 using PixiEditor.Models.Position;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -282,6 +283,8 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     public Guid GuidValue { get; } = Guid.NewGuid();
 
+    private MouseUpdateController mouseUpdateController;
+
     public Viewport()
     {
         InitializeComponent();
@@ -292,6 +295,8 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         MainImage!.Loaded += OnImageLoaded;
         Loaded += OnLoad;
         Unloaded += OnUnload;
+        
+        mouseUpdateController = new MouseUpdateController(this, Image_MouseMove);
     }
 
     public Image? MainImage => (Image?)((Grid?)((Border?)zoombox.AdditionalContent)?.Child)?.Children[1];