Browse Source

Clipboard support, random improvements

Equbuxu 3 years ago
parent
commit
d450a8db1f

+ 29 - 5
src/ChunkyImageLib/Surface.cs

@@ -14,6 +14,7 @@ public class Surface : IDisposable
     public VecI Size { get; }
 
     private SKPaint drawingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
+    private SKPaint nearestNeighborReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src, FilterQuality = SKFilterQuality.None };
 
     public Surface(VecI size)
     {
@@ -38,14 +39,36 @@ public class Surface : IDisposable
     {
         if (!File.Exists(path))
             throw new FileNotFoundException(null, path);
-        using var bitmap = SKBitmap.Decode(path);
-        if (bitmap is null)
+        using var image = SKImage.FromEncodedData(path);
+        if (image is null)
             throw new ArgumentException($"The image with path {path} couldn't be loaded");
-        var surface = new Surface(new VecI(bitmap.Width, bitmap.Height));
-        surface.SkiaSurface.Canvas.DrawBitmap(bitmap, 0, 0);
+
+        var surface = new Surface(new VecI(image.Width, image.Height));
+        surface.SkiaSurface.Canvas.DrawImage(image, 0, 0);
+
         return surface;
     }
 
+    public unsafe void DrawBytes(VecI size, byte[] bytes, SKColorType colorType, SKAlphaType alphaType)
+    {
+        SKImageInfo info = new SKImageInfo(size.X, size.Y, colorType, alphaType);
+
+        fixed (void* pointer = bytes)
+        {
+            using SKPixmap map = new(info, new IntPtr(pointer));
+            using SKSurface surface = SKSurface.Create(map);
+            surface.Draw(SkiaSurface.Canvas, 0, 0, drawingPaint);
+        }
+    }
+
+    public Surface ResizeNearestNeighbor(VecI newSize)
+    {
+        using SKImage image = SkiaSurface.Snapshot();
+        Surface newSurface = new(newSize);
+        newSurface.SkiaSurface.Canvas.DrawImage(image, new SKRect(0, 0, newSize.X, newSize.Y), nearestNeighborReplacingPaint);
+        return newSurface;
+    }
+
     public unsafe void CopyTo(Surface other)
     {
         if (other.Size != Size)
@@ -104,7 +127,7 @@ public class Surface : IDisposable
         return surface;
     }
 
-    private unsafe static IntPtr CreateBuffer(int width, int height, int bytesPerPixel)
+    private static unsafe IntPtr CreateBuffer(int width, int height, int bytesPerPixel)
     {
         int byteC = width * height * bytesPerPixel;
         var buffer = Marshal.AllocHGlobal(byteC);
@@ -118,6 +141,7 @@ public class Surface : IDisposable
             return;
         disposed = true;
         drawingPaint.Dispose();
+        nearestNeighborReplacingPaint.Dispose();
         Marshal.FreeHGlobal(PixelBuffer);
         GC.SuppressFinalize(this);
     }

+ 22 - 1
src/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -1,4 +1,6 @@
-using ChunkyImageLib;
+using System;
+using System.IO;
+using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using SkiaSharp;
 using Xunit;
@@ -6,6 +8,25 @@ using Xunit;
 namespace ChunkyImageLibTest;
 public class ChunkyImageTests
 {
+    public static Surface ImportImage(string path, VecI size)
+    {
+        Surface original = Surface.Load(path);
+        if (original.Size != size)
+        {
+            Surface resized = original.ResizeNearestNeighbor(size);
+            original.Dispose();
+            return resized;
+        }
+        return original;
+    }
+
+    [Fact]
+    public void LoadDemo()
+    {
+        var path = @"C:\Users\egor0\Desktop\SpazzS1.png";
+        ImportImage(path, new VecI(5,5));
+    }
+
     [Fact]
     public void Dispose_ComplexImage_ReturnsAllChunks()
     {

+ 1 - 0
src/Custom.ruleset

@@ -46,6 +46,7 @@
     <Rule Id="SA1209" Action="None" />
     <Rule Id="SA1210" Action="None" />
     <Rule Id="SA1211" Action="None" />
+    <Rule Id="SA1214" Action="None" />
     <Rule Id="SA1216" Action="None" />
     <Rule Id="SA1217" Action="None" />
     <Rule Id="SA1303" Action="None" />

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Selection.cs

@@ -13,7 +13,7 @@ internal class Selection : IReadOnlySelection, IDisposable
             try
             {
                 // I think this might throw if another thread disposes SelectionPath at the wrong time?
-                return new SKPath(SelectionPath);
+                return new SKPath(SelectionPath) { FillType = SKPathFillType.EvenOdd };
             }
             catch (Exception)
             {

+ 16 - 0
src/PixiEditor/Helpers/SurfaceHelpers.cs

@@ -2,12 +2,28 @@
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
 using SkiaSharp;
 
 namespace PixiEditor.Helpers;
 
 public static class SurfaceHelpers
 {
+    public static Surface FromBitmapSource(BitmapSource original)
+    {
+        SKColorType color = original.Format.ToSkia(out SKAlphaType alpha);
+        if (original.PixelWidth <= 0 || original.PixelHeight <= 0)
+            throw new ArgumentException("Surface dimensions must be non-zero");
+
+        int stride = (original.PixelWidth * original.Format.BitsPerPixel + 7) / 8;
+        byte[] pixels = new byte[stride * original.PixelHeight];
+        original.CopyPixels(pixels, stride, 0);
+
+        Surface surface = new Surface(new VecI(original.PixelWidth, original.PixelHeight));
+        surface.DrawBytes(surface.Size, pixels, color, alpha);
+        return surface;
+    }
+
     public static WriteableBitmap ToWriteableBitmap(this Surface surface)
     {
         int width = surface.Size.X;

+ 47 - 187
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -1,6 +1,13 @@
-using System.IO;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
 using System.Runtime.InteropServices;
 using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.IO;
@@ -8,60 +15,28 @@ using PixiEditor.ViewModels.SubViewModels.Document;
 
 namespace PixiEditor.Models.Controllers;
 
+#nullable enable
 internal static class ClipboardController
 {
     public static readonly string TempCopyFilePath = Path.Join(
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
         "PixiEditor",
         "Copied.png");
-    /*
-    /// <summary>
-    /// Copies the selection to clipboard in PNG, Bitmap and DIB formats. <para/>
-    /// Also serailizes the <paramref name="document"/> in the PIXI format and copies it to the clipboard.
-    /// </summary>
-    public static void CopyToClipboard(Document document)
-    {
-        CopyToClipboard(
-            document.Layers.Where(x => document.GetFinalLayerIsVisible(x) && x.IsActive).ToArray(),
-            document.ActiveSelection.SelectionLayer,
-            document.LayerStructure,
-            document.Width,
-            document.Height,
-            null/*document.ToSerializable());
-    }*/
-    /*
-    private static Surface CreateMaskedCombinedSurface(Layer[] layers, LayerStructure structure, Layer selLayer)
-    {
-        if (layers.Length == 0)
-            throw new ArgumentException("Can't combine 0 layers");
-        selLayer.ClipCanvas();
-
-        Surface combined = BitmapUtils.CombineLayers(new Int32Rect(selLayer.OffsetX, selLayer.OffsetY, selLayer.Width, selLayer.Height), layers, structure);
-        using SKImage snapshot = selLayer.LayerBitmap.SkiaSurface.Snapshot();
-        combined.SkiaSurface.Canvas.DrawImage(snapshot, 0, 0, Surface.MaskingPaint);
-        return combined;
-    }
     
     /// <summary>
     ///     Copies the selection to clipboard in PNG, Bitmap and DIB formats.
     /// </summary>
-    /// <param name="layers">Layers where selection is.</param>
-    public static void CopyToClipboard(Layer[] layers, Layer selLayer, LayerStructure structure, int originalImageWidth, int originalImageHeight, SerializableDocument document = null)
+    public static void CopyToClipboard(DocumentViewModel document)
     {
         if (!ClipboardHelper.TryClear())
             return;
-        if (layers.Length == 0)
+        
+        using Surface? surface = document.MaybeExtractSelectedArea();
+        if (surface is null)
             return;
 
-        using Surface surface = CreateMaskedCombinedSurface(layers, structure, selLayer);
         DataObject data = new DataObject();
 
-
-        //BitmapSource croppedBmp = BitmapSelectionToBmpSource(finalBitmap, selLayer, out int offsetX, out int offsetY, out int width, out int height);
-
-        //Remove for now
-        //data.SetData(typeof(CropData), new CropData(width, height, offsetX, offsetY).ToStream());
-
         using (SKData pngData = surface.SkiaSurface.Snapshot().Encode())
         {
             // Stream should not be disposed
@@ -71,7 +46,7 @@ internal static class ClipboardController
             data.SetData("PNG", pngStream, false); // PNG, supports transparency
 
             pngStream.Position = 0;
-            Directory.CreateDirectory(Path.GetDirectoryName(TempCopyFilePath));
+            Directory.CreateDirectory(Path.GetDirectoryName(TempCopyFilePath)!);
             using FileStream fileStream = new FileStream(TempCopyFilePath, FileMode.Create, FileAccess.Write);
             pngStream.CopyTo(fileStream);
             data.SetFileDropList(new StringCollection() { TempCopyFilePath });
@@ -81,148 +56,65 @@ internal static class ClipboardController
         data.SetData(DataFormats.Bitmap, finalBitmap, true); // Bitmap, no transparency
         data.SetImage(finalBitmap); // DIB format, no transparency
 
-        // Remove pixi copying for now
-        /*
-        if (document != null)
-        {
-            MemoryStream memoryStream = new();
-            PixiParser.Serialize(document, memoryStream);
-            data.SetData("PIXI", memoryStream); // PIXI, supports transparency, layers, folders and swatches
-            ClipboardHelper.TrySetDataObject(data, true);
-        }
-        
-
         ClipboardHelper.TrySetDataObject(data, true);
     }
-*/
+
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     /// </summary>
-    public static void PasteFromClipboard(DocumentViewModel document)
+    public static bool TryPasteFromClipboard(DocumentViewModel document)
     {
-        /*
-        Layer[] layers;
-        try
-        {
-            layers = GetLayersFromClipboard(document).ToArray();
-        }
-        catch
-        {
-            return;
-        }
-
-        int resizedCount = 0;
-
-        Guid[] guids = layers.Select(x => x.GuidValue).ToArray();
-
-        var undoArgs = new object[] { guids, document, new PixelSize(document.Width, document.Height) };
-
-        foreach (var layer in layers)
-        {
-            document.Layers.Add(layer);
-
-            if (layer.Width > document.Width || layer.Height > document.Height)
-            {
-                ResizeToLayer(document, layer);
-                resizedCount++;
-            }
-        }
-
-        StorageBasedChange change = new StorageBasedChange(document, layers, false);
+        List<(string? name, Surface image)> images = GetImagesFromClipboard();
+        if (images.Count == 0)
+            return false;
 
-        document.UndoManager.AddUndoChange(change.ToChange(RemoveLayersProcess, undoArgs,
-            RestoreLayersProcess, new object[] { document }, "Paste from clipboard"));
-        */
-    }
-    /*
-    private static void RemoveLayersProcess(object[] parameters)
-    {
-        if (parameters.Length > 2 && parameters[1] is Document document && parameters[2] is PixelSize size)
+        if (images.Count == 1)
         {
-            document.RemoveLayersProcess(parameters);
-            document.ResizeCanvas(size.Width, size.Height, Enums.AnchorPoint.Left | Enums.AnchorPoint.Top, false);
+            document.Operations.PasteImageWithTransform(images[0].image, VecI.Zero);
+            return true;
         }
-    }
 
-    private static void RestoreLayersProcess(Layer[] layers, UndoLayer[] data, object[] parameters)
-    {
-        if (parameters.Length > 0 && parameters[0] is Document document)
-        {
-            document.RestoreLayersProcess(layers, data);
-            foreach (var layer in layers)
-            {
-                ResizeToLayer(document, layer);
-            }
-        }
+        document.Operations.PasteImagesAsLayers(images);
+        return true;
     }
-
+    
     /// <summary>
-    ///     Gets image from clipboard, supported PNG, Dib and Bitmap.
+    /// Gets images from clipboard, supported PNG, Dib and Bitmap.
     /// </summary>
-    private static IEnumerable<Layer> GetLayersFromClipboard(Document document)
+    private static List<(string? name, Surface image)> GetImagesFromClipboard()
     {
         DataObject data = ClipboardHelper.TryGetDataObject();
-        if (data == null)
-            yield break;
-
-        //Remove pixi for now
-        /*
-        if (data.GetDataPresent("PIXI"))
-        {
-            SerializableDocument document = GetSerializable(data, out CropData crop);
-            SKRectI cropRect = SKRectI.Create(crop.OffsetX, crop.OffsetY, crop.Width, crop.Height);
-
-            foreach (SerializableLayer sLayer in document)
-            {
-                SKRectI intersect;
-
-                if (//layer.OffsetX > crop.OffsetX + crop.Width || layer.OffsetY > crop.OffsetY + crop.Height ||
-                    !sLayer.IsVisible || sLayer.Opacity == 0 ||
-                    (intersect = SKRectI.Intersect(cropRect, sLayer.GetRect())) == SKRectI.Empty)
-                {
-                    continue;
-                }
+        List<(string? name, Surface image)> surfaces = new();
 
-                var layer = sLayer.ToLayer();
-
-                layer.Crop(intersect);
+        if (data == null)
+            return surfaces;
 
-                yield return layer;
-            }
-        }
-        else 
-        if (TryFromSingleImage(data, out Surface singleImage))
+        if (TryExtractSingleImage(data, out Surface? singleImage))
         {
-            yield return new Layer("Image", singleImage, document.Width, document.Height);
+            surfaces.Add((null, singleImage));
+            return surfaces;
         }
         else if (data.GetDataPresent(DataFormats.FileDrop))
         {
-            foreach (string path in data.GetFileDropList())
+            foreach (string? path in data.GetFileDropList())
             {
-                if (!Importer.IsSupportedFile(path))
-                {
+                if (path is null || !Importer.IsSupportedFile(path))
                     continue;
-                }
-
-                Layer layer = null;
-
                 try
                 {
-                    layer = new(Path.GetFileName(path), Importer.ImportSurface(path), document.Width, document.Height);
+                    Surface imported = Surface.Load(path);
+                    string filename = Path.GetFileName(path);
+                    surfaces.Add((filename, imported));
                 }
-                catch (CorruptedFileException)
+                catch
                 {
+                    continue;
                 }
-
-                yield return layer ?? new($"Corrupt {path}", document.Width, document.Height);
             }
         }
-        else
-        {
-            yield break;
-        }
+        return surfaces;
     }
-    */
+    
     public static bool IsImageInClipboard()
     {
         DataObject dao = ClipboardHelper.TryGetDataObject();
@@ -249,45 +141,18 @@ internal static class ClipboardController
         }
 
         return dao.GetDataPresent("PNG") || dao.GetDataPresent(DataFormats.Dib) ||
-               dao.GetDataPresent(DataFormats.Bitmap) || dao.GetDataPresent(DataFormats.FileDrop) ||
-               dao.GetDataPresent("PIXI");
-    }
-    /*
-    private static BitmapSource BitmapSelectionToBmpSource(WriteableBitmap bitmap, Coordinates[] selection, out int offsetX, out int offsetY, out int width, out int height)
-    {
-        offsetX = selection.Min(min => min.X);
-        offsetY = selection.Min(min => min.Y);
-        width = selection.Max(max => max.X) - offsetX + 1;
-        height = selection.Max(max => max.Y) - offsetY + 1;
-        return bitmap.Crop(offsetX, offsetY, width, height);
+               dao.GetDataPresent(DataFormats.Bitmap) || dao.GetDataPresent(DataFormats.FileDrop);
     }
 
     private static BitmapSource FromPNG(DataObject data)
     {
-        MemoryStream pngStream = data.GetData("PNG") as MemoryStream;
+        MemoryStream pngStream = (MemoryStream)data.GetData("PNG");
         PngBitmapDecoder decoder = new PngBitmapDecoder(pngStream, BitmapCreateOptions.IgnoreImageCache, BitmapCacheOption.OnLoad);
 
         return decoder.Frames[0];
     }
 
-    private static unsafe SerializableDocument GetSerializable(DataObject data, out CropData cropData)
-    {
-        MemoryStream pixiStream = data.GetData("PIXI") as MemoryStream;
-        SerializableDocument document = PixiParser.Deserialize(pixiStream);
-
-        if (data.GetDataPresent(typeof(CropData)))
-        {
-            cropData = CropData.FromStream(data.GetData(typeof(CropData)) as MemoryStream);
-        }
-        else
-        {
-            cropData = new CropData(document.Width, document.Height, 0, 0);
-        }
-
-        return document;
-    }
-
-    private static bool TryFromSingleImage(DataObject data, out Surface result)
+    private static bool TryExtractSingleImage(DataObject data, [NotNullWhen(true)] out Surface? result)
     {
         try
         {
@@ -309,7 +174,7 @@ internal static class ClipboardController
 
             if (source.Format.IsSkiaSupported())
             {
-                result = new Surface(source);
+                result = SurfaceHelpers.FromBitmapSource(source);
             }
             else
             {
@@ -319,7 +184,7 @@ internal static class ClipboardController
                 newFormat.DestinationFormat = PixelFormats.Bgra32;
                 newFormat.EndInit();
 
-                result = new Surface(newFormat);
+                result = SurfaceHelpers.FromBitmapSource(newFormat);
             }
 
             return true;
@@ -329,9 +194,4 @@ internal static class ClipboardController
         result = null;
         return false;
     }
-
-    private static void ResizeToLayer(Document document, Layer layer)
-    {
-        document.ResizeCanvas(Math.Max(document.Width, layer.Width), Math.Max(document.Height, layer.Height), Enums.AnchorPoint.Left | Enums.AnchorPoint.Top, false);
-    }*/
 }

+ 4 - 1
src/PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs

@@ -1,6 +1,7 @@
 using System.Diagnostics;
 using System.IO;
 using System.Windows.Media.Imaging;
+using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Helpers;
 using PixiEditor.Models.IO;
@@ -70,6 +71,7 @@ internal class RecentlyOpenedDocument : NotifyableObject
     {
         if (FileExtension == ".pixi")
         {
+            /*
             SerializableDocument serializableDocument;
 
             try
@@ -87,7 +89,8 @@ internal class RecentlyOpenedDocument : NotifyableObject
                     .Where(x => x.Opacity > 0.8)
                     .Select(x => (x.ToSKImage(), new VecI(x.OffsetX, x.OffsetY))));
 
-            return DownscaleToMaxSize(surface.ToWriteableBitmap());
+            return DownscaleToMaxSize(surface.ToWriteableBitmap());*/
+            return null;
         }
         else if (SupportedFilesHelper.IsExtensionSupported(FileExtension))
         {

+ 0 - 297
src/PixiEditor/Models/DataHolders/Surface.cs

@@ -1,297 +0,0 @@
-using System.Collections;
-using System.Diagnostics;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Windows;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using ChunkyImageLib.DataHolders;
-using PixiEditor.Helpers.Extensions;
-using SkiaSharp;
-
-namespace PixiEditor.Models.DataHolders;
-
-internal class Surface : IDisposable
-{
-    public static SKPaint ReplacingPaint { get; } = new() { BlendMode = SKBlendMode.Src };
-    public static SKPaint BlendingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
-    public static SKPaint MaskingPaint { get; } = new() { BlendMode = SKBlendMode.DstIn };
-    public static SKPaint InverseMaskingPaint { get; } = new() { BlendMode = SKBlendMode.DstOut };
-
-    private static readonly SKPaint nearestNeighborReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src, FilterQuality = SKFilterQuality.None };
-
-    public SKSurface SkiaSurface { get; private set; }
-    public int Width { get; }
-    public int Height { get; }
-
-    public bool Disposed { get; private set; } = false;
-
-    private SKPaint drawingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
-    private IntPtr surfaceBuffer;
-
-    public Surface(int w, int h)
-    {
-        if (w <= 0 || h <= 0)
-            throw new ArgumentException("Surface dimensions must be non-zero");
-        InitSurface(w, h);
-        Width = w;
-        Height = h;
-    }
-
-    public Surface(Surface original)
-    {
-        Width = original.Width;
-        Height = original.Height;
-        InitSurface(Width, Height);
-        original.SkiaSurface.Draw(SkiaSurface.Canvas, 0, 0, ReplacingPaint);
-    }
-
-    public Surface(int w, int h, byte[] pbgra32Bytes)
-    {
-        if (w <= 0 || h <= 0)
-            throw new ArgumentException("Surface dimensions must be non-zero");
-        Width = w;
-        Height = h;
-        InitSurface(w, h);
-        DrawBytes(w, h, pbgra32Bytes, SKColorType.Bgra8888, SKAlphaType.Premul);
-    }
-
-    public Surface(BitmapSource original)
-    {
-        SKColorType color = original.Format.ToSkia(out SKAlphaType alpha);
-        if (original.PixelWidth <= 0 || original.PixelHeight <= 0)
-            throw new ArgumentException("Surface dimensions must be non-zero");
-
-        int stride = (original.PixelWidth * original.Format.BitsPerPixel + 7) / 8;
-        byte[] pixels = new byte[stride * original.PixelHeight];
-        original.CopyPixels(pixels, stride, 0);
-
-        Width = original.PixelWidth;
-        Height = original.PixelHeight;
-        InitSurface(Width, Height);
-        DrawBytes(Width, Height, pixels, color, alpha);
-    }
-
-    public Surface(SKImage image)
-    {
-        Width = image.Width;
-        Height = image.Height;
-        InitSurface(Width, Height);
-        SkiaSurface.Canvas.DrawImage(image, 0, 0);
-    }
-
-    /// <summary>
-    /// Combines the <paramref name="images"/> into a <see cref="Surface"/>
-    /// </summary>
-    /// <param name="width">The width of the <see cref="Surface"/></param>
-    /// <param name="height">The height of the <see cref="Surface"/></param>
-    /// <returns>A surface that has the <paramref name="images"/> drawn on it</returns>
-    public static Surface Combine(int width, int height, IEnumerable<(SKImage image, VecI offset)> images)
-    {
-        Surface surface = new Surface(width, height);
-
-        foreach (var image in images)
-        {
-            surface.SkiaSurface.Canvas.DrawImage(image.image, (SKPoint)image.offset);
-        }
-
-        return surface;
-    }
-
-    public Surface ResizeNearestNeighbor(int newW, int newH)
-    {
-        SKImage image = SkiaSurface.Snapshot();
-        Surface newSurface = new(newW, newH);
-        newSurface.SkiaSurface.Canvas.DrawImage(image, new SKRect(0, 0, newW, newH), nearestNeighborReplacingPaint);
-        return newSurface;
-    }
-
-    public Surface Crop(int x, int y, int width, int height)
-    {
-        Surface result = new Surface(width, height);
-        SkiaSurface.Draw(result.SkiaSurface.Canvas, x, y, ReplacingPaint);
-        return result;
-    }
-
-    public unsafe SKColor GetSRGBPixel(int x, int y)
-    {
-        Half* ptr = (Half*)(surfaceBuffer + (x + y * Width) * 8);
-        float a = (float)ptr[3];
-        return (SKColor)new SKColorF((float)ptr[0] / a, (float)ptr[1] / a, (float)ptr[2] / a, (float)ptr[3]);
-    }
-
-    public void SetSRGBPixel(int x, int y, SKColor color)
-    {
-        // It's possible that this function can be sped up by writing into surfaceBuffer, not sure if skia will like it though
-        drawingPaint.Color = color;
-        SkiaSurface.Canvas.DrawPoint(x, y, drawingPaint);
-    }
-
-    public unsafe void SetSRGBPixelUnmanaged(int x, int y, SKColor color)
-    {
-        Half* ptr = (Half*)(surfaceBuffer + (x + y * Width) * 8);
-
-        float normalizedAlpha = color.Alpha / 255.0f;
-
-        ptr[0] = (Half)(color.Red / 255f * normalizedAlpha);
-        ptr[1] = (Half)(color.Green / 255f * normalizedAlpha);
-        ptr[2] = (Half)(color.Blue / 255f * normalizedAlpha);
-        ptr[3] = (Half)(normalizedAlpha);
-    }
-
-    public unsafe byte[] ToByteArray(SKColorType colorType = SKColorType.Bgra8888, SKAlphaType alphaType = SKAlphaType.Premul)
-    {
-        var imageInfo = new SKImageInfo(Width, Height, colorType, alphaType, SKColorSpace.CreateSrgb());
-
-        byte[] buffer = new byte[Width * Height * imageInfo.BytesPerPixel];
-        fixed (void* pointer = buffer)
-        {
-            if (!SkiaSurface.ReadPixels(imageInfo, new IntPtr(pointer), imageInfo.RowBytes, 0, 0))
-            {
-                throw new InvalidOperationException("Could not read surface into buffer");
-            }
-        }
-
-        return buffer;
-    }
-
-    public WriteableBitmap ToWriteableBitmap()
-    {
-        WriteableBitmap result = new WriteableBitmap(Width, Height, 96, 96, PixelFormats.Pbgra32, null);
-        result.Lock();
-        var dirty = new Int32Rect(0, 0, Width, Height);
-        result.WritePixels(dirty, ToByteArray(), Width * 4, 0);
-        result.AddDirtyRect(dirty);
-        result.Unlock();
-        return result;
-    }
-
-    public void Dispose()
-    {
-        if (Disposed)
-            return;
-        Disposed = true;
-        SkiaSurface.Dispose();
-        drawingPaint.Dispose();
-        Marshal.FreeHGlobal(surfaceBuffer);
-        GC.SuppressFinalize(this);
-    }
-
-    ~Surface()
-    {
-        Marshal.FreeHGlobal(surfaceBuffer);
-    }
-
-    private static SKSurface CreateSurface(int w, int h, IntPtr buffer)
-    {
-        var surface = SKSurface.Create(new SKImageInfo(w, h, SKColorType.RgbaF16, SKAlphaType.Premul, SKColorSpace.CreateSrgb()), buffer);
-        if (surface == null)
-            throw new Exception("Could not create surface");
-        return surface;
-    }
-    private static SKSurface CreateSurface(int w, int h)
-    {
-        var surface = SKSurface.Create(new SKImageInfo(w, h, SKColorType.RgbaF16, SKAlphaType.Premul, SKColorSpace.CreateSrgb()));
-        if (surface == null)
-            throw new Exception("Could not create surface");
-        return surface;
-    }
-
-    private unsafe void InitSurface(int w, int h)
-    {
-        int byteC = w * h * 8;
-        surfaceBuffer = Marshal.AllocHGlobal(byteC);
-        Unsafe.InitBlockUnaligned((byte*)surfaceBuffer, 0, (uint)byteC);
-        SkiaSurface = CreateSurface(w, h, surfaceBuffer);
-    }
-
-    private unsafe void DrawBytes(int w, int h, byte[] bytes, SKColorType colorType, SKAlphaType alphaType)
-    {
-        SKImageInfo info = new SKImageInfo(w, h, colorType, alphaType);
-
-        fixed (void* pointer = bytes)
-        {
-            using SKPixmap map = new(info, new IntPtr(pointer));
-            using SKSurface surface = SKSurface.Create(map);
-            surface.Draw(SkiaSurface.Canvas, 0, 0, ReplacingPaint);
-        }
-    }
-
-#if DEBUG
-    // Used to iterate the surface's pixels during development
-
-    [Obsolete("Only meant for use in a debugger like Visual Studio", true)]
-    private SurfaceDebugger Debugger => new SurfaceDebugger(this);
-
-    [Obsolete("Only meant for use in a debugger like Visual Studio", true)]
-    private class SurfaceDebugger : IEnumerable
-    {
-        private readonly Surface _surface;
-
-        public SurfaceDebugger(Surface surface)
-        {
-            _surface = surface;
-        }
-
-        IEnumerator IEnumerable.GetEnumerator()
-        {
-            var pixmap = _surface.SkiaSurface.PeekPixels();
-
-            for (int y = 0; y < pixmap.Width; y++)
-            {
-                yield return new DebugPixel(y);
-
-                for (int x = 0; x < pixmap.Height; x++)
-                {
-                    yield return new DebugPixel(x, y, pixmap.GetPixelColor(x, y).ToString());
-                }
-            }
-        }
-
-        [DebuggerDisplay("{DebuggerDisplay,nq}")]
-        private struct DebugPixel
-        {
-            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
-            private string DebuggerDisplay
-            {
-                get
-                {
-                    if (isPixel)
-                    {
-                        return $"X: {x}; Y: {y} - {hex}";
-                    }
-                    else
-                    {
-                        return $"|- Y: {y} -|";
-                    }
-                }
-            }
-
-            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
-            private readonly int x;
-            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
-            private readonly int y;
-            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
-            private readonly string hex;
-            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
-            private readonly bool isPixel;
-
-            public DebugPixel(int y)
-            {
-                x = 0;
-                this.y = y;
-                hex = null;
-                isPixel = false;
-            }
-
-            public DebugPixel(int x, int y, string hex)
-            {
-                this.x = x;
-                this.y = y;
-                this.hex = hex;
-                isPixel = true;
-            }
-        }
-    }
-#endif
-}

+ 13 - 0
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -44,6 +44,19 @@ internal class ChangeExecutionController
         return false;
     }
 
+    public bool TryStartUpdateableChange(UpdateableChangeExecutor brandNewExecutor)
+    {
+        if (currentSession is not null)
+            return false;
+        brandNewExecutor.Initialize(document, internals, this, EndChange);
+        if (brandNewExecutor.Start() == ExecutionState.Success)
+        {
+            currentSession = brandNewExecutor;
+            return true;
+        }
+        return false;
+    }
+
     private void EndChange(UpdateableChangeExecutor executor)
     {
         if (executor != currentSession)

+ 16 - 10
src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs

@@ -26,7 +26,7 @@ internal class DocumentStructureHelper
         return $"{name} {count}";
     }
 
-    public void CreateNewStructureMember(StructureMemberType type)
+    public Guid CreateNewStructureMember(StructureMemberType type, string? name = null, bool finish = true)
     {
         StructureMemberViewModel? member = doc.SelectedStructureMember;
         if (member is null)
@@ -34,18 +34,22 @@ internal class DocumentStructureHelper
             Guid guid = Guid.NewGuid();
             //put member on top
             internals.ActionAccumulator.AddActions(new CreateStructureMember_Action(doc.StructureRoot.GuidValue, guid, doc.StructureRoot.Children.Count, type));
-            string name = GetUniqueName(type == StructureMemberType.Layer ? "New Layer" : "New Folder", doc.StructureRoot);
-            internals.ActionAccumulator.AddFinishedActions(new StructureMemberName_Action(guid, name));
-            return;
+            name ??= GetUniqueName(type == StructureMemberType.Layer ? "New Layer" : "New Folder", doc.StructureRoot);
+            internals.ActionAccumulator.AddActions(new StructureMemberName_Action(guid, name));
+            if (finish)
+                internals.ActionAccumulator.AddFinishedActions();
+            return guid;
         }
         if (member is FolderViewModel folder)
         {
             Guid guid = Guid.NewGuid();
             //put member inside folder on top
             internals.ActionAccumulator.AddActions(new CreateStructureMember_Action(folder.GuidValue, guid, folder.Children.Count, type));
-            string name = GetUniqueName(type == StructureMemberType.Layer ? "New Layer" : "New Folder", folder);
-            internals.ActionAccumulator.AddFinishedActions(new StructureMemberName_Action(guid, name));
-            return;
+            name ??= GetUniqueName(type == StructureMemberType.Layer ? "New Layer" : "New Folder", folder);
+            internals.ActionAccumulator.AddActions(new StructureMemberName_Action(guid, name));
+            if (finish)
+                internals.ActionAccumulator.AddFinishedActions();
+            return guid;
         }
         if (member is LayerViewModel layer)
         {
@@ -56,9 +60,11 @@ internal class DocumentStructureHelper
                 throw new InvalidOperationException("Couldn't find a path to the selected member");
             FolderViewModel parent = (FolderViewModel)path[1];
             internals.ActionAccumulator.AddActions(new CreateStructureMember_Action(parent.GuidValue, guid, parent.Children.IndexOf(layer) + 1, type));
-            string name = GetUniqueName(type == StructureMemberType.Layer ? "New Layer" : "New Folder", parent);
-            internals.ActionAccumulator.AddFinishedActions(new StructureMemberName_Action(guid, name));
-            return;
+            name ??= GetUniqueName(type == StructureMemberType.Layer ? "New Layer" : "New Folder", parent);
+            internals.ActionAccumulator.AddActions(new StructureMemberName_Action(guid, name));
+            if (finish)
+                internals.ActionAccumulator.AddFinishedActions();
+            return guid;
         }
         throw new ArgumentException($"Unknown member type: {type}");
     }

+ 61 - 2
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -3,15 +3,18 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Position;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
 namespace PixiEditor.Models.DocumentModels.Public;
+#nullable enable
 internal class DocumentOperationsModule
 {
     private DocumentViewModel Document { get; }
@@ -39,6 +42,20 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClearSelection_Action());
     }
 
+    public void DeleteSelectedPixels(bool clearSelection = false)
+    {
+        var member = Document.SelectedStructureMember;
+        if (Internals.ChangeController.IsChangeActive || member is null)
+            return;
+        bool drawOnMask = member is LayerViewModel layer ? layer.ShouldDrawOnMask : true;
+        if (drawOnMask && !member.HasMaskBindable)
+            return;
+        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.GuidValue, drawOnMask));
+        if (clearSelection)
+            Internals.ActionAccumulator.AddActions(new ClearSelection_Action());
+        Internals.ActionAccumulator.AddFinishedActions();
+    }
+
     public void SetMemberOpacity(Guid memberGuid, float value)
     {
         if (Internals.ChangeController.IsChangeActive || value is > 1 or < 0)
@@ -59,11 +76,33 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddActions(new DeleteRecordedChanges_Action());
     }
 
-    public void CreateStructureMember(StructureMemberType type)
+    public void PasteImagesAsLayers(List<(string? name, Surface image)> images)
     {
         if (Internals.ChangeController.IsChangeActive)
             return;
-        Internals.StructureHelper.CreateNewStructureMember(type);
+        
+        RectI maxSize = new RectI(VecI.Zero, Document.SizeBindable);
+        foreach (var imageWithName in images)
+        {
+            maxSize = maxSize.Union(new RectI(VecI.Zero, imageWithName.image.Size));
+        }
+
+        if (maxSize.Size != Document.SizeBindable)
+            Internals.ActionAccumulator.AddActions(new ResizeCanvas_Action(maxSize.Size, ResizeAnchor.TopLeft));
+
+        foreach (var imageWithName in images)
+        {
+            var layerGuid = Internals.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer, imageWithName.name, true);
+            DrawImage(imageWithName.image, new ShapeCorners(new RectD(VecD.Zero, imageWithName.image.Size)), layerGuid, true, false, false);
+        }
+        Internals.ActionAccumulator.AddFinishedActions();
+    }
+
+    public Guid? CreateStructureMember(StructureMemberType type, string? name = null)
+    {
+        if (Internals.ChangeController.IsChangeActive)
+            return null;
+        return Internals.StructureHelper.CreateNewStructureMember(type, name, true);
     }
 
     public void DuplicateLayer(Guid guidValue)
