Browse Source

Fixed line issues

flabbet 11 months ago
parent
commit
5fc6cbd54e

+ 10 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+using System.Diagnostics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
@@ -56,22 +57,25 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
 
     private void Rasterize(DrawingSurface drawingSurface, ChunkResolution resolution, Paint paint, bool applyTransform)
     {
-        RectD adjustedAABB = GeometryAABB.RoundOutwards().Inflate(1);
+        RectD adjustedAABB = GeometryAABB.RoundOutwards();
+        adjustedAABB = adjustedAABB with { Size = adjustedAABB.Size + new VecD(1, 1) };
         var imageSize = (VecI)adjustedAABB.Size;
         
         using ChunkyImage img = new ChunkyImage(imageSize);
 
         if (StrokeWidth == 1)
         {
+            VecD adjustment = new VecD(0.5, 0.5); 
+            
             img.EnqueueDrawBresenhamLine(
-                (VecI)Start - (VecI)adjustedAABB.TopLeft,
-                (VecI)End - (VecI)adjustedAABB.TopLeft, StrokeColor, BlendMode.SrcOver);
+                (VecI)(Start - adjustedAABB.TopLeft - adjustment),
+                (VecI)(End - adjustedAABB.TopLeft - adjustment), StrokeColor, BlendMode.SrcOver); 
         }
         else
         {
             img.EnqueueDrawSkiaLine(
-                (VecI)Start - (VecI)adjustedAABB.TopLeft,
-                (VecI)End - (VecI)adjustedAABB.TopLeft, StrokeCap.Butt, StrokeWidth, StrokeColor, BlendMode.SrcOver);
+                (VecI)Start.Round() - (VecI)adjustedAABB.TopLeft,
+                (VecI)End.Round() - (VecI)adjustedAABB.TopLeft, StrokeCap.Butt, StrokeWidth, StrokeColor, BlendMode.SrcOver);
         }
 
         img.CommitChanges();

+ 13 - 4
src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs

@@ -44,8 +44,11 @@ internal class SetShapeGeometry_UpdateableChange : UpdateableChange
         var node = target.FindNode<VectorLayerNode>(TargetId);
         node.ShapeData = Data;
 
+        RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
+        aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
+
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
-            (RectI)node.ShapeData.TransformedAABB, ChunkyImage.FullChunkSize));
+            (RectI)aabb, ChunkyImage.FullChunkSize));
 
         var tmp = new AffectedArea(affected);
         
@@ -66,8 +69,11 @@ internal class SetShapeGeometry_UpdateableChange : UpdateableChange
         var node = target.FindNode<VectorLayerNode>(TargetId);
         node.ShapeData = Data;
         
+        RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
+        aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
+
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
-            (RectI)node.ShapeData.TransformedAABB, ChunkyImage.FullChunkSize));
+            (RectI)aabb, ChunkyImage.FullChunkSize));
 
         return new VectorShape_ChangeInfo(node.Id, affected);
     }
@@ -80,9 +86,12 @@ internal class SetShapeGeometry_UpdateableChange : UpdateableChange
         AffectedArea affected = new AffectedArea();
         
         if (node.ShapeData != null)
-        {
+        { 
+            RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
+            aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
+         
             affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
-                (RectI)node.ShapeData.TransformedAABB, ChunkyImage.FullChunkSize));
+                (RectI)aabb, ChunkyImage.FullChunkSize));
         }
 
         return new VectorShape_ChangeInfo(node.Id, affected);

+ 2 - 0
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -83,6 +83,7 @@
             
             <Color x:Key="HorizontalSnapAxisColor">#B00022</Color>
             <Color x:Key="VerticalSnapAxisColor">#5fad65</Color>
+            <Color x:Key="SnapPointPreviewColor">#68abdf</Color>
 
             <system:Double x:Key="ThemeDisabledOpacity">0.4</system:Double>
 
@@ -168,6 +169,7 @@
 
             <SolidColorBrush x:Key="HorizontalSnapAxisBrush" Color="{StaticResource HorizontalSnapAxisColor}"/>
             <SolidColorBrush x:Key="VerticalSnapAxisBrush" Color="{StaticResource VerticalSnapAxisColor}"/>
+            <SolidColorBrush x:Key="SnapPointPreviewBrush" Color="{StaticResource SnapPointPreviewColor}"/>
             
             <CornerRadius x:Key="ControlCornerRadius">5</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadiusTop">5, 5, 0, 0</CornerRadius>

+ 86 - 27
src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs

