Browse Source

Merge pull request #149 from PixiEditor/pixelperfect

Implemented pixel-perfect pen option
Krzysztof Krysiński 4 years ago
parent
commit
3cdc2c530d

+ 230 - 201
PixiEditor/Models/Controllers/BitmapOperationsUtility.cs

@@ -1,202 +1,231 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.ImageManipulation;
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Position;
-using PixiEditor.Models.Tools;
-using PixiEditor.Models.Undo;
-
-namespace PixiEditor.Models.Controllers
-{
-    public class BitmapOperationsUtility
-    {
-        private LayerChange[] lastModifiedLayers;
-
-        private Coordinates lastMousePos;
-
-        public BitmapOperationsUtility(BitmapManager manager)
-        {
-            Manager = manager;
-        }
-
-        public event EventHandler<BitmapChangedEventArgs> BitmapChanged;
-
-        public BitmapManager Manager { get; set; }
-
-        public void DeletePixels(Layer[] layers, Coordinates[] pixels)
-        {
-            if (Manager.ActiveDocument == null)
-            {
-                return;
-            }
-
-            BitmapPixelChanges changes = BitmapPixelChanges.FromSingleColoredArray(pixels, Color.FromArgb(0, 0, 0, 0));
-            Dictionary<Guid, Color[]> oldValues = BitmapUtils.GetPixelsForSelection(layers, pixels);
-            LayerChange[] old = new LayerChange[layers.Length];
-            LayerChange[] newChange = new LayerChange[layers.Length];
-            for (int i = 0; i < layers.Length; i++)
-            {
-                Guid guid = layers[i].LayerGuid;
-                old[i] = new LayerChange(
-                    BitmapPixelChanges.FromArrays(pixels, oldValues[layers[i].LayerGuid]), guid);
-                newChange[i] = new LayerChange(changes, guid);
-                layers[i].SetPixels(changes);
-            }
-
-            Manager.ActiveDocument.UndoManager.AddUndoChange(new Change("UndoChanges", old, newChange, "Deleted pixels"));
-        }
-
-        /// <summary>
-        ///     Executes tool Use() method with given parameters. NOTE: mouseMove is reversed inside function!.
-        /// </summary>
-        /// <param name="newPos">Most recent coordinates.</param>
-        /// <param name="mouseMove">Last mouse movement coordinates.</param>
-        /// <param name="tool">Tool to execute.</param>
-        public void ExecuteTool(Coordinates newPos, List<Coordinates> mouseMove, BitmapOperationTool tool)
-        {
-            if (Manager.ActiveDocument != null && tool != null)
-            {
-                if (Manager.ActiveDocument.Layers.Count == 0 || mouseMove.Count == 0)
-                {
-                    return;
-                }
-
-                mouseMove.Reverse();
-                UseTool(mouseMove, tool, Manager.PrimaryColor);
-
-                lastMousePos = newPos;
-            }
-        }
-
-        /// <summary>
-        ///     Applies pixels from preview layer to selected layer.
-        /// </summary>
-        public void ApplyPreviewLayer()
-        {
-            if (lastModifiedLayers == null)
-            {
-                return;
-            }
-
-            for (int i = 0; i < lastModifiedLayers.Length; i++)
-            {
-                Layer layer = Manager.ActiveDocument.Layers.FirstOrDefault(x => x.LayerGuid == lastModifiedLayers[i].LayerGuid);
-
-                if (layer != null)
-                {
-                    BitmapPixelChanges oldValues = ApplyToLayer(layer, lastModifiedLayers[i]).PixelChanges;
-
-                    BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
-                        lastModifiedLayers[i].PixelChanges,
-                        oldValues,
-                        lastModifiedLayers[i].LayerGuid));
-                    Manager.ActiveDocument.GeneratePreviewLayer();
-                }
-            }
-        }
-
-        private void UseTool(List<Coordinates> mouseMoveCords, BitmapOperationTool tool, Color color)
-        {
-            if (Keyboard.IsKeyDown(Key.LeftShift) && !MouseCordsNotInLine(mouseMoveCords))
-            {
-                mouseMoveCords = GetSquareCoordiantes(mouseMoveCords);
-            }
-
-            if (!tool.RequiresPreviewLayer)
-            {
-                LayerChange[] modifiedLayers = tool.Use(Manager.ActiveLayer, mouseMoveCords.ToArray(), color);
-                LayerChange[] oldPixelsValues = new LayerChange[modifiedLayers.Length];
-                for (int i = 0; i < modifiedLayers.Length; i++)
-                {
-                    Layer layer = Manager.ActiveDocument.Layers.First(x => x.LayerGuid == modifiedLayers[i].LayerGuid);
-                    oldPixelsValues[i] = ApplyToLayer(layer, modifiedLayers[i]);
-
-                    BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
-                        modifiedLayers[i].PixelChanges,
-                        oldPixelsValues[i].PixelChanges,
-                        modifiedLayers[i].LayerGuid));
-                }
-            }
-            else
-            {
-                UseToolOnPreviewLayer(mouseMoveCords);
-            }
-        }
-
-        private LayerChange ApplyToLayer(Layer layer, LayerChange change)
-        {
-            layer.DynamicResize(change.PixelChanges);
-
-            LayerChange oldPixelsValues = new LayerChange(
-                GetOldPixelsValues(change.PixelChanges.ChangedPixels.Keys.ToArray()),
-                change.LayerGuid);
-
-            layer.SetPixels(change.PixelChanges, false);
-            return oldPixelsValues;
-        }
-
-        private bool MouseCordsNotInLine(List<Coordinates> cords)
-        {
-            return cords[0].X == cords[^1].X || cords[0].Y == cords[^1].Y;
-        }
-
-        /// <summary>
-        ///     Extracts square from rectangle mouse drag, used to draw symmetric shapes.
-        /// </summary>
-        private List<Coordinates> GetSquareCoordiantes(List<Coordinates> mouseMoveCords)
-        {
-            int xLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
-            int yLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
-            if (mouseMoveCords[^1].Y > mouseMoveCords[0].Y)
-            {
-                xLength *= -1;
-            }
-
-            if (mouseMoveCords[^1].X > mouseMoveCords[0].X)
-            {
-                xLength *= -1;
-            }
-
-            mouseMoveCords[0] = new Coordinates(mouseMoveCords[^1].X + xLength, mouseMoveCords[^1].Y + yLength);
-            return mouseMoveCords;
-        }
-
-        private BitmapPixelChanges GetOldPixelsValues(Coordinates[] coordinates)
-        {
-            Dictionary<Coordinates, Color> values = new Dictionary<Coordinates, Color>();
-            using (Manager.ActiveLayer.LayerBitmap.GetBitmapContext(ReadWriteMode.ReadOnly))
-            {
-                Coordinates[] relativeCoords = Manager.ActiveLayer.ConvertToRelativeCoordinates(coordinates);
-                for (int i = 0; i < coordinates.Length; i++)
-                {
-                    values.Add(
-                        coordinates[i],
-                        Manager.ActiveLayer.GetPixel(relativeCoords[i].X, relativeCoords[i].Y));
-                }
-            }
-
-            return new BitmapPixelChanges(values);
-        }
-
-        private void UseToolOnPreviewLayer(List<Coordinates> mouseMove)
-        {
-            LayerChange[] modifiedLayers;
-            if (mouseMove.Count > 0 && mouseMove[0] != lastMousePos)
-            {
-                Manager.ActiveDocument.GeneratePreviewLayer();
-                modifiedLayers = ((BitmapOperationTool)Manager.SelectedTool).Use(
-                    Manager.ActiveDocument.ActiveLayer,
-                    mouseMove.ToArray(),
-                    Manager.PrimaryColor);
-                BitmapPixelChanges[] changes = modifiedLayers.Select(x => x.PixelChanges).ToArray();
-                Manager.ActiveDocument.PreviewLayer.SetPixels(BitmapPixelChanges.CombineOverride(changes));
-                lastModifiedLayers = modifiedLayers;
-            }
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using PixiEditor.Models.Undo;
+
+namespace PixiEditor.Models.Controllers
+{
+    public class BitmapOperationsUtility
+    {
+        public List<LayerChange> PreviewLayerChanges => previewLayerChanges;
+
+        private List<LayerChange> previewLayerChanges;
+
+        private Coordinates lastMousePos;
+
+        public BitmapOperationsUtility(BitmapManager manager)
+        {
+            Manager = manager;
+        }
+
+        public event EventHandler<BitmapChangedEventArgs> BitmapChanged;
+
+        public BitmapManager Manager { get; set; }
+
+        public void DeletePixels(Layer[] layers, Coordinates[] pixels)
+        {
+            if (Manager.ActiveDocument == null)
+            {
+                return;
+            }
+
+            BitmapPixelChanges changes = BitmapPixelChanges.FromSingleColoredArray(pixels, Color.FromArgb(0, 0, 0, 0));
+            Dictionary<Guid, Color[]> oldValues = BitmapUtils.GetPixelsForSelection(layers, pixels);
+            LayerChange[] old = new LayerChange[layers.Length];
+            LayerChange[] newChange = new LayerChange[layers.Length];
+            for (int i = 0; i < layers.Length; i++)
+            {
+                Guid guid = layers[i].LayerGuid;
+                old[i] = new LayerChange(
+                    BitmapPixelChanges.FromArrays(pixels, oldValues[layers[i].LayerGuid]), guid);
+                newChange[i] = new LayerChange(changes, guid);
+                layers[i].SetPixels(changes);
+            }
+
+            Manager.ActiveDocument.UndoManager.AddUndoChange(new Change("UndoChanges", old, newChange, "Deleted pixels"));
+        }
+
+        /// <summary>
+        ///     Executes tool Use() method with given parameters. NOTE: mouseMove is reversed inside function!.
+        /// </summary>
+        /// <param name="newPos">Most recent coordinates.</param>
+        /// <param name="mouseMove">Last mouse movement coordinates.</param>
+        /// <param name="tool">Tool to execute.</param>
+        public void ExecuteTool(Coordinates newPos, List<Coordinates> mouseMove, BitmapOperationTool tool)
+        {
+            if (Manager.ActiveDocument != null && tool != null)
+            {
+                if (Manager.ActiveDocument.Layers.Count == 0 || mouseMove.Count == 0)
+                {
+                    return;
+                }
+
+                mouseMove.Reverse();
+                UseTool(mouseMove, tool, Manager.PrimaryColor);
+
+                lastMousePos = newPos;
+            }
+        }
+
+        /// <summary>
+        ///     Applies pixels from preview layer to selected layer.
+        /// </summary>
+        public void ApplyPreviewLayer()
+        {
+            if (previewLayerChanges == null)
+            {
+                return;
+            }
+
+            foreach (var modifiedLayer in previewLayerChanges)
+            {
+                Layer layer = Manager.ActiveDocument.Layers.FirstOrDefault(x => x.LayerGuid == modifiedLayer.LayerGuid);
+
+                if (layer != null)
+                {
+                    BitmapPixelChanges oldValues = ApplyToLayer(layer, modifiedLayer).PixelChanges;
+
+                    BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
+                        modifiedLayer.PixelChanges,
+                        oldValues,
+                        modifiedLayer.LayerGuid));
+                    Manager.ActiveDocument.GeneratePreviewLayer();
+                }
+            }
+
+            previewLayerChanges = null;
+        }
+
+        private void UseTool(List<Coordinates> mouseMoveCords, BitmapOperationTool tool, Color color)
+        {
+            if (Keyboard.IsKeyDown(Key.LeftShift) && !MouseCordsNotInLine(mouseMoveCords))
+            {
+                mouseMoveCords = GetSquareCoordiantes(mouseMoveCords);
+            }
+
+            if (!tool.RequiresPreviewLayer)
+            {
+                LayerChange[] modifiedLayers = tool.Use(Manager.ActiveLayer, mouseMoveCords.ToArray(), color);
+                LayerChange[] oldPixelsValues = new LayerChange[modifiedLayers.Length];
+                for (int i = 0; i < modifiedLayers.Length; i++)
+                {
+                    Layer layer = Manager.ActiveDocument.Layers.First(x => x.LayerGuid == modifiedLayers[i].LayerGuid);
+                    oldPixelsValues[i] = ApplyToLayer(layer, modifiedLayers[i]);
+
+                    BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
+                        modifiedLayers[i].PixelChanges,
+                        oldPixelsValues[i].PixelChanges,
+                        modifiedLayers[i].LayerGuid));
+                }
+            }
+            else
+            {
+                UseToolOnPreviewLayer(mouseMoveCords, tool.ClearPreviewLayerOnEachIteration);
+            }
+        }
+
+        private LayerChange ApplyToLayer(Layer layer, LayerChange change)
+        {
+            layer.DynamicResize(change.PixelChanges);
+
+            LayerChange oldPixelsValues = new LayerChange(
+                GetOldPixelsValues(change.PixelChanges.ChangedPixels.Keys.ToArray()),
+                change.LayerGuid);
+
+            layer.SetPixels(change.PixelChanges, false);
+            return oldPixelsValues;
+        }
+
+        private bool MouseCordsNotInLine(List<Coordinates> cords)
+        {
+            return cords[0].X == cords[^1].X || cords[0].Y == cords[^1].Y;
+        }
+
+        /// <summary>
+        ///     Extracts square from rectangle mouse drag, used to draw symmetric shapes.
+        /// </summary>
+        private List<Coordinates> GetSquareCoordiantes(List<Coordinates> mouseMoveCords)
+        {
+            int xLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
+            int yLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
+            if (mouseMoveCords[^1].Y > mouseMoveCords[0].Y)
+            {
+                xLength *= -1;
+            }
+
+            if (mouseMoveCords[^1].X > mouseMoveCords[0].X)
+            {
+                xLength *= -1;
+            }
+
+            mouseMoveCords[0] = new Coordinates(mouseMoveCords[^1].X + xLength, mouseMoveCords[^1].Y + yLength);
+            return mouseMoveCords;
+        }
+
+        private BitmapPixelChanges GetOldPixelsValues(Coordinates[] coordinates)
+        {
+            Dictionary<Coordinates, Color> values = new Dictionary<Coordinates, Color>();
+            using (Manager.ActiveLayer.LayerBitmap.GetBitmapContext(ReadWriteMode.ReadOnly))
+            {
+                Coordinates[] relativeCoords = Manager.ActiveLayer.ConvertToRelativeCoordinates(coordinates);
+                for (int i = 0; i < coordinates.Length; i++)
+                {
+                    values.Add(
+                        coordinates[i],
+                        Manager.ActiveLayer.GetPixel(relativeCoords[i].X, relativeCoords[i].Y));
+                }
+            }
+
+            return new BitmapPixelChanges(values);
+        }
+
+        private void UseToolOnPreviewLayer(List<Coordinates> mouseMove, bool clearPreviewLayer = true)
+        {
+            LayerChange[] modifiedLayers;
+            if (mouseMove.Count > 0 && mouseMove[0] != lastMousePos)
+            {
+                if (clearPreviewLayer || Manager.ActiveDocument.PreviewLayer == null)
+                {
+                    Manager.ActiveDocument.GeneratePreviewLayer();
+                }
+
+                modifiedLayers = ((BitmapOperationTool)Manager.SelectedTool).Use(
+                    Manager.ActiveDocument.ActiveLayer,
+                    mouseMove.ToArray(),
+                    Manager.PrimaryColor);
+
+                BitmapPixelChanges[] changes = modifiedLayers.Select(x => x.PixelChanges).ToArray();
+                Manager.ActiveDocument.PreviewLayer.SetPixels(BitmapPixelChanges.CombineOverride(changes));
+
+                if (clearPreviewLayer || previewLayerChanges == null)
+                {
+                    previewLayerChanges = new List<LayerChange>(modifiedLayers);
+                }
+                else
+                {
+                    InjectPreviewLayerChanges(modifiedLayers);
+                }
+            }
+        }
+
+        private void InjectPreviewLayerChanges(LayerChange[] modifiedLayers)
+        {
+            for (int i = 0; i < modifiedLayers.Length; i++)
+            {
+                var layer = previewLayerChanges.First(x => x.LayerGuid == modifiedLayers[i].LayerGuid);
+                layer.PixelChanges.ChangedPixels.AddRangeOverride(modifiedLayers[i].PixelChanges.ChangedPixels);
+                layer.PixelChanges = layer.PixelChanges.WithoutTransparentPixels();
+            }
+        }
+    }
 }

