Browse Source

Refactor and fix warnings

Equbuxu 3 years ago
parent
commit
722ebcc3e4

+ 1 - 1
PixiEditor/Helpers/Converters/ViewboxInverseTransformConverter.cs

@@ -15,7 +15,7 @@ namespace PixiEditor.Helpers.Converters
             return transform.Inverse;
         }
 
-        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        public override object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
         {
             throw new NotImplementedException();
         }

+ 140 - 0
PixiEditor/Helpers/EllipseGenerator.cs

@@ -0,0 +1,140 @@
+using PixiEditor.Models.Position;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers
+{
+    internal static class EllipseGenerator
+    {
+
+        public static List<Coordinates> GenerateEllipseFromRect(DoubleCoords rect)
+        {
+            float radiusX = (rect.Coords2.X - rect.Coords1.X) / 2.0f;
+            float radiusY = (rect.Coords2.Y - rect.Coords1.Y) / 2.0f;
+            float centerX = (rect.Coords1.X + rect.Coords2.X + 1) / 2.0f;
+            float centerY = (rect.Coords1.Y + rect.Coords2.Y + 1) / 2.0f;
+
+            return GenerateMidpointEllipse(radiusX, radiusY, centerX, centerY);
+        }
+
+        /// <summary>
+        /// Draws an ellipse using it's center and radii
+        ///
+        /// Here is a usage example:
+        /// Let's say you want an ellipse that's 3 pixels wide and 3 pixels tall located in the top right corner of the canvas
+        /// It's center is at (1.5; 1.5). That's in the middle of a pixel
+        /// The radii are both equal to 1. Notice that it's 1 and not 1.5, since we want the ellipse to land in the middle of the pixel, not outside of it.
+        /// See desmos (note the inverted y axis): https://www.desmos.com/calculator/tq9uqg0hcq
+        ///
+        /// Another example:
+        /// 4x4 ellipse in the top right corner of the canvas
+        /// Center is at (2; 2). It's a place where 4 pixels meet
+        /// Both radii are 1.5. Making them 2 would make the ellipse touch the edges of pixels, whereas we want it to stay in the middle
+        /// </summary>
+        public static List<Coordinates> GenerateMidpointEllipse(double halfWidth, double halfHeight, double centerX, double centerY)
+        {
+            if (halfWidth < 1 || halfHeight < 1)
+            {
+                return GenerateFallbackRectangle(halfWidth, halfHeight, centerX, centerY);
+            }
+
+            // ellipse formula: halfHeight^2 * x^2 + halfWidth^2 * y^2 - halfHeight^2 * halfWidth^2 = 0
+
+            // Make sure we are always at the center of a pixel
+            double currentX = Math.Ceiling(centerX - 0.5) + 0.5;
+            double currentY = centerY + halfHeight;
+
+            List<Coordinates> outputCoordinates = new List<Coordinates>();
+
+            double currentSlope;
+
+            // from PI/2 to PI/4
+            do
+            {
+                AddRegionPoints(outputCoordinates, currentX, centerX, currentY, centerY);
+
+                // calculate next pixel coords
+                currentX++;
+
+                if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX, 2)) +
+                    (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY - 0.5, 2)) -
+                    (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) >= 0)
+                {
+                    currentY--;
+                }
+
+                // calculate how far we've advanced
+                double derivativeX = 2 * Math.Pow(halfHeight, 2) * (currentX - centerX);
+                double derivativeY = 2 * Math.Pow(halfWidth, 2) * (currentY - centerY);
+                currentSlope = -(derivativeX / derivativeY);
+            }
+            while (currentSlope > -1 && currentY - centerY > 0.5);
+
+            // from PI/4 to 0
+            while (currentY - centerY >= 0)
+            {
+                AddRegionPoints(outputCoordinates, currentX, centerX, currentY, centerY);
+
+                currentY--;
+                if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX + 0.5, 2)) +
+                    (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY, 2)) -
+                    (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) < 0)
+                {
+                    currentX++;
+                }
+            }
+
+            return outputCoordinates;
+        }
+
+        private static List<Coordinates> GenerateFallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY)
+        {
+            List<Coordinates> coordinates = new List<Coordinates>();
+
+            int left = (int)Math.Floor(centerX - halfWidth);
+            int top = (int)Math.Floor(centerY - halfHeight);
+            int right = (int)Math.Floor(centerX + halfWidth);
+            int bottom = (int)Math.Floor(centerY + halfHeight);
+
+            for (int x = left; x <= right; x++)
+            {
+                coordinates.Add(new Coordinates(x, top));
+                coordinates.Add(new Coordinates(x, bottom));
+            }
+
+            for (int y = top; y <= bottom; y++)
+            {
+                coordinates.Add(new Coordinates(left, y));
+                coordinates.Add(new Coordinates(right, y));
+            }
+
+            return coordinates;
+        }
+
+        private static void AddRegionPoints(List<Coordinates> coordinates, double x, double xc, double y, double yc)
+        {
+            int xFloor = (int)Math.Floor(x);
+            int yFloor = (int)Math.Floor(y);
+            int xFloorInv = (int)Math.Floor(-x + 2 * xc);
+            int yFloorInv = (int)Math.Floor(-y + 2 * yc);
+
+            //top and bottom or left and right
+            if (xFloor == xFloorInv || yFloor == yFloorInv)
+            {
+                coordinates.Add(new Coordinates(xFloor, yFloor));
+                coordinates.Add(new Coordinates(xFloorInv, yFloorInv));
+            }
+            //part of the arc
+            else
+            {
+                coordinates.Add(new Coordinates(xFloor, yFloor));
+                coordinates.Add(new Coordinates(xFloorInv, yFloorInv));
+                coordinates.Add(new Coordinates(xFloorInv, yFloor));
+                coordinates.Add(new Coordinates(xFloor, yFloorInv));
+            }
+        }
+    }
+}

+ 157 - 158
PixiEditor/Helpers/Extensions/ParserHelpers.cs

@@ -1,158 +1,157 @@
-using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.Layers;
-using PixiEditor.Parser;

-using PixiEditor.Parser.Skia;

-using SkiaSharp;

-using System;

-using System.Collections.Generic;

-using System.Collections.ObjectModel;
-

