Browse Source

Merge branch 'master' into fixes/03.04.2025

Krzysztof Krysiński 4 months ago
parent
commit
97ad2f72f5
63 changed files with 683 additions and 238 deletions
  1. 6 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  2. 14 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  3. 3 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  4. 61 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/BoolOperationNode.cs
  5. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs
  6. 5 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs
  7. 5 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  8. 53 10
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs
  9. 20 8
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterLine_UpdateableChange.cs
  10. 50 11
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  11. 1 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PixelPerfectPen_UpdateableChange.cs
  12. 41 22
      src/PixiEditor.UpdateModule/UpdateChecker.cs
  13. 9 5
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  14. 13 3
      src/PixiEditor/Data/Localization/Languages/en.json
  15. 6 3
      src/PixiEditor/Helpers/CrashHelper.cs
  16. 20 5
      src/PixiEditor/Models/AnalyticsAPI/AnalyticsPeriodicReporter.cs
  17. 5 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/BrightnessToolExecutor.cs
  18. 5 6
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  19. 5 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs
  20. 4 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  21. 4 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs
  22. 16 29
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterLineToolExecutor.cs
  23. 4 1
      src/PixiEditor/Models/Handlers/Toolbars/IPenToolbar.cs
  24. 7 0
      src/PixiEditor/Models/Handlers/Toolbars/PaintBrushShape.cs
  25. 1 0
      src/PixiEditor/Models/Handlers/Tools/IBrightnessToolHandler.cs
  26. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  27. 18 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  28. 11 0
      src/PixiEditor/ViewModels/Document/Nodes/Shapes/BoolOperationNodeViewModel.cs
  29. 1 1
      src/PixiEditor/ViewModels/Tools/ShapeTool.cs
  30. 8 0
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/PenToolbar.cs
  31. 1 1
      src/PixiEditor/ViewModels/Tools/ToolViewModel.cs
  32. 18 1
      src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs
  33. 13 1
      src/PixiEditor/ViewModels/Tools/Tools/ColorPickerToolViewModel.cs
  34. 19 2
      src/PixiEditor/ViewModels/Tools/Tools/EraserToolViewModel.cs
  35. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/FloodFillToolViewModel.cs
  36. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/LassoToolViewModel.cs
  37. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/MagicWandToolViewModel.cs
  38. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs
  39. 8 1
      src/PixiEditor/ViewModels/Tools/Tools/MoveViewportToolViewModel.cs
  40. 26 3
      src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs
  41. 11 6
      src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs
  42. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RotateViewportToolViewModel.cs
  43. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/SelectToolViewModel.cs
  44. 0 2
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  45. 11 7
      src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs
  46. 0 2
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  47. 0 2
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  48. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/ZoomToolViewModel.cs
  49. 27 42
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml
  50. 13 1
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  51. 13 3
      src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs
  52. 20 1
      src/PixiEditor/Views/Overlays/Overlay.cs
  53. 1 1
      src/PixiEditor/Views/Overlays/SnappingOverlay.cs
  54. 7 2
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  55. 1 7
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformUpdateHelper.cs
  56. 1 0
      src/PixiEditor/Views/Rendering/Scene.cs
  57. 66 0
      tests/PixiEditor.Tests/BlendingTests.cs
  58. 1 12
      tests/PixiEditor.Tests/EditableVectorPathTests.cs
  59. 19 0
      tests/PixiEditor.Tests/PixiEditorTest.cs
  60. BIN
      tests/SampleFiles/Blending/FolderFolderBlend.pixi
  61. BIN
      tests/SampleFiles/Blending/LayerFolderBlend.pixi
  62. BIN
      tests/SampleFiles/Blending/TwoLayersBlend.pixi
  63. BIN
      tests/SampleFiles/Nodes/VectorRasterMergeNode.pixi

+ 6 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -123,6 +123,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         bool useFilters)
     {
         blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
+        var finalPaint = blendPaint;
 
         var targetSurface = workingSurface;
         Texture? tex = null;
@@ -135,21 +136,23 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 ColorSpace.CreateSrgb());
             targetSurface = tex.DrawingSurface;
             workingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+
+            finalPaint = new Paint();
         }
         if (useFilters && Filters.Value != null)
         {
             blendPaint.SetFilters(Filters.Value);
-            DrawWithFilters(ctx, targetSurface, blendPaint);
+            DrawWithFilters(ctx, targetSurface, finalPaint);
         }
         else
         {
             blendPaint.SetFilters(null);
-            DrawWithoutFilters(ctx, targetSurface, blendPaint);
+            DrawWithoutFilters(ctx, targetSurface, finalPaint);
         }
 
         if (targetSurface != workingSurface)
         {
-            workingSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+            workingSurface.Canvas.DrawSurface(targetSurface, 0, 0, blendPaint);
             tex.Dispose();
             workingSurface.Canvas.RestoreToCount(saved);
         }

+ 14 - 13
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -17,11 +17,11 @@ public class MergeNode : RenderNode
     public RenderInputProperty Bottom { get; }
 
     private Paint paint = new Paint();
-    
+
     private int topLayer;
     private int bottomLayer;
-    
-    public MergeNode() 
+
+    public MergeNode()
     {
         BlendMode = CreateInput("BlendMode", "BlendMode", Enums.BlendMode.Normal);
         Top = CreateRenderInput("Top", "TOP");
@@ -36,16 +36,17 @@ public class MergeNode : RenderNode
 
     protected override void OnPaint(RenderContext context, DrawingSurface target)
     {
-        if(Top.Value == null && Bottom.Value == null)
+        if (Top.Value == null && Bottom.Value == null)
         {
             return;
         }
-        
-        if(target == null || target.DeviceClipBounds.Size == VecI.Zero)
+
+        if (target == null || target.DeviceClipBounds.Size == VecI.Zero)
         {
             return;
         }
 
+        AllowHighDpiRendering = true;
         Merge(target, context);
     }
 
@@ -59,7 +60,7 @@ public class MergeNode : RenderNode
 
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             target.Canvas.SaveLayer(paint);
-            
+
             Top.Value?.Paint(context, target);
             target.Canvas.RestoreToCount(saved);
             return;
@@ -71,13 +72,13 @@ public class MergeNode : RenderNode
 
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
-        if(Top.Value == null && Bottom.Value == null)
+        if (Top.Value == null && Bottom.Value == null)
         {
             return null;
         }
 
-        RectD? totalBounds = null; 
-        
+        RectD? totalBounds = null;
+
         if (Top.Connection != null && Top.Connection.Node is IPreviewRenderable topPreview)
         {
             var topBounds = topPreview.GetPreviewBounds(frame, elementToRenderName);
@@ -86,16 +87,16 @@ public class MergeNode : RenderNode
                 totalBounds = totalBounds?.Union(topBounds.Value) ?? topBounds;
             }
         }
-        
+
         if (Bottom.Connection != null && Bottom.Connection.Node is IPreviewRenderable bottomPreview)
         {
             var bottomBounds = bottomPreview.GetPreviewBounds(frame, elementToRenderName);
             if (bottomBounds != null)
             {
                 totalBounds = totalBounds?.Union(bottomBounds.Value) ?? bottomBounds;
-            } 
+            }
         }
-        
+
         return totalBounds;
     }
 

+ 3 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -1,5 +1,6 @@
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
@@ -41,7 +42,7 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
         lastDocumentSize = context.DocumentSize;
     }
 
-    private void Paint(RenderContext context, DrawingSurface surface)
+    protected virtual void Paint(RenderContext context, DrawingSurface surface)
     {
         DrawingSurface target = surface;
         bool useIntermediate = !AllowHighDpiRendering
@@ -49,7 +50,7 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
                                && surface.DeviceClipBounds.Size != context.DocumentSize;
         if (useIntermediate)
         {
-            Texture intermediate = textureCache.RequestTexture(0, context.DocumentSize, context.ProcessingColorSpace);
+            Texture intermediate = textureCache.RequestTexture(-6451, context.DocumentSize, context.ProcessingColorSpace);
             target = intermediate.DrawingSurface;
         }
 

+ 61 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/BoolOperationNode.cs

@@ -0,0 +1,61 @@
+using Drawie.Backend.Core.Vector;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+
+[NodeInfo("BoolOperation")]
+public class BoolOperationNode : Node
+{
+    public InputProperty<ShapeVectorData> ShapeA { get; }
+    public InputProperty<ShapeVectorData> ShapeB { get; }
+    public InputProperty<VectorPathOp> Operation { get; }
+    public OutputProperty<ShapeVectorData> Result { get; }
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
+
+    public BoolOperationNode()
+    {
+        ShapeA = CreateInput<ShapeVectorData>("ShapeA", "FIRST_SHAPE", null);
+        ShapeB = CreateInput<ShapeVectorData>("ShapeB", "SECOND_SHAPE", null);
+        Operation = CreateInput<VectorPathOp>("Operation", "OPERATION", VectorPathOp.Union);
+        Result = CreateOutput<ShapeVectorData>("Result", "RESULT", null);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        if (ShapeA.Value == null && ShapeB.Value == null)
+        {
+            Result.Value = null;
+            return;
+        }
+
+        if (ShapeA.Value == null)
+        {
+            Result.Value = ShapeB.Value;
+            return;
+        }
+
+        if (ShapeB.Value == null)
+        {
+            Result.Value = ShapeA.Value;
+            return;
+        }
+
+        ShapeVectorData shapeA = ShapeA.Value;
+        ShapeVectorData shapeB = ShapeB.Value;
+
+        Result.Value = new PathVectorData(shapeA.ToPath(true).Op(shapeB.ToPath(true), Operation.Value))
+        {
+            Fill = shapeA.Fill,
+            Stroke = shapeA.Stroke,
+            StrokeWidth = shapeA.StrokeWidth,
+            FillPaintable = shapeA.FillPaintable,
+        };
+    }
+
+    public override Node CreateCopy()
+    {
+        return new BoolOperationNode();
+    }
+}

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs

@@ -11,6 +11,8 @@ public class DistributePointsNode : ShapeNode<PointsVectorData>
 
     public InputProperty<int> Seed { get; }
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
+
     public DistributePointsNode()
     {
         MaxPointCount = CreateInput("MaxPointCount", "MAX_POINTS", 10).

+ 5 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using System.Diagnostics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Numerics;
@@ -14,6 +15,8 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
 
     public InputProperty<int> Seed { get; }
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
+
     public RemoveClosePointsNode()
     {
         Input = CreateInput<PointsVectorData>("Input", "POINTS", null);
@@ -33,7 +36,7 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
             return null;
         }
 
-        var availablePoints = data.Points.Distinct().ToList();
+        var availablePoints = data.Points.ToList();
         List<VecD> newPoints = new List<VecD>();
 
         var random = new Random(Seed.Value);

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -124,6 +124,11 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         }
     }
 
