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)
         bool useFilters)
     {
     {
         blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
         blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
+        var finalPaint = blendPaint;
 
 
         var targetSurface = workingSurface;
         var targetSurface = workingSurface;
         Texture? tex = null;
         Texture? tex = null;
@@ -135,21 +136,23 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 ColorSpace.CreateSrgb());
                 ColorSpace.CreateSrgb());
             targetSurface = tex.DrawingSurface;
             targetSurface = tex.DrawingSurface;
             workingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
             workingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+
+            finalPaint = new Paint();
         }
         }
         if (useFilters && Filters.Value != null)
         if (useFilters && Filters.Value != null)
         {
         {
             blendPaint.SetFilters(Filters.Value);
             blendPaint.SetFilters(Filters.Value);
-            DrawWithFilters(ctx, targetSurface, blendPaint);
+            DrawWithFilters(ctx, targetSurface, finalPaint);
         }
         }
         else
         else
         {
         {
             blendPaint.SetFilters(null);
             blendPaint.SetFilters(null);
-            DrawWithoutFilters(ctx, targetSurface, blendPaint);
+            DrawWithoutFilters(ctx, targetSurface, finalPaint);
         }
         }
 
 
         if (targetSurface != workingSurface)
         if (targetSurface != workingSurface)
         {
         {
-            workingSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+            workingSurface.Canvas.DrawSurface(targetSurface, 0, 0, blendPaint);
             tex.Dispose();
             tex.Dispose();
             workingSurface.Canvas.RestoreToCount(saved);
             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; }
     public RenderInputProperty Bottom { get; }
 
 
     private Paint paint = new Paint();
     private Paint paint = new Paint();
-    
+
     private int topLayer;
     private int topLayer;
     private int bottomLayer;
     private int bottomLayer;
-    
-    public MergeNode() 
+
+    public MergeNode()
     {
     {
         BlendMode = CreateInput("BlendMode", "BlendMode", Enums.BlendMode.Normal);
         BlendMode = CreateInput("BlendMode", "BlendMode", Enums.BlendMode.Normal);
         Top = CreateRenderInput("Top", "TOP");
         Top = CreateRenderInput("Top", "TOP");
@@ -36,16 +36,17 @@ public class MergeNode : RenderNode
 
 
     protected override void OnPaint(RenderContext context, DrawingSurface target)
     protected override void OnPaint(RenderContext context, DrawingSurface target)
     {
     {
-        if(Top.Value == null && Bottom.Value == null)
+        if (Top.Value == null && Bottom.Value == null)
         {
         {
             return;
             return;
         }
         }
-        
-        if(target == null || target.DeviceClipBounds.Size == VecI.Zero)
+
+        if (target == null || target.DeviceClipBounds.Size == VecI.Zero)
         {
         {
             return;
             return;
         }
         }
 
 
+        AllowHighDpiRendering = true;
         Merge(target, context);
         Merge(target, context);
     }
     }
 
 
@@ -59,7 +60,7 @@ public class MergeNode : RenderNode
 
 
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             target.Canvas.SaveLayer(paint);
             target.Canvas.SaveLayer(paint);
-            
+
             Top.Value?.Paint(context, target);
             Top.Value?.Paint(context, target);
             target.Canvas.RestoreToCount(saved);
             target.Canvas.RestoreToCount(saved);
             return;
             return;
@@ -71,13 +72,13 @@ public class MergeNode : RenderNode
 
 
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
     {
-        if(Top.Value == null && Bottom.Value == null)
+        if (Top.Value == null && Bottom.Value == null)
         {
         {
             return null;
             return null;
         }
         }
 
 
-        RectD? totalBounds = null; 
-        
+        RectD? totalBounds = null;
+
         if (Top.Connection != null && Top.Connection.Node is IPreviewRenderable topPreview)
         if (Top.Connection != null && Top.Connection.Node is IPreviewRenderable topPreview)
         {
         {
             var topBounds = topPreview.GetPreviewBounds(frame, elementToRenderName);
             var topBounds = topPreview.GetPreviewBounds(frame, elementToRenderName);
@@ -86,16 +87,16 @@ public class MergeNode : RenderNode
                 totalBounds = totalBounds?.Union(topBounds.Value) ?? topBounds;
                 totalBounds = totalBounds?.Union(topBounds.Value) ?? topBounds;
             }
             }
         }
         }
-        
+
         if (Bottom.Connection != null && Bottom.Connection.Node is IPreviewRenderable bottomPreview)
         if (Bottom.Connection != null && Bottom.Connection.Node is IPreviewRenderable bottomPreview)
         {
         {
             var bottomBounds = bottomPreview.GetPreviewBounds(frame, elementToRenderName);
             var bottomBounds = bottomPreview.GetPreviewBounds(frame, elementToRenderName);
             if (bottomBounds != null)
             if (bottomBounds != null)
             {
             {
                 totalBounds = totalBounds?.Union(bottomBounds.Value) ?? bottomBounds;
                 totalBounds = totalBounds?.Union(bottomBounds.Value) ?? bottomBounds;
-            } 
+            }
         }
         }
-        
+
         return totalBounds;
         return totalBounds;
     }
     }
 
 

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

@@ -1,5 +1,6 @@
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
@@ -41,7 +42,7 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
         lastDocumentSize = context.DocumentSize;
         lastDocumentSize = context.DocumentSize;
     }
     }
 
 