@@ -6,6 +6,10 @@ public class SnappingController
 {
     public const double DefaultSnapDistance = 16;
     
+    private string highlightedXAxis = string.Empty;
+    private string highlightedYAxis = string.Empty;
+    private VecD? highlightedPoint = null;
+
     /// <summary>
     ///     Minimum distance that object has to be from snap point to snap to it. Expressed in pixels.
     /// </summary>
@@ -13,11 +17,42 @@ public class SnappingController
 
     public Dictionary<string, Func<double>> HorizontalSnapPoints { get; } = new();
     public Dictionary<string, Func<double>> VerticalSnapPoints { get; } = new();
-    
-    public string HighlightedXAxis { get; set; } = string.Empty;
-    public string HighlightedYAxis { get; set; } = string.Empty;
-    
-    
+
+    public string HighlightedXAxis
+    {
+        get => highlightedXAxis;
+        set
+        {
+            highlightedXAxis = value;
+            HorizontalHighlightChanged?.Invoke(value);
+        }
+    }
+
+    public string HighlightedYAxis
+    {
+        get => highlightedYAxis;
+        set
+        {
+            highlightedYAxis = value;
+            VerticalHighlightChanged?.Invoke(value);
+        }
+    }
+
+    public VecD? HighlightedPoint
+    {
+        get => highlightedPoint;
+        set
+        {
+            highlightedPoint = value;
+            HighlightedPointChanged?.Invoke(value);
+        }
+    }
+
+    public event Action<string> HorizontalHighlightChanged;
+    public event Action<string> VerticalHighlightChanged;
+    public event Action<VecD?> HighlightedPointChanged;
+
+
     public double? SnapToHorizontal(double xPos, out string snapAxis)
     {
         if (HorizontalSnapPoints.Count == 0)
@@ -25,7 +60,7 @@ public class SnappingController
             snapAxis = string.Empty;
             return null;
         }
-        
+
         snapAxis = HorizontalSnapPoints.First().Key;
         double closest = HorizontalSnapPoints.First().Value();
         foreach (var snapPoint in HorizontalSnapPoints)
@@ -36,16 +71,16 @@ public class SnappingController
                 snapAxis = snapPoint.Key;
             }
         }
-        
+
         if (Math.Abs(closest - xPos) > SnapDistance)
         {
             snapAxis = string.Empty;
             return null;
         }
-        
+
         return closest;
     }
-    
+
     public double? SnapToVertical(double yPos, out string snapAxisKey)
     {
         if (VerticalSnapPoints.Count == 0)
@@ -64,7 +99,7 @@ public class SnappingController
                 snapAxisKey = snapPoint.Key;
             }
         }
-        
+
         if (Math.Abs(closest - yPos) > SnapDistance)
         {
             snapAxisKey = string.Empty;
@@ -79,37 +114,37 @@ public class SnappingController
         HorizontalSnapPoints[identifier] = () => axisVector.X;
         VerticalSnapPoints[identifier] = () => axisVector.Y;
     }
-    
+
     public void AddBounds(string identifier, Func<RectD> tightBounds)
     {
         HorizontalSnapPoints[$"{identifier}.center"] = () => tightBounds().Center.X;
         VerticalSnapPoints[$"{identifier}.center"] = () => tightBounds().Center.Y;
-        
+
         HorizontalSnapPoints[$"{identifier}.left"] = () => tightBounds().Left;
         VerticalSnapPoints[$"{identifier}.top"] = () => tightBounds().Top;
-        
+
         HorizontalSnapPoints[$"{identifier}.right"] = () => tightBounds().Right;
         VerticalSnapPoints[$"{identifier}.bottom"] = () => tightBounds().Bottom;
     }
-    
+
     /// <summary>
     ///     Removes all snap points with root identifier. All identifiers that start with root will be removed.
     /// </summary>
     /// <param name="id">Root identifier of snap points to remove.</param>
     public void RemoveAll(string id)
     {
-       var toRemoveHorizontal = HorizontalSnapPoints.Keys.Where(x => x.StartsWith(id)).ToList();
-       var toRemoveVertical = VerticalSnapPoints.Keys.Where(x => x.StartsWith(id)).ToList();
-        
-       foreach (var key in toRemoveHorizontal)
-       {
-           HorizontalSnapPoints.Remove(key);
-       }
-       
-       foreach (var key in toRemoveVertical)
-       {
-           VerticalSnapPoints.Remove(key);
-       }
+        var toRemoveHorizontal = HorizontalSnapPoints.Keys.Where(x => x.StartsWith(id)).ToList();
+        var toRemoveVertical = VerticalSnapPoints.Keys.Where(x => x.StartsWith(id)).ToList();
+
+        foreach (var key in toRemoveHorizontal)
+        {
+            HorizontalSnapPoints.Remove(key);
+        }
+
+        foreach (var key in toRemoveVertical)
+        {
+            VerticalSnapPoints.Remove(key);
+        }
     }
 
     public VecD GetSnapDeltaForPoints(VecD[] points, out string xAxis, out string yAxis)
@@ -148,7 +183,31 @@ public class SnappingController
 
         xAxis = snapAxisX;
         yAxis = snapAxisY;