@@ -172,4 +211,24 @@ internal class DocumentOperationsModule
             Internals.ActionAccumulator.AddActions(new DeleteStructureMember_Action(member));
         Internals.ActionAccumulator.AddActions(new ChangeBoundary_Action());
     }
+
+    public void PasteImageWithTransform(Surface image, VecI startPos)
+    {
+        Internals.ChangeController.TryStartUpdateableChange(new PasteImageExecutor(image, startPos));
+    }
+
+    public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask) =>
+        DrawImage(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, true);
+
+    private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, bool finish)
+    {
+        if (Internals.ChangeController.IsChangeActive)
+            return;
+        Internals.ActionAccumulator.AddActions(
+            new PasteImage_Action(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask),
+            new EndPasteImage_Action()
+            );
+        if (finish)
+            Internals.ActionAccumulator.AddFinishedActions();
+    }
 }

+ 64 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+#nullable enable
+internal class PasteImageExecutor : UpdateableChangeExecutor
+{
+    private readonly Surface image;
+    private readonly VecI pos;
+    private bool drawOnMask;
+    private Guid memberGuid;
+
+    public PasteImageExecutor(Surface image, VecI pos)
+    {
+        this.image = image;
+        this.pos = pos;
+    }
+
+    public override ExecutionState Start()
+    {
+        var member = document!.SelectedStructureMember;
+
+        if (member is null)
+            return ExecutionState.Error;
+        drawOnMask = member is LayerViewModel layer ? layer.ShouldDrawOnMask : true;
+        if (drawOnMask && !member.HasMaskBindable)
+            return ExecutionState.Error;
+        if (!drawOnMask && member is not LayerViewModel)
+            return ExecutionState.Error;
+
+        memberGuid = member.GuidValue;
+
+        ShapeCorners corners = new(new RectD(pos, image.Size));
+        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid, false, drawOnMask));
+        document.TransformViewModel.ShowTransform(DocumentTransformMode.Freeform, corners);
+
+        return ExecutionState.Success;
+    }
+
+    public override void OnTransformMoved(ShapeCorners corners)
+    {
+        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid, false, drawOnMask));
+    }
+
+    public override void OnTransformApplied()
+    {
+        internals!.ActionAccumulator.AddFinishedActions(new EndPasteImage_Action());
+        document!.TransformViewModel.HideTransform();
+        onEnded!.Invoke(this);
+    }
+
+    public override void ForceStop()
+    {
+        document!.TransformViewModel.HideTransform();
+        internals!.ActionAccumulator.AddActions(new EndPasteImage_Action());
+    }
+}