-    private void Paint(RenderContext context, DrawingSurface surface)
+    protected virtual void Paint(RenderContext context, DrawingSurface surface)
     {
     {
         DrawingSurface target = surface;
         DrawingSurface target = surface;
         bool useIntermediate = !AllowHighDpiRendering
         bool useIntermediate = !AllowHighDpiRendering
@@ -49,7 +50,7 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
                                && surface.DeviceClipBounds.Size != context.DocumentSize;
                                && surface.DeviceClipBounds.Size != context.DocumentSize;
         if (useIntermediate)
         if (useIntermediate)
         {
         {
-            Texture intermediate = textureCache.RequestTexture(0, context.DocumentSize, context.ProcessingColorSpace);
+            Texture intermediate = textureCache.RequestTexture(-6451, context.DocumentSize, context.ProcessingColorSpace);
             target = intermediate.DrawingSurface;
             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; }
     public InputProperty<int> Seed { get; }
 
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
+
     public DistributePointsNode()
     public DistributePointsNode()
     {
     {
         MaxPointCount = CreateInput("MaxPointCount", "MAX_POINTS", 10).
         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 PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Numerics;
 using Drawie.Numerics;
@@ -14,6 +15,8 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
 
 
     public InputProperty<int> Seed { get; }
     public InputProperty<int> Seed { get; }
 
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
+
     public RemoveClosePointsNode()
     public RemoveClosePointsNode()
     {
     {
         Input = CreateInput<PointsVectorData>("Input", "POINTS", null);
         Input = CreateInput<PointsVectorData>("Input", "POINTS", null);
@@ -33,7 +36,7 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
             return null;
             return null;
         }
         }
 
 
-        var availablePoints = data.Points.Distinct().ToList();
+        var availablePoints = data.Points.ToList();
         List<VecD> newPoints = new List<VecD>();
         List<VecD> newPoints = new List<VecD>();
 
 
         var random = new Random(Seed.Value);
         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)
     protected override void OnPaint(RenderContext context, DrawingSurface renderTarget)
     {
     {
         if (Output.Connections.Count > 0)
         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 readonly bool repeat;
     private int frame;
     private int frame;
     private int lastAppliedPointIndex = -1;
     private int lastAppliedPointIndex = -1;
+    private bool squareBrush;
 
 
     private List<VecI> ellipseLines;
     private List<VecI> ellipseLines;
+    private RectI squareRect;
 
 
     private CommittedChunkStorage? savedChunks;
     private CommittedChunkStorage? savedChunks;
 
 
     [GenerateUpdateableChangeActions]
     [GenerateUpdateableChangeActions]
     public ChangeBrightness_UpdateableChange(Guid layerGuid, VecI pos, float correctionFactor, int strokeWidth,
     public ChangeBrightness_UpdateableChange(Guid layerGuid, VecI pos, float correctionFactor, int strokeWidth,
+        bool square,
         bool repeat, int frame)
         bool repeat, int frame)
     {
     {
         this.layerGuid = layerGuid;
         this.layerGuid = layerGuid;
@@ -30,11 +33,16 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
         this.strokeWidth = strokeWidth;
         this.strokeWidth = strokeWidth;
         this.repeat = repeat;
         this.repeat = repeat;
         this.frame = frame;
         this.frame = frame;
+        this.squareBrush = square;
         positions.Add(pos);
         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]
     [UpdateChangeMethod]
@@ -67,8 +75,16 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
         for (int i = Math.Max(lastAppliedPointIndex, 0); i < positions.Count; i++)
         for (int i = Math.Max(lastAppliedPointIndex, 0); i < positions.Count; i++)
         {
         {
             VecI pos = positions[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);
         var affected = layerImage.FindAffectedArea(queueLength);
@@ -79,11 +95,9 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
     }
     }
 
 
     private static void ChangeBrightness(
     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)
         ChunkyImage layerImage)
     {
     {
-        // TODO: Circle diameter is unused, check if it should be used
-
         for (var i = 0; i < circleLines.Count - 1; i++)
         for (var i = 0; i < circleLines.Count - 1; i++)
         {
         {
             VecI left = circleLines[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,
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
         out bool ignoreInUndo)
     {
     {
@@ -121,8 +156,16 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, layerImage, layerGuid, false);
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, layerImage, layerGuid, false);
             foreach (VecI pos in positions)
             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]
     [GenerateUpdateableChangeActions]
     public DrawRasterLine_UpdateableChange
     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.memberGuid = memberGuid;
         this.from = from;
         this.from = from;
@@ -34,8 +35,10 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         this.frame = frame;
         this.frame = frame;
         this.antiAliasing = antiAliasing;
         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);
         paint.SetPaintable(paintable);
     }
     }
 
 
@@ -47,7 +50,7 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         this.paintable = paintable;
         this.paintable = paintable;
         this.caps = caps;
         this.caps = caps;
         this.strokeWidth = strokeWidth;
         this.strokeWidth = strokeWidth;
-        
+
         paint.SetPaintable(paintable);
         paint.SetPaintable(paintable);
         paint.StrokeWidth = strokeWidth;
         paint.StrokeWidth = strokeWidth;
         paint.StrokeCap = caps;
         paint.StrokeCap = caps;
@@ -66,15 +69,23 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         if (from != to)
         if (from != to)
         {
         {
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
             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
             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();
         var totalAffected = image.FindAffectedArea();
         totalAffected.UnionWith(oldAffected);
         totalAffected.UnionWith(oldAffected);
         return totalAffected;
         return totalAffected;
@@ -85,7 +96,8 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, CommonApply(target), drawOnMask);
         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)
         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 erasing;
     private readonly bool drawOnMask;
     private readonly bool drawOnMask;
     private readonly bool antiAliasing;
     private readonly bool antiAliasing;
+    private bool squareBrush;
     private float hardness;
     private float hardness;
     private float spacing = 1;
     private float spacing = 1;
     private readonly Paint srcPaint = new Paint() { BlendMode = BlendMode.Src };
     private readonly Paint srcPaint = new Paint() { BlendMode = BlendMode.Src };
@@ -32,6 +33,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         bool antiAliasing,
         bool antiAliasing,
         float hardness,
         float hardness,
         float spacing,
         float spacing,
+        bool squareBrush,
         bool drawOnMask, int frame)
         bool drawOnMask, int frame)
     {
     {
         this.memberGuid = memberGuid;
         this.memberGuid = memberGuid;
@@ -42,6 +44,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         this.drawOnMask = drawOnMask;
         this.drawOnMask = drawOnMask;
         this.hardness = hardness;
         this.hardness = hardness;
         this.spacing = spacing;
         this.spacing = spacing;
+        this.squareBrush = squareBrush;
         points.Add(pos);
         points.Add(pos);
         this.frame = frame;
         this.frame = frame;
 
 
@@ -101,12 +104,23 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             lastPos = point;
             lastPos = point;
             var rect = new RectI(point - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             var rect = new RectI(point - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             Paintable finalPaintable = color;
             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;
         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));
             var rect = new RectI(points[0] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             Paintable finalPaintable = color;
             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;
             return;
         }
         }
 
 
@@ -143,12 +170,24 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             lastPos = points[i];
             lastPos = points[i];
             var rect = new RectI(points[i] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             var rect = new RectI(points[i] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             Paintable? finalPaintable = color;
             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);
             pixelsToConfirm.Add(pixel);
         }
         }
+
         image.EnqueueDrawPixels(line.Select(point => new VecI((int)point.X, (int)point.Y)), color, BlendMode.Src);
         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]))
         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; }
     public ReleaseInfo LatestReleaseInfo { get; private set; }
 
 
     private UpdateChannel _channel;
     private UpdateChannel _channel;