+ 6 - 0
PixiEditor/Models/DataHolders/BitmapPixelChanges.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
@@ -87,5 +88,10 @@ namespace PixiEditor.Models.DataHolders
 
             return new BitmapPixelChanges(dict);
         }
+
+        public BitmapPixelChanges WithoutTransparentPixels()
+        {
+            return new BitmapPixelChanges(ChangedPixels.Where(x => x.Value.A > 0).ToDictionary(y => y.Key, y => y.Value));
+        }
     }
 }

+ 2 - 0
PixiEditor/Models/Tools/BitmapOperationTool.cs

@@ -9,6 +9,8 @@ namespace PixiEditor.Models.Tools
     public abstract class BitmapOperationTool : Tool
     {
         public bool RequiresPreviewLayer { get; set; }
+
+        public bool ClearPreviewLayerOnEachIteration { get; set; } = true;
 
         public bool UseDefaultUndoMethod { get; set; } = true;
 

+ 1 - 0
PixiEditor/Models/Tools/ShapeTool.cs

@@ -18,6 +18,7 @@ namespace PixiEditor.Models.Tools
             Toolbar = new BasicShapeToolbar();
         }
 
+        // TODO: Add cache for lines 31, 32 (hopefully it would speed up calculation)
         public abstract override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color);
 
         protected IEnumerable<Coordinates> GetThickShape(IEnumerable<Coordinates> shape, int thickness)