-namespace PixiEditor.Helpers.Extensions
-{
-    public static class ParserHelpers
-    {
-        public static Document ToDocument(this SerializableDocument serializableDocument)
-        {
-            Document document = new Document(serializableDocument.Width, serializableDocument.Height)
-            {
-                Layers = serializableDocument.ToLayers(),
-                Swatches = new ObservableCollection<SKColor>(serializableDocument.Swatches.ToSKColors())
-            };
-
-            document.LayerStructure.Groups = serializableDocument.ToGroups(document);
-
-            if (document.Layers.Count > 0)
-            {
-                document.SetMainActiveLayer(0);
-            }
-
-            return document;
-        }
-
-        public static ObservableCollection<Layer> ToLayers(this SerializableDocument document)

-        {

-            ObservableCollection<Layer> layers = new();

-

-            foreach (SerializableLayer slayer in document)

-            {

-                layers.Add(slayer.ToLayer());

-            }

-

-            return layers;

-        }
-
-        public static Layer ToLayer(this SerializableLayer layer)

-        {

-            return new Layer(layer.Name, new Surface(layer.ToSKImage()))

-            {

-                Opacity = layer.Opacity,

-                IsVisible = layer.IsVisible,

-                Offset = new(layer.OffsetX, layer.OffsetY, 0, 0)

-            };

-        }
-
-        public static ObservableCollection<GuidStructureItem> ToGroups(this SerializableDocument sdocument, Document document)

-        {

-            ObservableCollection<GuidStructureItem> groups = new();

-

-            if (sdocument.Groups == null)

-            {

-                return groups;

-            }

-

-            foreach (SerializableGroup sgroup in sdocument.Groups)

-            {

-                groups.Add(sgroup.ToGroup(null, document));

-            }

-

-            return groups;

-        }
-
-        public static GuidStructureItem ToGroup(this SerializableGroup sgroup, GuidStructureItem parent, Document document)

-        {

-            GuidStructureItem group = new GuidStructureItem(sgroup.Name, Guid.Empty)

-            {

-                Opacity = sgroup.Opacity,

-                IsVisible = sgroup.IsVisible,

-                Parent = parent,

-                StartLayerGuid = document.Layers[sgroup.StartLayer].LayerGuid,

-                EndLayerGuid = document.Layers[sgroup.EndLayer].LayerGuid

-            };

-

-            group.Subgroups = new(sgroup.Subgroups.ToGroups(document, group));

-

-            return group;

-        }

-
-        public static SerializableDocument ToSerializable(this Document document)

-        {

-            return new SerializableDocument(document.Width, document.Height,

-                                            document.LayerStructure.Groups.ToSerializable(document),

-                                            document.Layers.ToSerializable()).AddSwatches(document.Swatches);

-        }
-
-        public static IEnumerable<SerializableLayer> ToSerializable(this IEnumerable<Layer> layers)

-        {

-            foreach (Layer layer in layers)

-            {

-                yield return layer.ToSerializable();

-            }

-        }
-
-        public static SerializableLayer ToSerializable(this Layer layer)

-        {

-            return new SerializableLayer(layer.Width, layer.Height, layer.OffsetX, layer.OffsetY)

-            {

-                IsVisible = layer.IsVisible,

-                Opacity = layer.Opacity,

-                Name = layer.Name

-            }.FromSKImage(layer.LayerBitmap.SkiaSurface.Snapshot());

-        }
-
-        public static IEnumerable<SerializableGroup> ToSerializable(this IEnumerable<GuidStructureItem> groups, Document document)

-        {

-            foreach (GuidStructureItem group in groups)

-            {

-                yield return group.ToSerializable(document);

-            }

-        }
-
-        public static SerializableGroup ToSerializable(this GuidStructureItem group, Document document)

-        {

-            SerializableGroup serializable = new SerializableGroup(group.Name, group.Subgroups.ToSerializable(document))

-            {

-                Opacity = group.Opacity,

-                IsVisible = group.IsVisible

-            };

-

-            for (int i = 0; i < document.Layers.Count; i++)

-            {

-                if (group.StartLayerGuid == document.Layers[i].LayerGuid)

-                {

-                    serializable.StartLayer = i;

-                }

-

-                if (group.EndLayerGuid == document.Layers[i].LayerGuid)

-                {

-                    serializable.EndLayer = i;

-                }

-            }

-

-            return serializable;

-        }

-

-        private static IEnumerable<GuidStructureItem> ToGroups(this IEnumerable<SerializableGroup> groups, Document document, GuidStructureItem parent)

-        {

-            foreach (SerializableGroup sgroup in groups)

-            {

-                yield return sgroup.ToGroup(parent, document);

-            }

-        }
-
-        private static SerializableDocument AddSwatches(this SerializableDocument document, IEnumerable<SKColor> colors)

-        {

-            document.Swatches.AddRange(colors);

-            return document;

-        }
-    }
-}
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using PixiEditor.Parser;
+using PixiEditor.Parser.Skia;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    public static class ParserHelpers
+    {
+        public static Document ToDocument(this SerializableDocument serializableDocument)
+        {
+            Document document = new Document(serializableDocument.Width, serializableDocument.Height)
+            {
+                Layers = serializableDocument.ToLayers(),
+                Swatches = new ObservableCollection<SKColor>(serializableDocument.Swatches.ToSKColors())
+            };
+
+            document.LayerStructure.Groups = serializableDocument.ToGroups(document);
+
+            if (document.Layers.Count > 0)
+            {
+                document.SetMainActiveLayer(0);
+            }
+
+            return document;
+        }
+
+        public static ObservableCollection<Layer> ToLayers(this SerializableDocument document)
+        {
+            ObservableCollection<Layer> layers = new();
+            foreach (SerializableLayer slayer in document)
+            {
+                layers.Add(slayer.ToLayer());
+            }
+
+            return layers;
+        }
+
+        public static Layer ToLayer(this SerializableLayer layer)
+        {
+            return new Layer(layer.Name, new Surface(layer.ToSKImage()))
+            {
+                Opacity = layer.Opacity,
+                IsVisible = layer.IsVisible,
+                Offset = new(layer.OffsetX, layer.OffsetY, 0, 0)
+            };
+        }
+
+        public static ObservableCollection<GuidStructureItem> ToGroups(this SerializableDocument sdocument, Document document)
+        {
+            ObservableCollection<GuidStructureItem> groups = new();
+
+            if (sdocument.Groups == null)
+            {
+                return groups;
+            }
+
+            foreach (SerializableGroup sgroup in sdocument.Groups)
+            {
+                groups.Add(sgroup.ToGroup(null, document));
+            }
+
+            return groups;
+        }
+
+        public static GuidStructureItem ToGroup(this SerializableGroup sgroup, GuidStructureItem parent, Document document)
+        {
+            GuidStructureItem group = new GuidStructureItem(sgroup.Name, Guid.Empty)
+            {
+                Opacity = sgroup.Opacity,
+                IsVisible = sgroup.IsVisible,
+                Parent = parent,
+                StartLayerGuid = document.Layers[sgroup.StartLayer].LayerGuid,
+                EndLayerGuid = document.Layers[sgroup.EndLayer].LayerGuid
+            };
+
+            group.Subgroups = new(sgroup.Subgroups.ToGroups(document, group));
+
+            return group;
+        }
+
+        public static SerializableDocument ToSerializable(this Document document)
+        {
+            return new SerializableDocument(document.Width, document.Height,
+                                            document.LayerStructure.Groups.ToSerializable(document),
+                                            document.Layers.ToSerializable()).AddSwatches(document.Swatches);
+        }
+
+        public static IEnumerable<SerializableLayer> ToSerializable(this IEnumerable<Layer> layers)
+        {
+            foreach (Layer layer in layers)
+            {
+                yield return layer.ToSerializable();
+            }
+        }
+
+        public static SerializableLayer ToSerializable(this Layer layer)
+        {
+            return new SerializableLayer(layer.Width, layer.Height, layer.OffsetX, layer.OffsetY)
+            {
+                IsVisible = layer.IsVisible,
+                Opacity = layer.Opacity,
+                Name = layer.Name
+            }.FromSKImage(layer.LayerBitmap.SkiaSurface.Snapshot());
+        }
+
+        public static IEnumerable<SerializableGroup> ToSerializable(this IEnumerable<GuidStructureItem> groups, Document document)
+        {
+            foreach (GuidStructureItem group in groups)
+            {
+                yield return group.ToSerializable(document);
+            }
+        }
+
+        public static SerializableGroup ToSerializable(this GuidStructureItem group, Document document)
+        {
+            SerializableGroup serializable = new SerializableGroup(group.Name, group.Subgroups.ToSerializable(document))
+            {
+                Opacity = group.Opacity,
+                IsVisible = group.IsVisible
+            };
+
+            for (int i = 0; i < document.Layers.Count; i++)
+            {
+                if (group.StartLayerGuid == document.Layers[i].LayerGuid)
+                {
+                    serializable.StartLayer = i;
+                }
+
+                if (group.EndLayerGuid == document.Layers[i].LayerGuid)
+                {
+                    serializable.EndLayer = i;
+                }
+            }
+
+            return serializable;
+        }
+
+        private static IEnumerable<GuidStructureItem> ToGroups(this IEnumerable<SerializableGroup> groups, Document document, GuidStructureItem parent)
+        {
+            foreach (SerializableGroup sgroup in groups)
+            {
+                yield return sgroup.ToGroup(parent, document);
+            }
+        }
+
+        private static SerializableDocument AddSwatches(this SerializableDocument document, IEnumerable<SKColor> colors)
+        {
+            document.Swatches.AddRange(colors);
+            return document;
+        }
+    }
+}