-    public UpdateChannel Channel 
+
+    public UpdateChannel Channel
     {
     {
         get => _channel;
         get => _channel;
         set
         set
@@ -45,7 +46,7 @@ public class UpdateChecker
     {
     {
         return ExtractVersionString(originalVer) != ExtractVersionString(newVer);
         return ExtractVersionString(originalVer) != ExtractVersionString(newVer);
     }
     }
-    
+
     /// <summary>
     /// <summary>
     ///     Checks if originalVer is smaller than newVer
     ///     Checks if originalVer is smaller than newVer
     /// </summary>
     /// </summary>
@@ -59,24 +60,17 @@ public class UpdateChecker
 
 
         if (normalizedOriginal == normalizedNew) return false;
         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()
     public async Task<bool> CheckUpdateAvailable()
@@ -89,20 +83,45 @@ public class UpdateChecker
     {
     {
         if (latestRelease == null || string.IsNullOrEmpty(latestRelease.TagName)) return false;
         if (latestRelease == null || string.IsNullOrEmpty(latestRelease.TagName)) return false;
         if (CurrentVersionTag == null) return false;
         if (CurrentVersionTag == null) return false;
-        
+
         return latestRelease.WasDataFetchSuccessful && VersionDifferent(CurrentVersionTag, latestRelease.TagName);
         return latestRelease.WasDataFetchSuccessful && VersionDifferent(CurrentVersionTag, latestRelease.TagName);
     }
     }
 
 
     public bool IsUpdateCompatible(string[] incompatibleVersions)
     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()
     public async Task<bool> IsUpdateCompatible()
     {
     {
         string[] incompatibleVersions = await GetUpdateIncompatibleVersionsAsync(LatestReleaseInfo.TagName);
         string[] incompatibleVersions = await GetUpdateIncompatibleVersionsAsync(LatestReleaseInfo.TagName);
         bool isDowngrading = VersionSmaller(LatestReleaseInfo.TagName, CurrentVersionTag);
         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)
     public async Task<string[]> GetUpdateIncompatibleVersionsAsync(string tag)
@@ -140,7 +159,7 @@ public class UpdateChecker
     private static string ExtractVersionString(string versionString)
     private static string ExtractVersionString(string versionString)
     {
     {
         if (string.IsNullOrEmpty(versionString)) return string.Empty;
         if (string.IsNullOrEmpty(versionString)) return string.Empty;
-        
+
         for (int i = 0; i < versionString.Length; i++)
         for (int i = 0; i < versionString.Length; i++)
         {
         {
             if (!char.IsDigit(versionString[i]) && versionString[i] != '.')
             if (!char.IsDigit(versionString[i]) && versionString[i] != '.')
@@ -148,7 +167,7 @@ public class UpdateChecker
                 return versionString[..i];
                 return versionString[..i];
             }
             }
         }
         }
-        
+
         return versionString;
         return versionString;
     }
     }
 }
 }

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

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

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

@@ -55,7 +55,7 @@
   "APPLY_TRANSFORM": "Apply transform",
   "APPLY_TRANSFORM": "Apply transform",
   "INCREASE_TOOL_SIZE": "Increase tool size",
   "INCREASE_TOOL_SIZE": "Increase tool size",
   "DECREASE_TOOL_SIZE": "Decrease 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",
   "NEW_UPDATE": "New update",
   "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Couldn't update without admin privileges. Please run PixiEditor as administrator.",
   "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Couldn't update without admin privileges. Please run PixiEditor as administrator.",
   "INSUFFICIENT_PERMISSIONS": "Insufficient permissions",
   "INSUFFICIENT_PERMISSIONS": "Insufficient permissions",
@@ -757,7 +757,7 @@
   "MODULO": "Modulo",
   "MODULO": "Modulo",
   "STEP": "Step",
   "STEP": "Step",
   "SMOOTH_STEP": "Smoothstep",
   "SMOOTH_STEP": "Smoothstep",
-  "PIXEL_ART_TOOLSET": "Pixel Art",
+  "PIXEL_PERFECT_TOOLSET": "Pixel Perfect",
   "VECTOR_TOOLSET": "Vector",
   "VECTOR_TOOLSET": "Vector",
   "VECTOR_LAYER": "Vector Layer",
   "VECTOR_LAYER": "Vector Layer",
   "STROKE_COLOR_LABEL": "Stroke",
   "STROKE_COLOR_LABEL": "Stroke",
@@ -997,5 +997,15 @@
   "SWITCH_TO_NEW_VERSION": "Switch",
   "SWITCH_TO_NEW_VERSION": "Switch",
   "DOWNLOAD_UPDATE": "Download",
   "DOWNLOAD_UPDATE": "Download",
   "DOWNLOADING_UPDATE": "Downloading update...",
   "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;
             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>
     /// <summary>

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

@@ -57,7 +57,15 @@ public class AnalyticsPeriodicReporter
     {
     {
         await _cancellationToken.CancelAsync();
         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)
     public void AddEvent(AnalyticEvent value)
@@ -87,14 +95,21 @@ public class AnalyticsPeriodicReporter
     {
     {
         if (!_resumeSession)
         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;
                 return;
             }
             }
-
-            SessionId = createSession.Value;
         }
         }
 
 
         Task.Run(RunHeartbeatAsync);
         Task.Run(RunHeartbeatAsync);

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

@@ -15,6 +15,7 @@ internal class BrightnessToolExecutor : UpdateableChangeExecutor
     private bool repeat;
     private bool repeat;
     private float correctionFactor;
     private float correctionFactor;
     private int toolSize;
     private int toolSize;