+ 17 - 0
PixiEditor/Models/Tools/ToolSettings/SettingValueChangedEventArgs.cs

@@ -0,0 +1,17 @@
+using System;
+
+namespace PixiEditor.Models.Tools.ToolSettings
+{
+    public class SettingValueChangedEventArgs<T> : EventArgs
+    {
+        public T OldValue { get; set; }
+
+        public T NewValue { get; set; }
+
+        public SettingValueChangedEventArgs(T oldValue, T newValue)
+        {
+            OldValue = oldValue;
+            NewValue = newValue;
+        }
+    }
+}

+ 12 - 2
PixiEditor/Models/Tools/ToolSettings/Settings/Setting.cs

@@ -1,4 +1,5 @@
-using System.Windows.Controls;
+using System;
+using System.Windows.Controls;
 using PixiEditor.Helpers;
 
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
@@ -14,13 +15,22 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
         {
         }
 
+        public event EventHandler<SettingValueChangedEventArgs<T>> ValueChanged;
+
         public new T Value
         {
             get => (T)base.Value;
             set
             {
+                T oldValue = default;
+                if (base.Value != null)
+                {
+                    oldValue = Value;
+                }
+
                 base.Value = value;
-                RaisePropertyChanged("Value");
+                ValueChanged?.Invoke(this, new SettingValueChangedEventArgs<T>(oldValue, Value));
+                RaisePropertyChanged(nameof(Value));
             }
         }
     }