+ 1 - 1
PixiEditor/Helpers/Extensions/PixiParserHelper.cs

@@ -5,7 +5,7 @@ namespace PixiEditor.Helpers.Extensions
 {
     public static class PixiParserHelper
     {
-        public static SKRectI GetRect(this SerializableLayer layer) => 
+        public static SKRectI GetRect(this SerializableLayer layer) =>
             SKRectI.Create(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height);
     }
 }

+ 38 - 38
PixiEditor/Models/Controllers/BitmapManager.cs

@@ -138,7 +138,44 @@ namespace PixiEditor.Models.Controllers
             {
                 throw new InvalidOperationException($"'{activeTool.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)}'");
             }
-        }
+        }
+
+        public void HighlightPixels(Coordinates newPosition)
+        {
+            if (ActiveDocument == null || ActiveDocument.Layers.Count == 0 || _tools.ActiveTool.HideHighlight)
+            {
+                return;
+            }
+
+            var previewLayer = ActiveDocument.PreviewLayer;
+
+            if (_tools.ToolSize != previewLayerSize || previewLayer.IsReset)
+            {
+                previewLayerSize = _tools.ToolSize;
+                halfSize = (int)Math.Floor(_tools.ToolSize / 2f);
+                previewLayer.CreateNewBitmap(_tools.ToolSize, _tools.ToolSize);
+
+                Coordinates cords = new Coordinates(halfSize, halfSize);
+
+                previewLayer.Offset = new Thickness(0, 0, 0, 0);
+                _highlightPen.Draw(previewLayer, cords, cords, _highlightColor, _tools.ToolSize);
+
+                AdjustOffset(newPosition, previewLayer);
+
+            }
+
+            previewLayer.InvokeLayerBitmapChange();
+
+            AdjustOffset(newPosition, previewLayer);
+
+            if (newPosition.X > ActiveDocument.Width
+                || newPosition.Y > ActiveDocument.Height
+                || newPosition.X < 0 || newPosition.Y < 0)
+            {
+                previewLayer.Reset();
+                previewLayerSize = -1;
+            }
+        }
 
         private void BitmapManager_DocumentChanged(object sender, DocumentChangedEventArgs e)
         {
@@ -211,43 +248,6 @@ namespace PixiEditor.Models.Controllers
             HighlightPixels(MousePositionConverter.CurrentCoordinates);
 
             startPosition = null;
-        }
-
-        public void HighlightPixels(Coordinates newPosition)
-        {
-            if (ActiveDocument == null || ActiveDocument.Layers.Count == 0 || _tools.ActiveTool.HideHighlight)
-            {
-                return;
-            }
-
-            var previewLayer = ActiveDocument.PreviewLayer;
-
-            if (_tools.ToolSize != previewLayerSize || previewLayer.IsReset)
-            {
-                previewLayerSize = _tools.ToolSize;
-                halfSize = (int)Math.Floor(_tools.ToolSize / 2f);
-                previewLayer.CreateNewBitmap(_tools.ToolSize, _tools.ToolSize);
-
-                Coordinates cords = new Coordinates(halfSize, halfSize);
-
-                previewLayer.Offset = new Thickness(0, 0, 0, 0);
-                _highlightPen.Draw(previewLayer, cords, cords, _highlightColor, _tools.ToolSize);
-
-                AdjustOffset(newPosition, previewLayer);
-
-            }
-
-            previewLayer.InvokeLayerBitmapChange();
-
-            AdjustOffset(newPosition, previewLayer);
-
-            if (newPosition.X > ActiveDocument.Width
-                || newPosition.Y > ActiveDocument.Height
-                || newPosition.X < 0 || newPosition.Y < 0)
-            {
-                previewLayer.Reset();
-                previewLayerSize = -1;
-            }
         }
 
         private void AdjustOffset(Coordinates newPosition, Layer previewLayer)

+ 23 - 22
PixiEditor/Models/DataHolders/Document/Document.Operations.cs

@@ -77,6 +77,29 @@ namespace PixiEditor.Models.DataHolders
                 $"Flip layer: {flip}"));
         }
 
+        /// <summary>
+        ///     Resizes all document layers using NearestNeighbor interpolation.
+        /// </summary>
+        /// <param name="newWidth">New document width.</param>
+        /// <param name="newHeight">New document height.</param>
+        public void Resize(int newWidth, int newHeight)
+        {
+            object[] reverseArgs = { Width, Height };
+            object[] args = { newWidth, newHeight };
+            StorageBasedChange change = new StorageBasedChange(this, Layers);
+
+            ResizeDocument(newWidth, newHeight);
+
+            UndoManager.AddUndoChange(
+                change.ToChange(
+                    RestoreDocumentLayersProcess,
+                    reverseArgs,
+                    ResizeDocumentProcess,
+                    args,
+                    "Resize document"));
+        }
+
+
         private void FlipDocumentProcess(object[] processArgs)
         {
             FlipType flip = (FlipType)processArgs[0];
@@ -180,28 +203,6 @@ namespace PixiEditor.Models.DataHolders
                 new DocumentSizeChangedEventArgs(oldWidth, oldHeight, Width, Height));
         }
 
-        /// <summary>
-        ///     Resizes all document layers using NearestNeighbor interpolation.
-        /// </summary>
-        /// <param name="newWidth">New document width.</param>
-        /// <param name="newHeight">New document height.</param>
-        public void Resize(int newWidth, int newHeight)
-        {
-            object[] reverseArgs = { Width, Height };
-            object[] args = { newWidth, newHeight };
-            StorageBasedChange change = new StorageBasedChange(this, Layers);
-
-            ResizeDocument(newWidth, newHeight);
-
-            UndoManager.AddUndoChange(
-                change.ToChange(
-                    RestoreDocumentLayersProcess,
-                    reverseArgs,
-                    ResizeDocumentProcess,
-                    args,
-                    "Resize document"));
-        }
-
         private void RestoreDocumentLayersProcess(Layer[] layers, UndoLayer[] data, object[] args)
         {
             Width = (int)args[0];

+ 70 - 71
PixiEditor/Models/ImageManipulation/ToolCalculator.cs

@@ -44,84 +44,14 @@ namespace PixiEditor.Models.ImageManipulation
             PerformFloodFIll(layer, floodFillQueue, colorToReplace, ref dirtyRect, width, height, visited, output);
         }
 