+    private bool squareBrush;
 
 
     public override ExecutionState Start()
     public override ExecutionState Start()
     {
     {
@@ -30,7 +31,9 @@ internal class BrightnessToolExecutor : UpdateableChangeExecutor
         toolSize = (int)toolbar.ToolSize;
         toolSize = (int)toolbar.ToolSize;
         correctionFactor = tool.Darken || tool.UsedWith == MouseButton.Right ? -tool.CorrectionFactor : tool.CorrectionFactor;
         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);
         internals!.ActionAccumulator.AddActions(action);
 
 
         return ExecutionState.Success;
         return ExecutionState.Success;
@@ -38,7 +41,7 @@ internal class BrightnessToolExecutor : UpdateableChangeExecutor
 
 
     public override void OnPixelPositionChange(VecI pos)
     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);
         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 void DrawShape(VecD currentPos, double rotationRad, bool firstDraw);
     protected abstract IAction SettingsChangedAction(string name, object value);
     protected abstract IAction SettingsChangedAction(string name, object value);
     protected abstract IAction TransformMovedAction(ShapeData data, ShapeCorners corners);
     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 bool CanEditShape(IStructureMemberHandler layer);
     protected abstract IAction EndDrawAction();
     protected abstract IAction EndDrawAction();
     protected virtual DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective;
     protected virtual DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective;
@@ -234,7 +234,6 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
                 EndDrawAction(),
                 EndDrawAction(),
                 SettingsChangedAction("FillAndStroke", color),
                 SettingsChangedAction("FillAndStroke", color),
                 EndDrawAction());
                 EndDrawAction());
-            // TODO add to undo
         }
         }
     }
     }
 
 
@@ -315,7 +314,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
 
         if (highlight)
         if (highlight)
         {
         {
-            HighlightSnapAxis(snapXAxis, snapYAxis);
+            HighlightSnapAxis(snapXAxis, snapYAxis, string.IsNullOrEmpty(snapXAxis) && string.IsNullOrEmpty(snapYAxis) ? null : snapped);
         }
         }
 
 
         if (AlignToPixels)
         if (AlignToPixels)
@@ -345,7 +344,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
 
         if (highlight)
         if (highlight)
         {
         {
-            HighlightSnapAxis(snapXAxis, snapYAxis);
+            HighlightSnapAxis(snapXAxis, snapYAxis, string.IsNullOrEmpty(snapXAxis) && string.IsNullOrEmpty(snapYAxis) ? null : snapped);
         }
         }
 
 
         if (snapped != VecI.Zero)
         if (snapped != VecI.Zero)
@@ -364,11 +363,11 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         return snapped;
         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.HighlightedXAxis = snapXAxis;
         document.SnappingHandler.SnappingController.HighlightedYAxis = snapYAxis;
         document.SnappingHandler.SnappingController.HighlightedYAxis = snapYAxis;
-        document.SnappingHandler.SnappingController.HighlightedPoint = null;
+        document.SnappingHandler.SnappingController.HighlightedPoint = snapPoint;
     }
     }
 
 
     public override void OnSettingsChanged(string name, object value)
     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
 internal class EraserToolExecutor : UpdateableChangeExecutor
 {
 {
+    public bool SquareBrush => penToolbar.PaintShape == PaintBrushShape.Square;
     private Guid guidValue;
     private Guid guidValue;
     private Color color;
     private Color color;
     private double toolSize;
     private double toolSize;
@@ -22,12 +23,14 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
     private float spacing;
     private float spacing;
     
     
     private bool drawOnMask;
     private bool drawOnMask;
+    private IPenToolbar penToolbar;
 
 
     public override ExecutionState Start()
     public override ExecutionState Start()
     {
     {
         IStructureMemberHandler? member = document!.SelectedStructureMember;
         IStructureMemberHandler? member = document!.SelectedStructureMember;
         IEraserToolHandler? eraserTool = GetHandler<IEraserToolHandler>();
         IEraserToolHandler? eraserTool = GetHandler<IEraserToolHandler>();
         IPenToolbar? toolbar = eraserTool?.Toolbar as IPenToolbar;
         IPenToolbar? toolbar = eraserTool?.Toolbar as IPenToolbar;
+        penToolbar = toolbar;
         IColorsHandler? colorsHandler = GetHandler<IColorsHandler>();
         IColorsHandler? colorsHandler = GetHandler<IColorsHandler>();
 
 
         if (colorsHandler is null || eraserTool is null || member is null || toolbar is null)
         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));
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, (float)toolSize, true,
         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);
         internals!.ActionAccumulator.AddActions(action);
 
 
         return ExecutionState.Success;
         return ExecutionState.Success;
@@ -56,7 +59,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
 
     public override void OnPixelPositionChange(VecI pos)
     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);
         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;
                 return ExecutionState.Success;
             }
             }
 
 
+            toolbar.StrokeBrush = data.Stroke.ToBrush();
+
             if (!InitShapeData(data))
             if (!InitShapeData(data))
             {
             {
                 ActiveMode = ShapeToolMode.Preview;
                 ActiveMode = ShapeToolMode.Preview;
@@ -192,8 +194,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         var moveOverlayAction = TransformOverlayMoved(start, end);
         var moveOverlayAction = TransformOverlayMoved(start, end);
         internals!.ActionAccumulator.AddActions(moveOverlayAction);
         internals!.ActionAccumulator.AddActions(moveOverlayAction);
 
 
-        startDrawingPos = (VecI)start;
-        curPos = (VecI)end;
+        startDrawingPos = start;
+        curPos = end;
     }
     }
 
 
     public override void OnColorChanged(Color color, bool primary)
     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 Guid guidValue;
     private Color color;
     private Color color;
     public double ToolSize => penToolbar.ToolSize;
     public double ToolSize => penToolbar.ToolSize;
+    public bool SquareBrush => penToolbar.PaintShape == PaintBrushShape.Square;
+
     private bool drawOnMask;
     private bool drawOnMask;
     private bool pixelPerfect;
     private bool pixelPerfect;
     private bool antiAliasing;
     private bool antiAliasing;