+ 4 - 2
src/PixiEditor/Models/IO/Exporter.cs

@@ -3,6 +3,8 @@ using System.IO.Compression;
 using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows.Media.Imaging;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
 using Microsoft.Win32;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
@@ -125,10 +127,10 @@ internal class Exporter
     }
     public static void SaveAsGZippedBytes(string path, Surface surface)
     {
-        SaveAsGZippedBytes(path, surface, SKRectI.Create(0, 0, surface.Width, surface.Height));
+        SaveAsGZippedBytes(path, surface, new RectI(VecI.Zero, surface.Size));
     }
 
-    public static void SaveAsGZippedBytes(string path, Surface surface, SKRectI rectToSave)
+    public static void SaveAsGZippedBytes(string path, Surface surface, RectI rectToSave)
     {
         var imageInfo = new SKImageInfo(rectToSave.Width, rectToSave.Height, SKColorType.RgbaF16);
         var unmanagedBuffer = Marshal.AllocHGlobal(rectToSave.Width * rectToSave.Height * 8);

+ 13 - 26
src/PixiEditor/Models/IO/Importer.cs

@@ -2,6 +2,8 @@
 using System.IO.Compression;
 using System.Runtime.InteropServices;
 using System.Windows.Media.Imaging;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
 using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
@@ -14,35 +16,19 @@ internal class Importer : NotifyableObject
     /// <summary>
     ///     Imports image from path and resizes it to given dimensions.
     /// </summary>
-    /// <param name="path">Path of image.</param>
-    /// <param name="width">New width of image.</param>
-    /// <param name="height">New height of image.</param>
+    /// <param name="path">Path of the image.</param>
+    /// <param name="size">New size of the image.</param>
     /// <returns>WriteableBitmap of imported image.</returns>
-    public static Surface ImportImage(string path, int width, int height)
+    public static Surface ImportImage(string path, VecI size)
     {
-        Surface wbmp = ImportSurface(path);
-        if (wbmp.Width != width || wbmp.Height != height)
+        Surface original = Surface.Load(path);
+        if (original.Size != size)
         {
-            Surface resized = wbmp.ResizeNearestNeighbor(width, height);
-            wbmp.Dispose();
+            Surface resized = original.ResizeNearestNeighbor(size);
+            original.Dispose();
             return resized;
         }
-
-        return wbmp;
-    }
-
-    /// <summary>
-    ///     Imports image from path and resizes it to given dimensions.
-    /// </summary>
-    /// <param name="path">Path of image.</param>
-    public static Surface ImportSurface(string path)
-    {
-        using SKImage image = SKImage.FromEncodedData(path);
-        if (image == null)
-            throw new CorruptedFileException();
-        Surface surface = new Surface(image.Width, image.Height);
-        surface.SkiaSurface.Canvas.DrawImage(image, new SKPoint(0, 0));
-        return surface;
+        return original;
     }
 
     public static WriteableBitmap ImportWriteableBitmap(string path)
@@ -107,8 +93,9 @@ internal class Importer : NotifyableObject
             Marshal.Copy(bytes, 8, ptr, bytes.Length - 8);
             SKPixmap map = new(info, ptr);
             SKSurface surface = SKSurface.Create(map);
-            Surface finalSurface = new Surface(width, height);
-            surface.Draw(finalSurface.SkiaSurface.Canvas, 0, 0, Surface.ReplacingPaint);
+            Surface finalSurface = new Surface(new VecI(width, height));
+            using SKPaint paint = new() { BlendMode = SKBlendMode.Src };
+            surface.Draw(finalSurface.SkiaSurface.Canvas, 0, 0, paint);
             return finalSurface;
         }
         finally