-        private static void PerformLinearFill(
-            Layer layer, Queue<FloodFillRange> floodFillQueue,
-            Coordinates coords, int width, SKColor colorToReplace, ref Int32Rect dirtyRect, bool[] visited, List<Coordinates> output)
-        {
-            // Find the Left Edge of the Color Area
-            int fillXLeft = coords.X;
-            while (true)
-            {
-                // Indicate that this pixel has been checked
-                int pixelIndex = (coords.Y * width) + fillXLeft;
-                visited[pixelIndex] = true;
-
-                // Move one pixel to the left
-                fillXLeft--;
-                // Exit the loop if we're at edge of the bitmap or the color area
-                if (fillXLeft < 0 || visited[pixelIndex - 1] || layer.GetPixelWithOffset(fillXLeft, coords.Y) != colorToReplace)
-                    break;
-            }
-            int lastCheckedPixelLeft = fillXLeft + 1;
-
-            // Find the Right Edge of the Color Area
-            int fillXRight = coords.X;
-            while (true)
-            {
-                int pixelIndex = (coords.Y * width) + fillXRight;
-                visited[pixelIndex] = true;
-
-                fillXRight++;
-                if (fillXRight >= width || visited[pixelIndex + 1] || layer.GetPixelWithOffset(fillXRight, coords.Y) != colorToReplace)
-                    break;
-            }
-            int lastCheckedPixelRight = fillXRight - 1;
-
-            int relativeY = coords.Y - layer.OffsetY;
-            LineTool.CalculateBresenhamLine(new Coordinates(lastCheckedPixelLeft, relativeY), new Coordinates(lastCheckedPixelRight, relativeY), output);
-            dirtyRect = dirtyRect.Expand(new Int32Rect(lastCheckedPixelLeft, coords.Y, lastCheckedPixelRight - lastCheckedPixelLeft + 1, 1));
-
-            FloodFillRange range = new FloodFillRange(lastCheckedPixelLeft, lastCheckedPixelRight, coords.Y);
-            floodFillQueue.Enqueue(range);
-        }
-
-        private static void PerformFloodFIll(
-            Layer layer, Queue<FloodFillRange> floodFillQueue,
-            SKColor colorToReplace, ref Int32Rect dirtyRect, int width, int height, bool[] pixelsVisited, List<Coordinates> output)
-        {
-            while (floodFillQueue.Count > 0)
-            {
-                FloodFillRange range = floodFillQueue.Dequeue();
-
-                //START THE LOOP UPWARDS AND DOWNWARDS
-                int upY = range.Y - 1; //so we can pass the y coord by ref
-                int downY = range.Y + 1;
-                int downPixelxIndex = (width * (range.Y + 1)) + range.StartX;
-                int upPixelIndex = (width * (range.Y - 1)) + range.StartX;
-                for (int i = range.StartX; i <= range.EndX; i++)
-                {
-                    //START LOOP UPWARDS
-                    //if we're not above the top of the bitmap and the pixel above this one is within the color tolerance
-                    if (range.Y > 0 && (!pixelsVisited[upPixelIndex]) && layer.GetPixelWithOffset(i, upY) == colorToReplace)
-                        PerformLinearFill(layer, floodFillQueue, new Coordinates(i, upY), width, colorToReplace, ref dirtyRect, pixelsVisited, output);
-                    //START LOOP DOWNWARDS
-                    if (range.Y < (height - 1) && (!pixelsVisited[downPixelxIndex]) && layer.GetPixelWithOffset(i, downY) == colorToReplace)
-                        PerformLinearFill(layer, floodFillQueue, new Coordinates(i, downY), width, colorToReplace, ref dirtyRect, pixelsVisited, output);
-                    downPixelxIndex++;
-                    upPixelIndex++;
-                }
-            }
-        }
-
         public static void GenerateEllipseNonAlloc(Coordinates start, Coordinates end, bool fill,
             List<Coordinates> output)
         {
-
             DoubleCoords fixedCoordinates = CalculateCoordinatesForShapeRotation(start, end);
 
             CreateEllipse(fixedCoordinates.Coords1, fixedCoordinates.Coords2, output);
 
-            if(fill)
+            if (fill)
             {
                 CalculateFillForEllipse(output);
             }
@@ -193,6 +123,75 @@ namespace PixiEditor.Models.ImageManipulation
             return new DoubleCoords(startingCords, secondCoordinates);
         }
 