+    protected override void Paint(RenderContext context, DrawingSurface surface)
+    {
+        OnPaint(context, surface);
+    }
+
     protected override void OnPaint(RenderContext context, DrawingSurface renderTarget)
     {
         if (Output.Connections.Count > 0)

+ 53 - 10
src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs

@@ -16,13 +16,16 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
     private readonly bool repeat;
     private int frame;
     private int lastAppliedPointIndex = -1;
+    private bool squareBrush;
 
     private List<VecI> ellipseLines;
+    private RectI squareRect;
 
     private CommittedChunkStorage? savedChunks;
 
     [GenerateUpdateableChangeActions]
     public ChangeBrightness_UpdateableChange(Guid layerGuid, VecI pos, float correctionFactor, int strokeWidth,
+        bool square,
         bool repeat, int frame)
     {
         this.layerGuid = layerGuid;
@@ -30,11 +33,16 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
         this.strokeWidth = strokeWidth;
         this.repeat = repeat;
         this.frame = frame;
+        this.squareBrush = square;
         positions.Add(pos);
 
-        ellipseLines =
-            EllipseHelper.SplitEllipseIntoLines(
-                (EllipseHelper.GenerateEllipseFromRect(new RectI(0, 0, strokeWidth, strokeWidth), 0)));
+        squareRect = new RectI(0, 0, strokeWidth, strokeWidth);
+        if (!square)
+        {
+            ellipseLines =
+                EllipseHelper.SplitEllipseIntoLines(
+                    (EllipseHelper.GenerateEllipseFromRect(squareRect, 0)));
+        }
     }
 
     [UpdateChangeMethod]
@@ -67,8 +75,16 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
         for (int i = Math.Max(lastAppliedPointIndex, 0); i < positions.Count; i++)
         {
             VecI pos = positions[i];
-            ChangeBrightness(ellipseLines, strokeWidth, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
-                layerImage);
+            if (squareBrush)
+            {
+                ChangeBrightness(squareRect, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
+                    layerImage);
+            }
+            else
+            {
+                ChangeBrightness(ellipseLines, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
+                    layerImage);
+            }
         }
 
         var affected = layerImage.FindAffectedArea(queueLength);
@@ -79,11 +95,9 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
     }
 
     private static void ChangeBrightness(
-        List<VecI> circleLines, int circleDiameter, VecI offset, float correctionFactor, bool repeat,
+        List<VecI> circleLines, VecI offset, float correctionFactor, bool repeat,
         ChunkyImage layerImage)
     {
-        // TODO: Circle diameter is unused, check if it should be used
-
         for (var i = 0; i < circleLines.Count - 1; i++)
         {
             VecI left = circleLines[i];
@@ -105,6 +119,27 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
         }
     }
 
+    private static void ChangeBrightness(
+        RectI square, VecI offset, float correctionFactor, bool repeat,
+        ChunkyImage layerImage)
+    {
+        for (int i = square.X; i < square.X + square.Width; i++)
+        {
+            for (int j = square.Y; j < square.Y + square.Height; j++)
+            {
+                layerImage.EnqueueDrawPixel(
+                    new VecI(i, j) + offset,
+                    (commitedColor, upToDateColor) =>
+                    {
+                        Color newColor = ColorHelper.ChangeColorBrightness(repeat ? upToDateColor : commitedColor,
+                            correctionFactor);
+                        return ColorHelper.ChangeColorBrightness(newColor, correctionFactor);
+                    },
+                    BlendMode.Src);
+            }
+        }
+    }
+
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
     {
@@ -121,8 +156,16 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, layerImage, layerGuid, false);
             foreach (VecI pos in positions)
             {
-                ChangeBrightness(ellipseLines, strokeWidth, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
-                    layerImage);
+                if (squareBrush)
+                {
+                    ChangeBrightness(squareRect, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
+                        layerImage);
+                }
+                else
+                {
+                    ChangeBrightness(ellipseLines, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
+                        layerImage);
+                }
             }
         }
 

+ 20 - 8
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterLine_UpdateableChange.cs

@@ -22,7 +22,8 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
 
     [GenerateUpdateableChangeActions]
     public DrawRasterLine_UpdateableChange
-        (Guid memberGuid, VecD from, VecD to, float strokeWidth, Paintable paintable, StrokeCap caps, bool antiAliasing, bool drawOnMask, int frame)
+    (Guid memberGuid, VecD from, VecD to, float strokeWidth, Paintable paintable, StrokeCap caps, bool antiAliasing,
+        bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.from = from;
@@ -34,8 +35,10 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         this.frame = frame;
         this.antiAliasing = antiAliasing;
 
-        paint = new Paint() {
-            StrokeWidth = strokeWidth, StrokeCap = caps, IsAntiAliased = antiAliasing, BlendMode = BlendMode.SrcOver };
+        paint = new Paint()
+        {
+            StrokeWidth = strokeWidth, StrokeCap = caps, IsAntiAliased = antiAliasing, BlendMode = BlendMode.SrcOver
+        };
         paint.SetPaintable(paintable);
     }
 
@@ -47,7 +50,7 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         this.paintable = paintable;
         this.caps = caps;
         this.strokeWidth = strokeWidth;
-        
+
         paint.SetPaintable(paintable);
         paint.StrokeWidth = strokeWidth;
         paint.StrokeCap = caps;
@@ -66,15 +69,23 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         if (from != to)
         {
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
-            if (Math.Abs(strokeWidth - 1) < 0.01f && !antiAliasing)
+            if (strokeWidth == 0)
             {
-                image.EnqueueDrawBresenhamLine((VecI)from, (VecI)to, paintable, BlendMode.SrcOver);
+                image.CancelChanges();
             }
             else
             {
-                image.EnqueueDrawSkiaLine(from, to, paint);
+                if (Math.Abs(strokeWidth - 1) < 0.01f && !antiAliasing)
+                {
+                    image.EnqueueDrawBresenhamLine((VecI)from, (VecI)to, paintable, BlendMode.SrcOver);
+                }
+                else
+                {
+                    image.EnqueueDrawSkiaLine(from, to, paint);
+                }
             }
         }
+
         var totalAffected = image.FindAffectedArea();
         totalAffected.UnionWith(oldAffected);
         return totalAffected;
@@ -85,7 +96,8 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, CommonApply(target), drawOnMask);
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         if (from == to)
         {

+ 50 - 11
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -17,6 +17,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     private readonly bool erasing;
     private readonly bool drawOnMask;
     private readonly bool antiAliasing;
+    private bool squareBrush;
     private float hardness;
     private float spacing = 1;
     private readonly Paint srcPaint = new Paint() { BlendMode = BlendMode.Src };
@@ -32,6 +33,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         bool antiAliasing,
         float hardness,
         float spacing,
+        bool squareBrush,
         bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
@@ -42,6 +44,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         this.drawOnMask = drawOnMask;
         this.hardness = hardness;
         this.spacing = spacing;
+        this.squareBrush = squareBrush;
         points.Add(pos);
         this.frame = frame;
 
@@ -101,12 +104,23 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             lastPos = point;
             var rect = new RectI(point - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             Paintable finalPaintable = color;
-            if (antiAliasing)
+
+            if (!squareBrush)
             {
-                finalPaintable = ApplySoftnessGradient((VecD)point);
-            }
+                if (antiAliasing)
+                {
+                    finalPaintable = ApplySoftnessGradient((VecD)point);
+                }
 
-            image.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing, srcPaint);
+                image.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing, srcPaint);
+            }
+            else
+            {
+                BlendMode blendMode = srcPaint.BlendMode;
+                ShapeData shapeData = new ShapeData(rect.Center, rect.Size, 0, 0, 0, finalPaintable, finalPaintable,
+                    blendMode);
+                image.EnqueueDrawRectangle(shapeData);
+            }
         }
 
         lastAppliedPointIndex = points.Count - 1;
@@ -122,12 +136,25 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         {
             var rect = new RectI(points[0] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             Paintable finalPaintable = color;
-            if (antiAliasing)
+
+            if (!squareBrush)
+            {
+                if (antiAliasing)
+                {
+                    finalPaintable = ApplySoftnessGradient(points[0]);
+                }
+
+                targetImage.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing,
+                    srcPaint);
+            }
+            else
             {
-                finalPaintable = ApplySoftnessGradient(points[0]);
+                BlendMode blendMode = srcPaint.BlendMode;
+                ShapeData shapeData = new ShapeData(rect.Center, rect.Size, 0, 0, 0, finalPaintable, finalPaintable,
+                    blendMode);
+                targetImage.EnqueueDrawRectangle(shapeData);
             }
 
-            targetImage.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing, srcPaint);
             return;
         }
 
@@ -143,12 +170,24 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             lastPos = points[i];
             var rect = new RectI(points[i] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             Paintable? finalPaintable = color;
-            if (antiAliasing)
+
+            if (!squareBrush)
             {
-                finalPaintable = ApplySoftnessGradient(points[i]);
-            }
+                if (antiAliasing)
+                {
+                    finalPaintable = ApplySoftnessGradient(points[i]);
+                }
 
-            targetImage.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing, srcPaint);
+                targetImage.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing,
+                    srcPaint);
+            }
+            else
+            {
+                BlendMode blendMode = srcPaint.BlendMode;
+                ShapeData shapeData = new ShapeData(rect.Center, rect.Size, 0, 0, 0, finalPaintable, finalPaintable,
+                    blendMode);
+                targetImage.EnqueueDrawRectangle(shapeData);
+            }
         }
     }
 

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/PixelPerfectPen_UpdateableChange.cs

@@ -78,6 +78,7 @@ internal class PixelPerfectPen_UpdateableChange : UpdateableChange
         {
             pixelsToConfirm.Add(pixel);
         }