@@ -48,7 +50,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = pixelPerfect switch
         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)
             true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         };
         internals!.ActionAccumulator.AddActions(action);
         internals!.ActionAccumulator.AddActions(action);
@@ -60,7 +62,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     {
     {
         IAction? action = pixelPerfect switch
         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)
             true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         };
         internals!.ActionAccumulator.AddActions(action);
         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)
     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)
     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)
     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()
     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
 internal interface IPenToolbar : IToolbar, IToolSizeToolbar
 {
 {
     public bool AntiAliasing { get; set; }
     public bool AntiAliasing { get; set; }
     public float Hardness { get; set; }
     public float Hardness { get; set; }
     public float Spacing { 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 bool Darken { get; }
     public MouseButton UsedWith { get; }
     public MouseButton UsedWith { get; }
     public float CorrectionFactor { 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
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [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 =>
     public bool AnySymmetryAxisEnabledBindable =>
         HorizontalSymmetryAxisEnabledBindable || VerticalSymmetryAxisEnabledBindable;
         HorizontalSymmetryAxisEnabledBindable || VerticalSymmetryAxisEnabledBindable;
 
 
+
+    public bool OverlayEventsSuppressed => overlaySuppressors.Count > 0;
+
+    private readonly HashSet<string> overlaySuppressors = new();
+
     private VecI size = new VecI(64, 64);
     private VecI size = new VecI(64, 64);
     public int Width => size.X;
     public int Width => size.X;
     public int Height => size.Y;
     public int Height => size.Y;
@@ -224,6 +229,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public AnimationDataViewModel AnimationDataViewModel { get; }
     public AnimationDataViewModel AnimationDataViewModel { get; }
     public TextOverlayViewModel TextOverlayViewModel { get; }
     public TextOverlayViewModel TextOverlayViewModel { get; }
 
 
+
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers => softSelectedStructureMembers;
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers => softSelectedStructureMembers;
     private DocumentInternalParts Internals { get; }
     private DocumentInternalParts Internals { get; }
     INodeGraphHandler IDocument.NodeGraphHandler => NodeGraph;
     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));
         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)
     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
         // 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
 internal abstract class ShapeTool : ToolViewModel, IShapeToolHandler
 {
 {
-    public override BrushShape BrushShape => BrushShape.Hidden;
+    public override BrushShape FinalBrushShape => BrushShape.Hidden;
 
 
     public override bool UsesColor => true;
     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.Models.Handlers.Toolbars;
 using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
+using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
 
 namespace PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 namespace PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 
@@ -29,6 +30,12 @@ internal class PenToolbar : Toolbar, IPenToolbar
         set => GetSetting<SizeSettingViewModel>(nameof(ToolSize)).Value = value;
         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()
     public override void OnLoadedSettings()
     {
     {
         OnPropertyChanged(nameof(ToolSize));
         OnPropertyChanged(nameof(ToolSize));
@@ -42,5 +49,6 @@ internal class PenToolbar : Toolbar, IPenToolbar
         var setting = new SizeSettingViewModel(nameof(ToolSize), "TOOL_SIZE_LABEL");
         var setting = new SizeSettingViewModel(nameof(ToolSize), "TOOL_SIZE_LABEL");
         setting.ValueChanged += (_, _) => OnPropertyChanged(nameof(ToolSize));
         setting.ValueChanged += (_, _) => OnPropertyChanged(nameof(ToolSize));
         AddSetting(setting);
         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 string DefaultIcon => $"\u25a1";
 
 
-    public virtual BrushShape BrushShape => BrushShape.Square;
+    public virtual BrushShape FinalBrushShape => BrushShape.Square;
 
 
     public abstract Type[]? SupportedLayerTypes { get; }
     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.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
@@ -29,7 +30,7 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
     public override bool IsErasable => true;
     public override bool IsErasable => true;
     public override LocalizedString Tooltip => new LocalizedString("BRIGHTNESS_TOOL_TOOLTIP", Shortcut);
     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;
     public override string DefaultIcon => PixiPerfectIcons.Sun;
 
 
@@ -51,6 +52,17 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
 
 
     [Settings.Enum("MODE_LABEL")]
     [Settings.Enum("MODE_LABEL")]
     public BrightnessMode BrightnessMode => GetValue<BrightnessMode>();
     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;
     public bool Darken { get; private set; } = false;
 
 
@@ -76,4 +88,9 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
     {
     {
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseBrightnessTool();
         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 bool UsesColor => true;
 
 
     public override string ToolNameLocalizationKey => "COLOR_PICKER_TOOL";
     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;
     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) =>
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey) =>
         UpdateActionDisplay(ctrlIsDown, shiftIsDown);
         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;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
@@ -26,7 +27,7 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
     public override bool IsErasable => true;
     public override bool IsErasable => true;
 
 
     public override string ToolNameLocalizationKey => "ERASER_TOOL";
     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 Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
 
 
     public override string DefaultIcon => PixiPerfectIcons.Eraser;
     public override string DefaultIcon => PixiPerfectIcons.Eraser;
@@ -39,6 +40,17 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
         Notify = nameof(BrushShapeChanged))]
         Notify = nameof(BrushShapeChanged))]
     public BrushShape BrushShapeSetting => GetValue<BrushShape>();
     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)
     public override void UseTool(VecD pos)
     {
     {
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseEraserTool();
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseEraserTool();
@@ -46,6 +58,11 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
 
 
     private void BrushShapeChanged()
     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";
     private readonly string defaultActionDisplay = "FLOOD_FILL_TOOL_ACTION_DISPLAY_DEFAULT";
 
 
     public override string ToolNameLocalizationKey => "FLOOD_FILL_TOOL";
     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 Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
 
 
     public override LocalizedString Tooltip => new("FLOOD_FILL_TOOL_TOOLTIP", Shortcut);
     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 ToolNameLocalizationKey => "LASSO_TOOL";
     public override string DefaultIcon => PixiPerfectIcons.Lasso;
     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
     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 LocalizedString Tooltip => new LocalizedString("MAGIC_WAND_TOOL_TOOLTIP", Shortcut);
 
 
     public override string ToolNameLocalizationKey => "MAGIC_WAND_TOOL";
     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) }; 
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) }; 
 
 
     [Settings.Enum("MODE_LABEL")]
     [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);
         set => SetValue(value);
     }
     }
 
 