+        private static void PerformLinearFill(
+            Layer layer, Queue<FloodFillRange> floodFillQueue,
+            Coordinates coords, int width, SKColor colorToReplace, ref Int32Rect dirtyRect, bool[] visited, List<Coordinates> output)
+        {
+            // Find the Left Edge of the Color Area
+            int fillXLeft = coords.X;
+            while (true)
+            {
+                // Indicate that this pixel has been checked
+                int pixelIndex = (coords.Y * width) + fillXLeft;
+                visited[pixelIndex] = true;
+
+                // Move one pixel to the left
+                fillXLeft--;
+                // Exit the loop if we're at edge of the bitmap or the color area
+                if (fillXLeft < 0 || visited[pixelIndex - 1] || layer.GetPixelWithOffset(fillXLeft, coords.Y) != colorToReplace)
+                    break;
+            }
+            int lastCheckedPixelLeft = fillXLeft + 1;
+
+            // Find the Right Edge of the Color Area
+            int fillXRight = coords.X;
+            while (true)
+            {
+                int pixelIndex = (coords.Y * width) + fillXRight;
+                visited[pixelIndex] = true;
+
+                fillXRight++;
+                if (fillXRight >= width || visited[pixelIndex + 1] || layer.GetPixelWithOffset(fillXRight, coords.Y) != colorToReplace)
+                    break;
+            }
+            int lastCheckedPixelRight = fillXRight - 1;
+
+            int relativeY = coords.Y - layer.OffsetY;
+            LineTool.CalculateBresenhamLine(new Coordinates(lastCheckedPixelLeft, relativeY), new Coordinates(lastCheckedPixelRight, relativeY), output);
+            dirtyRect = dirtyRect.Expand(new Int32Rect(lastCheckedPixelLeft, coords.Y, lastCheckedPixelRight - lastCheckedPixelLeft + 1, 1));
+
+            FloodFillRange range = new FloodFillRange(lastCheckedPixelLeft, lastCheckedPixelRight, coords.Y);
+            floodFillQueue.Enqueue(range);
+        }
+
+        private static void PerformFloodFIll(
+            Layer layer, Queue<FloodFillRange> floodFillQueue,
+            SKColor colorToReplace, ref Int32Rect dirtyRect, int width, int height, bool[] pixelsVisited, List<Coordinates> output)
+        {
+            while (floodFillQueue.Count > 0)
+            {
+                FloodFillRange range = floodFillQueue.Dequeue();
+
+                //START THE LOOP UPWARDS AND DOWNWARDS
+                int upY = range.Y - 1; //so we can pass the y coord by ref
+                int downY = range.Y + 1;
+                int downPixelxIndex = (width * (range.Y + 1)) + range.StartX;
+                int upPixelIndex = (width * (range.Y - 1)) + range.StartX;
+                for (int i = range.StartX; i <= range.EndX; i++)
+                {
+                    //START LOOP UPWARDS
+                    //if we're not above the top of the bitmap and the pixel above this one is within the color tolerance
+                    if (range.Y > 0 && (!pixelsVisited[upPixelIndex]) && layer.GetPixelWithOffset(i, upY) == colorToReplace)
+                        PerformLinearFill(layer, floodFillQueue, new Coordinates(i, upY), width, colorToReplace, ref dirtyRect, pixelsVisited, output);
+                    //START LOOP DOWNWARDS
+                    if (range.Y < (height - 1) && (!pixelsVisited[downPixelxIndex]) && layer.GetPixelWithOffset(i, downY) == colorToReplace)
+                        PerformLinearFill(layer, floodFillQueue, new Coordinates(i, downY), width, colorToReplace, ref dirtyRect, pixelsVisited, output);
+                    downPixelxIndex++;
+                    upPixelIndex++;
+                }
+            }
+        }
+
         private static void CalculateFillForEllipse(List<Coordinates> outlineCoordinates)
         {
             if (!outlineCoordinates.Any())

+ 2 - 1
PixiEditor/Models/Tools/Tools/BrightnessTool.cs

@@ -13,6 +13,7 @@ using PixiEditor.Models.Tools.ToolSettings.Settings;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 using SkiaSharp;
 using System.Windows;
+using PixiEditor.Helpers;
 
 namespace PixiEditor.Models.Tools.Tools
 {
@@ -113,7 +114,7 @@ namespace PixiEditor.Models.Tools.Tools
         {
             cachedCircleSize = newCircleSize;
             DoubleCoords rect = CoordinatesCalculator.CalculateThicknessCenter(new Coordinates(0, 0), newCircleSize);
-            List<Coordinates> circle = CircleTool.GenerateEllipseFromRect(rect);
+            List<Coordinates> circle = EllipseGenerator.GenerateEllipseFromRect(rect);
             circle.Sort((a, b) => a.Y != b.Y ? a.Y - b.Y : a.X - b.X);
 
             circleCache.Clear();

+ 20 - 144
PixiEditor/Models/Tools/Tools/CircleTool.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Models.Layers;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools.ToolSettings.Settings;
 using SkiaSharp;
@@ -14,37 +15,6 @@ namespace PixiEditor.Models.Tools.Tools
 {
     public class CircleTool : ShapeTool
     {
-        public CircleTool()
-        {
-            ActionDisplay = "Click and move mouse to draw a circle. Hold Shift to draw an even one.";
-        }
-
-        public override string Tooltip => "Draws circle on canvas (C). Hold Shift to draw even circle.";
-
-        public override void OnKeyDown(KeyEventArgs e)
-        {
-            if (e.Key == Key.LeftShift)
-            {
-                ActionDisplay = "Click and move mouse to draw an even circle.";
-            }
-        }
-
-        public override void OnKeyUp(KeyEventArgs e)
-        {
-            if (e.Key == Key.LeftShift)
-            {
-                ActionDisplay = "Click and move mouse to draw a circle. Hold Shift to draw an even one.";
-            }
-        }
-
-        public override void Use(Layer layer, List<Coordinates> coordinates, SKColor color)
-        {
-            int thickness = Toolbar.GetSetting<SizeSetting>("ToolSize").Value;
-            var hasFillColor = Toolbar.GetSetting<BoolSetting>("Fill").Value;
-            Color temp = Toolbar.GetSetting<ColorSetting>("FillColor").Value;
-            SKColor fill = new SKColor(temp.R, temp.G, temp.B, temp.A);
-            DrawEllipseFromCoordinates(layer, coordinates[^1], coordinates[0], color, fill, thickness, hasFillColor);
-        }
 
         public static void DrawEllipseFromCoordinates(Layer layer, Coordinates first, Coordinates second,
             SKColor color, SKColor fillColor, int thickness, bool hasFillColor)
@@ -62,7 +32,7 @@ namespace PixiEditor.Models.Tools.Tools
 
             using (SKPaint paint = new SKPaint())
             {
-                List<Coordinates> outline = GenerateEllipseFromRect(corners);
+                List<Coordinates> outline = EllipseGenerator.GenerateEllipseFromRect(corners);
                 if (hasFillColor)
                 {
                     DrawEllipseFill(layer, fillColor, outline);
@@ -123,131 +93,37 @@ namespace PixiEditor.Models.Tools.Tools
             }
         }
 
-        public static List<Coordinates> GenerateEllipseFromRect(DoubleCoords rect)
+        public CircleTool()
         {
-            float radiusX = (rect.Coords2.X - rect.Coords1.X) / 2.0f;
-            float radiusY = (rect.Coords2.Y - rect.Coords1.Y) / 2.0f;
-            float centerX = (rect.Coords1.X + rect.Coords2.X + 1) / 2.0f;
-            float centerY = (rect.Coords1.Y + rect.Coords2.Y + 1) / 2.0f;
-
-            return GenerateMidpointEllipse(radiusX, radiusY, centerX, centerY);
+            ActionDisplay = "Click and move mouse to draw a circle. Hold Shift to draw an even one.";
         }
 
-        /// <summary>
-        /// Draws an ellipse using it's center and radii
-        ///
-        /// Here is a usage example:
-        /// Let's say you want an ellipse that's 3 pixels wide and 3 pixels tall located in the top right corner of the canvas
-        /// It's center is at (1.5; 1.5). That's in the middle of a pixel
-        /// The radii are both equal to 1. Notice that it's 1 and not 1.5, since we want the ellipse to land in the middle of the pixel, not outside of it.
-        /// See desmos (note the inverted y axis): https://www.desmos.com/calculator/tq9uqg0hcq
-        ///
-        /// Another example:
-        /// 4x4 ellipse in the top right corner of the canvas
-        /// Center is at (2; 2). It's a place where 4 pixels meet
-        /// Both radii are 1.5. Making them 2 would make the ellipse touch the edges of pixels, whereas we want it to stay in the middle
-        /// </summary>
-        public static List<Coordinates> GenerateMidpointEllipse(double halfWidth, double halfHeight, double centerX, double centerY)
-        {
-            if (halfWidth < 1 || halfHeight < 1)
-            {
-                return GenerateFallbackRectangle(halfWidth, halfHeight, centerX, centerY);
-            }
-
-            // ellipse formula: halfHeight^2 * x^2 + halfWidth^2 * y^2 - halfHeight^2 * halfWidth^2 = 0
-
-            // Make sure we are always at the center of a pixel
-            double currentX = Math.Ceiling(centerX - 0.5) + 0.5;
-            double currentY = centerY + halfHeight;
-
-            List<Coordinates> outputCoordinates = new List<Coordinates>();
-
-            double currentSlope;
-
-            // from PI/2 to PI/4
-            do
-            {
-                AddRegionPoints(outputCoordinates, currentX, centerX, currentY, centerY);
-
-                // calculate next pixel coords
-                currentX++;
-
-                if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX, 2)) +
-                    (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY - 0.5, 2)) -
-                    (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) >= 0)
-                {
-                    currentY--;
-                }
-
-                // calculate how far we've advanced
-                double derivativeX = 2 * Math.Pow(halfHeight, 2) * (currentX - centerX);
-                double derivativeY = 2 * Math.Pow(halfWidth, 2) * (currentY - centerY);
-                currentSlope = -(derivativeX / derivativeY);
-            }
-            while (currentSlope > -1 && currentY - centerY > 0.5);
+        public override string Tooltip => "Draws circle on canvas (C). Hold Shift to draw even circle.";
 
-            // from PI/4 to 0
-            while (currentY - centerY >= 0)
+        public override void OnKeyDown(KeyEventArgs e)
+        {
+            if (e.Key == Key.LeftShift)
             {
-                AddRegionPoints(outputCoordinates, currentX, centerX, currentY, centerY);
-
-                currentY--;
-                if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX + 0.5, 2)) +
-                    (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY, 2)) -
-                    (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) < 0)
-                {
-                    currentX++;
-                }
+                ActionDisplay = "Click and move mouse to draw an even circle.";
             }
-
-            return outputCoordinates;
         }
 