+
         image.EnqueueDrawPixels(line.Select(point => new VecI((int)point.X, (int)point.Y)), color, BlendMode.Src);
 
         if (pointsCount >= 3 && IsLShape(pointsCount - 1) && !confirmedPixels.Contains(incomingPoints[pointsCount - 2]))

+ 41 - 22
src/PixiEditor.UpdateModule/UpdateChecker.cs

@@ -19,7 +19,8 @@ public class UpdateChecker
     public ReleaseInfo LatestReleaseInfo { get; private set; }
 
     private UpdateChannel _channel;
-    public UpdateChannel Channel 
+
+    public UpdateChannel Channel
     {
         get => _channel;
         set
@@ -45,7 +46,7 @@ public class UpdateChecker
     {
         return ExtractVersionString(originalVer) != ExtractVersionString(newVer);
     }
-    
+
     /// <summary>
     ///     Checks if originalVer is smaller than newVer
     /// </summary>
@@ -59,24 +60,17 @@ public class UpdateChecker
 
         if (normalizedOriginal == normalizedNew) return false;
 
-        bool parsed = TryParseToFloatVersion(normalizedOriginal, out float orgFloat);
-        if (!parsed) throw new Exception($"Couldn't parse version {originalVer} to float.");
-
-        parsed = TryParseToFloatVersion(normalizedNew, out float newFloat);
-        if (!parsed) throw new Exception($"Couldn't parse version {newVer} to float.");
-
-        return orgFloat < newFloat;
-    }
+        if (!Version.TryParse(normalizedOriginal, out Version original))
+        {
+            throw new ArgumentException($"Invalid version string: {normalizedOriginal}");
+        }
 
-    private static bool TryParseToFloatVersion(string normalizedString, out float ver)
-    {
-        if (string.IsNullOrEmpty(normalizedString))
+        if (!Version.TryParse(normalizedNew, out Version newVersion))
         {
-            ver = 0;
-            return false;
+            throw new ArgumentException($"Invalid version string: {normalizedNew}");
         }
-        
-        return float.TryParse(normalizedString.Replace(".", string.Empty).Insert(1, "."), NumberStyles.Any, CultureInfo.InvariantCulture, out ver);
+
+        return original < newVersion;
     }
 
     public async Task<bool> CheckUpdateAvailable()
@@ -89,20 +83,45 @@ public class UpdateChecker
     {
         if (latestRelease == null || string.IsNullOrEmpty(latestRelease.TagName)) return false;
         if (CurrentVersionTag == null) return false;
-        
+
         return latestRelease.WasDataFetchSuccessful && VersionDifferent(CurrentVersionTag, latestRelease.TagName);
     }
 
     public bool IsUpdateCompatible(string[] incompatibleVersions)
     {
-        return !incompatibleVersions.Select(x => x.Trim()).Contains(ExtractVersionString(CurrentVersionTag));
+        string extractedVersion = ExtractVersionString(CurrentVersionTag);
+        bool containsVersion = incompatibleVersions.Select(x => x.Trim()).Contains(extractedVersion);
+        if (containsVersion)
+        {
+            return false;
+        }
+
+        Version biggestIncompatibleVersion = incompatibleVersions
+            .Select(x => Version.TryParse(ExtractVersionString(x), out Version version) ? version : null)
+            .Where(x => x != null)
+            .OrderByDescending(x => x)
+            .FirstOrDefault();
+
+        if (biggestIncompatibleVersion == null)
+        {
+            return true;
+        }
+
+        Version currentVersion = Version.TryParse(ExtractVersionString(CurrentVersionTag), out Version version) ? version : null;
+
+        bool biggestVersionBiggerThanCurrent =
+            biggestIncompatibleVersion >= currentVersion;
+
+        return !biggestVersionBiggerThanCurrent;
     }
 
     public async Task<bool> IsUpdateCompatible()
     {
         string[] incompatibleVersions = await GetUpdateIncompatibleVersionsAsync(LatestReleaseInfo.TagName);
         bool isDowngrading = VersionSmaller(LatestReleaseInfo.TagName, CurrentVersionTag);
-        return IsUpdateCompatible(incompatibleVersions) && !isDowngrading; // Incompatible.json doesn't support backwards compatibility, thus downgrading always means update is not compatble
+        return
+            IsUpdateCompatible(incompatibleVersions) &&
+            !isDowngrading; // Incompatible.json doesn't support backwards compatibility, thus downgrading always means update is not compatble
     }
 
     public async Task<string[]> GetUpdateIncompatibleVersionsAsync(string tag)
@@ -140,7 +159,7 @@ public class UpdateChecker
     private static string ExtractVersionString(string versionString)
     {
         if (string.IsNullOrEmpty(versionString)) return string.Empty;
-        
+
         for (int i = 0; i < versionString.Length; i++)
         {
             if (!char.IsDigit(versionString[i]) && versionString[i] != '.')
@@ -148,7 +167,7 @@ public class UpdateChecker
                 return versionString[..i];
             }
         }
-        
+
         return versionString;
     }
 }

+ 9 - 5
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -1,6 +1,6 @@
 [
   {
-    "Name": "PIXEL_ART_TOOLSET",
+    "Name": "PIXEL_PERFECT_TOOLSET",
     "Tools": [
       "MoveViewport",
       "RotateViewport",
@@ -9,7 +9,8 @@
         "ToolName": "Pen",
         "Settings": {
           "ExposePixelPerfectEnabled": true,
-          "Spacing": 0
+          "Spacing": 0,
+          "ExposePaintShape": true
         }
       },
       "Select",
@@ -30,7 +31,8 @@
       {
         "ToolName": "Eraser",
         "Settings": {
-          "Spacing": 0
+          "Spacing": 0,
+          "ExposePaintShape": true
         }
       },
       "ColorPicker",
@@ -50,7 +52,8 @@
           "AntiAliasing": true,
           "ExposeHardness": true,
           "ExposeSpacing": true,
-          "BrushShapeSetting": "CircleSmooth"
+          "BrushShapeSetting": "CircleSmooth",
+          "PaintShape": "Circle"
         }
       },
       "Select",
@@ -102,7 +105,8 @@
           "AntiAliasing": true,
           "ExposeHardness": true,
           "ExposeSpacing": true,
-          "BrushShapeSetting": "CircleSmooth"
+          "BrushShapeSetting": "CircleSmooth",
+          "PaintShape": "Circle"
         }
       },
       "ColorPicker",

+ 13 - 3
src/PixiEditor/Data/Localization/Languages/en.json

@@ -55,7 +55,7 @@
   "APPLY_TRANSFORM": "Apply transform",
   "INCREASE_TOOL_SIZE": "Increase tool size",
   "DECREASE_TOOL_SIZE": "Decrease tool size",
- "UPDATE_READY": "Update is ready to be installed. Do you want to install it now?",
+  "UPDATE_READY": "Update is ready to be installed. Do you want to install it now?",
   "NEW_UPDATE": "New update",
   "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Couldn't update without admin privileges. Please run PixiEditor as administrator.",
   "INSUFFICIENT_PERMISSIONS": "Insufficient permissions",
@@ -757,7 +757,7 @@
   "MODULO": "Modulo",
   "STEP": "Step",
   "SMOOTH_STEP": "Smoothstep",
-  "PIXEL_ART_TOOLSET": "Pixel Art",
+  "PIXEL_PERFECT_TOOLSET": "Pixel Perfect",
   "VECTOR_TOOLSET": "Vector",
   "VECTOR_LAYER": "Vector Layer",
   "STROKE_COLOR_LABEL": "Stroke",
@@ -997,5 +997,15 @@
   "SWITCH_TO_NEW_VERSION": "Switch",
   "DOWNLOAD_UPDATE": "Download",
   "DOWNLOADING_UPDATE": "Downloading update...",
-  "CHECKING_FOR_UPDATES": "Checking for updates..."
+  "CHECKING_FOR_UPDATES": "Checking for updates...",
+  "PAINT_SHAPE_SETTING": "Brush shape",
+  "BOOL_OPERATION_NODE": "Boolean Operation",
+  "FIRST_SHAPE": "First shape",
+  "SECOND_SHAPE": "Second shape",
+  "OPERATION": "Operation",
+  "UNION_VECTOR_PATH_OP": "Union",
+  "DIFFERENCE_VECTOR_PATH_OP": "Difference",
+  "INTERSECT_VECTOR_PATH_OP": "Intersect",
+  "XOR_VECTOR_PATH_OP": "XOR",
+  "REVERSE_DIFFERENCE_VECTOR_PATH_OP": "Reverse Difference"
 }

+ 6 - 3
src/PixiEditor/Helpers/CrashHelper.cs

@@ -190,10 +190,13 @@ internal partial class CrashHelper
         {
             return;
         }
-        
-        using var analyticsClient = new AnalyticsClient(analyticsUrl);
 
-        await analyticsClient.SendReportAsync(report.ApiReportJson);
+        try
+        {
+            using var analyticsClient = new AnalyticsClient(analyticsUrl);
+            await analyticsClient.SendReportAsync(report.ApiReportJson);
+        }
+        catch { }
     }
 
     /// <summary>

+ 20 - 5
src/PixiEditor/Models/AnalyticsAPI/AnalyticsPeriodicReporter.cs

@@ -57,7 +57,15 @@ public class AnalyticsPeriodicReporter
     {
         await _cancellationToken.CancelAsync();
 
-        await _client.EndSessionAsync(SessionId).WaitAsync(TimeSpan.FromSeconds(1));
+        try
+        {
+            await _client.EndSessionAsync(SessionId).WaitAsync(TimeSpan.FromSeconds(1));
+        }
+        catch (TaskCanceledException) { }
+        catch (Exception e)
+        {
+            await SendExceptionAsync(e);
+        }
     }
 
     public void AddEvent(AnalyticEvent value)
