Browse Source

Transform selected area

Equbuxu 3 years ago
parent
commit
4f617a86ef

+ 4 - 4
src/ChunkyImageLib/ChunkyImage.cs

@@ -330,12 +330,12 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
     /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
     /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
     /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
     /// </summary>
     /// </summary>
-    public void EnqueueDrawImage(ShapeCorners corners, Surface image, bool copyImage = true)
+    public void EnqueueDrawImage(ShapeCorners corners, Surface image, SKPaint? paint = null, bool copyImage = true)
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
             ThrowIfDisposed();
             ThrowIfDisposed();
-            ImageOperation operation = new(corners, image, copyImage);
+            ImageOperation operation = new(corners, image, paint, copyImage);
             EnqueueOperation(operation);
             EnqueueOperation(operation);
         }
         }
     }
     }
@@ -346,12 +346,12 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
     /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
     /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
     /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
     /// </summary>
     /// </summary>
-    public void EnqueueDrawImage(VecI pos, Surface image, bool copyImage = true)
+    public void EnqueueDrawImage(VecI pos, Surface image, SKPaint? paint = null, bool copyImage = true)
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
             ThrowIfDisposed();
             ThrowIfDisposed();
-            ImageOperation operation = new(pos, image, copyImage);
+            ImageOperation operation = new(pos, image, paint, copyImage);
             EnqueueOperation(operation);
             EnqueueOperation(operation);
         }
         }
     }
     }

+ 25 - 0
src/ChunkyImageLib/ChunkyImageEx.cs

@@ -0,0 +1,25 @@
+using ChunkyImageLib.DataHolders;
+using ChunkyImageLib.Operations;
+using SkiaSharp;
+
+namespace ChunkyImageLib;
+public static class IReadOnlyChunkyImageEx
+{
+    public static void DrawMostUpToDateRegionOn
+        (this IReadOnlyChunkyImage image, SKRectI fullResRegion, ChunkResolution resolution, SKSurface surface, VecI pos, SKPaint? paint = null)
+    {
+        VecI chunkTopLeft = OperationHelper.GetChunkPos(fullResRegion.Location, ChunkyImage.FullChunkSize);
+        VecI chunkBotRigth = OperationHelper.GetChunkPos(fullResRegion.Location + fullResRegion.Size, ChunkyImage.FullChunkSize);
+        VecI offsetFullRes = (chunkTopLeft * ChunkyImage.FullChunkSize) - (VecI)fullResRegion.Location;
+        VecI offsetTargetRes = (VecI)(offsetFullRes * resolution.Multiplier());
+
+        for (int j = chunkTopLeft.Y; j <= chunkBotRigth.Y; j++)
+        {
+            for (int i = chunkTopLeft.X; i <= chunkBotRigth.X; i++)
+            {
+                var chunkPos = new VecI(i, j);
+                image.DrawMostUpToDateChunkOn(chunkPos, resolution, surface, offsetTargetRes + chunkPos * resolution.PixelSize() + pos, paint);
+            }
+        }
+    }
+}

+ 1 - 1
src/ChunkyImageLib/CommittedChunkStorage.cs

@@ -33,7 +33,7 @@ public class CommittedChunkStorage : IDisposable
             if (chunk is null)
             if (chunk is null)
                 image.EnqueueClearRegion(pos * ChunkPool.FullChunkSize, new(ChunkPool.FullChunkSize, ChunkPool.FullChunkSize));
                 image.EnqueueClearRegion(pos * ChunkPool.FullChunkSize, new(ChunkPool.FullChunkSize, ChunkPool.FullChunkSize));
             else
             else
-                image.EnqueueDrawImage(pos * ChunkPool.FullChunkSize, chunk.Surface);
+                image.EnqueueDrawImage(pos * ChunkPool.FullChunkSize, chunk.Surface, ReplacingPaint);
         }
         }
     }
     }
 
 

+ 8 - 0
src/ChunkyImageLib/DataHolders/VecD.cs

@@ -185,6 +185,14 @@ public struct VecD
     {
     {
         return new VecD(size.Width, size.Height);
         return new VecD(size.Width, size.Height);
     }
     }