-        private static List<Coordinates> GenerateFallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY)
+        public override void OnKeyUp(KeyEventArgs e)
         {
-            List<Coordinates> coordinates = new List<Coordinates>();
-
-            int left = (int)Math.Floor(centerX - halfWidth);
-            int top = (int)Math.Floor(centerY - halfHeight);
-            int right = (int)Math.Floor(centerX + halfWidth);
-            int bottom = (int)Math.Floor(centerY + halfHeight);
-
-            for (int x = left; x <= right; x++)
-            {
-                coordinates.Add(new Coordinates(x, top));
-                coordinates.Add(new Coordinates(x, bottom));
-            }
-
-            for (int y = top; y <= bottom; y++)
+            if (e.Key == Key.LeftShift)
             {
-                coordinates.Add(new Coordinates(left, y));
-                coordinates.Add(new Coordinates(right, y));
+                ActionDisplay = "Click and move mouse to draw a circle. Hold Shift to draw an even one.";
             }
-
-            return coordinates;
         }
 
-        private static void AddRegionPoints(List<Coordinates> coordinates, double x, double xc, double y, double yc)
+        public override void Use(Layer layer, List<Coordinates> coordinates, SKColor color)
         {
-            int xFloor = (int)Math.Floor(x);
-            int yFloor = (int)Math.Floor(y);
-            int xFloorInv = (int)Math.Floor(-x + 2 * xc);
-            int yFloorInv = (int)Math.Floor(-y + 2 * yc);
-
-            //top and bottom or left and right
-            if (xFloor == xFloorInv || yFloor == yFloorInv)
-            {
-                coordinates.Add(new Coordinates(xFloor, yFloor));
-                coordinates.Add(new Coordinates(xFloorInv, yFloorInv));
-            }
-            //part of the arc
-            else
-            {
-                coordinates.Add(new Coordinates(xFloor, yFloor));
-                coordinates.Add(new Coordinates(xFloorInv, yFloorInv));
-                coordinates.Add(new Coordinates(xFloorInv, yFloor));
-                coordinates.Add(new Coordinates(xFloor, yFloorInv));
-            }
+            int thickness = Toolbar.GetSetting<SizeSetting>("ToolSize").Value;
+            var hasFillColor = Toolbar.GetSetting<BoolSetting>("Fill").Value;
+            Color temp = Toolbar.GetSetting<ColorSetting>("FillColor").Value;
+            SKColor fill = new SKColor(temp.R, temp.G, temp.B, temp.A);
+            DrawEllipseFromCoordinates(layer, coordinates[^1], coordinates[0], color, fill, thickness, hasFillColor);
         }
+
     }
 }

+ 237 - 237
PixiEditor/Models/Tools/Tools/MoveTool.cs

@@ -1,237 +1,237 @@
-using PixiEditor.Models.Controllers;
-using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.Enums;
-using PixiEditor.Models.ImageManipulation;
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Position;
-using PixiEditor.Models.Undo;

-using PixiEditor.ViewModels;
-using SkiaSharp;