@@ -87,14 +95,21 @@ public class AnalyticsPeriodicReporter
     {
         if (!_resumeSession)
         {
-            var createSession = await _client.CreateSessionAsync(_cancellationToken.Token);
+            try
+            {
+                var createSession = await _client.CreateSessionAsync(_cancellationToken.Token);
+                if (!createSession.HasValue)
+                {
+                    return;
+                }
 
-            if (!createSession.HasValue)
+                SessionId = createSession.Value;
+            }
+            catch (Exception e)
             {
+                await SendExceptionAsync(e);
                 return;
             }
-
-            SessionId = createSession.Value;
         }
 
         Task.Run(RunHeartbeatAsync);

+ 5 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/BrightnessToolExecutor.cs

@@ -15,6 +15,7 @@ internal class BrightnessToolExecutor : UpdateableChangeExecutor
     private bool repeat;
     private float correctionFactor;
     private int toolSize;
+    private bool squareBrush;
 
     public override ExecutionState Start()
     {
@@ -30,7 +31,9 @@ internal class BrightnessToolExecutor : UpdateableChangeExecutor
         toolSize = (int)toolbar.ToolSize;
         correctionFactor = tool.Darken || tool.UsedWith == MouseButton.Right ? -tool.CorrectionFactor : tool.CorrectionFactor;
 
-        ChangeBrightness_Action action = new(guidValue, controller!.LastPixelPosition, correctionFactor, toolSize, repeat, document.AnimationHandler.ActiveFrameBindable);
+        squareBrush = tool.BrushShape == PaintBrushShape.Square;
+
+        ChangeBrightness_Action action = new(guidValue, controller!.LastPixelPosition, correctionFactor, toolSize, squareBrush, repeat, document.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
 
         return ExecutionState.Success;
@@ -38,7 +41,7 @@ internal class BrightnessToolExecutor : UpdateableChangeExecutor
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        ChangeBrightness_Action action = new(guidValue, pos, correctionFactor, toolSize, repeat, document!.AnimationHandler.ActiveFrameBindable);
+        ChangeBrightness_Action action = new(guidValue, pos, correctionFactor, toolSize, squareBrush, repeat, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
     }
 

+ 5 - 6
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs

@@ -128,7 +128,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     protected abstract void DrawShape(VecD currentPos, double rotationRad, bool firstDraw);
     protected abstract IAction SettingsChangedAction(string name, object value);
     protected abstract IAction TransformMovedAction(ShapeData data, ShapeCorners corners);
-    protected virtual bool InitShapeData(IReadOnlyShapeVectorData data) { return true; }
+    protected virtual bool InitShapeData(IReadOnlyShapeVectorData data) { return false; }
     protected abstract bool CanEditShape(IStructureMemberHandler layer);
     protected abstract IAction EndDrawAction();
     protected virtual DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective;
@@ -234,7 +234,6 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
                 EndDrawAction(),
                 SettingsChangedAction("FillAndStroke", color),
                 EndDrawAction());
-            // TODO add to undo
         }
     }
 
@@ -315,7 +314,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
         if (highlight)
         {
-            HighlightSnapAxis(snapXAxis, snapYAxis);
+            HighlightSnapAxis(snapXAxis, snapYAxis, string.IsNullOrEmpty(snapXAxis) && string.IsNullOrEmpty(snapYAxis) ? null : snapped);
         }
 
         if (AlignToPixels)
@@ -345,7 +344,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
         if (highlight)
         {
-            HighlightSnapAxis(snapXAxis, snapYAxis);
+            HighlightSnapAxis(snapXAxis, snapYAxis, string.IsNullOrEmpty(snapXAxis) && string.IsNullOrEmpty(snapYAxis) ? null : snapped);
         }
 
         if (snapped != VecI.Zero)
@@ -364,11 +363,11 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         return snapped;
     }
 
-    private void HighlightSnapAxis(string snapXAxis, string snapYAxis)
+    private void HighlightSnapAxis(string snapXAxis, string snapYAxis, VecD? snapPoint)
     {
         document.SnappingHandler.SnappingController.HighlightedXAxis = snapXAxis;
         document.SnappingHandler.SnappingController.HighlightedYAxis = snapYAxis;
-        document.SnappingHandler.SnappingController.HighlightedPoint = null;
+        document.SnappingHandler.SnappingController.HighlightedPoint = snapPoint;
     }
 
     public override void OnSettingsChanged(string name, object value)

+ 5 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs

@@ -14,6 +14,7 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
 internal class EraserToolExecutor : UpdateableChangeExecutor
 {
+    public bool SquareBrush => penToolbar.PaintShape == PaintBrushShape.Square;
     private Guid guidValue;
     private Color color;
     private double toolSize;
@@ -22,12 +23,14 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
     private float spacing;
     
     private bool drawOnMask;
+    private IPenToolbar penToolbar;
 
     public override ExecutionState Start()
     {
         IStructureMemberHandler? member = document!.SelectedStructureMember;
         IEraserToolHandler? eraserTool = GetHandler<IEraserToolHandler>();
         IPenToolbar? toolbar = eraserTool?.Toolbar as IPenToolbar;
+        penToolbar = toolbar;
         IColorsHandler? colorsHandler = GetHandler<IColorsHandler>();
 
         if (colorsHandler is null || eraserTool is null || member is null || toolbar is null)
@@ -48,7 +51,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, (float)toolSize, true,
-            antiAliasing, hardness, spacing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+            antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
 
         return ExecutionState.Success;
@@ -56,7 +59,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, pos, (float)toolSize, true, antiAliasing, hardness, spacing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, pos, (float)toolSize, true, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
     }
 

+ 4 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -91,6 +91,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
                 return ExecutionState.Success;
             }
 
+            toolbar.StrokeBrush = data.Stroke.ToBrush();
+
             if (!InitShapeData(data))
             {
                 ActiveMode = ShapeToolMode.Preview;
@@ -192,8 +194,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         var moveOverlayAction = TransformOverlayMoved(start, end);
         internals!.ActionAccumulator.AddActions(moveOverlayAction);
 
-        startDrawingPos = (VecI)start;
-        curPos = (VecI)end;
+        startDrawingPos = start;
+        curPos = end;
     }
 
     public override void OnColorChanged(Color color, bool primary)

+ 4 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -15,6 +15,8 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     private Guid guidValue;
     private Color color;
     public double ToolSize => penToolbar.ToolSize;
+    public bool SquareBrush => penToolbar.PaintShape == PaintBrushShape.Square;
+
     private bool drawOnMask;
     private bool pixelPerfect;
     private bool antiAliasing;
@@ -48,7 +50,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize, false, antiAliasing, hardness, spacing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize, false, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);
@@ -60,7 +62,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     {
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, false, antiAliasing, hardness, spacing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, false, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);

+ 16 - 29
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterLineToolExecutor.cs

@@ -19,46 +19,33 @@ internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
 
     protected override IAction DrawLine(VecD pos)
     {
-        VecD dir = GetSignedDirection(startDrawingPos, pos);
-        VecD oppositeDir = new VecD(-dir.X, -dir.Y);
-        return new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos, oppositeDir), ToPixelPos(pos, dir), (float)StrokeWidth,
-            StrokePaintable, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+        return new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos), ToPixelPos(pos),
+            (float)StrokeWidth,
+            StrokePaintable, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask,
+            document!.AnimationHandler.ActiveFrameBindable);
     }
 
     protected override IAction TransformOverlayMoved(VecD start, VecD end)
     {
-        VecD dir = GetSignedDirection(start, end);
-        VecD oppositeDir = new VecD(-dir.X, -dir.Y);
-        return new DrawRasterLine_Action(memberId, ToPixelPos(start, oppositeDir), ToPixelPos(end, dir), 
-            (float)StrokeWidth, StrokePaintable, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+        return new DrawRasterLine_Action(memberId, ToPixelPos(start), ToPixelPos(end),
+            (float)StrokeWidth, StrokePaintable, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask,
+            document!.AnimationHandler.ActiveFrameBindable);
     }
 
     protected override IAction[] SettingsChange(string name, object value)
     {
-        VecD dir = GetSignedDirection(startDrawingPos, curPos);
-        VecD oppositeDir = new VecD(-dir.X, -dir.Y);
-        return [new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos, oppositeDir), ToPixelPos(curPos, dir), (float)StrokeWidth,
-            StrokePaintable, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)];
+        return
+        [
+            new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos), ToPixelPos(curPos),
+                (float)StrokeWidth,
+                StrokePaintable, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask,
+                document!.AnimationHandler.ActiveFrameBindable)
+        ];
     }
 
-    private VecI ToPixelPos(VecD pos, VecD dir)
+    private VecD ToPixelPos(VecD pos)
     {
-        if (StrokeWidth > 1) return (VecI)pos.Round();
-        
-        double xAdjustment = dir.X > 0 ? 0.5 : -0.5;
-        double yAdjustment = dir.Y > 0 ? 0.5 : -0.5;
-        
-        VecD adjustment = new VecD(xAdjustment, yAdjustment);
-
-        
-        VecI finalPos = (VecI)(pos - adjustment);
-
-        return finalPos;
-    }
-    
-    private VecD GetSignedDirection(VecD start, VecD end)
-    {
-        return new VecD(Math.Sign(end.X - start.X), Math.Sign(end.Y - start.Y));
+        return (VecD)pos;
     }
 
     protected override IAction EndDraw()

+ 4 - 1
src/PixiEditor/Models/Handlers/Toolbars/IPenToolbar.cs

@@ -1,8 +1,11 @@
-namespace PixiEditor.Models.Handlers.Toolbars;
+using PixiEditor.Views.Overlays.BrushShapeOverlay;
+
+namespace PixiEditor.Models.Handlers.Toolbars;
 
 internal interface IPenToolbar : IToolbar, IToolSizeToolbar
 {
     public bool AntiAliasing { get; set; }
     public float Hardness { get; set; }
     public float Spacing { get; set; }
+    public PaintBrushShape PaintShape { get; set; }
 }

+ 7 - 0
src/PixiEditor/Models/Handlers/Toolbars/PaintBrushShape.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Models.Handlers.Toolbars;
+
+public enum PaintBrushShape
+{
+    Circle,
+    Square,
+}

+ 1 - 0
src/PixiEditor/Models/Handlers/Tools/IBrightnessToolHandler.cs

@@ -10,4 +10,5 @@ internal interface IBrightnessToolHandler : IToolHandler
     public bool Darken { get; }
     public MouseButton UsedWith { get; }
     public float CorrectionFactor { get; }