-        
+
         return snapDelta;
     }
+
+    public VecD GetSnapPoint(VecD pos, out string xAxis, out string yAxis)
+    {
+        double? snapX = SnapToHorizontal(pos.X, out string snapAxisX);
+        double? snapY = SnapToVertical(pos.Y, out string snapAxisY);
+
+        xAxis = snapAxisX;
+        yAxis = snapAxisY;
+        
+        return new VecD(snapX ?? pos.X, snapY ?? pos.Y);
+    }
+
+    public VecD GetSnapDeltaForPoint(VecD pos, out string xAxis, out string yAxis)
+    {
+        double? snapX = SnapToHorizontal(pos.X, out string snapAxisX);
+        double? snapY = SnapToVertical(pos.Y, out string snapAxisY);
+
+        xAxis = snapAxisX;
+        yAxis = snapAxisY;
+
+        VecD snappedPos = new VecD(snapX ?? pos.X, snapY ?? pos.Y);
+
+        return snappedPos - pos;
+    }
 }

+ 80 - 11
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShapeToolExecutor.cs → src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ComplexShapeToolExecutor.cs

@@ -14,7 +14,7 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
 #nullable enable
 
-internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T : IShapeToolHandler
+internal abstract class ComplexShapeToolExecutor<T> : UpdateableChangeExecutor where T : IShapeToolHandler
 {
     protected int StrokeWidth => toolbar.ToolSize;
 
@@ -28,12 +28,14 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
     protected bool transforming = false;
     protected T? toolViewModel;
     protected VecI startPos;
+    protected VecI unsnappedStartPos;
     protected RectI lastRect;
     protected double lastRadians;
 
     private bool noMovement = true;
     private IBasicShapeToolbar toolbar;
     private IColorsHandler? colorsVM;
+    private bool previewMode = false;
 
     public override ExecutionState Start()
     {
@@ -54,26 +56,32 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
         if (controller.LeftMousePressed || member is not IVectorLayerHandler)
         {
             startPos = controller!.LastPixelPosition;
+            unsnappedStartPos = startPos;
             OnColorChanged(colorsVM.PrimaryColor, true);
             DrawShape(startPos, 0, true);
         }
         else
         {
-            transforming = true;
             if (member is IVectorLayerHandler)
             {
                 var node = (VectorLayerNode)internals.Tracker.Document.FindMember(member.Id);
                 if (!InitShapeData(node.ShapeData))
                 {
                     document.TransformHandler.HideTransform();
-                    return ExecutionState.Error;
+                    previewMode = true;
+                    return ExecutionState.Success;
                 }
 
+                transforming = true;
                 toolbar.StrokeColor = node.ShapeData.StrokeColor.ToColor();
                 toolbar.FillColor = node.ShapeData.FillColor.ToColor();
                 toolbar.ToolSize = node.ShapeData.StrokeWidth;
                 toolbar.Fill = node.ShapeData.FillColor != Colors.Transparent;
             }
+            else
+            {
+                previewMode = true;
+            }
         }
 
         document.SnappingHandler.Remove(member.Id.ToString());
@@ -88,17 +96,17 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
     protected abstract IAction EndDrawAction();
     protected virtual DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective;
 
-    public static VecI Get45IncrementedPosition(VecI startPos, VecI curPos)
+    public static VecI Get45IncrementedPosition(VecD startPos, VecD curPos)
     {
         Span<VecI> positions = stackalloc VecI[]
         {
-            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, 1)) -
+            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(1, 1)) -
                    new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
-            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, -1)) -
+            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(1, -1)) -
                    new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
-            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, 0)) -
+            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(1, 0)) -
                    new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
-            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(0, 1)) -
+            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(0, 1)) -
                    new VecD(0.25).Multiply((curPos - startPos).Signs())).Round()
         };
         VecI max = positions[0];
@@ -148,14 +156,19 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
 
     public override void OnTransformApplied()
     {
+        if (!transforming)
+            return;
+        
         internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
         document!.TransformHandler.HideTransform();
 
         AddToSnapController();
-        onEnded?.Invoke(this);
+        HighlightSnapAxis(null, null);
 
         colorsVM.AddSwatch(StrokeColor.ToPaletteColor());
         colorsVM.AddSwatch(FillColor.ToPaletteColor());
+
+        previewMode = true;
     }
 
     public override void OnColorChanged(Color color, bool primary)
@@ -190,12 +203,65 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        if (transforming)
+        if (previewMode)
+        {
+            VecD mouseSnap = document.SnappingHandler.SnappingController.GetSnapPoint(pos, out string snapXAxis, out string snapYAxis);
+            HighlightSnapAxis(snapXAxis, snapYAxis);
+            
+            if (!string.IsNullOrEmpty(snapXAxis) || !string.IsNullOrEmpty(snapYAxis))
+            {
+                document.SnappingHandler.SnappingController.HighlightedPoint = mouseSnap;
+            }
+            else
+            {
+                document.SnappingHandler.SnappingController.HighlightedPoint = null;
+            }
+        }
+
+        if (transforming || previewMode)
             return;