-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Windows;
-using System.Windows.Input;
-using Transform = PixiEditor.Models.ImageManipulation.Transform;
-
-namespace PixiEditor.Models.Tools.Tools
-{
-    public class MoveTool : BitmapOperationTool
-    {
-        private Layer[] affectedLayers;
-        private Dictionary<Guid, bool> clearedPixels = new Dictionary<Guid, bool>();
-        private Coordinates[] currentSelection;
-        private Coordinates lastMouseMove;
-        private Dictionary<Guid, SKColor[]> startPixelColors;
-        private Dictionary<Guid, SKColor[]> endPixelColors;
-        private Dictionary<Guid, Thickness> startingOffsets;
-        private Coordinates[] startSelection;
-        private bool updateViewModelSelection = true;

-

-        public MoveTool(BitmapManager bitmapManager)
-        {
-            ActionDisplay = "Hold mouse to move selected pixels. Hold Ctrl to move all layers.";
-            Cursor = Cursors.Arrow;
-            RequiresPreviewLayer = true;
-            UseDefaultUndoMethod = true;
-
-            BitmapManager = bitmapManager;
-        }

-

-        public override string Tooltip => "Moves selected pixels (V). Hold Ctrl to move all layers.";

-

-        public override bool HideHighlight => true;

-

-        public bool MoveAll { get; set; }
-
-        private BitmapManager BitmapManager { get; }
-
-        public override void OnKeyDown(KeyEventArgs e)

-        {

-            if (e.Key == Key.LeftCtrl)

-            {

-                ActionDisplay = "Hold mouse to move all selected layers.";

-            }

-        }
-
-        public override void OnKeyUp(KeyEventArgs e)

-        {

-            if (e.Key == Key.LeftCtrl)

-            {

-                ActionDisplay = "Hold mouse to move selected pixels. Hold Ctrl to move all layers.";

-            }

-        }
-
-        public override void AfterAddedUndo(UndoManager undoManager)
-        {

-            //if (currentSelection == null || currentSelection.Length == 0)
-            //{
-            //    return;
-            //}
-            //Change changes = undoManager.UndoStack.Peek();

-            //// Inject to default undo system change custom changes made by this tool
-            //foreach (var item in startPixelColors)
-            //{
-            //    BitmapPixelChanges beforeMovePixels = BitmapPixelChanges.FromArrays(startSelection, item.Value);
-            //    BitmapPixelChanges afterMovePixels = BitmapPixelChanges.FromArrays(currentSelection, endPixelColors[item.Key]);
-            //    Guid layerGuid = item.Key;
-            //    var oldValue = (LayerChange[])changes.OldValue;

-            //    if (oldValue.Any(x => x.LayerGuid == layerGuid))
-            //    {
-            //        var layer = oldValue.First(x => x.LayerGuid == layerGuid);
-            //        layer.PixelChanges.ChangedPixels.AddRangeOverride(afterMovePixels.ChangedPixels);
-            //        layer.PixelChanges.ChangedPixels
-            //            .AddRangeOverride(beforeMovePixels.ChangedPixels);

-            //        ((LayerChange[])changes.NewValue).First(x => x.LayerGuid == layerGuid).PixelChanges.ChangedPixels
-            //            .AddRangeNewOnly(BitmapPixelChanges
-            //                .FromSingleColoredArray(startSelection, SKColors.Transparent)
-            //                .ChangedPixels);
-            //    }
-            //}
-        }
-
-        // This adds undo if there is no selection, reason why this isn't in AfterUndoAdded,
-        // is because it doesn't fire if no pixel changes were made.
-        public override void OnStoppedRecordingMouseUp(MouseEventArgs e)
-        {
-            if (currentSelection != null && currentSelection.Length == 0)
-            {
-                BitmapManager.ActiveDocument.UndoManager.AddUndoChange(new Change(
-                    ApplyOffsets,
-                    new object[] { startingOffsets },
-                    ApplyOffsets,
-                    new object[] { GetOffsets(affectedLayers) },
-                    "Move layers"));
-            }
-        }
-
-        public override void OnStart(Coordinates startPos)
-        {
-            ResetSelectionValues(startPos);

-            // Move offset if no selection
-            Document doc = BitmapManager.ActiveDocument;
-            Selection selection = doc.ActiveSelection;
-            if (selection != null && selection.SelectedPoints.Count > 0)
-            {
-                currentSelection = selection.SelectedPoints.ToArray();
-            }
-            else
-            {
-                currentSelection = Array.Empty<Coordinates>();
-            }

-            if (Keyboard.IsKeyDown(Key.LeftCtrl) || MoveAll)
-            {
-                affectedLayers = doc.Layers.Where(x => x.IsVisible).ToArray();
-            }
-            else
-            {
-                affectedLayers = doc.Layers.Where(x => x.IsActive && doc.GetFinalLayerIsVisible(x)).ToArray();
-            }

-            startSelection = currentSelection;
-            startPixelColors = BitmapUtils.GetPixelsForSelection(affectedLayers, startSelection);
-            startingOffsets = GetOffsets(affectedLayers);
-        }
-
-        public override void Use(Layer layer, List<Coordinates> mouseMove, SKColor color)
-        {
-            //LayerChange[] result = new LayerChange[affectedLayers.Length];
-            //var end = mouseMove[0];

-            //var lastSelection = currentSelection.ToArray();
-            //for (int i = 0; i < affectedLayers.Length; i++)
-            //{
-            //    if (currentSelection.Length > 0)
-            //    {
-            //        endPixelColors = BitmapUtils.GetPixelsForSelection(affectedLayers, currentSelection);
-            //        var changes = MoveSelection(affectedLayers[i], mouseMove);

-            //        ClearSelectedPixels(affectedLayers[i], lastSelection);
-
-            //        changes = RemoveTransparentPixels(changes);
-
-            //        result[i] = new LayerChange(changes, affectedLayers[i]);
-            //    }
-            //    else
-            //    {
-            //        var vector = Transform.GetTranslation(lastMouseMove, end);
-            //        affectedLayers[i].Offset = new Thickness(affectedLayers[i].OffsetX + vector.X, affectedLayers[i].OffsetY + vector.Y, 0, 0);
-            //        result[i] = new LayerChange(BitmapPixelChanges.Empty, affectedLayers[i]);
-            //    }
-            //}
-

-            //lastMouseMove = end;
-
-            // return result;
-        }

-        public BitmapPixelChanges MoveSelection(Layer layer, IEnumerable<Coordinates> mouseMove)
-        {
-            Coordinates end = mouseMove.First();
-
-            currentSelection = TranslateSelection(end);
-            if (updateViewModelSelection)
-            {
-                ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(currentSelection, SelectionType.New);
-            }

-            lastMouseMove = end;
-            return BitmapPixelChanges.FromArrays(currentSelection, startPixelColors[layer.LayerGuid]);
-        }

-        private void ApplyOffsets(object[] parameters)
-        {
-            Dictionary<Guid, Thickness> offsets = (Dictionary<Guid, Thickness>)parameters[0];
-            foreach (var offset in offsets)
-            {
-                Layer layer = ViewModelMain.Current?.BitmapManager?.
-                    ActiveDocument?.Layers?.First(x => x.LayerGuid == offset.Key);
-                layer.Offset = offset.Value;
-            }
-        }
-
-        private Dictionary<Guid, Thickness> GetOffsets(Layer[] layers)
-        {
-            Dictionary<Guid, Thickness> dict = new Dictionary<Guid, Thickness>();
-            for (int i = 0; i < layers.Length; i++)
-            {
-                dict.Add(layers[i].LayerGuid, layers[i].Offset);
-            }
-
-            return dict;
-        }
-
-        private BitmapPixelChanges RemoveTransparentPixels(BitmapPixelChanges pixels)
-        {
-            foreach (var item in pixels.ChangedPixels.Where(x => x.Value.Alpha == 0).ToList())
-            {
-                pixels.ChangedPixels.Remove(item.Key);
-            }
-            return pixels;
-        }
-
-        private void ResetSelectionValues(Coordinates start)
-        {
-            lastMouseMove = start;
-            clearedPixels = new Dictionary<Guid, bool>();
-            endPixelColors = new Dictionary<Guid, SKColor[]>();
-            currentSelection = null;
-            affectedLayers = null;
-            updateViewModelSelection = true;
-            startPixelColors = null;
-            startSelection = null;
-        }
-
-        private Coordinates[] TranslateSelection(Coordinates end)
-        {
-            Coordinates translation = Transform.GetTranslation(lastMouseMove, end);
-            return Transform.Translate(currentSelection, translation);
-        }
-
-        private void ClearSelectedPixels(Layer layer, Coordinates[] selection)
-        {
-            Guid layerGuid = layer.LayerGuid;
-            if (!clearedPixels.ContainsKey(layerGuid) || clearedPixels[layerGuid] == false)
-            {
-                BitmapManager.ActiveDocument.Layers.First(x => x == layer)
-                    .SetPixels(BitmapPixelChanges.FromSingleColoredArray(selection, SKColors.Transparent));
-
-                clearedPixels[layerGuid] = true;
-            }
-        }
-    }
-}
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Enums;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
+using PixiEditor.ViewModels;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Input;
+using Transform = PixiEditor.Models.ImageManipulation.Transform;
+
+namespace PixiEditor.Models.Tools.Tools
+{
+    public class MoveTool : BitmapOperationTool
+    {
+        private Layer[] affectedLayers;
+        private Dictionary<Guid, bool> clearedPixels = new Dictionary<Guid, bool>();
+        private Coordinates[] currentSelection;
+        private Coordinates lastMouseMove;
+        private Dictionary<Guid, SKColor[]> startPixelColors;
+        private Dictionary<Guid, SKColor[]> endPixelColors;
+        private Dictionary<Guid, Thickness> startingOffsets;
+        private Coordinates[] startSelection;
+        private bool updateViewModelSelection = true;
+
+        public MoveTool(BitmapManager bitmapManager)
+        {
+            ActionDisplay = "Hold mouse to move selected pixels. Hold Ctrl to move all layers.";
+            Cursor = Cursors.Arrow;
+            RequiresPreviewLayer = true;
+            UseDefaultUndoMethod = true;
+
+            BitmapManager = bitmapManager;
+        }
+
+        public override string Tooltip => "Moves selected pixels (V). Hold Ctrl to move all layers.";
+
+        public override bool HideHighlight => true;
+
+        public bool MoveAll { get; set; }
+
+        private BitmapManager BitmapManager { get; }
+
+        public override void OnKeyDown(KeyEventArgs e)
+        {
+            if (e.Key == Key.LeftCtrl)
+            {
+                ActionDisplay = "Hold mouse to move all selected layers.";
+            }
+        }
+
+        public override void OnKeyUp(KeyEventArgs e)
+        {
+            if (e.Key == Key.LeftCtrl)
+            {
+                ActionDisplay = "Hold mouse to move selected pixels. Hold Ctrl to move all layers.";
+            }
+        }
+
+        public override void AfterAddedUndo(UndoManager undoManager)
+        {
+            //if (currentSelection == null || currentSelection.Length == 0)
+            //{
+            //    return;
+            //}
+            //Change changes = undoManager.UndoStack.Peek();
+            //// Inject to default undo system change custom changes made by this tool
+            //foreach (var item in startPixelColors)
+            //{
+            //    BitmapPixelChanges beforeMovePixels = BitmapPixelChanges.FromArrays(startSelection, item.Value);
+            //    BitmapPixelChanges afterMovePixels = BitmapPixelChanges.FromArrays(currentSelection, endPixelColors[item.Key]);
+            //    Guid layerGuid = item.Key;
+            //    var oldValue = (LayerChange[])changes.OldValue;
+            //    if (oldValue.Any(x => x.LayerGuid == layerGuid))
+            //    {
+            //        var layer = oldValue.First(x => x.LayerGuid == layerGuid);
+            //        layer.PixelChanges.ChangedPixels.AddRangeOverride(afterMovePixels.ChangedPixels);
+            //        layer.PixelChanges.ChangedPixels
+            //            .AddRangeOverride(beforeMovePixels.ChangedPixels);
+            //        ((LayerChange[])changes.NewValue).First(x => x.LayerGuid == layerGuid).PixelChanges.ChangedPixels
+            //            .AddRangeNewOnly(BitmapPixelChanges
+            //                .FromSingleColoredArray(startSelection, SKColors.Transparent)
+            //                .ChangedPixels);
+            //    }
+            //}
+        }
+
+        // This adds undo if there is no selection, reason why this isn't in AfterUndoAdded,
+        // is because it doesn't fire if no pixel changes were made.
+        public override void OnStoppedRecordingMouseUp(MouseEventArgs e)
+        {
+            if (currentSelection != null && currentSelection.Length == 0)
+            {
+                BitmapManager.ActiveDocument.UndoManager.AddUndoChange(new Change(
+                    ApplyOffsets,
+                    new object[] { startingOffsets },
+                    ApplyOffsets,
+                    new object[] { GetOffsets(affectedLayers) },
+                    "Move layers"));
+            }
+        }
+
+        public override void OnStart(Coordinates startPos)
+        {
+            ResetSelectionValues(startPos);
+            // Move offset if no selection
+            Document doc = BitmapManager.ActiveDocument;
+            Selection selection = doc.ActiveSelection;
+            if (selection != null && selection.SelectedPoints.Count > 0)
+            {
+                currentSelection = selection.SelectedPoints.ToArray();
+            }
+            else
+            {
+                currentSelection = Array.Empty<Coordinates>();
+            }
+            if (Keyboard.IsKeyDown(Key.LeftCtrl) || MoveAll)
+            {
+                affectedLayers = doc.Layers.Where(x => x.IsVisible).ToArray();
+            }
+            else
+            {
+                affectedLayers = doc.Layers.Where(x => x.IsActive && doc.GetFinalLayerIsVisible(x)).ToArray();
+            }
+            startSelection = currentSelection;
+            startPixelColors = BitmapUtils.GetPixelsForSelection(affectedLayers, startSelection);
+            startingOffsets = GetOffsets(affectedLayers);
+        }
+
+        public override void Use(Layer layer, List<Coordinates> mouseMove, SKColor color)
+        {
+            //LayerChange[] result = new LayerChange[affectedLayers.Length];
+            //var end = mouseMove[0];
+            //var lastSelection = currentSelection.ToArray();
+            //for (int i = 0; i < affectedLayers.Length; i++)
+            //{
+            //    if (currentSelection.Length > 0)
+            //    {
+            //        endPixelColors = BitmapUtils.GetPixelsForSelection(affectedLayers, currentSelection);
+            //        var changes = MoveSelection(affectedLayers[i], mouseMove);
+            //        ClearSelectedPixels(affectedLayers[i], lastSelection);
+
+            //        changes = RemoveTransparentPixels(changes);
+
+            //        result[i] = new LayerChange(changes, affectedLayers[i]);
+            //    }
+            //    else
+            //    {
+            //        var vector = Transform.GetTranslation(lastMouseMove, end);
+            //        affectedLayers[i].Offset = new Thickness(affectedLayers[i].OffsetX + vector.X, affectedLayers[i].OffsetY + vector.Y, 0, 0);
+            //        result[i] = new LayerChange(BitmapPixelChanges.Empty, affectedLayers[i]);
+            //    }
+            //}
+
+            //lastMouseMove = end;
+
+            // return result;
+        }
+        public BitmapPixelChanges MoveSelection(Layer layer, IEnumerable<Coordinates> mouseMove)
+        {
+            Coordinates end = mouseMove.First();
+
+            currentSelection = TranslateSelection(end);
+            if (updateViewModelSelection)
+            {
+                ViewModelMain.Current.BitmapManager.ActiveDocument.ActiveSelection.SetSelection(currentSelection, SelectionType.New);
+            }
+            lastMouseMove = end;
+            return BitmapPixelChanges.FromArrays(currentSelection, startPixelColors[layer.LayerGuid]);
+        }
+        private void ApplyOffsets(object[] parameters)
+        {
+            Dictionary<Guid, Thickness> offsets = (Dictionary<Guid, Thickness>)parameters[0];
+            foreach (var offset in offsets)
+            {
+                Layer layer = ViewModelMain.Current?.BitmapManager?.
+                    ActiveDocument?.Layers?.First(x => x.LayerGuid == offset.Key);
+                layer.Offset = offset.Value;
+            }
+        }
+
+        private Dictionary<Guid, Thickness> GetOffsets(Layer[] layers)
+        {
+            Dictionary<Guid, Thickness> dict = new Dictionary<Guid, Thickness>();
+            for (int i = 0; i < layers.Length; i++)
+            {
+                dict.Add(layers[i].LayerGuid, layers[i].Offset);
+            }
+
+            return dict;
+        }
+
+        private BitmapPixelChanges RemoveTransparentPixels(BitmapPixelChanges pixels)
+        {
+            foreach (var item in pixels.ChangedPixels.Where(x => x.Value.Alpha == 0).ToList())
+            {
+                pixels.ChangedPixels.Remove(item.Key);
+            }
+            return pixels;
+        }
+
+        private void ResetSelectionValues(Coordinates start)
+        {
+            lastMouseMove = start;
+            clearedPixels = new Dictionary<Guid, bool>();
+            endPixelColors = new Dictionary<Guid, SKColor[]>();
+            currentSelection = null;
+            affectedLayers = null;
+            updateViewModelSelection = true;
+            startPixelColors = null;
+            startSelection = null;
+        }
+
+        private Coordinates[] TranslateSelection(Coordinates end)
+        {
+            Coordinates translation = Transform.GetTranslation(lastMouseMove, end);
+            return Transform.Translate(currentSelection, translation);
+        }
+
+        private void ClearSelectedPixels(Layer layer, Coordinates[] selection)
+        {
+            Guid layerGuid = layer.LayerGuid;
+            if (!clearedPixels.ContainsKey(layerGuid) || clearedPixels[layerGuid] == false)
+            {
+                BitmapManager.ActiveDocument.Layers.First(x => x == layer)
+                    .SetPixels(BitmapPixelChanges.FromSingleColoredArray(selection, SKColors.Transparent));
+
+                clearedPixels[layerGuid] = true;
+            }
+        }
+    }
+}