+    public PaintBrushShape BrushShape { get; }
 }

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -41,5 +41,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.69")]
-[assembly: AssemblyFileVersion("2.0.0.69")]
+[assembly: AssemblyVersion("2.0.0.70")]
+[assembly: AssemblyFileVersion("2.0.0.70")]

+ 18 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -157,6 +157,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public bool AnySymmetryAxisEnabledBindable =>
         HorizontalSymmetryAxisEnabledBindable || VerticalSymmetryAxisEnabledBindable;
 
+
+    public bool OverlayEventsSuppressed => overlaySuppressors.Count > 0;
+
+    private readonly HashSet<string> overlaySuppressors = new();
+
     private VecI size = new VecI(64, 64);
     public int Width => size.X;
     public int Height => size.Y;
@@ -224,6 +229,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public AnimationDataViewModel AnimationDataViewModel { get; }
     public TextOverlayViewModel TextOverlayViewModel { get; }
 
+
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers => softSelectedStructureMembers;
     private DocumentInternalParts Internals { get; }
     INodeGraphHandler IDocument.NodeGraphHandler => NodeGraph;
@@ -770,6 +776,18 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         return bitmap.GetSRGBPixel(new VecI((int)transformed.X, (int)transformed.Y));
     }
 
+    public void SuppressAllOverlayEvents(string suppressor)
+    {
+        overlaySuppressors.Add(suppressor);
+        OnPropertyChanged(nameof(OverlayEventsSuppressed));
+    }
+
+    public void RestoreAllOverlayEvents(string suppressor)
+    {
+        overlaySuppressors.Remove(suppressor);
+        OnPropertyChanged(nameof(OverlayEventsSuppressed));
+    }
+
     public Color PickColorFromCanvas(VecI pos, DocumentScope scope, KeyFrameTime frameTime, string? customOutput = null)
     {
         // there is a tiny chance that the image might get disposed by another thread

+ 11 - 0
src/PixiEditor/ViewModels/Document/Nodes/Shapes/BoolOperationNodeViewModel.cs

@@ -0,0 +1,11 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Shapes;
+
+[NodeViewModel("BOOL_OPERATION_NODE", "SHAPE", PixiPerfectIcons.Intersect)]
+internal class BoolOperationNodeViewModel : NodeViewModel<BoolOperationNode>
+{
+
+}

+ 1 - 1
src/PixiEditor/ViewModels/Tools/ShapeTool.cs

@@ -8,7 +8,7 @@ namespace PixiEditor.ViewModels.Tools;
 
 internal abstract class ShapeTool : ToolViewModel, IShapeToolHandler
 {
-    public override BrushShape BrushShape => BrushShape.Hidden;
+    public override BrushShape FinalBrushShape => BrushShape.Hidden;
 
     public override bool UsesColor => true;
 

+ 8 - 0
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/PenToolbar.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
+using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
 namespace PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
@@ -29,6 +30,12 @@ internal class PenToolbar : Toolbar, IPenToolbar
         set => GetSetting<SizeSettingViewModel>(nameof(ToolSize)).Value = value;
     }
 
+    public PaintBrushShape PaintShape
+    {
+        get => GetSetting<EnumSettingViewModel<PaintBrushShape>>(nameof(PaintShape)).Value;
+        set => GetSetting<EnumSettingViewModel<PaintBrushShape>>(nameof(PaintShape)).Value = value;
+    }
+
     public override void OnLoadedSettings()
     {
         OnPropertyChanged(nameof(ToolSize));
@@ -42,5 +49,6 @@ internal class PenToolbar : Toolbar, IPenToolbar
         var setting = new SizeSettingViewModel(nameof(ToolSize), "TOOL_SIZE_LABEL");
         setting.ValueChanged += (_, _) => OnPropertyChanged(nameof(ToolSize));
         AddSetting(setting);
+        AddSetting(new EnumSettingViewModel<PaintBrushShape>(nameof(PaintShape), "PAINT_SHAPE_SETTING") { IsExposed = false });
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/Tools/ToolViewModel.cs

@@ -28,7 +28,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
 
     public virtual string DefaultIcon => $"\u25a1";
 
-    public virtual BrushShape BrushShape => BrushShape.Square;
+    public virtual BrushShape FinalBrushShape => BrushShape.Square;
 
     public abstract Type[]? SupportedLayerTypes { get; }
 

+ 18 - 1
src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs

@@ -7,6 +7,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
@@ -29,7 +30,7 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
     public override bool IsErasable => true;
     public override LocalizedString Tooltip => new LocalizedString("BRIGHTNESS_TOOL_TOOLTIP", Shortcut);
 
-    public override BrushShape BrushShape => BrushShape.CirclePixelated;
+    public override BrushShape FinalBrushShape => BrushShape == PaintBrushShape.Square ? Views.Overlays.BrushShapeOverlay.BrushShape.Square : Views.Overlays.BrushShapeOverlay.BrushShape.CirclePixelated;
 
     public override string DefaultIcon => PixiPerfectIcons.Sun;
 
@@ -51,6 +52,17 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
 
     [Settings.Enum("MODE_LABEL")]
     public BrightnessMode BrightnessMode => GetValue<BrightnessMode>();
+
+    [Settings.Enum("PAINT_SHAPE_SETTING", PaintBrushShape.Circle, Notify = nameof(BrushShapeChanged))]
+    public PaintBrushShape BrushShape
+    {
+        get => GetValue<PaintBrushShape>();
+        set
+        {
+            SetValue(value);
+            OnPropertyChanged(nameof(FinalBrushShape));
+        }
+    }
     
     public bool Darken { get; private set; } = false;
 
@@ -76,4 +88,9 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
     {
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseBrightnessTool();
     }
+
+    private void BrushShapeChanged()
+    {
+        OnPropertyChanged(nameof(FinalBrushShape));
+    }
 }

+ 13 - 1
src/PixiEditor/ViewModels/Tools/Tools/ColorPickerToolViewModel.cs

@@ -28,7 +28,7 @@ internal class ColorPickerToolViewModel : ToolViewModel, IColorPickerHandler
     public override bool UsesColor => true;
 
     public override string ToolNameLocalizationKey => "COLOR_PICKER_TOOL";
-    public override BrushShape BrushShape => BrushShape.Pixel;
+    public override BrushShape FinalBrushShape => BrushShape.Pixel;
 
     public override string DefaultIcon => PixiPerfectIcons.Picker;
 
@@ -170,4 +170,16 @@ internal class ColorPickerToolViewModel : ToolViewModel, IColorPickerHandler
 
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey) =>
         UpdateActionDisplay(ctrlIsDown, shiftIsDown);
+
+    protected override void OnSelected(bool restoring)
+    {
+        base.OnSelected(restoring);
+        ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument.SuppressAllOverlayEvents(ToolName);
+    }
+
+    protected override void OnDeselecting(bool transient)
+    {
+        base.OnDeselecting(transient);
+        ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument.RestoreAllOverlayEvents(ToolName);
+    }
 }

+ 19 - 2
src/PixiEditor/ViewModels/Tools/Tools/EraserToolViewModel.cs

@@ -6,6 +6,7 @@ using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
@@ -26,7 +27,7 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
     public override bool IsErasable => true;
 
     public override string ToolNameLocalizationKey => "ERASER_TOOL";
-    public override BrushShape BrushShape => BrushShapeSetting;
+    public override BrushShape FinalBrushShape => PaintShape == PaintBrushShape.Square ? BrushShape.Square : BrushShapeSetting;
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
 
     public override string DefaultIcon => PixiPerfectIcons.Eraser;
@@ -39,6 +40,17 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
         Notify = nameof(BrushShapeChanged))]
     public BrushShape BrushShapeSetting => GetValue<BrushShape>();
 
+    [Settings.Inherited(Notify = nameof(PenShapeChanged))]
+    public PaintBrushShape PaintShape
+    {
+        get => GetValue<PaintBrushShape>();
+        set
+        {
+            SetValue(value);
+            OnPropertyChanged(nameof(FinalBrushShape));
+        }
+    }
+
     public override void UseTool(VecD pos)
     {
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseEraserTool();
@@ -46,6 +58,11 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
 
     private void BrushShapeChanged()
     {
-        OnPropertyChanged(nameof(BrushShape));
+        OnPropertyChanged(nameof(FinalBrushShape));
+    }
+
+    private void PenShapeChanged()
+    {
+        OnPropertyChanged(nameof(FinalBrushShape));
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/FloodFillToolViewModel.cs

@@ -18,7 +18,7 @@ internal class FloodFillToolViewModel : ToolViewModel, IFloodFillToolHandler
     private readonly string defaultActionDisplay = "FLOOD_FILL_TOOL_ACTION_DISPLAY_DEFAULT";
 
     public override string ToolNameLocalizationKey => "FLOOD_FILL_TOOL";
-    public override BrushShape BrushShape => BrushShape.Pixel;
+    public override BrushShape FinalBrushShape => BrushShape.Pixel;
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
 
     public override LocalizedString Tooltip => new("FLOOD_FILL_TOOL_TOOLTIP", Shortcut);

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/LassoToolViewModel.cs

@@ -50,7 +50,7 @@ internal class LassoToolViewModel : ToolViewModel, ILassoToolHandler
 
     public override string ToolNameLocalizationKey => "LASSO_TOOL";
     public override string DefaultIcon => PixiPerfectIcons.Lasso;
-    public override BrushShape BrushShape => BrushShape.Pixel;
+    public override BrushShape FinalBrushShape => BrushShape.Pixel;
     
     public override Type[]? SupportedLayerTypes { get; } = null; // all layer types are supported
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/MagicWandToolViewModel.cs

@@ -19,7 +19,7 @@ internal class MagicWandToolViewModel : ToolViewModel, IMagicWandToolHandler
     public override LocalizedString Tooltip => new LocalizedString("MAGIC_WAND_TOOL_TOOLTIP", Shortcut);
 
     public override string ToolNameLocalizationKey => "MAGIC_WAND_TOOL";
-    public override BrushShape BrushShape => BrushShape.Pixel;
+    public override BrushShape FinalBrushShape => BrushShape.Pixel;
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) }; 
 
     [Settings.Enum("MODE_LABEL")]

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -49,7 +49,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
         set => SetValue(value);
     }
 
