Browse Source

Merge pull request #434 from PixiEditor/flip-canvas

Flip canvas n stuff
Krzysztof Krysiński 2 years ago
parent
commit
649f843910

+ 1 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -156,7 +156,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
                 preciseBounds ??= globalChunkBounds;
                 preciseBounds ??= globalChunkBounds;
                 preciseBounds = preciseBounds.Value.Union(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;
             return preciseBounds;
         }
         }

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

@@ -262,7 +262,7 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         return false;
         return false;
     }
     }
     
     
-    public List<Guid> ExtractLayers(List<Guid> members)
+    public List<Guid> ExtractLayers(IList<Guid> members)
     {
     {
         var result = new List<Guid>();
         var result = new List<Guid>();
         foreach (var member in members)
         foreach (var member in members)
@@ -282,7 +282,7 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         return result;
         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)
         foreach (var member in folder.Children)
         {
         {
@@ -295,6 +295,5 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
                 ExtractLayers(childFolder, list);
                 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
 internal class CenterContent_Change : Change
 {
 {
     private VecI _oldOffset;
     private VecI _oldOffset;
-    private List<Guid> _affectedLayers;
+    private List<Guid> affectedLayers;
     private Dictionary<Guid, CommittedChunkStorage>? originalLayerChunks;
     private Dictionary<Guid, CommittedChunkStorage>? originalLayerChunks;
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
     public CenterContent_Change(List<Guid> layers)
     public CenterContent_Change(List<Guid> layers)
     {
     {
-        _affectedLayers = layers;
+        affectedLayers = layers;
     }
     }
     
     
     public override bool InitializeAndValidate(Document target)
     public override bool InitializeAndValidate(Document target)
     {
     {
-        if (_affectedLayers.Count == 0)
+        if (affectedLayers.Count == 0)
         {
         {
             return false;
             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;
             if (!target.HasMember(layer)) return false;
         }
         }
@@ -38,7 +38,7 @@ internal class CenterContent_Change : Change
     {
     {
         VecI currentCenter = new VecI(0, 0);
         VecI currentCenter = new VecI(0, 0);
         RectI? currentBounds = null;
         RectI? currentBounds = null;
-        foreach (var layerGuid in _affectedLayers)
+        foreach (var layerGuid in affectedLayers)
         {
         {
             Layer layer = document.FindMemberOrThrow<Layer>(layerGuid);
             Layer layer = document.FindMemberOrThrow<Layer>(layerGuid);
             RectI? tightBounds = layer.LayerImage.FindPreciseCommittedBounds();
             RectI? tightBounds = layer.LayerImage.FindPreciseCommittedBounds();
@@ -64,7 +64,7 @@ internal class CenterContent_Change : Change
         List<IChangeInfo> changes = new List<IChangeInfo>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
         originalLayerChunks = new Dictionary<Guid, CommittedChunkStorage>();
         originalLayerChunks = new Dictionary<Guid, CommittedChunkStorage>();
         
         
-        foreach (var layerGuid in _affectedLayers)
+        foreach (var layerGuid in affectedLayers)
         {
         {
             Layer layer = target.FindMemberOrThrow<Layer>(layerGuid);
             Layer layer = target.FindMemberOrThrow<Layer>(layerGuid);
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, false, shift);
             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)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         List<IChangeInfo> changes = new List<IChangeInfo>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
-        foreach (var layerGuid in _affectedLayers)
+        foreach (var layerGuid in affectedLayers)
         {
         {
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             CommittedChunkStorage? originalChunks = originalLayerChunks?[layerGuid];
             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;
         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.EnqueueResize(size);
         img.EnqueueClear();
         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)
+        };
+    }
+}

+ 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>
 /// </remarks>
 public struct Matrix3X3 : IEquatable<Matrix3X3>
 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 };
     public static readonly Matrix3X3 Identity = new() { ScaleX = 1f, ScaleY = 1f, Persp2 = 1f };
     
     

+ 21 - 0
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -3,6 +3,7 @@ using System.Windows.Interop;
 using System.Windows.Shapes;
 using System.Windows.Shapes;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.ChangeInfos.Root.ReferenceLayerChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos.Root.ReferenceLayerChangeInfos;
@@ -289,6 +290,26 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action());
         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)
     public void CenterContent(IReadOnlyList<Guid> structureMembers)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         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
-}

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

@@ -1,8 +1,10 @@
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
 using System.Windows.Input;
 using System.Windows.Input;
+using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.Events;
 using PixiEditor.Models.Events;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
@@ -55,6 +57,67 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         
         
         ActiveDocument?.Operations.ClipCanvas();
         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")]
     [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "Toggle vertical symmetry axis", "Toggle vertical symmetry axis", CanExecute = "PixiEditor.HasDocument", IconPath = "SymmetryVertical.png")]
     public void ToggleVerticalSymmetryAxis()
     public void ToggleVerticalSymmetryAxis()
@@ -132,11 +195,9 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Document.CenterContent", "Center Content", "Center Content", CanExecute = "PixiEditor.HasDocument")]
     [Command.Basic("PixiEditor.Document.CenterContent", "Center Content", "Center Content", CanExecute = "PixiEditor.HasDocument")]
     public void CenterContent()
     public void CenterContent()
     {
     {
-        if(ActiveDocument is null || ActiveDocument.SelectedStructureMember == null)
+        if(ActiveDocument?.SelectedStructureMember == null)
             return;
             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 InternalAddSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Add(member);
     public void InternalRemoveSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Remove(member);
     public void InternalRemoveSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Remove(member);
     #endregion
     #endregion
+
+    public List<Guid> GetSelectedMembers()
+    {
+        List<Guid> layerGuids = new List<Guid>() { SelectedStructureMember.GuidValue };
+        layerGuids.AddRange( SoftSelectedStructureMembers.Select(x => x.GuidValue));
+        return layerGuids;
+    }
 }
 }

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

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

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

@@ -256,21 +256,23 @@
                             IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
                             IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
                             IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.VerticalSymmetryAxisEnabledBindable}"
                             IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.VerticalSymmetryAxisEnabledBindable}"
                             Header="_Vertical Line Symmetry"/>
                             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>
                     <MenuItem
                     <MenuItem
                         Header="_View">
                         Header="_View">
@@ -548,16 +550,16 @@
                                             Stylus.IsTouchFeedbackEnabled="False"
                                             Stylus.IsTouchFeedbackEnabled="False"
                                             Document="{Binding Document}">
                                             Document="{Binding Document}">
                                             <usercontrols:Viewport.ContextMenu>
                                             <usercontrols:Viewport.ContextMenu>
-                                                <ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
+                                                <ContextMenu DataContext="{Binding PlacementTarget.Document, RelativeSource={RelativeSource Self}}">
                                                     <ContextMenu.Template>
                                                     <ContextMenu.Template>
                                                         <ControlTemplate>
                                                         <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>
                                                                     <Grid.ColumnDefinitions>
-                                                                        <ColumnDefinition Width="100"/>
                                                                         <ColumnDefinition Width="{Binding Palette, Converter={converters:PaletteItemsToWidthConverter}}"/>
                                                                         <ColumnDefinition Width="{Binding Palette, Converter={converters:PaletteItemsToWidthConverter}}"/>
+                                                                        <ColumnDefinition />
                                                                     </Grid.ColumnDefinitions>
                                                                     </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">
                                                                         <StackPanel Orientation="Vertical" Grid.Column="0">
                                                                             <MenuItem
                                                                             <MenuItem
 																		Header="_Select All"
 																		Header="_Select All"
@@ -575,9 +577,16 @@
                                                                             <MenuItem
                                                                             <MenuItem
 																		Header="_Paste"
 																		Header="_Paste"
 																		cmds:ContextMenu.Command="PixiEditor.Clipboard.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>
                                                                         </StackPanel>
                                                                     </Border>
                                                                     </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 ItemsSource="{Binding Palette}" AlternationCount="9999">
                                                                             <ItemsControl.ItemsPanel>
                                                                             <ItemsControl.ItemsPanel>
                                                                                 <ItemsPanelTemplate>
                                                                                 <ItemsPanelTemplate>