+ 0 - 1
src/PixiEditor/PixiEditor.csproj

@@ -216,7 +216,6 @@
 		<PackageReference Include="PixiEditor.ColorPicker" Version="3.3.1" />
 		<PackageReference Include="PixiEditor.Parser" Version="2.1.0.3" />
 		<PackageReference Include="PixiEditor.Parser.Skia" Version="2.1.0" />
-		<PackageReference Include="SkiaSharp" Version="2.88.0" />
 		<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
 		<PackageReference Include="WpfAnimatedGif" Version="2.0.2" />
 		<PackageReference Include="WriteableBitmapEx">

+ 2 - 110
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs

@@ -12,20 +12,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Document;
 [Command.Group("PixiEditor.Document", "Image")]
 internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
 {
-    public DocumentManagerViewModel(ViewModelMain owner) : base(owner)
-    {
-        /*ToolSessionController = new ToolSessionController();
-        ToolSessionController.SessionStarted += OnSessionStart;
-        ToolSessionController.SessionEnded += OnSessionEnd;
-        ToolSessionController.PixelMousePositionChanged += OnPixelMousePositionChange;
-        ToolSessionController.PreciseMousePositionChanged += OnPreciseMousePositionChange;
-        ToolSessionController.KeyStateChanged += (_, _) => UpdateActionDisplay(_tools.ActiveTool);*/
-
-        //undo.UndoRedoCalled += (_, _) => ToolSessionController.ForceStopActiveSessionIfAny();
-    }
-
-    //private ToolSessionController ToolSessionController { get; set; }
-    //public ICanvasInputTarget InputTarget => ToolSessionController;
+    public DocumentManagerViewModel(ViewModelMain owner) : base(owner) { }
 
     public ObservableCollection<DocumentViewModel> Documents { get; set; } = new ObservableCollection<DocumentViewModel>();
     public event EventHandler<DocumentChangedEventArgs>? ActiveDocumentChanged;
@@ -62,12 +49,6 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    public event EventHandler? StopUsingTool;
-
-    //private readonly ToolsViewModel _tools;
-
-    //private UpdateableChangeSession? activeSession = null;
-
     [Evaluator.CanExecute("PixiEditor.HasDocument")]
     public bool DocumentNotNull() => ActiveDocument != null;
 
@@ -90,90 +71,6 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
     {
         //tool?.UpdateActionDisplay(ToolSessionController.IsCtrlDown, ToolSessionController.IsShiftDown, ToolSessionController.IsAltDown);
     }
-    /*
-    private void OnSessionStart(object sender, UpdateableChangeSession e)
-    {
-        activeSession = e;
-
-        ExecuteTool();
-    }
-
-    private void OnSessionEnd(object sender, UpdateableChangeSession e)
-    {
-        activeSession = null;
-
-        //HighlightPixels(ToolSessionController.LastPixelPosition);
-        StopUsingTool?.Invoke(this, EventArgs.Empty);
-    }
-
-    private void OnPreciseMousePositionChange(object sender, (double, double) e)
-    {
-        /*
-        if (activeSession == null || !activeSession.Tool.RequiresPreciseMouseData)
-            return;
-        ExecuteTool();
-        
-    }
-
-    private void OnPixelMousePositionChange(object sender, MouseMovementEventArgs e)
-    {*/
-    /*
-    if (activeSession != null)
-    {
-        if (activeSession.Tool.RequiresPreciseMouseData)
-            return;
-        ExecuteTool();
-        return;
-    }
-    else
-    {
-        HighlightPixels(e.NewPosition);
-    }
-
-}*/
-
-    private void ExecuteTool()
-    {
-        /*
-        if (activeSession == null)
-            throw new Exception("Can't execute tool's Use outside a session");
-
-        if (activeSession.Tool is BitmapOperationTool operationTool)
-        {
-            //BitmapOperations.UseTool(activeSession.MouseMovement, operationTool, PrimaryColor);
-        }
-        else if (activeSession.Tool is ReadonlyTool readonlyTool)
-        {
-            //readonlyTool.Use(activeSession.MouseMovement);
-        }
-        else
-        {
-            throw new InvalidOperationException($"'{activeSession.Tool.GetType().Name}' is either not a Tool or can't inherit '{nameof(Tool)}' directly.\nChanges the base type to either '{nameof(BitmapOperationTool)}' or '{nameof(ReadonlyTool)}'");
-        }
-        */
-    }
-
-    private void BitmapManager_DocumentChanged(object sender)
-    {
-        /*
-        e.NewDocument?.GeneratePreviewLayer();
-        if (e.OldDocument != e.NewDocument)
-            ToolSessionController.ForceStopActiveSessionIfAny();*/
-    }
-
-    public void UpdateHighlightIfNecessary(bool forceHide = false)
-    {
-        //if (activeSession != null)
-        //return;
-
-        //HighlightPixels(forceHide ? new(-1, -1) : ToolSessionController.LastPixelPosition);
-    }
-
-    private void HighlightPixels(VecI position)
-    {
-
-    }
-
 
     [Command.Basic("PixiEditor.Document.ClipCanvas", "Clip Canvas", "Clip Canvas", CanExecute = "PixiEditor.HasDocument")]
     public void ClipCanvas()
@@ -208,12 +105,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Document.DeletePixels", "Delete pixels", "Delete selected pixels", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete, IconPath = "Tools/EraserImage.png")]
     public void DeletePixels()
     {
-        /*
-        var doc = Owner.BitmapManager.ActiveDocument;
-        Owner.BitmapManager.BitmapOperations.DeletePixels(
-            doc.Layers.Where(x => x.IsActive && doc.GetFinalLayerIsVisible(x)).ToArray(),
-            doc.ActiveSelection.SelectedPoints.ToArray());
-        */
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels();
     }
 
 

+ 6 - 1
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentTransformViewModel.cs

@@ -52,7 +52,12 @@ internal class DocumentTransformViewModel : NotifyableObject
     public ShapeCorners RequestedCorners
     {
         get => requestedCorners;
-        set => SetProperty(ref requestedCorners, value);
+        set
+        {
+            // We must raise the event if if the value hasn't changed, so I'm not using SetProperty
+            requestedCorners = value;
+            RaisePropertyChanged(nameof(RequestedCorners));
+        }
     }
 
     private ShapeCorners corners;

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

@@ -173,6 +173,36 @@ internal class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(AllChangesSaved));
     }
 