-    public override BrushShape BrushShape => BrushShape.Hidden;
+    public override BrushShape FinalBrushShape => BrushShape.Hidden;
     public override Type[]? SupportedLayerTypes { get; } = null;
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
     public override bool HideHighlight => true;

+ 8 - 1
src/PixiEditor/ViewModels/Tools/Tools/MoveViewportToolViewModel.cs

@@ -10,7 +10,7 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 internal class MoveViewportToolViewModel : ToolViewModel
 {
     public override string ToolNameLocalizationKey => "MOVE_VIEWPORT_TOOL";
-    public override BrushShape BrushShape => BrushShape.Hidden;
+    public override BrushShape FinalBrushShape => BrushShape.Hidden;
     public override Type[]? SupportedLayerTypes { get; } = null;
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
     public override bool HideHighlight => true;
@@ -28,5 +28,12 @@ internal class MoveViewportToolViewModel : ToolViewModel
     protected override void OnSelected(bool restoring)
     {
         ActionDisplay = new LocalizedString("MOVE_VIEWPORT_ACTION_DISPLAY");
+        ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument.SuppressAllOverlayEvents(ToolName);
+    }
+
+    protected override void OnDeselecting(bool transient)
+    {
+        base.OnDeselecting(transient);
+        ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument.RestoreAllOverlayEvents(ToolName);
     }
 }

+ 26 - 3
src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs

@@ -7,6 +7,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Input;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
@@ -20,7 +21,9 @@ namespace PixiEditor.ViewModels.Tools.Tools
         private double actualToolSize;
 
         public override string ToolNameLocalizationKey => "PEN_TOOL";
-        public override BrushShape BrushShape => BrushShapeSetting;
+
+        public override BrushShape FinalBrushShape =>
+            PaintShape == PaintBrushShape.Square ? BrushShape.Square : BrushShapeSetting;
         
         public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
 
@@ -41,7 +44,21 @@ namespace PixiEditor.ViewModels.Tools.Tools
         public bool PixelPerfectEnabled => GetValue<bool>();
         
         [Settings.Enum("BRUSH_SHAPE_SETTING", BrushShape.CirclePixelated, ExposedByDefault = false, Notify = nameof(BrushShapeChanged))]
-        public BrushShape BrushShapeSetting => GetValue<BrushShape>();
+        public BrushShape BrushShapeSetting
+        {
+            get
+            {
+                return GetValue<BrushShape>();
+            }
+            set
+            {
+                SetValue(value);
+                OnPropertyChanged(nameof(FinalBrushShape));
+            }
+        }
+
+        [Settings.Inherited(Notify = nameof(PenShapeChanged))]
+        public PaintBrushShape PaintShape => GetValue<PaintBrushShape>();
 
         public override string DefaultIcon => PixiPerfectIcons.Pen;
 
@@ -133,7 +150,13 @@ namespace PixiEditor.ViewModels.Tools.Tools
         
         private void BrushShapeChanged()
         {
-            OnPropertyChanged(nameof(BrushShape));
+            OnPropertyChanged(nameof(FinalBrushShape));
+        }
+
+        private void PenShapeChanged()
+        {
+            OnPropertyChanged(nameof(PaintShape));
+            OnPropertyChanged(nameof(FinalBrushShape));
         }
     }
 }

+ 11 - 6
src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs

@@ -17,12 +17,6 @@ internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
 {
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";
 
-    public RasterLineToolViewModel()
-    {
-        ActionDisplay = defaultActionDisplay;
-        Toolbar = ToolbarFactory.Create<RasterLineToolViewModel, ShapeToolbar>(this);
-    }
-
     public override string ToolNameLocalizationKey => "LINE_TOOL";
     public override LocalizedString Tooltip => new LocalizedString("LINE_TOOL_TOOLTIP", Shortcut);
 
@@ -35,6 +29,17 @@ internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
 
+    public RasterLineToolViewModel()
+    {
+        ActionDisplay = defaultActionDisplay;
+        Toolbar = ToolbarFactory.Create<RasterLineToolViewModel, ShapeToolbar>(this);
+        var strokeSetting = Toolbar.GetSetting(nameof(ShapeToolbar.ToolSize));
+        if (strokeSetting != null)
+        {
+            strokeSetting.Value = 1d;
+        }
+    }
+
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         DrawFromCenter = ctrlIsDown;

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/RotateViewportToolViewModel.cs

@@ -10,7 +10,7 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 internal class RotateViewportToolViewModel : ToolViewModel
 {
     public override string ToolNameLocalizationKey => "ROTATE_VIEWPORT_TOOL";
-    public override BrushShape BrushShape => BrushShape.Hidden;
+    public override BrushShape FinalBrushShape => BrushShape.Hidden;
     public override Type[]? SupportedLayerTypes { get; } = null; // null = all
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
     public override bool HideHighlight => true;

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/SelectToolViewModel.cs

@@ -57,7 +57,7 @@ internal class SelectToolViewModel : ToolViewModel, ISelectToolHandler
     [Settings.Enum("SHAPE_LABEL")]
     public SelectionShape SelectShape => GetValue<SelectionShape>();
 
-    public override BrushShape BrushShape => BrushShape.Pixel;
+    public override BrushShape FinalBrushShape => BrushShape.Pixel;
     public override Type[]? SupportedLayerTypes { get; } = null;
 
     public override LocalizedString Tooltip => new LocalizedString("SELECT_TOOL_TOOLTIP", Shortcut);

+ 0 - 2
src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs

@@ -60,8 +60,6 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 
     protected override void OnSelected(bool restoring)
     {
-        if (restoring) return;
-
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorEllipseTool();
     }
 

+ 11 - 7
src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs

@@ -24,11 +24,6 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 
     public override bool IsErasable => false;
 
-    public VectorLineToolViewModel()
-    {
-        ActionDisplay = defaultActionDisplay;
-        Toolbar = ToolbarFactory.Create<VectorLineToolViewModel, ShapeToolbar>(this);
-    }
 
     public override string ToolNameLocalizationKey => "LINE_TOOL";
     public override LocalizedString Tooltip => new LocalizedString("LINE_TOOL_TOOLTIP", Shortcut);
@@ -43,6 +38,17 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
 
+    public VectorLineToolViewModel()
+    {
+        ActionDisplay = defaultActionDisplay;
+        Toolbar = ToolbarFactory.Create<VectorLineToolViewModel, ShapeToolbar>(this);
+        var strokeSetting = Toolbar.GetSetting(nameof(ShapeToolbar.ToolSize));
+        if (strokeSetting != null)
+        {
+            strokeSetting.Value = 1d;
+        }
+    }
+
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
         DrawFromCenter = ctrlIsDown;
@@ -66,8 +72,6 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 
     protected override void OnSelected(bool restoring)
     {
-        if (restoring) return;
-
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         document.Tools.UseVectorLineTool();
     }

+ 0 - 2
src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs

@@ -119,8 +119,6 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
     
     protected override void OnSelected(bool restoring)
     {
-        if (restoring) return;
-
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorPathTool();
         isActivated = true;
     }

+ 0 - 2
src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs

@@ -73,8 +73,6 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
 
     protected override void OnSelected(bool restoring)
     {
-        if (restoring) return;
-
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorRectangleTool();
     }
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/ZoomToolViewModel.cs

@@ -19,7 +19,7 @@ internal class ZoomToolViewModel : ToolViewModel
     private string defaultActionDisplay = new LocalizedString("ZOOM_TOOL_ACTION_DISPLAY_DEFAULT");
 
     public override string ToolNameLocalizationKey => "ZOOM_TOOL";
-    public override BrushShape BrushShape => BrushShape.Hidden;
+    public override BrushShape FinalBrushShape => BrushShape.Hidden;
     public override Type[]? SupportedLayerTypes { get; } = null;
 
     public override bool StopsLinkedToolOnUse => false;

+ 27 - 42
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml

@@ -12,11 +12,8 @@
                          x:Class="PixiEditor.Views.Dialogs.ExportFilePopup"
                          x:ClassModifier="internal"
                          ui1:Translator.Key="EXPORT_IMAGE">
-    <DockPanel Background="{DynamicResource ThemeBackgroundBrush}">
-        <Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" IsDefault="True"
-                ui1:Translator.Key="EXPORT" Command="{Binding ExportCommand, ElementName=saveFilePopup}" />
-        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Stretch" Orientation="Vertical"
-                    Margin="0,15,0,0">
+    <DockPanel Margin="10" Background="{DynamicResource ThemeBackgroundBrush}">
+        <StackPanel Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Stretch" Orientation="Vertical">
             <TabControl SelectedIndex="{Binding SelectedExportIndex, ElementName=saveFilePopup}">
                 <TabControl.Styles>
                     <Style Selector="TabControl">
@@ -27,11 +24,8 @@
                     </Style>
                 </TabControl.Styles>
                 <TabControl.Items>
-                    <TabItem IsSelected="True" ui1:Translator.Key="EXPORT_IMAGE_HEADER">
-                    </TabItem>
-                    <TabItem ui1:Translator.Key="EXPORT_ANIMATION_HEADER">
-
-                    </TabItem>
+                    <TabItem IsSelected="True" ui1:Translator.Key="EXPORT_IMAGE_HEADER"/>
+                    <TabItem ui1:Translator.Key="EXPORT_ANIMATION_HEADER"/>
                     <TabItem ui1:Translator.Key="EXPORT_SPRITESHEET_HEADER">
                         <Grid>
                             <Grid.ColumnDefinitions>
@@ -42,11 +36,11 @@
                                 <RowDefinition Height="Auto" />
                                 <RowDefinition Height="Auto" />
                             </Grid.RowDefinitions>
-                            <TextBlock ui1:Translator.Key="ROWS" Grid.Row="0" Grid.Column="0"/>
+                            <TextBlock ui1:Translator.Key="ROWS" Grid.Row="0" Grid.Column="0" />
                             <input:NumberInput Min="0" Width="50" Grid.Column="1" Grid.Row="0"
                                                Decimals="0"
                                                Value="{Binding ElementName=saveFilePopup, Path=SpriteSheetRows, Mode=TwoWay}" />
-                            <TextBlock ui1:Translator.Key="COLUMNS" Grid.Row="1" Grid.Column="0"/>
+                            <TextBlock ui1:Translator.Key="COLUMNS" Grid.Row="1" Grid.Column="0" />
                             <input:NumberInput Min="0" Width="50" Grid.Column="1" Grid.Row="1"
                                                Decimals="0"
                                                Value="{Binding ElementName=saveFilePopup, Path=SpriteSheetColumns, Mode=TwoWay}" />
@@ -54,15 +48,15 @@
                     </TabItem>
                 </TabControl.Items>
             </TabControl>
-            <Border Margin="15, 30" Padding="10"
-                    Background="{DynamicResource ThemeBackgroundBrush1}"
-                    CornerRadius="{DynamicResource ControlCornerRadius}">
-                <Grid MinHeight="205" MinWidth="400">
+            <Border
+                Background="{DynamicResource ThemeBackgroundBrush1}"
+                CornerRadius="{DynamicResource ControlCornerRadius}">
+                <Grid MinWidth="400">
                     <Grid.ColumnDefinitions>
                         <ColumnDefinition Width="*" />
-                        <ColumnDefinition Width="160" />
+                        <ColumnDefinition Width="192" />
                     </Grid.ColumnDefinitions>
-                    <Grid>
+                    <Grid Margin="5">
                         <Grid.RowDefinitions>
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="Auto" />
@@ -86,33 +80,24 @@
                         </TextBlock>
                     </Grid>
                     <Grid Grid.Column="1">
-                        <Grid.RowDefinitions>
-                            <RowDefinition Height="30" />
-                            <RowDefinition Height="Auto" />
-                        </Grid.RowDefinitions>
-
-                        <TextBlock Text="Export Preview" />
-                        <indicators:LoadingIndicator Grid.Row="1"
-                                                     IsVisible="{Binding IsGeneratingPreview, ElementName=saveFilePopup}"
-                                                     Margin="0, 10, 0, 0" />
-                        <Border Grid.Row="1" BorderThickness="1" Height="200" Width="150"
-                                IsVisible="{Binding !IsGeneratingPreview, ElementName=saveFilePopup}">
-                            <Border RenderOptions.BitmapInterpolationMode="None">
-                                <visuals:SurfaceControl x:Name="surfaceControl"
-                                                        Surface="{Binding ExportPreview, ElementName=saveFilePopup}"
-                                                        Stretch="Uniform" HorizontalAlignment="Center"
-                                                        VerticalAlignment="Center"
-                                                        RenderOptions.BitmapInterpolationMode="None">
-                                    <visuals:SurfaceControl.Background>
-                                        <ImageBrush Source="/Images/CheckerTile.png"
-                                                    TileMode="Tile" DestinationRect="0, 0, 25, 25" />
-                                    </visuals:SurfaceControl.Background>
-                                </visuals:SurfaceControl>
-                            </Border>
-                        </Border>
+                        <indicators:LoadingIndicator
+                            IsVisible="{Binding IsGeneratingPreview, ElementName=saveFilePopup}" />
+                        <visuals:SurfaceControl x:Name="surfaceControl" Width="190" Height="190"
+                                                Surface="{Binding ExportPreview, ElementName=saveFilePopup}"
+                                                Stretch="Uniform" HorizontalAlignment="Center"
+                                                IsVisible="{Binding !IsGeneratingPreview, ElementName=saveFilePopup}"
+                                                VerticalAlignment="Center"
+                                                RenderOptions.BitmapInterpolationMode="None">
+                            <visuals:SurfaceControl.Background>
+                                <ImageBrush Source="/Images/CheckerTile.png"
+                                            TileMode="Tile" DestinationRect="0, 0, 25, 25" />
+                            </visuals:SurfaceControl.Background>
+                        </visuals:SurfaceControl>
                     </Grid>
                 </Grid>
             </Border>
+            <Button HorizontalAlignment="Center" IsDefault="True"
+                    ui1:Translator.Key="EXPORT" Command="{Binding ExportCommand, ElementName=saveFilePopup}" />
         </StackPanel>
     </DockPanel>
 </dialogs:PixiEditorPopup>

+ 13 - 1
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -69,6 +69,13 @@ internal class ViewportOverlays
         textOverlay = new TextOverlay();
         BindTextOverlay();
 
+        Binding suppressOverlayEventsBinding = new()
+        {
+            Source = Viewport,
+            Path = "Document.OverlayEventsSuppressed",
+            Mode = BindingMode.OneWay
+        };
+
         Viewport.ActiveOverlays.Add(gridLinesOverlay);
         Viewport.ActiveOverlays.Add(referenceLayerOverlay);
         Viewport.ActiveOverlays.Add(selectionOverlay);
@@ -79,6 +86,11 @@ internal class ViewportOverlays
         Viewport.ActiveOverlays.Add(snappingOverlay);
         Viewport.ActiveOverlays.Add(brushShapeOverlay);
         Viewport.ActiveOverlays.Add(textOverlay);
+
+        foreach (var overlay in Viewport.ActiveOverlays)
+        {
+            overlay.Bind(Overlay.SuppressEventsProperty, suppressOverlayEventsBinding);
+        }
     }
 
     private void BindReferenceLayerOverlay()
@@ -432,7 +444,7 @@ internal class ViewportOverlays
 
         Binding brushShapeBinding = new()
         {
-            Source = ViewModelMain.Current.ToolsSubViewModel, Path = "ActiveTool.BrushShape", Mode = BindingMode.OneWay
+            Source = ViewModelMain.Current.ToolsSubViewModel, Path = "ActiveTool.FinalBrushShape", Mode = BindingMode.OneWay
         };
 
         MultiBinding isVisibleMultiBinding = new()

+ 13 - 3
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -167,6 +167,7 @@ internal class LineToolOverlay : Overlay
         {
             SnappingController.HighlightedXAxis = null;
             SnappingController.HighlightedYAxis = null;
+            SnappingController.HighlightedPoint = null;
             Refresh();
         }
 
@@ -273,6 +274,7 @@ internal class LineToolOverlay : Overlay
 
             SnappingController.HighlightedXAxis = snapAxisX;
             SnappingController.HighlightedYAxis = snapAxisY;
+            SnappingController.HighlightedPoint = x != null || y != null ? final : null;
         }
 
         return final;