+ 17 - 0
PixiEditor/Models/Tools/ToolSettings/Toolbars/PenToolbar.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using PixiEditor.Models.Tools.ToolSettings.Settings;
+
+namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
+{
+    public class PenToolbar : BasicToolbar
+    {
+        public PenToolbar()
+        {
+            Settings.Add(new BoolSetting("PixelPerfectEnabled", "Pixel perfect"));
+        }
+    }
+}

+ 2 - 2
PixiEditor/Models/Tools/ToolSettings/Toolbars/Toolbar.cs

@@ -16,7 +16,7 @@ namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
         ///     Gets setting in toolbar by name.
         /// </summary>
         /// <param name="name">Setting name, non case sensitive.</param>
-        /// <returns></returns>
+        /// <returns>Generic Setting.</returns>
         public virtual Setting GetSetting(string name)
         {
             return Settings.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.CurrentCultureIgnoreCase));
@@ -26,7 +26,7 @@ namespace PixiEditor.Models.Tools.ToolSettings.Toolbars
         ///     Gets setting of given type T in toolbar by name.
         /// </summary>
         /// <param name="name">Setting name, non case sensitive.</param>
-        /// <returns></returns>
+        /// <returns>Setting of given type.</returns>
         public T GetSetting<T>(string name)
             where T : Setting
         {

+ 116 - 7
PixiEditor/Models/Tools/Tools/PenTool.cs

@@ -1,40 +1,149 @@
-using System.Windows.Input;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Input;
 using System.Windows.Media;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools.ToolSettings;
 using PixiEditor.Models.Tools.ToolSettings.Settings;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Tools.Tools
 {
-    public class PenTool : BitmapOperationTool
+    public class PenTool : ShapeTool
     {
         private readonly SizeSetting toolSizeSetting;
+        private readonly BoolSetting pixelPerfectSetting;
         private readonly LineTool lineTool;
+        private Coordinates[] lastChangedPixels = new Coordinates[3];
+        private List<Coordinates> confirmedPixels = new List<Coordinates>();
+        private byte changedPixelsindex = 0;
 
         public PenTool()
         {
             Cursor = Cursors.Pen;
             ActionDisplay = "Click and move to draw.";
             Tooltip = "Standard brush. (B)";
-            Toolbar = new BasicToolbar();
+            Toolbar = new PenToolbar();
             toolSizeSetting = Toolbar.GetSetting<SizeSetting>("ToolSize");
+            pixelPerfectSetting = Toolbar.GetSetting<BoolSetting>("PixelPerfectEnabled");
+            pixelPerfectSetting.ValueChanged += PixelPerfectSettingValueChanged;
             lineTool = new LineTool();
+            ClearPreviewLayerOnEachIteration = false;
+        }
+
+        public override void OnRecordingLeftMouseDown(MouseEventArgs e)
+        {
+            base.OnRecordingLeftMouseDown(e);
+            changedPixelsindex = 0;
+            lastChangedPixels = new Coordinates[3];
+            confirmedPixels.Clear();
         }
 
         public override LayerChange[] Use(Layer layer, Coordinates[] coordinates, Color color)
         {
             Coordinates startingCords = coordinates.Length > 1 ? coordinates[1] : coordinates[0];
-            BitmapPixelChanges pixels = Draw(startingCords, coordinates[0], color, toolSizeSetting.Value);
+            BitmapPixelChanges pixels = Draw(
+                startingCords, 
+                coordinates[0], 
+                color, 
+                toolSizeSetting.Value, 
+                pixelPerfectSetting.Value,
+                ViewModelMain.Current.BitmapManager.ActiveDocument.PreviewLayer);
             return Only(pixels, layer);
         }
 
-        public BitmapPixelChanges Draw(Coordinates startingCoords, Coordinates latestCords, Color color, int toolSize)
+        public BitmapPixelChanges Draw(Coordinates startingCoords, Coordinates latestCords, Color color, int toolSize, bool pixelPerfect = false, Layer previewLayer = null)
+        {
+            if (!pixelPerfect)
+            {
+                return BitmapPixelChanges.FromSingleColoredArray(
+                    lineTool.CreateLine(startingCoords, latestCords, toolSize), color);
+            }
+
+            if (previewLayer != null && previewLayer.GetPixelWithOffset(latestCords.X, latestCords.Y).A > 0)
+            {
+                confirmedPixels.Add(latestCords);
+            }
+
+            var latestPixels = lineTool.CreateLine(startingCoords, latestCords, 1);
+            SetPixelToCheck(latestPixels);
+
+            if (changedPixelsindex == 2)
+            {
+                var changes = ApplyPixelPerfectToPixels(
+                    lastChangedPixels[0],
+                    lastChangedPixels[1],
+                    lastChangedPixels[2],
+                    color,
+                    toolSize);
+
+                MovePixelsToCheck(changes);
+
+                changes.ChangedPixels.AddRangeNewOnly(
+                    BitmapPixelChanges.FromSingleColoredArray(GetThickShape(latestPixels, toolSize), color).ChangedPixels);
+
+                return changes;
+            }
+
+            changedPixelsindex += changedPixelsindex >= 2 ? 0 : 1;
+
+            var result = BitmapPixelChanges.FromSingleColoredArray(GetThickShape(latestPixels, toolSize), color);
+
+            return result;
+        }
+
+        private void MovePixelsToCheck(BitmapPixelChanges changes)
+        {
+            if (changes.ChangedPixels[lastChangedPixels[1]].A != 0)
+            {
+                lastChangedPixels[0] = lastChangedPixels[1];
+                lastChangedPixels[1] = lastChangedPixels[2];
+                changedPixelsindex = 2;
+            }
+            else
+            {
+                lastChangedPixels[0] = lastChangedPixels[2];
+                changedPixelsindex = 1;
+            }
+        }
+
+        private void SetPixelToCheck(IEnumerable<Coordinates> latestPixels)
+        {
+            if (latestPixels.Count() == 1)
+            {
+                lastChangedPixels[changedPixelsindex] = latestPixels.First();
+            }
+            else
+            {
+                lastChangedPixels[changedPixelsindex] = latestPixels.ElementAt(1);
+            }
+        }
+
+        private BitmapPixelChanges ApplyPixelPerfectToPixels(Coordinates p1, Coordinates p2, Coordinates p3, Color color, int toolSize)
+        {
+            if (Math.Abs(p3.X - p1.X) == 1 && Math.Abs(p3.Y - p1.Y) == 1 && !confirmedPixels.Contains(p2))
+            {
+                var changes = BitmapPixelChanges.FromSingleColoredArray(GetThickShape(new Coordinates[] { p1, p3 }, toolSize), color);
+                changes.ChangedPixels.AddRangeNewOnly(
+                    BitmapPixelChanges.FromSingleColoredArray(
+                        GetThickShape(new[] { p2 }, toolSize),
+                        System.Windows.Media.Colors.Transparent).ChangedPixels);
+                return changes;
+            }
+
+            return BitmapPixelChanges.FromSingleColoredArray(GetThickShape(new Coordinates[] { p2, p3 }.Distinct(), toolSize), color);
+        }
+
+        private void PixelPerfectSettingValueChanged(object sender, SettingValueChangedEventArgs<bool> e)
         {
-            return BitmapPixelChanges.FromSingleColoredArray(
-                lineTool.CreateLine(startingCoords, latestCords, toolSize), color);
+            RequiresPreviewLayer = e.NewValue;
         }
     }
 }

+ 27 - 0
PixiEditorTests/ModelsTests/ToolsTests/PenToolTests.cs

@@ -0,0 +1,27 @@
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools.Tools;
+using Xunit;
+
+namespace PixiEditorTests.ModelsTests.ToolsTests
+{
+    [Collection("Application collection")]
+    public class PenToolTests
+    {
+        [StaFact]
+        public void TestThatPixelPerfectPenReturnsShapeWithoutLShapePixels()
+        {
+            PenTool pen = new PenTool();
+
+            Coordinates start = new Coordinates(0, 0);
+            Coordinates end = new Coordinates(0, 0);
+            Coordinates end2 = new Coordinates(1, 0);
+            Coordinates start2 = new Coordinates(1, 1);
+
+            pen.Draw(start, end, System.Windows.Media.Colors.Black, 1, true);
+            pen.Draw(end, end2, System.Windows.Media.Colors.Black, 1, true);
+            var points = pen.Draw(end2, start2, System.Windows.Media.Colors.Black, 1, true);
+
+            Assert.Contains(points.ChangedPixels, x => x.Value.A == 0);
+        }
+    }
+}