+    public Surface? MaybeExtractSelectedArea()
+    {
+        if (SelectedStructureMember is null || SelectedStructureMember is not LayerViewModel layerVm || SelectionPathBindable.IsEmpty)
+            return null;
+
+        IReadOnlyLayer? layer = (IReadOnlyLayer?)Internals.Tracker.Document.FindMember(layerVm.GuidValue);
+        if (layer is null)
+            return null;
+            
+        RectI bounds = (RectI)SelectionPathBindable.TightBounds;
+        Surface output = new(bounds.Size);
+
+        SKPath clipPath = new SKPath(SelectionPathBindable) { FillType = SKPathFillType.EvenOdd };
+        clipPath.Transform(SKMatrix.CreateTranslation(-bounds.X, -bounds.Y));
+        output.SkiaSurface.Canvas.Save();
+        output.SkiaSurface.Canvas.ClipPath(clipPath);
+        try
+        {
+            layer.LayerImage.DrawMostUpToDateRegionOn(bounds, ChunkResolution.Full, output.SkiaSurface, VecI.Zero);
+        }
+        catch (ObjectDisposedException)
+        {
+            output.Dispose();
+            return null;
+        }
+        output.SkiaSurface.Canvas.Restore();
+
+        return output;
+    }
+
     public SKColor PickColor(VecI pos, bool fromAllLayers)
     {
         // there is a tiny chance that the image might get disposed by another thread

+ 14 - 22
src/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs

@@ -6,7 +6,7 @@ using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Controllers;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
-
+#nullable enable
 [Command.Group("PixiEditor.Clipboard", "Clipboard")]
 internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 {
@@ -15,33 +15,22 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     {
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Duplicate", "Duplicate", "Duplicate selected area/layer", CanExecute = "PixiEditor.HasDocument", Key = Key.J, Modifiers = ModifierKeys.Control)]
-    public void Duplicate()
-    {
-        /*
-        Copy();
-        Paste();
-        */
-    }
-
-    [Command.Basic("PixiEditor.Clipboard.Cut", "Cut", "Cut selected area/layer", CanExecute = "PixiEditor.HasDocument", Key = Key.X, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Clipboard.Cut", "Cut", "Cut selected area/layer", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.X, Modifiers = ModifierKeys.Control)]
     public void Cut()
     {
-        /*
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
         Copy();
-        Owner.BitmapManager.BitmapOperations.DeletePixels(
-            new[] { Owner.BitmapManager.ActiveDocument.ActiveLayer },
-            Owner.BitmapManager.ActiveDocument.ActiveSelection.SelectedPoints.ToArray());
-        */
+        doc.Operations.DeleteSelectedPixels(true);
     }
 
     [Command.Basic("PixiEditor.Clipboard.Paste", "Paste", "Paste from clipboard", CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = ModifierKeys.Control)]
     public void Paste()
     {
-        /*
-        if (Owner.BitmapManager.ActiveDocument == null) return;
-        ClipboardController.PasteFromClipboard(Owner.BitmapManager.ActiveDocument);
-        */
+        if (Owner.DocumentManagerSubViewModel.ActiveDocument is null) 
+            return;
+        ClipboardController.TryPasteFromClipboard(Owner.DocumentManagerSubViewModel.ActiveDocument);
     }
 
     [Command.Basic("PixiEditor.Clipboard.PasteColor", "Paste color", "Paste color from clipboard", CanExecute = "PixiEditor.Clipboard.CanPasteColor", IconEvaluator = "PixiEditor.Clipboard.PasteColorIcon")]
@@ -50,10 +39,13 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         Owner.ColorsSubViewModel.PrimaryColor = SKColor.Parse(Clipboard.GetText().Trim());
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Copy", "Copy", "Copy to clipboard", CanExecute = "PixiEditor.HasDocument", Key = Key.C, Modifiers = ModifierKeys.Control)]
+    [Command.Basic("PixiEditor.Clipboard.Copy", "Copy", "Copy to clipboard", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.C, Modifiers = ModifierKeys.Control)]
     public void Copy()
     {
-        //ClipboardController.CopyToClipboard(Owner.BitmapManager.ActiveDocument);
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+        ClipboardController.CopyToClipboard(doc);
     }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPaste")]

+ 1 - 1
src/PixiEditor/Views/MainWindow.xaml

@@ -217,7 +217,7 @@
                             cmds:Menu.Command="PixiEditor.Clipboard.Paste" />
                         <MenuItem
                             Header="_Duplicate"
-                            cmds:Menu.Command="PixiEditor.Clipboard.Duplicate" />
+                            cmds:Menu.Command="PixiEditor.Layer.DuplicateSelectedLayer" />
                         <Separator />
                         <MenuItem
                             Header="_Delete Selected"

+ 5 - 1
src/PixiEditor/Views/UserControls/TransformOverlay/TransformOverlay.cs

@@ -301,7 +301,11 @@ internal class TransformOverlay : Decorator
         }
         else if (anchor is not null)
         {
-            finalCursor = TransformHelper.GetResizeCursor((Anchor)anchor, Corners);
+            if ((TransformHelper.IsCorner((Anchor)anchor) && CornerFreedom == TransformCornerFreedom.Free) ||
+                (TransformHelper.IsSide((Anchor)anchor) && SideFreedom == TransformSideFreedom.Free))
+                finalCursor = Cursors.Arrow;
+            else
+                finalCursor = TransformHelper.GetResizeCursor((Anchor)anchor, Corners);
         }
 
         if (Cursor != finalCursor)