@@ -294,12 +296,13 @@ internal class LineToolOverlay : Overlay
         VecD mappedStart = lineStartOnMouseDown;
         VecD mappedEnd = lineEndOnMouseDown;
 
-        ((string, string), VecD) snapDeltaResult = TrySnapLine(mappedStart, mappedEnd, delta);
+        ((string, string), VecD) snapDeltaResult = TrySnapLine(mappedStart, mappedEnd, delta, out VecD? snapSource);
 
         if (SnappingController != null)
         {
             SnappingController.HighlightedXAxis = snapDeltaResult.Item1.Item1;
             SnappingController.HighlightedYAxis = snapDeltaResult.Item1.Item2;
+            SnappingController.HighlightedPoint = snapSource;
         }
 
         LineStart = lineStartOnMouseDown + delta + snapDeltaResult.Item2;
@@ -319,10 +322,11 @@ internal class LineToolOverlay : Overlay
             ActionCompleted.Execute(null);
     }
 
-    private ((string, string), VecD) TrySnapLine(VecD originalStart, VecD originalEnd, VecD delta)
+    private ((string, string), VecD) TrySnapLine(VecD originalStart, VecD originalEnd, VecD delta, out VecD? snapSource)
     {
         if (SnappingController == null)
         {
+            snapSource = null;
             return ((string.Empty, string.Empty), delta);
         }
 
@@ -330,7 +334,13 @@ internal class LineToolOverlay : Overlay
         VecD[] pointsToTest = new VecD[] { center + delta, originalStart + delta, originalEnd + delta, };
 
         VecD snapDelta =
-            SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX, out string snapAxisY, out _);
+            SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX, out string snapAxisY,
+                out snapSource);
+
+        if (snapSource != null)
+        {
+            snapSource += snapDelta;
+        }
 
         return ((snapAxisX, snapAxisY), snapDelta);
     }

+ 20 - 1
src/PixiEditor/Views/Overlays/Overlay.cs

@@ -57,6 +57,18 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     public Handle? CapturedHandle { get; set; } = null!;
     public VecD PointerPosition { get; internal set; }
 
+    public static readonly StyledProperty<bool> SuppressEventsProperty = AvaloniaProperty.Register<Overlay, bool>(
+        nameof(SuppressEvents));
+
+    public bool SuppressEvents
+    {
+        get => GetValue(SuppressEventsProperty);
+        set
+        {
+            SetValue(SuppressEventsProperty, value);
+        }
+    }
+
     private readonly Dictionary<AvaloniaProperty, OverlayTransition> activeTransitions = new();
 
     private DispatcherTimer? transitionTimer;