+ 8 - 7
PixiEditor/Views/UserControls/PlainLayerView.xaml.cs

@@ -27,6 +27,14 @@ namespace PixiEditor.Views.UserControls
             SizeChanged += OnControlSizeChanged;
         }
 
+        public void Resize(int newWidth, int newHeight)
+        {
+            renderer?.Dispose();
+            renderer = new SurfaceRenderer(newWidth, newHeight);
+            image.Source = renderer.FinalBitmap;
+            Update();
+        }
+
         private static void OnLayerChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
         {
             var view = (PlainLayerView)sender;
@@ -39,13 +47,6 @@ namespace PixiEditor.Views.UserControls
                 view.Resize(layer.Width, layer.Height);
             }
         }
-        public void Resize(int newWidth, int newHeight)
-        {
-            renderer?.Dispose();
-            renderer = new SurfaceRenderer(newWidth, newHeight);
-            image.Source = renderer.FinalBitmap;
-            Update();
-        }
 
         private void Update()
         {

+ 2 - 2
PixiEditorTests/PixiEditorTests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0-windows</TargetFramework>
+    <TargetFramework>net6.0-windows10.0.22000.0</TargetFramework>
 
     <IsPackable>false</IsPackable>
 
@@ -35,7 +35,7 @@
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
     <PackageReference Include="Moq" Version="4.16.1" />