-    public override BrushShape BrushShape => BrushShape.Hidden;
+    public override BrushShape FinalBrushShape => BrushShape.Hidden;
     public override Type[]? SupportedLayerTypes { get; } = null;
     public override Type[]? SupportedLayerTypes { get; } = null;
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
     public override bool HideHighlight => true;
     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
 internal class MoveViewportToolViewModel : ToolViewModel
 {
 {
     public override string ToolNameLocalizationKey => "MOVE_VIEWPORT_TOOL";
     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[]? SupportedLayerTypes { get; } = null;
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
     public override bool HideHighlight => true;
     public override bool HideHighlight => true;
@@ -28,5 +28,12 @@ internal class MoveViewportToolViewModel : ToolViewModel
     protected override void OnSelected(bool restoring)
     protected override void OnSelected(bool restoring)
     {
     {
         ActionDisplay = new LocalizedString("MOVE_VIEWPORT_ACTION_DISPLAY");
         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.Handlers.Tools;
 using PixiEditor.Models.Input;
 using PixiEditor.Models.Input;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
@@ -20,7 +21,9 @@ namespace PixiEditor.ViewModels.Tools.Tools
         private double actualToolSize;
         private double actualToolSize;
 
 
         public override string ToolNameLocalizationKey => "PEN_TOOL";
         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) };
         public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
 
 
@@ -41,7 +44,21 @@ namespace PixiEditor.ViewModels.Tools.Tools
         public bool PixelPerfectEnabled => GetValue<bool>();
         public bool PixelPerfectEnabled => GetValue<bool>();
         
         
         [Settings.Enum("BRUSH_SHAPE_SETTING", BrushShape.CirclePixelated, ExposedByDefault = false, Notify = nameof(BrushShapeChanged))]
         [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;
         public override string DefaultIcon => PixiPerfectIcons.Pen;
 
 
@@ -133,7 +150,13 @@ namespace PixiEditor.ViewModels.Tools.Tools
         
         
         private void BrushShapeChanged()
         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";
     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 string ToolNameLocalizationKey => "LINE_TOOL";
     public override LocalizedString Tooltip => new LocalizedString("LINE_TOOL_TOOLTIP", Shortcut);
     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 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)
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
     {
         DrawFromCenter = ctrlIsDown;
         DrawFromCenter = ctrlIsDown;

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

@@ -10,7 +10,7 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 internal class RotateViewportToolViewModel : ToolViewModel
 internal class RotateViewportToolViewModel : ToolViewModel
 {
 {
     public override string ToolNameLocalizationKey => "ROTATE_VIEWPORT_TOOL";
     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[]? SupportedLayerTypes { get; } = null; // null = all
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
     public override Type LayerTypeToCreateOnEmptyUse { get; } = null;
     public override bool HideHighlight => true;
     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")]
     [Settings.Enum("SHAPE_LABEL")]
     public SelectionShape SelectShape => GetValue<SelectionShape>();
     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 Type[]? SupportedLayerTypes { get; } = null;
 
 
     public override LocalizedString Tooltip => new LocalizedString("SELECT_TOOL_TOOLTIP", Shortcut);
     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)
     protected override void OnSelected(bool restoring)
     {
     {
-        if (restoring) return;
-
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorEllipseTool();
         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 override bool IsErasable => false;
 
 
-    public VectorLineToolViewModel()
-    {
-        ActionDisplay = defaultActionDisplay;
-        Toolbar = ToolbarFactory.Create<VectorLineToolViewModel, ShapeToolbar>(this);
-    }
 
 
     public override string ToolNameLocalizationKey => "LINE_TOOL";
     public override string ToolNameLocalizationKey => "LINE_TOOL";
     public override LocalizedString Tooltip => new LocalizedString("LINE_TOOL_TOOLTIP", Shortcut);
     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 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)
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
     {
         DrawFromCenter = ctrlIsDown;
         DrawFromCenter = ctrlIsDown;
@@ -66,8 +72,6 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 
 
     protected override void OnSelected(bool restoring)
     protected override void OnSelected(bool restoring)
     {
     {
-        if (restoring) return;
-
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         document.Tools.UseVectorLineTool();
         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)
     protected override void OnSelected(bool restoring)
     {
     {
-        if (restoring) return;
-
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorPathTool();
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorPathTool();
         isActivated = true;
         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)
     protected override void OnSelected(bool restoring)
     {
     {
-        if (restoring) return;
-
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorRectangleTool();
         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");
     private string defaultActionDisplay = new LocalizedString("ZOOM_TOOL_ACTION_DISPLAY_DEFAULT");
 
 
     public override string ToolNameLocalizationKey => "ZOOM_TOOL";
     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 Type[]? SupportedLayerTypes { get; } = null;
 
 
     public override bool StopsLinkedToolOnUse => false;
     public override bool StopsLinkedToolOnUse => false;

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

@@ -12,11 +12,8 @@
                          x:Class="PixiEditor.Views.Dialogs.ExportFilePopup"
                          x:Class="PixiEditor.Views.Dialogs.ExportFilePopup"
                          x:ClassModifier="internal"
                          x:ClassModifier="internal"
                          ui1:Translator.Key="EXPORT_IMAGE">
                          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 SelectedIndex="{Binding SelectedExportIndex, ElementName=saveFilePopup}">
                 <TabControl.Styles>
                 <TabControl.Styles>
                     <Style Selector="TabControl">
                     <Style Selector="TabControl">
@@ -27,11 +24,8 @@
                     </Style>
                     </Style>
                 </TabControl.Styles>
                 </TabControl.Styles>
                 <TabControl.Items>
                 <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">
                     <TabItem ui1:Translator.Key="EXPORT_SPRITESHEET_HEADER">
                         <Grid>
                         <Grid>
                             <Grid.ColumnDefinitions>
                             <Grid.ColumnDefinitions>
@@ -42,11 +36,11 @@
                                 <RowDefinition Height="Auto" />
                                 <RowDefinition Height="Auto" />
                                 <RowDefinition Height="Auto" />
                                 <RowDefinition Height="Auto" />
                             </Grid.RowDefinitions>
                             </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"
                             <input:NumberInput Min="0" Width="50" Grid.Column="1" Grid.Row="0"
                                                Decimals="0"
                                                Decimals="0"
                                                Value="{Binding ElementName=saveFilePopup, Path=SpriteSheetRows, Mode=TwoWay}" />
                                                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"
                             <input:NumberInput Min="0" Width="50" Grid.Column="1" Grid.Row="1"
                                                Decimals="0"
                                                Decimals="0"
                                                Value="{Binding ElementName=saveFilePopup, Path=SpriteSheetColumns, Mode=TwoWay}" />
                                                Value="{Binding ElementName=saveFilePopup, Path=SpriteSheetColumns, Mode=TwoWay}" />
@@ -54,15 +48,15 @@
                     </TabItem>
                     </TabItem>
                 </TabControl.Items>
                 </TabControl.Items>
             </TabControl>
             </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>
                     <Grid.ColumnDefinitions>
                         <ColumnDefinition Width="*" />
                         <ColumnDefinition Width="*" />
-                        <ColumnDefinition Width="160" />
+                        <ColumnDefinition Width="192" />
                     </Grid.ColumnDefinitions>
                     </Grid.ColumnDefinitions>
-                    <Grid>
+                    <Grid Margin="5">
                         <Grid.RowDefinitions>
                         <Grid.RowDefinitions>
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="Auto" />
@@ -86,33 +80,24 @@
                         </TextBlock>
                         </TextBlock>
                     </Grid>
                     </Grid>
                     <Grid Grid.Column="1">
                     <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>
                 </Grid>
                 </Grid>
             </Border>
             </Border>
+            <Button HorizontalAlignment="Center" IsDefault="True"
+                    ui1:Translator.Key="EXPORT" Command="{Binding ExportCommand, ElementName=saveFilePopup}" />
         </StackPanel>
         </StackPanel>
     </DockPanel>
     </DockPanel>
 </dialogs:PixiEditorPopup>
 </dialogs:PixiEditorPopup>

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

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

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

@@ -167,6 +167,7 @@ internal class LineToolOverlay : Overlay
         {
         {
             SnappingController.HighlightedXAxis = null;
             SnappingController.HighlightedXAxis = null;
             SnappingController.HighlightedYAxis = null;
             SnappingController.HighlightedYAxis = null;
+            SnappingController.HighlightedPoint = null;
             Refresh();
             Refresh();
         }
         }
 
 
@@ -273,6 +274,7 @@ internal class LineToolOverlay : Overlay
 
 
             SnappingController.HighlightedXAxis = snapAxisX;
             SnappingController.HighlightedXAxis = snapAxisX;
             SnappingController.HighlightedYAxis = snapAxisY;
             SnappingController.HighlightedYAxis = snapAxisY;
+            SnappingController.HighlightedPoint = x != null || y != null ? final : null;
         }
         }
 
 
         return final;
         return final;
@@ -294,12 +296,13 @@ internal class LineToolOverlay : Overlay
         VecD mappedStart = lineStartOnMouseDown;
         VecD mappedStart = lineStartOnMouseDown;
         VecD mappedEnd = lineEndOnMouseDown;
         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)
         if (SnappingController != null)
         {
         {
             SnappingController.HighlightedXAxis = snapDeltaResult.Item1.Item1;
             SnappingController.HighlightedXAxis = snapDeltaResult.Item1.Item1;
             SnappingController.HighlightedYAxis = snapDeltaResult.Item1.Item2;
             SnappingController.HighlightedYAxis = snapDeltaResult.Item1.Item2;
+            SnappingController.HighlightedPoint = snapSource;
         }
         }
 
 
         LineStart = lineStartOnMouseDown + delta + snapDeltaResult.Item2;
         LineStart = lineStartOnMouseDown + delta + snapDeltaResult.Item2;
@@ -319,10 +322,11 @@ internal class LineToolOverlay : Overlay
             ActionCompleted.Execute(null);
             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)
         if (SnappingController == null)
         {
         {
+            snapSource = null;
             return ((string.Empty, string.Empty), delta);
             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[] pointsToTest = new VecD[] { center + delta, originalStart + delta, originalEnd + delta, };
 
 
         VecD snapDelta =
         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);
         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 Handle? CapturedHandle { get; set; } = null!;
     public VecD PointerPosition { get; internal set; }
     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 readonly Dictionary<AvaloniaProperty, OverlayTransition> activeTransitions = new();
 
 
     private DispatcherTimer? transitionTimer;
     private DispatcherTimer? transitionTimer;
@@ -99,6 +111,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
 
 
     public void EnterPointer(OverlayPointerArgs args)
     public void EnterPointer(OverlayPointerArgs args)
     {
     {
+        if(SuppressEvents) return;
         OnOverlayPointerEntered(args);
         OnOverlayPointerEntered(args);
         if (args.Handled) return;
         if (args.Handled) return;
         InvokeHandleEvent(HandleEventType.PointerEnteredOverlay, args);
         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)
     public void ExitPointer(OverlayPointerArgs args)
     {
     {
+        if(SuppressEvents) return;
         OnOverlayPointerExited(args);
         OnOverlayPointerExited(args);
         if (args.Handled) return;
         if (args.Handled) return;
         InvokeHandleEvent(HandleEventType.PointerExitedOverlay, args);
         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)
     public void MovePointer(OverlayPointerArgs args)
     {
     {
+        if(SuppressEvents) return;
         InvokeHandleEvent(HandleEventType.PointerMovedOverlay, args);
         InvokeHandleEvent(HandleEventType.PointerMovedOverlay, args);
         if (args.Handled) return;
         if (args.Handled) return;
         OnOverlayPointerMoved(args);
         OnOverlayPointerMoved(args);
@@ -138,6 +153,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
 
 
     public void PressPointer(OverlayPointerArgs args)
     public void PressPointer(OverlayPointerArgs args)
     {
     {
+        if(SuppressEvents) return;
         InvokeHandleEvent(HandleEventType.PointerPressedOverlay, args);
         InvokeHandleEvent(HandleEventType.PointerPressedOverlay, args);
         if (args.Handled) return;
         if (args.Handled) return;
         OnOverlayPointerPressed(args);
         OnOverlayPointerPressed(args);
@@ -147,6 +163,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
 
 
     public void ReleasePointer(OverlayPointerArgs args)
     public void ReleasePointer(OverlayPointerArgs args)
     {
     {
+        if(SuppressEvents) return;
         InvokeHandleEvent(HandleEventType.PointerReleasedOverlay, args);
         InvokeHandleEvent(HandleEventType.PointerReleasedOverlay, args);
         if (args.Handled)
         if (args.Handled)
         {
         {
@@ -165,19 +182,21 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     
     
     public void KeyPressed(KeyEventArgs args)
     public void KeyPressed(KeyEventArgs args)
     {
     {
+        if(SuppressEvents) return;
         OnKeyPressed(args.Key, args.KeyModifiers, args.KeySymbol);
         OnKeyPressed(args.Key, args.KeyModifiers, args.KeySymbol);
         KeyPressedOverlay?.Invoke(args.Key, args.KeyModifiers);
         KeyPressedOverlay?.Invoke(args.Key, args.KeyModifiers);
     }
     }
 
 
     public void KeyReleased(KeyEventArgs keyEventArgs)
     public void KeyReleased(KeyEventArgs keyEventArgs)
     {
     {
+        if(SuppressEvents) return;
         OnKeyReleased(keyEventArgs.Key, keyEventArgs.KeyModifiers);
         OnKeyReleased(keyEventArgs.Key, keyEventArgs.KeyModifiers);
         KeyReleasedOverlay?.Invoke(keyEventArgs.Key, keyEventArgs.KeyModifiers);
         KeyReleasedOverlay?.Invoke(keyEventArgs.Key, keyEventArgs.KeyModifiers);
     }
     }
 
 
     public virtual bool TestHit(VecD point)
     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)
     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)
     public override void RenderOverlay(Canvas context, RectD canvasBounds)
     {
     {
-        if (SnappingController is null)
+        if (SnappingController is null || SuppressEvents)
         {
         {
             return;
             return;
         }
         }

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

@@ -902,9 +902,9 @@ internal class TransformOverlay : Overlay
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos,
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos,
                 ScaleFromCenter,
                 ScaleFromCenter,
                 SnappingController,
                 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)
             if (newCorners is not null)
             {
             {
@@ -917,8 +917,13 @@ internal class TransformOverlay : Overlay
                     : (ShapeCorners)newCorners;
                     : (ShapeCorners)newCorners;
 
 
                 Corners = (ShapeCorners)newCorners;
                 Corners = (ShapeCorners)newCorners;
+                if (!string.IsNullOrEmpty(snapX) || !string.IsNullOrEmpty(snapY))
+                {
+                    snapPoint = TransformHelper.GetAnchorPosition((ShapeCorners)newCorners, (Anchor)capturedAnchor);
+                }
             }
             }
 
 
+            HighlightSnappedAxis(snapX, snapY, snapPoint);
             UpdateOriginPos();
             UpdateOriginPos();
         }
         }
         else if (TransformHelper.IsSide((Anchor)capturedAnchor))
         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
     public static ShapeCorners? UpdateShapeFromCorner
     (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
     (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
         VecD desiredPos, bool scaleFromCenter,
         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))
         if (!TransformHelper.IsCorner(targetCorner))
             throw new ArgumentException($"{targetCorner} is not a corner");
             throw new ArgumentException($"{targetCorner} is not a corner");
@@ -21,7 +21,6 @@ internal static class TransformUpdateHelper
         if (freedom == TransformCornerFreedom.Locked)
         if (freedom == TransformCornerFreedom.Locked)
         {
         {
             snapX = snapY = "";
             snapX = snapY = "";
-            snapPoint = null;
             return corners;
             return corners;
         }
         }
 
 
@@ -33,7 +32,6 @@ internal static class TransformUpdateHelper
             VecD oppositePos = TransformHelper.GetAnchorPosition(corners, opposite);
             VecD oppositePos = TransformHelper.GetAnchorPosition(corners, opposite);
 
 
             snapX = snapY = "";
             snapX = snapY = "";
-            snapPoint = null;
 
 
             // constrain desired pos to a "propotional" diagonal line if needed
             // constrain desired pos to a "propotional" diagonal line if needed
             if (freedom == TransformCornerFreedom.ScaleProportionally && corners.IsRect)
             if (freedom == TransformCornerFreedom.ScaleProportionally && corners.IsRect)
@@ -45,7 +43,6 @@ internal static class TransformUpdateHelper
                 if (snappingController is not null)
                 if (snappingController is not null)
                 {
                 {
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
-                    snapPoint = string.IsNullOrEmpty(snapX) && string.IsNullOrEmpty(snapY) ? null : desiredPos;
                 }
                 }
             }
             }
             else if (freedom == TransformCornerFreedom.ScaleProportionally)
             else if (freedom == TransformCornerFreedom.ScaleProportionally)
@@ -56,7 +53,6 @@ internal static class TransformUpdateHelper
                 if (snappingController is not null)
                 if (snappingController is not null)
                 {
                 {
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
-                    snapPoint = string.IsNullOrEmpty(snapX) && string.IsNullOrEmpty(snapY) ? null : desiredPos;
                 }
                 }
             }
             }
             else
             else
@@ -64,7 +60,6 @@ internal static class TransformUpdateHelper
                 if (snappingController is not null)
                 if (snappingController is not null)
                 {
                 {
                     desiredPos = snappingController.GetSnapPoint(desiredPos, out snapX, out snapY);
                     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)
         if (freedom == TransformCornerFreedom.Free)
         {
         {
             snapX = snapY = "";
             snapX = snapY = "";
-            snapPoint = null;
             ShapeCorners newCorners = TransformHelper.UpdateCorner(corners, targetCorner, desiredPos);
             ShapeCorners newCorners = TransformHelper.UpdateCorner(corners, targetCorner, desiredPos);
             return newCorners.IsLegal ? newCorners : null;
             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.PointerPosition = lastMousePosition;
+
                 overlay.ZoomScale = Scale;
                 overlay.ZoomScale = Scale;
 
 
                 if (!overlay.CanRender()) continue;
                 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;
 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]
     [Fact]
     public void TestThatRectVectorShapeReturnsCorrectSubShapes()
     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