+    public static implicit operator VecD(SKPointI point)
+    {
+        return new VecD(point.X, point.Y);
+    }
+    public static implicit operator VecD(SKSizeI size)
+    {
+        return new VecD(size.Width, size.Height);
+    }
     public static explicit operator VecI(VecD vec)
     public static explicit operator VecI(VecD vec)
     {
     {
         return new VecI((int)vec.X, (int)vec.Y);
         return new VecI((int)vec.X, (int)vec.Y);

+ 16 - 1
src/ChunkyImageLib/DataHolders/VecI.cs

@@ -99,7 +99,22 @@ public struct VecI
     {
     {
         return !(a.X == b.X && a.Y == b.Y);
         return !(a.X == b.X && a.Y == b.Y);
     }
     }
-
+    public static explicit operator VecI(SKPoint point)
+    {
+        return new VecI((int)point.X, (int)point.Y);
+    }
+    public static explicit operator VecI(SKSize size)
+    {
+        return new VecI((int)size.Width, (int)size.Height);
+    }
+    public static implicit operator VecI(SKPointI point)
+    {
+        return new VecI(point.X, point.Y);
+    }
+    public static implicit operator VecI(SKSizeI size)
+    {
+        return new VecI(size.Width, size.Height);
+    }
     public static implicit operator VecD(VecI vec)
     public static implicit operator VecD(VecI vec)
     {
     {
         return new VecD(vec.X, vec.Y);
         return new VecD(vec.X, vec.Y);

+ 15 - 8
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -9,12 +9,15 @@ internal class ImageOperation : IDrawOperation
     private ShapeCorners corners;
     private ShapeCorners corners;
     private Surface toPaint;
     private Surface toPaint;
     private bool imageWasCopied = false;
     private bool imageWasCopied = false;
-    private static SKPaint ReplacingPaint = new() { BlendMode = SKBlendMode.Src };
+    private readonly SKPaint? customPaint;
 
 
     public bool IgnoreEmptyChunks => false;
     public bool IgnoreEmptyChunks => false;
 
 
-    public ImageOperation(VecI pos, Surface image, bool copyImage = true)
+    public ImageOperation(VecI pos, Surface image, SKPaint? paint = null, bool copyImage = true)
     {
     {
+        if (paint is not null)
+            customPaint = paint.Clone();
+
         corners = new()
         corners = new()
         {
         {
             TopLeft = pos,
             TopLeft = pos,
@@ -34,8 +37,11 @@ internal class ImageOperation : IDrawOperation
         imageWasCopied = copyImage;
         imageWasCopied = copyImage;
     }
     }
 
 
-    public ImageOperation(ShapeCorners corners, Surface image, bool copyImage = true)
+    public ImageOperation(ShapeCorners corners, Surface image, SKPaint? paint = null, bool copyImage = true)
     {
     {
+        if (paint is not null)
+            customPaint = paint.Clone();
+
         this.corners = corners;
         this.corners = corners;
         transformMatrix = OperationHelper.CreateMatrixFromPoints(corners, image.Size);
         transformMatrix = OperationHelper.CreateMatrixFromPoints(corners, image.Size);
 
 
@@ -57,7 +63,7 @@ internal class ImageOperation : IDrawOperation
 
 
         chunk.Surface.SkiaSurface.Canvas.Save();
         chunk.Surface.SkiaSurface.Canvas.Save();
         chunk.Surface.SkiaSurface.Canvas.SetMatrix(finalMatrix);
         chunk.Surface.SkiaSurface.Canvas.SetMatrix(finalMatrix);
-        chunk.Surface.SkiaSurface.Canvas.DrawSurface(toPaint.SkiaSurface, 0, 0, ReplacingPaint);
+        chunk.Surface.SkiaSurface.Canvas.DrawSurface(toPaint.SkiaSurface, 0, 0, customPaint);
         chunk.Surface.SkiaSurface.Canvas.Restore();
         chunk.Surface.SkiaSurface.Canvas.Restore();
     }
     }
 
 
@@ -70,19 +76,20 @@ internal class ImageOperation : IDrawOperation
     {
     {
         if (imageWasCopied)
         if (imageWasCopied)
             toPaint.Dispose();
             toPaint.Dispose();
+        customPaint?.Dispose();
     }
     }
 
 
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
     {
     {
         if (verAxisX is not null && horAxisY is not null)
         if (verAxisX is not null && horAxisY is not null)
             return new ImageOperation
             return new ImageOperation
-                (corners.AsMirroredAcrossVerAxis((int)verAxisX).AsMirroredAcrossHorAxis((int)horAxisY), toPaint, imageWasCopied);
+                (corners.AsMirroredAcrossVerAxis((int)verAxisX).AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
         if (verAxisX is not null)
         if (verAxisX is not null)
             return new ImageOperation
             return new ImageOperation
-                (corners.AsMirroredAcrossVerAxis((int)verAxisX), toPaint, imageWasCopied);
+                (corners.AsMirroredAcrossVerAxis((int)verAxisX), toPaint, customPaint, imageWasCopied);
         if (horAxisY is not null)
         if (horAxisY is not null)
             return new ImageOperation
             return new ImageOperation
-                (corners.AsMirroredAcrossHorAxis((int)horAxisY), toPaint, imageWasCopied);
-        return new ImageOperation(corners, toPaint, imageWasCopied);
+                (corners.AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
+        return new ImageOperation(corners, toPaint, customPaint, imageWasCopied);
     }
     }
 }
 }

+ 62 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs

@@ -0,0 +1,62 @@
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+internal class ClearSelectedArea_Change : Change
+{
+    private readonly Guid memberGuid;
+    private readonly bool drawOnMask;
+    private CommittedChunkStorage? savedChunks;
+
+    [GenerateMakeChangeAction]
+    public ClearSelectedArea_Change(Guid memberGuid, bool drawOnMask)
+    {
+        this.memberGuid = memberGuid;
+        this.drawOnMask = drawOnMask;
+    }
+
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        if (target.Selection.SelectionPath.IsEmpty)
+            return new Error();
+        if (!DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask))
+            return new Error();
+        return new Success();
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
+    {
+        if (savedChunks is not null)
+            throw new InvalidOperationException("trying to save chunks while they are already saved");
+
+        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
+
+        SKRect bounds = target.Selection.SelectionPath.Bounds;
+        bounds.Intersect(SKRect.Create(0, 0, target.Size.X, target.Size.Y));
+        VecI pixelTopLeft = (VecI)((VecD)bounds.Location).Floor();
+        VecI pixelSize = (VecI)((VecD)bounds.Location + (VecD)bounds.Size - pixelTopLeft).Ceiling();
+
+        image.SetClippingPath(target.Selection.SelectionPath);
+        image.EnqueueClearRegion(pixelTopLeft, pixelSize);
+        var affChunks = image.FindAffectedChunks();
+        savedChunks = new(image, affChunks);
+        image.CommitChanges();
+        ignoreInUndo = false;
+        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
+        savedChunks!.ApplyChunksToImage(image);
+        var affChunks = image.FindAffectedChunks();
+        image.CommitChanges();
+        savedChunks.Dispose();
+        savedChunks = null;
+        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+    }
+
+    public override void Dispose()
+    {
+        savedChunks?.Dispose();
+    }
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkStorage.cs

@@ -32,7 +32,7 @@ internal class FloodFillChunkStorage : IDisposable
     {
     {
         foreach (var (pos, chunk) in acquiredChunks)
         foreach (var (pos, chunk) in acquiredChunks)
         {
         {
-            chunkyImage.EnqueueDrawImage(pos * ChunkResolution.Full.PixelSize(), chunk.Surface, false);
+            chunkyImage.EnqueueDrawImage(pos * ChunkResolution.Full.PixelSize(), chunk.Surface, ReplacingPaint, false);
         }
         }
         var affected = chunkyImage.FindAffectedChunks();
         var affected = chunkyImage.FindAffectedChunks();
         var affectedChunkStorage = new CommittedChunkStorage(chunkyImage, affected);
         var affectedChunkStorage = new CommittedChunkStorage(chunkyImage, affected);

+ 5 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs

@@ -1,4 +1,6 @@
-namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 internal class PasteImage_UpdateableChange : UpdateableChange
 internal class PasteImage_UpdateableChange : UpdateableChange
 {
 {
     private ShapeCorners corners;
     private ShapeCorners corners;
@@ -6,6 +8,7 @@ internal class PasteImage_UpdateableChange : UpdateableChange
     private readonly bool drawOnMask;
     private readonly bool drawOnMask;
     private readonly Surface imageToPaste;
     private readonly Surface imageToPaste;
     private CommittedChunkStorage? savedChunks;
     private CommittedChunkStorage? savedChunks;
+    private static SKPaint RegularPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
 
 
     private bool hasEnqueudImage = false;
     private bool hasEnqueudImage = false;
 
 
@@ -37,7 +40,7 @@ internal class PasteImage_UpdateableChange : UpdateableChange
 
 
         targetImage.CancelChanges();
         targetImage.CancelChanges();
         DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, targetImage, memberGuid, drawOnMask);
         DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, targetImage, memberGuid, drawOnMask);
-        targetImage.EnqueueDrawImage(corners, imageToPaste, false);
+        targetImage.EnqueueDrawImage(corners, imageToPaste, RegularPaint, false);
         hasEnqueudImage = true;
         hasEnqueudImage = true;
 
 
         var affectedChunks = targetImage.FindAffectedChunks();
         var affectedChunks = targetImage.FindAffectedChunks();

+ 41 - 0
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -9,6 +9,7 @@ using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Microsoft.Win32;
 using Microsoft.Win32;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Actions.Undo;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditorPrototype.CustomControls.SymmetryOverlay;
 using PixiEditorPrototype.CustomControls.SymmetryOverlay;
 using PixiEditorPrototype.Models;
 using PixiEditorPrototype.Models;
@@ -68,6 +69,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
     public RelayCommand? EndDragSymmetryCommand { get; }
     public RelayCommand? EndDragSymmetryCommand { get; }
     public RelayCommand? ClipToMemberBelowCommand { get; }
     public RelayCommand? ClipToMemberBelowCommand { get; }
     public RelayCommand? TransformSelectionPathCommand { get; }
     public RelayCommand? TransformSelectionPathCommand { get; }
+    public RelayCommand? TransformSelectedAreaCommand { get; }
 
 
     public int Width => Helpers.Tracker.Document.Size.X;
     public int Width => Helpers.Tracker.Document.Size.X;
     public int Height => Helpers.Tracker.Document.Size.Y;
     public int Height => Helpers.Tracker.Document.Size.Y;
@@ -123,6 +125,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
         ClipToMemberBelowCommand = new RelayCommand(ClipToMemberBelow);
         ClipToMemberBelowCommand = new RelayCommand(ClipToMemberBelow);
         ApplyMaskCommand = new RelayCommand(ApplyMask);
         ApplyMaskCommand = new RelayCommand(ApplyMask);
         TransformSelectionPathCommand = new RelayCommand(TransformSelectionPath);
         TransformSelectionPathCommand = new RelayCommand(TransformSelectionPath);
+        TransformSelectedAreaCommand = new RelayCommand(TransformSelectedArea);
 
 
         foreach (var bitmap in Bitmaps)
         foreach (var bitmap in Bitmaps)
         {
         {
@@ -136,6 +139,44 @@ internal class DocumentViewModel : INotifyPropertyChanged
             (new CreateStructureMember_Action(StructureRoot.GuidValue, Guid.NewGuid(), 0, StructureMemberType.Layer));
             (new CreateStructureMember_Action(StructureRoot.GuidValue, Guid.NewGuid(), 0, StructureMemberType.Layer));
     }
     }
 
 
+    private void TransformSelectedArea(object? obj)
+    {
+        if (updateableChangeActive || SelectedStructureMember is not LayerViewModel layer || SelectionPath.IsEmpty)
+            return;
+        IReadOnlyChunkyImage? layerImage = (Helpers.Tracker.Document.FindMember(layer.GuidValue) as IReadOnlyLayer)?.LayerImage;
+        if (layerImage is null)
+            return;
+
+        // find area location and size
+        using SKPath path = SelectionPath;
+        var bounds = path.TightBounds;
+        bounds.Intersect(SKRect.Create(0, 0, Width, Height));
+        VecI pixelTopLeft = (VecI)((VecD)bounds.Location).Floor();
+        VecI pixelSize = (VecI)((VecD)bounds.Location + (VecD)bounds.Size - pixelTopLeft).Ceiling();
+
+        // extract surface to be transformed
+        path.Transform(SKMatrix.CreateTranslation(-pixelTopLeft.X, -pixelTopLeft.Y));
+        Surface surface = new(pixelSize);
+        surface.SkiaSurface.Canvas.Save();
+        surface.SkiaSurface.Canvas.ClipPath(path);
+        layerImage.DrawMostUpToDateRegionOn(SKRectI.Create(pixelTopLeft, pixelSize), ChunkResolution.Full, surface.SkiaSurface, VecI.Zero);
+        surface.SkiaSurface.Canvas.Restore();
+
+        // clear area
+        if (!owner.KeepOriginalImageOnTransform)
+        {
+            Helpers.ActionAccumulator.AddActions(new ClearSelectedArea_Action(layer.GuidValue, false));
+        }
+        Helpers.ActionAccumulator.AddActions(new ClearSelection_Action());
+
+        // initiate transform using paste image logic
+        pastedImage = surface;
+        pastingImage = true;
+        ShapeCorners corners = new(pixelTopLeft, pixelSize);
+        Helpers.ActionAccumulator.AddActions(new PasteImage_Action(pastedImage, corners, layer.GuidValue, false));
+        TransformViewModel.ShowFreeTransform(corners);
+    }
+
     private void ApplyMask(object? obj)
     private void ApplyMask(object? obj)
     {
     {
         if (updateableChangeActive || SelectedStructureMember is not LayerViewModel layer || !layer.HasMask)
         if (updateableChangeActive || SelectedStructureMember is not LayerViewModel layer || !layer.HasMask)

+ 1 - 0
src/PixiEditorPrototype/ViewModels/ViewModelMain.cs

@@ -22,6 +22,7 @@ internal class ViewModelMain : INotifyPropertyChanged
     public RelayCommand SetSelectionModeCommand { get; }
     public RelayCommand SetSelectionModeCommand { get; }
 
 
     public Color SelectedColor { get; set; } = Colors.Black;
     public Color SelectedColor { get; set; } = Colors.Black;
+    public bool KeepOriginalImageOnTransform { get; set; }
 
 
     public event PropertyChangedEventHandler? PropertyChanged;
     public event PropertyChangedEventHandler? PropertyChanged;
 
 

+ 9 - 5
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -184,6 +184,7 @@
                     <Button Width="50" Margin="5" Command="{Binding ActiveDocument.RedoCommand}">Redo</Button>
                     <Button Width="50" Margin="5" Command="{Binding ActiveDocument.RedoCommand}">Redo</Button>
                     <Button Width="100" Margin="5" Command="{Binding ActiveDocument.ClearSelectionCommand}">Clear selection</Button>
                     <Button Width="100" Margin="5" Command="{Binding ActiveDocument.ClearSelectionCommand}">Clear selection</Button>
                     <Button Width="110" Margin="5" Command="{Binding ActiveDocument.TransformSelectionPathCommand}">Transform sel. path</Button>
                     <Button Width="110" Margin="5" Command="{Binding ActiveDocument.TransformSelectionPathCommand}">Transform sel. path</Button>
+                    <Button Width="110" Margin="5" Command="{Binding ActiveDocument.TransformSelectedAreaCommand}">Transform sel. area</Button>
                     <ComboBox Width="70" Height="20" Margin="5" SelectedIndex="0" x:Name="selectionModeComboBox">
                     <ComboBox Width="70" Height="20" Margin="5" SelectedIndex="0" x:Name="selectionModeComboBox">
                         <ComboBoxItem Tag="{x:Static chen:SelectionMode.New}">New</ComboBoxItem>
                         <ComboBoxItem Tag="{x:Static chen:SelectionMode.New}">New</ComboBoxItem>
                         <ComboBoxItem Tag="{x:Static chen:SelectionMode.Add}">Add</ComboBoxItem>
                         <ComboBoxItem Tag="{x:Static chen:SelectionMode.Add}">Add</ComboBoxItem>
@@ -208,17 +209,20 @@
         </Border>
         </Border>
         <Border BorderThickness="1" Background="White" BorderBrush="Black" DockPanel.Dock="Left" Margin="5">
         <Border BorderThickness="1" Background="White" BorderBrush="Black" DockPanel.Dock="Left" Margin="5">
             <StackPanel Orientation="Vertical" Background="White">
             <StackPanel Orientation="Vertical" Background="White">
-                <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Rectangle}">Rect</Button>
-                <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Select}">Select</Button>
-                <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Lasso}">Lasso</Button>
-                <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.ShiftLayer}">Shift Layer</Button>
-                <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.FloodFill}">Fill</Button>
+                <Button Width="70" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Rectangle}">Rect</Button>
+                <Button Width="70" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Select}">Select</Button>
+                <Button Width="70" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Lasso}">Lasso</Button>
+                <Button Width="70" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.ShiftLayer}">Shift Layer</Button>
+                <Button Width="70" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.FloodFill}">Fill</Button>
                 <colorpicker:PortableColorPicker Margin="5" SelectedColor="{Binding SelectedColor, Mode=TwoWay}" Width="30" Height="30"/>
                 <colorpicker:PortableColorPicker Margin="5" SelectedColor="{Binding SelectedColor, Mode=TwoWay}" Width="30" Height="30"/>
                 <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding NormalZoombox, Mode=OneWayToSource}">Normal</RadioButton>
                 <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding NormalZoombox, Mode=OneWayToSource}">Normal</RadioButton>
                 <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding MoveZoombox, Mode=OneWayToSource}">Move</RadioButton>
                 <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding MoveZoombox, Mode=OneWayToSource}">Move</RadioButton>
                 <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding RotateZoombox, Mode=OneWayToSource}">Rotate</RadioButton>
                 <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding RotateZoombox, Mode=OneWayToSource}">Rotate</RadioButton>
                 <CheckBox x:Name="flipXCheckbox" Margin="5, 0">Flip X</CheckBox>
                 <CheckBox x:Name="flipXCheckbox" Margin="5, 0">Flip X</CheckBox>
                 <CheckBox x:Name="flipYCheckbox" Margin="5, 0">Flip Y</CheckBox>
                 <CheckBox x:Name="flipYCheckbox" Margin="5, 0">Flip Y</CheckBox>