+
+        startPos = Snap(unsnappedStartPos, pos);
+        var snapped = Snap(pos, startPos);
+        
         noMovement = false;
+
+        pos = snapped;
+
         DrawShape(pos, lastRadians, false);
     }
 
+    private VecI Snap(VecI pos, VecD adjustPos)
+    {
+        VecI snapped =
+            (VecI)document.SnappingHandler.SnappingController.GetSnapPoint(pos, out string snapXAxis,
+                out string snapYAxis);
+
+        HighlightSnapAxis(snapXAxis, snapYAxis);
+
+        if (snapped != VecI.Zero)
+        {
+            if (adjustPos.X < pos.X)
+            {
+                snapped -= new VecI(1, 0);
+            }
+
+            if (adjustPos.Y < pos.Y)
+            {
+                snapped -= new VecI(0, 1);
+            }
+        }
+
+        return snapped;
+    }
+
+    private void HighlightSnapAxis(string snapXAxis, string snapYAxis)
+    {
+        document.SnappingHandler.SnappingController.HighlightedXAxis = snapXAxis;
+        document.SnappingHandler.SnappingController.HighlightedYAxis = snapYAxis;
+        document.SnappingHandler.SnappingController.HighlightedPoint = null;
+    }
+
     public override void OnSettingsChanged(string name, object value)
     {
         internals!.ActionAccumulator.AddActions(SettingsChangedAction());
@@ -203,6 +269,8 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
 
     public override void OnLeftMouseButtonUp()
     {
+        HighlightSnapAxis(null, null);
+
         if (transforming)
             return;
 
@@ -210,7 +278,7 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
         {
             internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
             AddToSnapController();
-            
+
             onEnded?.Invoke(this);
             return;
         }
@@ -227,6 +295,7 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
         internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
 
         AddToSnapController();
+        HighlightSnapAxis(null, null);
     }
 
     private void AddToSnapController()

+ 69 - 60
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -1,10 +1,7 @@
 using PixiEditor.ChangeableDocument.Actions;
-using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
-using PixiEditor.DrawingApi.Core.Numerics;
-using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Handlers;
@@ -15,25 +12,25 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
-internal abstract class LineExecutor<T> : UpdateableChangeExecutor where T : ILineToolHandler
+internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILineToolHandler
 {
     public override ExecutorType Type => ExecutorType.ToolLinked;
 
-    protected VecI startPos;
     protected Color StrokeColor => toolbar!.StrokeColor.ToColor();
     protected int StrokeWidth => toolViewModel!.ToolSize;
-    protected Guid memberGuid;
     protected bool drawOnMask;
 
-    protected VecI curPos;
-    private bool started = false;
-    private bool transforming = false;
+    protected VecD curPos;
+    private bool startedDrawing = false;
     private T? toolViewModel;
     private IColorsHandler? colorsVM;
     private ILineToolbar? toolbar;
 
     public override ExecutionState Start()
     {
+        if (base.Start() == ExecutionState.Error)
+            return ExecutionState.Error;
+
         colorsVM = GetHandler<IColorsHandler>();
         toolViewModel = GetHandler<T>();
         IStructureMemberHandler? member = document?.SelectedStructureMember;
@@ -47,79 +44,101 @@ internal abstract class LineExecutor<T> : UpdateableChangeExecutor where T : ILi
         if (!drawOnMask && member is not ILayerHandler)
             return ExecutionState.Error;
 
-        memberGuid = member.Id;
-
-        if (controller.LeftMousePressed || member is not IVectorLayerHandler)
+        if (ActiveMode == ShapeToolMode.Drawing)
         {
-            startPos = controller!.LastPixelPosition;
-            OnColorChanged(colorsVM.PrimaryColor, true);
+            return ExecutionState.Success;
         }
-        else
+        
+        if (member is IVectorLayerHandler)
         {
-            transforming = true;
             var node = (VectorLayerNode)internals.Tracker.Document.FindMember(member.Id);
-            IReadOnlyLineData data = node.ShapeData as IReadOnlyLineData;
-            
-            if(data is null)
+
+            if (node.ShapeData is not IReadOnlyLineData data)
             {
-                document.TransformHandler.HideTransform();
-                return ExecutionState.Error;
+                ActiveMode = ShapeToolMode.Preview;
+                return ExecutionState.Success;
             }
 
             toolbar.StrokeColor = data.StrokeColor.ToColor();
-            
-            if (!InitShapeData(node.ShapeData as IReadOnlyLineData))
+
+            if (!InitShapeData(data))
             {
-                document.TransformHandler.HideTransform();
-                return ExecutionState.Error;
+                ActiveMode = ShapeToolMode.Preview;
+                return ExecutionState.Success;
             }
+            
+            ActiveMode = ShapeToolMode.Transform;
+        }
+        else
+        {
+            ActiveMode = ShapeToolMode.Preview;
         }
 
-        document.SnappingHandler.Remove(memberGuid.ToString());
-        
         return ExecutionState.Success;
     }
 
     protected abstract bool InitShapeData(IReadOnlyLineData? data);
-    protected abstract IAction DrawLine(VecI pos);
+    protected abstract IAction DrawLine(VecD pos);
     protected abstract IAction TransformOverlayMoved(VecD start, VecD end);
     protected abstract IAction SettingsChange();
     protected abstract IAction EndDraw();
+    
+    protected override void PrecisePositionChangeTransformMode(VecD pos)
+    {
+    }
 
-    public override void OnPixelPositionChange(VecI pos)
+    protected override void PrecisePositionChangeDrawingMode(VecD pos)
     {
-        if (transforming)
-            return;
-        started = true;
+        startedDrawing = true;
 
         if (toolViewModel!.Snap)
-            pos = ShapeToolExecutor<IShapeToolHandler>.Get45IncrementedPosition(startPos, pos);
-        curPos = pos;
-        var drawLineAction = DrawLine(pos);
+            pos = ComplexShapeToolExecutor<IShapeToolHandler>.Get45IncrementedPosition(startDrawingPos, pos);
+
+        VecD snapped =
+            document!.SnappingHandler.SnappingController.GetSnapPoint(pos, out string snapX, out string snapY);
+
+        if (snapped != VecI.Zero)
+        {
+            if (startDrawingPos.X < pos.X)
+            {
+                snapped -= new VecI(1, 0);
+            }
+
+            if (startDrawingPos.Y < pos.Y)
+            {
+                snapped -= new VecI(0, 1);
+            }
+        }
+
+        HighlightSnapping(snapX, snapY);
+
+        curPos = snapped;
+
+        var drawLineAction = DrawLine(curPos);
         internals!.ActionAccumulator.AddActions(drawLineAction);
     }
 
     public override void OnLeftMouseButtonUp()
     {
-        if (!started)
+        if (!startedDrawing)
         {
             onEnded!(this);
             return;
         }
 
-        document!.LineToolOverlayHandler.Show(startPos + new VecD(0.5), curPos + new VecD(0.5), true);
-        transforming = true;
+        document!.LineToolOverlayHandler.Show(startDrawingPos + new VecD(0.5), curPos + new VecD(0.5), true);
+        base.OnLeftMouseButtonUp();
     }
 
     public override void OnLineOverlayMoved(VecD start, VecD end)
     {
-        if (!transforming)
+        if (ActiveMode != ShapeToolMode.Transform)
             return;
-        
+
         var moveOverlayAction = TransformOverlayMoved(start, end);
         internals!.ActionAccumulator.AddActions(moveOverlayAction);
 
-        startPos = (VecI)start;
+        startDrawingPos = (VecI)start;
         curPos = (VecI)end;
     }
 
@@ -135,8 +154,9 @@ internal abstract class LineExecutor<T> : UpdateableChangeExecutor where T : ILi
 
     public override void OnSelectedObjectNudged(VecI distance)
     {
-        if (!transforming)
+        if (ActiveMode != ShapeToolMode.Transform)
             return;
+        
         document!.LineToolOverlayHandler.Nudge(distance);
     }
 
@@ -148,45 +168,34 @@ internal abstract class LineExecutor<T> : UpdateableChangeExecutor where T : ILi
 
     public override void OnMidChangeUndo()
     {
-        if (!transforming)
+        if (ActiveMode != ShapeToolMode.Transform)
             return;
+        
         document!.LineToolOverlayHandler.Undo();
     }
 
     public override void OnMidChangeRedo()
     {
-        if (!transforming)
+        if (ActiveMode != ShapeToolMode.Transform)
             return;
+        
         document!.LineToolOverlayHandler.Redo();
     }
 
     public override void OnTransformApplied()
     {
-        if (!transforming)
-            return;
-
-        document!.LineToolOverlayHandler.Hide();
+        base.OnTransformApplied();
         var endDrawAction = EndDraw();
         internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
-        AddMemberToSnapping();
-        onEnded!(this);
+        //onEnded!(this);
 
         colorsVM.AddSwatch(new PaletteColor(StrokeColor.R, StrokeColor.G, StrokeColor.B));
     }
 
     public override void ForceStop()
     {
-        if (transforming)
-            document!.LineToolOverlayHandler.Hide();
-
+        base.ForceStop();
         var endDrawAction = EndDraw();
         internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
-        AddMemberToSnapping();
-    }
-    
-    private void AddMemberToSnapping()
-    {
-        var member = document.StructureHelper.Find(memberGuid);
-        document!.SnappingHandler.AddFromBounds(memberGuid.ToString(), () => member!.TightBounds ?? RectD.Empty);
     }
 }

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs

@@ -9,7 +9,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
-internal class RasterEllipseToolExecutor : ShapeToolExecutor<IRasterEllipseToolHandler>
+internal class RasterEllipseToolExecutor : ComplexShapeToolExecutor<IRasterEllipseToolHandler>
 {
     private void DrawEllipseOrCircle(VecI curPos, double rotationRad, bool firstDraw)
     {

+ 4 - 4
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterLineToolExecutor.cs

@@ -14,21 +14,21 @@ internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
         return false;
     }
 
-    protected override IAction DrawLine(VecI pos)
+    protected override IAction DrawLine(VecD pos)
     {
-        return new DrawRasterLine_Action(memberGuid, startPos, pos, StrokeWidth,
+        return new DrawRasterLine_Action(memberId, (VecI)startDrawingPos.Floor(), (VecI)pos.Floor(), StrokeWidth,
             StrokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
     protected override IAction TransformOverlayMoved(VecD start, VecD end)
     {
-        return new DrawRasterLine_Action(memberGuid, (VecI)start, (VecI)end,
+        return new DrawRasterLine_Action(memberId, (VecI)start, (VecI)end,
             StrokeWidth, StrokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
     protected override IAction SettingsChange()
     {
-        return new DrawRasterLine_Action(memberGuid, startPos, curPos, StrokeWidth,
+        return new DrawRasterLine_Action(memberId, (VecI)startDrawingPos.Floor(), (VecI)curPos.Floor(), StrokeWidth,
             StrokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
-internal class RasterRectangleToolExecutor : ShapeToolExecutor<IRasterRectangleToolHandler>
+internal class RasterRectangleToolExecutor : ComplexShapeToolExecutor<IRasterRectangleToolHandler>
 {
     private ShapeData lastData;
     public override ExecutorType Type => ExecutorType.ToolLinked;

+ 195 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs

@@ -0,0 +1,195 @@
+using PixiEditor.Models.Handlers;
+using PixiEditor.Models.Tools;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+
+/// <summary>
+/// This class is responsible for handling the execution of a simple shape tool.
+///  This executor handles: Shape tool state management and snapping
+///  Drawing a shape can be either on raster layer or vector.
+/// 
+///  - Preview mode: a state when the tool is selected and editing of current shape is disabled or impossible. During this state,
+///      snapping overlays are shown under mouse position, axes and snap point.
+///  - Drawing mode: a state when the user clicked on the canvas and is dragging the mouse to draw a shape.
+///        During this state, snapping axes are highlighted.
+///  - Transform mode: a state when the user is transforming existing shape.
+///     During this state, snapping axes are highlighted.
+///
+///     Possible state transitions:
+///         - Preview -> Drawing (when user clicks on the canvas)
+///         - Drawing -> Transform (when user releases the mouse after drawing)
+///         - Transform -> Preview (when user applies the transform)
+///         - Transform -> Drawing (when user clicks outside of shape transform bounds)
+/// </summary>
+internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor
+{
+    private ShapeToolMode activeMode;
+
+    protected ShapeToolMode ActiveMode
+    {
+        get => activeMode;
+        set
+        {
+            StopMode(activeMode);
+            activeMode = value;
+            StartMode(activeMode);
+        }
+    }
+
+    protected Guid memberId;
+    protected VecD startDrawingPos;
+
+    public override ExecutionState Start()
+    {
+        IStructureMemberHandler? member = document?.SelectedStructureMember;
+
+        if (member is null)
+            return ExecutionState.Error;
+
+        memberId = member.Id;
+
+        if (controller.LeftMousePressed)
+        {
+            ActiveMode = ShapeToolMode.Drawing;
+        }
+        else
+        {
+            ActiveMode = ShapeToolMode.Preview;
+        }
+
+        document.SnappingHandler.Remove(memberId.ToString()); // This disables self-snapping
+
+        return ExecutionState.Success;
+    }
+
+    protected virtual void StartMode(ShapeToolMode mode)
+    {
+        switch (mode)
+        {
+            case ShapeToolMode.Preview:
+                break;
+            case ShapeToolMode.Drawing:
+                StartDrawingMode();
+                break;
+            case ShapeToolMode.Transform:
+                break;
+        }
+    }
+
+    private void StartDrawingMode()
+    {
+        startDrawingPos = SnapAndHighlight(controller.LastPrecisePosition);
+    }
+
+    protected virtual void StopMode(ShapeToolMode mode)
+    {
+        switch (mode)
+        {
+            case ShapeToolMode.Preview:
+                break;
+            case ShapeToolMode.Drawing:
+                break;
+            case ShapeToolMode.Transform:
+                StopTransformMode();
+                break;
+        }
+    }
+
+    private void StopTransformMode()
+    {
+        document!.TransformHandler.HideTransform();
+        document!.LineToolOverlayHandler.Hide();
+    }
+
+    public override void OnLeftMouseButtonDown(VecD pos)
+    {
+        if (ActiveMode == ShapeToolMode.Preview)
+        {
+            ActiveMode = ShapeToolMode.Drawing;
+        }
+    }
+
+    public override void OnPrecisePositionChange(VecD pos)
+    {
+        if (ActiveMode == ShapeToolMode.Preview)
+        {
+            PrecisePositionChangePreviewMode(pos);
+        }
+        else if (ActiveMode == ShapeToolMode.Drawing)
+        {
+            PrecisePositionChangeDrawingMode(pos);
+        }
+        else if (ActiveMode == ShapeToolMode.Transform)
+        {
+            PrecisePositionChangeTransformMode(pos);
+        }
+    }
+
+    public override void OnLeftMouseButtonUp()
+    {
+        HighlightSnapping(null, null);
+        ActiveMode = ShapeToolMode.Transform;
+    }
+
+    public override void OnTransformApplied()
+    {
+        ActiveMode = ShapeToolMode.Preview;
+        AddMemberToSnapping();
+        HighlightSnapping(null, null);
+    }
+
+    public override void ForceStop()
+    {
+        StopMode(activeMode);
+        AddMemberToSnapping();
+        HighlightSnapping(null, null);
+    }
+
+    protected void HighlightSnapping(string? snapX, string? snapY)
+    {
+        document!.SnappingHandler.SnappingController.HighlightedXAxis = snapX;
+        document!.SnappingHandler.SnappingController.HighlightedYAxis = snapY;
+        document.SnappingHandler.SnappingController.HighlightedPoint = null;
+    }
+
+    protected void AddMemberToSnapping()
+    {
+        var member = document.StructureHelper.Find(memberId);
+        document!.SnappingHandler.AddFromBounds(memberId.ToString(), () => member!.TightBounds ?? RectD.Empty);
+    }
+    
+    protected VecD SnapAndHighlight(VecD pos)
+    {
+        VecD snapped = document.SnappingHandler.SnappingController.GetSnapPoint(pos, out string snapX, out string snapY);
+        HighlightSnapping(snapX, snapY);
+        return (VecI)snapped;
+    }
+
+    protected virtual void PrecisePositionChangePreviewMode(VecD pos)
+    {
+        VecD mouseSnap =
+            document.SnappingHandler.SnappingController.GetSnapPoint(pos, out string snapXAxis,
+                out string snapYAxis);
+        HighlightSnapping(snapXAxis, snapYAxis);
+
+        if (!string.IsNullOrEmpty(snapXAxis) || !string.IsNullOrEmpty(snapYAxis))
+        {
+            document.SnappingHandler.SnappingController.HighlightedPoint = mouseSnap;
+        }
+        else
+        {
+            document.SnappingHandler.SnappingController.HighlightedPoint = null;
+        }
+    }
+    
+    protected virtual void PrecisePositionChangeDrawingMode(VecD pos) { }
+    protected virtual void PrecisePositionChangeTransformMode(VecD pos) { }
+}
+
+enum ShapeToolMode
+{
+    Preview,
+    Drawing,
+    Transform
+}

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorEllipseToolExecutor.cs

@@ -10,7 +10,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
-internal class VectorEllipseToolExecutor : ShapeToolExecutor<IVectorEllipseToolHandler>
+internal class VectorEllipseToolExecutor : ComplexShapeToolExecutor<IVectorEllipseToolHandler>
 {
     public override ExecutorType Type => ExecutorType.ToolLinked;
     protected override DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_Shear_NoPerspective;

+ 5 - 5
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorLineToolExecutor.cs

@@ -24,18 +24,18 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
         return true;
     }
 
-    protected override IAction DrawLine(VecI pos)
+    protected override IAction DrawLine(VecD pos)
     {
-        LineVectorData data = new LineVectorData(startPos, pos)
+        LineVectorData data = new LineVectorData(startDrawingPos, pos)
         {
             StrokeColor = StrokeColor,
             StrokeWidth = StrokeWidth,
         };
         
-        startPoint = startPos;
+        startPoint = startDrawingPos;
         endPoint = pos;
 
-        return new SetShapeGeometry_Action(memberGuid, data);
+        return new SetShapeGeometry_Action(memberId, data);
     }
 
     protected override IAction TransformOverlayMoved(VecD start, VecD end)
@@ -49,7 +49,7 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
         startPoint = start;
         endPoint = end;
 
-        return new SetShapeGeometry_Action(memberGuid, data);
+        return new SetShapeGeometry_Action(memberId, data);
     }
 
     protected override IAction SettingsChange()

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorRectangleToolExecutor.cs

@@ -10,7 +10,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
-internal class VectorRectangleToolExecutor : ShapeToolExecutor<IVectorRectangleToolHandler>
+internal class VectorRectangleToolExecutor : ComplexShapeToolExecutor<IVectorRectangleToolHandler>
 {
     public override ExecutorType Type => ExecutorType.ToolLinked;
     protected override DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_Shear_NoPerspective;

+ 3 - 1
src/PixiEditor/Models/Handlers/ISnappingHandler.cs

@@ -1,9 +1,11 @@
-using PixiEditor.Numerics;
+using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.Handlers;
 
 public interface ISnappingHandler
 {
+    public SnappingController SnappingController { get; }
     public void Remove(string id);
     public void AddFromBounds(string id, Func<RectD> tightBounds);
 }

+ 51 - 2
src/PixiEditor/Views/Overlays/SnappingOverlay.cs

@@ -20,12 +20,22 @@ internal class SnappingOverlay : Overlay
 
     private Pen horizontalAxisPen;
     private Pen verticalAxisPen; 
+    private Pen previewPointPen;
+
+    private const double startSize = 2;
+    
+    static SnappingOverlay()
+    {
+        AffectsRender<SnappingOverlay>(SnappingControllerProperty);
+        SnappingControllerProperty.Changed.Subscribe(SnappingControllerChanged);
+    }
     
     public SnappingOverlay()
     {
         /*TODO: Theme variant is not present, that's why Dark is hardcoded*/        
-        horizontalAxisPen = Application.Current.Styles.TryGetResource("HorizontalSnapAxisBrush", ThemeVariant.Dark, out var horizontalAxisBrush) ? new Pen((IBrush)horizontalAxisBrush, 0.2f) : new Pen(Brushes.Red, 0.2f);
-        verticalAxisPen = Application.Current.Styles.TryGetResource("VerticalSnapAxisBrush", ThemeVariant.Dark, out var verticalAxisBrush) ? new Pen((IBrush)verticalAxisBrush, 0.2f) : new Pen(Brushes.Green, 0.2f);
+        horizontalAxisPen = Application.Current.Styles.TryGetResource("HorizontalSnapAxisBrush", ThemeVariant.Dark, out var horizontalAxisBrush) ? new Pen((IBrush)horizontalAxisBrush, startSize) : new Pen(Brushes.Red, startSize);
+        verticalAxisPen = Application.Current.Styles.TryGetResource("VerticalSnapAxisBrush", ThemeVariant.Dark, out var verticalAxisBrush) ? new Pen((IBrush)verticalAxisBrush, startSize) : new Pen(Brushes.Green, startSize);
+        previewPointPen = Application.Current.Styles.TryGetResource("SnapPointPreviewBrush", ThemeVariant.Dark, out var previewPointBrush) ? new Pen((IBrush)previewPointBrush, startSize) : new Pen(Brushes.DodgerBlue, startSize);
         IsHitTestVisible = false;
     }
 
@@ -57,5 +67,44 @@ internal class SnappingOverlay : Overlay
                 }
             }
         }
+        
+        if (SnappingController.HighlightedPoint.HasValue)
+        {
+            context.DrawEllipse(previewPointPen.Brush, previewPointPen, new Point(SnappingController.HighlightedPoint.Value.X, SnappingController.HighlightedPoint.Value.Y), 5 / ZoomScale, 5 / ZoomScale);
+        }
+    }
+
+    protected override void ZoomChanged(double newZoom)
+    {
+        horizontalAxisPen.Thickness = startSize / newZoom;
+        verticalAxisPen.Thickness = startSize / newZoom;
+        previewPointPen.Thickness = startSize / newZoom;
+    }
+
+    private static void SnappingControllerChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        SnappingOverlay overlay = (SnappingOverlay)e.Sender;
+        if (e.OldValue is SnappingController oldSnappingController)
+        {
+            oldSnappingController.HorizontalHighlightChanged -= overlay.SnapAxisChanged;
+            oldSnappingController.VerticalHighlightChanged -= overlay.SnapAxisChanged;
+            oldSnappingController.HighlightedPointChanged -= overlay.OnHighlightedPointChanged;
+        }
+        if (e.NewValue is SnappingController snappingController)
+        {
+            snappingController.HorizontalHighlightChanged += overlay.SnapAxisChanged;
+            snappingController.VerticalHighlightChanged += overlay.SnapAxisChanged;
+            snappingController.HighlightedPointChanged += overlay.OnHighlightedPointChanged;
+        }
+    }
+
+    private void SnapAxisChanged(string axis)
+    {
+        Refresh();
+    }
+    
+    private void OnHighlightedPointChanged(VecD? point)
+    {
+        Refresh();
     }
 }