@@ -99,6 +111,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
 
     public void EnterPointer(OverlayPointerArgs args)
     {
+        if(SuppressEvents) return;
         OnOverlayPointerEntered(args);
         if (args.Handled) return;
         InvokeHandleEvent(HandleEventType.PointerEnteredOverlay, args);
@@ -108,6 +121,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
 
     public void ExitPointer(OverlayPointerArgs args)
     {
+        if(SuppressEvents) return;
         OnOverlayPointerExited(args);
         if (args.Handled) return;
         InvokeHandleEvent(HandleEventType.PointerExitedOverlay, args);
@@ -117,6 +131,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
 
     public void MovePointer(OverlayPointerArgs args)
     {
+        if(SuppressEvents) return;
         InvokeHandleEvent(HandleEventType.PointerMovedOverlay, args);
         if (args.Handled) return;
         OnOverlayPointerMoved(args);
@@ -138,6 +153,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
 
     public void PressPointer(OverlayPointerArgs args)
     {
+        if(SuppressEvents) return;
         InvokeHandleEvent(HandleEventType.PointerPressedOverlay, args);
         if (args.Handled) return;
         OnOverlayPointerPressed(args);
@@ -147,6 +163,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
 
     public void ReleasePointer(OverlayPointerArgs args)
     {
+        if(SuppressEvents) return;
         InvokeHandleEvent(HandleEventType.PointerReleasedOverlay, args);
         if (args.Handled)
         {
@@ -165,19 +182,21 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     
     public void KeyPressed(KeyEventArgs args)
     {
+        if(SuppressEvents) return;
         OnKeyPressed(args.Key, args.KeyModifiers, args.KeySymbol);
         KeyPressedOverlay?.Invoke(args.Key, args.KeyModifiers);
     }
 
     public void KeyReleased(KeyEventArgs keyEventArgs)
     {
+        if(SuppressEvents) return;
         OnKeyReleased(keyEventArgs.Key, keyEventArgs.KeyModifiers);
         KeyReleasedOverlay?.Invoke(keyEventArgs.Key, keyEventArgs.KeyModifiers);
     }
 
     public virtual bool TestHit(VecD point)
     {
-        return Handles.Any(handle => handle.IsWithinHandle(handle.Position, new VecD(point.X, point.Y), ZoomScale));
+        return !SuppressEvents && Handles.Any(handle => handle.IsWithinHandle(handle.Position, new VecD(point.X, point.Y), ZoomScale));
     }
 
     public void AddHandle(Handle handle)

+ 1 - 1
src/PixiEditor/Views/Overlays/SnappingOverlay.cs

@@ -53,7 +53,7 @@ internal class SnappingOverlay : Overlay
 
     public override void RenderOverlay(Canvas context, RectD canvasBounds)
     {
-        if (SnappingController is null)
+        if (SnappingController is null || SuppressEvents)
         {
             return;
         }

+ 7 - 2
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -902,9 +902,9 @@ internal class TransformOverlay : Overlay
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos,
                 ScaleFromCenter,
                 SnappingController,
-                out string snapX, out string snapY, out VecD? snapPoint);
+                out string snapX, out string snapY);
 
-            HighlightSnappedAxis(snapX, snapY, snapPoint);
+            VecD? snapPoint = null;
 
             if (newCorners is not null)
             {
@@ -917,8 +917,13 @@ internal class TransformOverlay : Overlay
                     : (ShapeCorners)newCorners;
 
                 Corners = (ShapeCorners)newCorners;
+                if (!string.IsNullOrEmpty(snapX) || !string.IsNullOrEmpty(snapY))
+                {
+                    snapPoint = TransformHelper.GetAnchorPosition((ShapeCorners)newCorners, (Anchor)capturedAnchor);
+                }
             }
 
+            HighlightSnappedAxis(snapX, snapY, snapPoint);
             UpdateOriginPos();
         }
         else if (TransformHelper.IsSide((Anchor)capturedAnchor))

+ 1 - 7
src/PixiEditor/Views/Overlays/TransformOverlay/TransformUpdateHelper.cs

@@ -13,7 +13,7 @@ internal static class TransformUpdateHelper
     public static ShapeCorners? UpdateShapeFromCorner
     (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
         VecD desiredPos, bool scaleFromCenter,
-        SnappingController? snappingController, out string snapX, out string snapY, out VecD? snapPoint)
+        SnappingController? snappingController, out string snapX, out string snapY)
     {
         if (!TransformHelper.IsCorner(targetCorner))
             throw new ArgumentException($"{targetCorner} is not a corner");
@@ -21,7 +21,6 @@ internal static class TransformUpdateHelper
         if (freedom == TransformCornerFreedom.Locked)
         {
             snapX = snapY = "";
-            snapPoint = null;
             return corners;
         }
 
@@ -33,7 +32,6 @@ internal static class TransformUpdateHelper
             VecD oppositePos = TransformHelper.GetAnchorPosition(corners, opposite);
 
             snapX = snapY = "";
-            snapPoint = null;
 
             // constrain desired pos to a "propotional" diagonal line if needed
             if (freedom == TransformCornerFreedom.ScaleProportionally && corners.IsRect)
@@ -45,7 +43,6 @@ internal static class TransformUpdateHelper
                 if (snappingController is not null)
                 {
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
-                    snapPoint = string.IsNullOrEmpty(snapX) && string.IsNullOrEmpty(snapY) ? null : desiredPos;
                 }
             }
             else if (freedom == TransformCornerFreedom.ScaleProportionally)
@@ -56,7 +53,6 @@ internal static class TransformUpdateHelper
                 if (snappingController is not null)
                 {
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
-                    snapPoint = string.IsNullOrEmpty(snapX) && string.IsNullOrEmpty(snapY) ? null : desiredPos;
                 }
             }
             else
@@ -64,7 +60,6 @@ internal static class TransformUpdateHelper
                 if (snappingController is not null)
                 {
                     desiredPos = snappingController.GetSnapPoint(desiredPos, out snapX, out snapY);
-                    snapPoint = string.IsNullOrEmpty(snapX) && string.IsNullOrEmpty(snapY) ? null : desiredPos;
                 }
             }
 
@@ -143,7 +138,6 @@ internal static class TransformUpdateHelper
         if (freedom == TransformCornerFreedom.Free)
         {
             snapX = snapY = "";
-            snapPoint = null;
             ShapeCorners newCorners = TransformHelper.UpdateCorner(corners, targetCorner, desiredPos);
             return newCorners.IsLegal ? newCorners : null;
         }

+ 1 - 0
src/PixiEditor/Views/Rendering/Scene.cs

@@ -305,6 +305,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
                 }
 
                 overlay.PointerPosition = lastMousePosition;
+
                 overlay.ZoomScale = Scale;
 
                 if (!overlay.CanRender()) continue;

+ 66 - 0
tests/PixiEditor.Tests/BlendingTests.cs

@@ -0,0 +1,66 @@
+using ChunkyImageLib.DataHolders;
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.Tests;
+
+public class BlendingTests : PixiEditorTest
+{
+    [Theory]
+    [InlineData(BlendMode.Normal, "#ffffff")]
+    [InlineData(BlendMode.Erase, "#000000")]
+    [InlineData(BlendMode.Darken, "#ff0000")]
+    [InlineData(BlendMode.Multiply, "#ff0000")]
+    [InlineData(BlendMode.ColorBurn, "#ff0000")]
+    [InlineData(BlendMode.Lighten, "#ffffff")]
+    [InlineData(BlendMode.Screen, "#ffffff")]
+    [InlineData(BlendMode.ColorDodge, "#ff0000")]
+    [InlineData(BlendMode.LinearDodge, "#ffffff")]
+    [InlineData(BlendMode.Overlay, "#ff0000")]
+    [InlineData(BlendMode.SoftLight, "#ff0000")]
+    [InlineData(BlendMode.HardLight, "#ffffff")]
+    [InlineData(BlendMode.Difference, "#00ffff")]
+    [InlineData(BlendMode.Exclusion, "#00ffff")]
+    [InlineData(BlendMode.Hue, "#949494")]
+    [InlineData(BlendMode.Saturation, "#949494")]
+    [InlineData(BlendMode.Luminosity, "#ffffff")]
+    [InlineData(BlendMode.Color, "#949494")]
+    public void TestThatBlendingWhiteOverRedBetweenTwoLayersInLinearSrgbWorksCorrectly(
+        BlendMode blendMode,
+        string? expectedColor)
+    {
+        NodeGraph graph = new NodeGraph();
+        var firstImageLayer = new ImageLayerNode(new VecI(1, 1), ColorSpace.CreateSrgbLinear());
+
+        var firstImg = firstImageLayer.GetLayerImageAtFrame(0);
+        firstImg.EnqueueDrawPixel(VecI.Zero, new Color(255, 0, 0, 255), Drawie.Backend.Core.Surfaces.BlendMode.Src);
+        firstImg.CommitChanges();
+
+        var secondImageLayer = new ImageLayerNode(new VecI(1, 1), ColorSpace.CreateSrgbLinear());
+        var secondImg = secondImageLayer.GetLayerImageAtFrame(0);
+        secondImg.EnqueueDrawPixel(VecI.Zero, new Color(255, 255, 255, 255), Drawie.Backend.Core.Surfaces.BlendMode.Src);
+        secondImg.CommitChanges();
+
+        var outputNode = new OutputNode();
+        graph.AddNode(firstImageLayer);
+        graph.AddNode(secondImageLayer);
+        graph.AddNode(outputNode);
+
+        firstImageLayer.Output.ConnectTo(secondImageLayer.Background);
+        secondImageLayer.Output.ConnectTo(outputNode.Input);
+
+        secondImageLayer.BlendMode.NonOverridenValue = blendMode;
+
+        Surface output = Surface.ForProcessing(VecI.One, ColorSpace.CreateSrgbLinear());
+        graph.Execute(new RenderContext(output.DrawingSurface, 0, ChunkResolution.Full, VecI.One, ColorSpace.CreateSrgbLinear(), 1));
+
+        Color result = output.GetSrgbPixel(VecI.Zero);
+        Assert.Equal(expectedColor, result.ToRgbHex());
+    }
+}

+ 1 - 12
tests/PixiEditor.Tests/EditableVectorPathTests.cs

@@ -7,19 +7,8 @@ using PixiEditor.Views.Overlays.PathOverlay;
 
 namespace PixiEditor.Tests;
 
-public class EditableVectorPathTests
+public class EditableVectorPathTests : PixiEditorTest
 {
-    public EditableVectorPathTests()
-    {
-        if (DrawingBackendApi.HasBackend)
-        {
-            return;
-        }
-        
-        SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
-        DrawingBackendApi.SetupBackend(skiaDrawingBackend, new DrawieRenderingDispatcher());
-    }
-
     [Fact]
     public void TestThatRectVectorShapeReturnsCorrectSubShapes()
     {

+ 19 - 0
tests/PixiEditor.Tests/PixiEditorTest.cs

@@ -0,0 +1,19 @@
+using Drawie.Backend.Core.Bridge;
+using Drawie.Skia;
+using DrawiEngine;
+
+namespace PixiEditor.Tests;
+
+public class PixiEditorTest
+{
+    public PixiEditorTest()
+    {
+        if (DrawingBackendApi.HasBackend)
+        {
+            return;
+        }
+
+        SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
+        DrawingBackendApi.SetupBackend(skiaDrawingBackend, new DrawieRenderingDispatcher());
+    }
+}

BIN
tests/SampleFiles/Blending/FolderFolderBlend.pixi


BIN
tests/SampleFiles/Blending/LayerFolderBlend.pixi


BIN
tests/SampleFiles/Blending/TwoLayersBlend.pixi


BIN
tests/SampleFiles/Nodes/VectorRasterMergeNode.pixi