+                <CheckBox 
+                    x:Name="keepOriginalImageCheckbox" Margin="5, 0"
+                    IsChecked="{Binding KeepOriginalImageOnTransform}">Keep area</CheckBox>
                 <CheckBox 
                 <CheckBox 
                     x:Name="horizontalSymmetryCheckbox" Margin="5,0" 
                     x:Name="horizontalSymmetryCheckbox" Margin="5,0" 
                     IsChecked="{Binding ActiveDocument.HorizontalSymmetryAxisEnabled}">Hor Sym</CheckBox>
                     IsChecked="{Binding ActiveDocument.HorizontalSymmetryAxisEnabled}">Hor Sym</CheckBox>

+ 3 - 3
src/README.md

@@ -104,13 +104,13 @@ Decouples the state of a document from the UI.
         - [ ] Magic wand
         - [ ] Magic wand
         - [x] Lasso
         - [x] Lasso
         - [x] Shift layer image
         - [x] Shift layer image
-        - [ ] Move/Rotate selection path
-        - [ ] Transform selected area
-        - [ ] Fill selection (fill with transparent = delete)
+        - [x] Transform selection path
+        - [x] Clear selected area
         - [x] Clip to selection
         - [x] Clip to selection
         - [x] Lock transparency
         - [x] Lock transparency
         - [x] Create/Delete mask
         - [x] Create/Delete mask
         - [x] Enable/Disable mask
         - [x] Enable/Disable mask
+        - [ ] Link mask
         - [x] Apply mask
         - [x] Apply mask
 - ViewModel
 - ViewModel
     - [ ] Loading window when background thread is busy
     - [ ] Loading window when background thread is busy