Browse Source

Merge pull request #650 from PixiEditor/improvements-18-11

Snapping improvements and proportional scaling
Krzysztof Krysiński 8 months ago
parent
commit
6d911a85fb
20 changed files with 373 additions and 105 deletions
  1. 35 0
      src/PixiEditor/Helpers/GeometryHelper.cs
  2. 97 7
      src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs
  3. 42 30
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ComplexShapeToolExecutor.cs
  4. 16 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  5. 2 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs
  6. 2 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs
  7. 2 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorEllipseToolExecutor.cs
  8. 2 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorRectangleToolExecutor.cs
  9. 0 1
      src/PixiEditor/Models/Handlers/Tools/IRasterEllipseToolHandler.cs
  10. 0 1
      src/PixiEditor/Models/Handlers/Tools/IRasterRectangleToolHandler.cs
  11. 1 1
      src/PixiEditor/Models/Handlers/Tools/IShapeToolHandler.cs
  12. 0 1
      src/PixiEditor/Models/Handlers/Tools/IVectorEllipseToolHandler.cs
  13. 0 1
      src/PixiEditor/Models/Handlers/Tools/IVectorRectangleToolHandler.cs
  14. 2 0
      src/PixiEditor/ViewModels/Tools/ShapeTool.cs
  15. 2 3
      src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs
  16. 2 3
      src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs
  17. 16 3
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  18. 2 3
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  19. 84 23
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  20. 66 16
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformUpdateHelper.cs

+ 35 - 0
src/PixiEditor/Helpers/GeometryHelper.cs

@@ -0,0 +1,35 @@
+using Drawie.Numerics;
+
+namespace PixiEditor.Helpers;
+
+public static class GeometryHelper
+{
+    public static VecI Get45IncrementedPosition(VecD startPos, VecD curPos)
+    {
+        Span<VecI> positions =
+        [
+            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(1, 1)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
+            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(1, -1)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
+            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(1, 0)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
+            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(0, 1)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round()
+        ];
+
+        VecI max = positions[0];
+        double maxLength = double.MaxValue;
+        foreach (var pos in positions)
+        {
+            double length = (pos - curPos).LengthSquared;
+            if (length < maxLength)
+            {
+                maxLength = length;
+                max = pos;
+            }
+        }
+
+        return max;
+    }
+}

+ 97 - 7
src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs

@@ -5,7 +5,7 @@ namespace PixiEditor.Models.Controllers.InputDevice;
 public class SnappingController
 public class SnappingController
 {
 {
     public const double DefaultSnapDistance = 16;
     public const double DefaultSnapDistance = 16;
-    
+
     private string highlightedXAxis = string.Empty;
     private string highlightedXAxis = string.Empty;
     private string highlightedYAxis = string.Empty;
     private string highlightedYAxis = string.Empty;
     private VecD? highlightedPoint = null;
     private VecD? highlightedPoint = null;
@@ -76,7 +76,7 @@ public class SnappingController
             snapAxis = string.Empty;
             snapAxis = string.Empty;
             return null;
             return null;
         }
         }
-        
+
         if (HorizontalSnapPoints.Count == 0)
         if (HorizontalSnapPoints.Count == 0)
         {
         {
             snapAxis = string.Empty;
             snapAxis = string.Empty;
@@ -110,7 +110,7 @@ public class SnappingController
             snapAxisKey = string.Empty;
             snapAxisKey = string.Empty;
             return null;
             return null;
         }
         }
-        
+
         if (VerticalSnapPoints.Count == 0)
         if (VerticalSnapPoints.Count == 0)
         {
         {
             snapAxisKey = string.Empty;
             snapAxisKey = string.Empty;
@@ -183,7 +183,7 @@ public class SnappingController
             yAxis = string.Empty;
             yAxis = string.Empty;
             return VecD.Zero;
             return VecD.Zero;
         }
         }
-        
+
         bool hasXSnap = false;
         bool hasXSnap = false;
         bool hasYSnap = false;
         bool hasYSnap = false;
         VecD snapDelta = VecD.Zero;
         VecD snapDelta = VecD.Zero;
@@ -230,13 +230,13 @@ public class SnappingController
             yAxis = string.Empty;
             yAxis = string.Empty;
             return pos;
             return pos;
         }
         }
-        
+
         double? snapX = SnapToHorizontal(pos.X, out string snapAxisX);
         double? snapX = SnapToHorizontal(pos.X, out string snapAxisX);
         double? snapY = SnapToVertical(pos.Y, out string snapAxisY);
         double? snapY = SnapToVertical(pos.Y, out string snapAxisY);
 
 
         xAxis = snapAxisX;
         xAxis = snapAxisX;
         yAxis = snapAxisY;
         yAxis = snapAxisY;
-        
+
         return new VecD(snapX ?? pos.X, snapY ?? pos.Y);
         return new VecD(snapX ?? pos.X, snapY ?? pos.Y);
     }
     }
 
 
@@ -248,7 +248,7 @@ public class SnappingController
             yAxis = string.Empty;
             yAxis = string.Empty;
             return VecD.Zero;
             return VecD.Zero;
         }
         }
-        
+
         double? snapX = SnapToHorizontal(pos.X, out string snapAxisX);
         double? snapX = SnapToHorizontal(pos.X, out string snapAxisX);
         double? snapY = SnapToVertical(pos.Y, out string snapAxisY);
         double? snapY = SnapToVertical(pos.Y, out string snapAxisY);
 
 
@@ -259,4 +259,94 @@ public class SnappingController
 
 
         return snappedPos - pos;
         return snappedPos - pos;
     }
     }
+
+    /// <summary>
+    ///     Gets the intersection of closest snap axis along projected axis.
+    /// </summary>
+    /// <param name="pos">Position to snap</param>
+    /// <param name="direction">Direction to project from <paramref name="pos">/></param>
+    /// <param name="xAxis">Intersected horizontal axis</param>
+    /// <param name="yAxis">Intersected vertical axis</param>
+    /// <returns>Snapped position to the closest snap point along projected axis from <paramref name="pos">/></returns> 
+    public VecD GetSnapPoint(VecD pos, VecD direction, out string xAxis, out string yAxis)
+    {
+        if (!SnappingEnabled)
+        {
+            xAxis = string.Empty;
+            yAxis = string.Empty;
+            return pos;
+        }
+
+        if (direction == VecD.Zero)
+        {
+            return GetSnapPoint(pos, out xAxis, out yAxis);
+        }
+
+        VecD snapDelta = GetSnapPoint(pos, out string closestXAxis, out string closestYAxis);
+
+        double? closestX = closestXAxis != string.Empty ? snapDelta.X : null;
+        double? closestY = closestYAxis != string.Empty ? snapDelta.Y : null;
+
+
+        VecD? xIntersect = null;
+        if (closestX != null)
+        {
+            double x = closestX.Value;
+            double y = pos.Y + direction.Y * (x - pos.X) / direction.X;
+            xIntersect = new VecD(x, y);
+        }
+
+        VecD? yIntersect = null;
+        if (closestY != null)
+        {
+            double y = closestY.Value;
+            double x = pos.X + direction.X * (y - pos.Y) / direction.Y;
+            yIntersect = new VecD(x, y);
+        }
+
+        if (xIntersect.HasValue && yIntersect.HasValue)
+        {
+            if (Math.Abs(xIntersect.Value.X - yIntersect.Value.X) < float.Epsilon
+                && Math.Abs(xIntersect.Value.Y - yIntersect.Value.Y) < float.Epsilon)
+            {
+                xAxis = closestXAxis;
+                yAxis = closestYAxis;
+                return xIntersect.Value;
+            }
+
+            double xDist = (xIntersect.Value - pos).LengthSquared;
+            double yDist = (yIntersect.Value - pos).LengthSquared;
+
+            if (xDist < yDist)
+            {
+                xAxis = closestXAxis;
+                yAxis = null;
+                return xIntersect.Value;
+            }
+
+            xAxis = null;
+            yAxis = closestYAxis;
+            return yIntersect.Value;
+        }
+
+        if (xIntersect != null)
+        {
+            xAxis = closestXAxis;
+            yAxis = null;
+
+            return xIntersect.Value;
+        }
+
+        if (yIntersect != null)
+        {
+            xAxis = null;
+            yAxis = closestYAxis;
+
+            return yIntersect.Value;
+        }
+
+        xAxis = string.Empty;
+        yAxis = string.Empty;
+        return pos;
+    }
 }
 }

+ 42 - 30
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ComplexShapeToolExecutor.cs

@@ -60,7 +60,8 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
                 toolbar.StrokeColor = colorsVM.PrimaryColor.ToColor();
                 toolbar.StrokeColor = colorsVM.PrimaryColor.ToColor();
             }
             }
 
 
-            document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect.Inflate(1)), false);
+            document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect.Inflate(1)),
+                false);
             document.TransformHandler.ShowHandles = false;
             document.TransformHandler.ShowHandles = false;
             document.TransformHandler.IsSizeBoxEnabled = true;
             document.TransformHandler.IsSizeBoxEnabled = true;
             return ExecutionState.Success;
             return ExecutionState.Success;
@@ -102,34 +103,6 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
     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;
 
 
-    public static VecI Get45IncrementedPosition(VecD startPos, VecD curPos)
-    {
-        Span<VecI> positions =
-        [
-            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(1, 1)) -
-                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
-            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(1, -1)) -
-                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
-            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(1, 0)) -
-                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
-            (VecI)(curPos.ProjectOntoLine(startPos, startPos + new VecD(0, 1)) -
-                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round()
-        ];
-
-        VecI max = positions[0];
-        double maxLength = double.MaxValue;
-        foreach (var pos in positions)
-        {
-            double length = (pos - curPos).LengthSquared;
-            if (length < maxLength)
-            {
-                maxLength = length;
-                max = pos;
-            }
-        }
-
-        return max;
-    }
 
 
     public static VecI GetSquaredPosition(VecI startPos, VecI curPos)
     public static VecI GetSquaredPosition(VecI startPos, VecI curPos)
     {
     {
@@ -207,7 +180,19 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
 
 
     protected override void PrecisePositionChangeDrawingMode(VecD pos)
     protected override void PrecisePositionChangeDrawingMode(VecD pos)
     {
     {
-        var snapped = Snap(pos, startDrawingPos, true);
+        VecI adjustedPos = (VecI)pos.Floor();
+
+        VecD snapped = adjustedPos;
+        if (toolViewModel.DrawEven)
+        {
+            adjustedPos = GetSquaredPosition((VecI)startDrawingPos, adjustedPos);
+            VecD dir = (adjustedPos - startDrawingPos).Normalize();
+            snapped = Snap(adjustedPos, startDrawingPos, dir, true);
+        }
+        else
+        {
+            snapped = Snap(adjustedPos, startDrawingPos, true);
+        }
 
 
         noMovement = false;
         noMovement = false;
 
 
@@ -244,6 +229,33 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
         return snapped;
         return snapped;
     }
     }
 
 
+    protected VecD Snap(VecD pos, VecD adjustPos, VecD dir, bool highlight = false)
+    {
+        VecD snapped =
+            document.SnappingHandler.SnappingController.GetSnapPoint(pos, dir, out string snapXAxis,
+                out string snapYAxis);
+
+        if (highlight)
+        {
+            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)
     private void HighlightSnapAxis(string snapXAxis, string snapYAxis)
     {
     {
         document.SnappingHandler.SnappingController.HighlightedXAxis = snapXAxis;
         document.SnappingHandler.SnappingController.HighlightedXAxis = snapXAxis;

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

@@ -9,6 +9,7 @@ using PixiEditor.Models.Handlers.Toolbars;
 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.Helpers;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
 
@@ -98,13 +99,24 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     {
     {
         startedDrawing = true;
         startedDrawing = true;
 
 
-        VecD snapped =
-            document!.SnappingHandler.SnappingController.GetSnapPoint(pos, out string snapX, out string snapY);
-
+        VecD endPos = pos;
+        VecD snapped = endPos;
+        string snapX = "";
+        string snapY = "";
+        
         if (toolViewModel!.Snap)
         if (toolViewModel!.Snap)
         {
         {
-            snapped = ComplexShapeToolExecutor<IShapeToolHandler>.Get45IncrementedPosition(startDrawingPos, pos);
+            endPos = GeometryHelper.Get45IncrementedPosition(startDrawingPos, pos);
+            VecD directionConstraint = endPos - startDrawingPos;
+            snapped =
+                document!.SnappingHandler.SnappingController.GetSnapPoint(endPos, directionConstraint, out snapX,
+                    out snapY);
+        }
+        else
+        {
+            snapped = document!.SnappingHandler.SnappingController.GetSnapPoint(endPos, out snapX, out snapY);
         }
         }
+        
 
 
         HighlightSnapping(snapX, snapY);
         HighlightSnapping(snapX, snapY);
         document!.LineToolOverlayHandler.LineEnd = snapped;
         document!.LineToolOverlayHandler.LineEnd = snapped;

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

@@ -17,9 +17,9 @@ internal class RasterEllipseToolExecutor : ComplexShapeToolExecutor<IRasterEllip
         VecI startPos = (VecI)Snap(startDrawingPos, curPos).Floor();
         VecI startPos = (VecI)Snap(startDrawingPos, curPos).Floor();
         if (firstDraw)
         if (firstDraw)
             rect = new RectI(curPos, VecI.Zero);
             rect = new RectI(curPos, VecI.Zero);
-        else if (toolViewModel!.DrawCircle)
+        /*else if (toolViewModel!.DrawCircle)
             rect = GetSquaredCoordinates(startPos, curPos);
             rect = GetSquaredCoordinates(startPos, curPos);
-        else
+        else*/
             rect = RectI.FromTwoPixels(startPos, curPos);
             rect = RectI.FromTwoPixels(startPos, curPos);
 
 
         lastRect = rect;
         lastRect = rect;

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

@@ -18,9 +18,9 @@ internal class RasterRectangleToolExecutor : ComplexShapeToolExecutor<IRasterRec
         VecI startPos = (VecI)Snap(startDrawingPos, curPos).Floor();
         VecI startPos = (VecI)Snap(startDrawingPos, curPos).Floor();
         if (firstDraw)
         if (firstDraw)
             rect = new RectI(curPos, VecI.Zero);
             rect = new RectI(curPos, VecI.Zero);
-        else if (toolViewModel!.DrawSquare)
+        /*else if (toolViewModel!.DrawSquare)
             rect = GetSquaredCoordinates(startPos, curPos);
             rect = GetSquaredCoordinates(startPos, curPos);
-        else
+        else*/
             rect = RectI.FromTwoPixels(startPos, curPos);
             rect = RectI.FromTwoPixels(startPos, curPos);
         lastRect = rect;
         lastRect = rect;
         lastRadians = rotationRad;
         lastRadians = rotationRad;

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

@@ -38,9 +38,9 @@ internal class VectorEllipseToolExecutor : ComplexShapeToolExecutor<IVectorEllip
         VecI startPos = (VecI)Snap(startDrawingPos, curPos).Floor();
         VecI startPos = (VecI)Snap(startDrawingPos, curPos).Floor();
         if (firstDraw)
         if (firstDraw)
             rect = new RectI(curPos, VecI.Zero);
             rect = new RectI(curPos, VecI.Zero);
-        else if (toolViewModel!.DrawCircle)
+        /*else if (toolViewModel!.DrawCircle)
             rect = GetSquaredCoordinates(startPos, curPos);
             rect = GetSquaredCoordinates(startPos, curPos);
-        else
+        else*/
             rect = RectI.FromTwoPixels(startPos, curPos);
             rect = RectI.FromTwoPixels(startPos, curPos);
 
 
         firstCenter = rect.Center;
         firstCenter = rect.Center;

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

@@ -38,9 +38,9 @@ internal class VectorRectangleToolExecutor : ComplexShapeToolExecutor<IVectorRec
         VecI startPos = (VecI)Snap(startDrawingPos, curPos).Floor();
         VecI startPos = (VecI)Snap(startDrawingPos, curPos).Floor();
         if (firstDraw)
         if (firstDraw)
             rect = new RectI(curPos, VecI.Zero);
             rect = new RectI(curPos, VecI.Zero);
-        else if (toolViewModel!.DrawSquare)
+        /*else if (toolViewModel!.DrawSquare)
             rect = GetSquaredCoordinates(startPos, curPos);
             rect = GetSquaredCoordinates(startPos, curPos);
-        else
+        else*/
             rect = RectI.FromTwoPixels(startPos, curPos);
             rect = RectI.FromTwoPixels(startPos, curPos);
 
 
         firstCenter = rect.Center;
         firstCenter = rect.Center;

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

@@ -2,5 +2,4 @@
 
 
 internal interface IRasterEllipseToolHandler : IShapeToolHandler
 internal interface IRasterEllipseToolHandler : IShapeToolHandler
 {
 {
-    public bool DrawCircle { get; }
 }
 }

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

@@ -2,5 +2,4 @@
 
 
 internal interface IRasterRectangleToolHandler : IShapeToolHandler
 internal interface IRasterRectangleToolHandler : IShapeToolHandler
 {
 {
-    public bool DrawSquare { get; }
 }
 }

+ 1 - 1
src/PixiEditor/Models/Handlers/Tools/IShapeToolHandler.cs

@@ -2,5 +2,5 @@
 
 
 internal interface IShapeToolHandler : IToolHandler
 internal interface IShapeToolHandler : IToolHandler
 {
 {
-
+    public bool DrawEven { get; }
 }
 }

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

@@ -2,5 +2,4 @@
 
 
 internal interface IVectorEllipseToolHandler : IShapeToolHandler
 internal interface IVectorEllipseToolHandler : IShapeToolHandler
 {
 {
-    public bool DrawCircle { get; }
 }
 }

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

@@ -2,5 +2,4 @@
 
 
 internal interface IVectorRectangleToolHandler : IShapeToolHandler
 internal interface IVectorRectangleToolHandler : IShapeToolHandler
 {
 {
-    public bool DrawSquare { get; }
 }
 }

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

@@ -13,6 +13,7 @@ internal abstract class ShapeTool : ToolViewModel, IShapeToolHandler
     public override bool UsesColor => true;
     public override bool UsesColor => true;
 
 
     public override bool IsErasable => true;
     public override bool IsErasable => true;
+    public bool DrawEven { get; protected set; }
 
 
     public ShapeTool()
     public ShapeTool()
     {
     {
@@ -27,4 +28,5 @@ internal abstract class ShapeTool : ToolViewModel, IShapeToolHandler
             ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TryStopToolLinkedExecutor();
             ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TryStopToolLinkedExecutor();
         }
         }
     }
     }
+
 }
 }

+ 2 - 3
src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs

@@ -23,7 +23,6 @@ internal class RasterEllipseToolViewModel : ShapeTool, IRasterEllipseToolHandler
 
 
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
     public override LocalizedString Tooltip => new LocalizedString("ELLIPSE_TOOL_TOOLTIP", Shortcut);
     public override LocalizedString Tooltip => new LocalizedString("ELLIPSE_TOOL_TOOLTIP", Shortcut);
-    public bool DrawCircle { get; private set; }
 
 
     public override string DefaultIcon => PixiPerfectIcons.LowResCircle;
     public override string DefaultIcon => PixiPerfectIcons.LowResCircle;
 
 
@@ -34,12 +33,12 @@ internal class RasterEllipseToolViewModel : ShapeTool, IRasterEllipseToolHandler
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {
             ActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT";
             ActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT";
-            DrawCircle = true;
+            DrawEven = true;
         }
         }
         else
         else
         {
         {
             ActionDisplay = defaultActionDisplay;
             ActionDisplay = defaultActionDisplay;
-            DrawCircle = false;
+            DrawEven = false;
         }
         }
     }
     }
 
 

+ 2 - 3
src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs

@@ -24,7 +24,6 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
     public override LocalizedString Tooltip => new LocalizedString("RECTANGLE_TOOL_TOOLTIP", Shortcut);
     public override LocalizedString Tooltip => new LocalizedString("RECTANGLE_TOOL_TOOLTIP", Shortcut);
 
 
     public bool Filled { get; set; } = false;
     public bool Filled { get; set; } = false;
-    public bool DrawSquare { get; private set; } = false;
 
 
     public override string DefaultIcon => PixiPerfectIcons.LowResSquare;
     public override string DefaultIcon => PixiPerfectIcons.LowResSquare;
 
 
@@ -34,12 +33,12 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
     {
     {
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {
-            DrawSquare = true;
+            DrawEven = true;
             ActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT";
             ActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT";
         }
         }
         else
         else
         {
         {
-            DrawSquare = false;
+            DrawEven = false;
             ActionDisplay = defaultActionDisplay;
             ActionDisplay = defaultActionDisplay;
         }
         }
     }
     }

+ 16 - 3
src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs

@@ -26,7 +26,6 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
     // This doesn't include a Vector layer because it is designed to create new layer each use
     // This doesn't include a Vector layer because it is designed to create new layer each use
     public override Type[]? SupportedLayerTypes { get; } = [];
     public override Type[]? SupportedLayerTypes { get; } = [];
     public override LocalizedString Tooltip => new LocalizedString("ELLIPSE_TOOL_TOOLTIP", Shortcut);
     public override LocalizedString Tooltip => new LocalizedString("ELLIPSE_TOOL_TOOLTIP", Shortcut);
-    public bool DrawCircle { get; private set; }
 
 
     public override string DefaultIcon => PixiPerfectIcons.Circle;
     public override string DefaultIcon => PixiPerfectIcons.Circle;
 
 
@@ -39,13 +38,27 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorEllipseTool();
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorEllipseTool();
     }
     }
 
 
+    public override void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+    {
+        if (shiftIsDown)
+        {
+            DrawEven = true;
+            ActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT";
+        }
+        else
+        {
+            DrawEven = false;
+            ActionDisplay = defaultActionDisplay;
+        }
+    }
+
     public override void OnSelected(bool restoring)
     public override void OnSelected(bool restoring)
     {
     {
         if (restoring) return;
         if (restoring) return;
-        
+
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
         var layer = document.SelectedStructureMember;
         var layer = document.SelectedStructureMember;
-        if (layer is IVectorLayerHandler vectorLayer && 
+        if (layer is IVectorLayerHandler vectorLayer &&
             vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyEllipseData)
             vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyEllipseData)
         {
         {
             ShapeCorners corners = vectorLayer.TransformationCorners;
             ShapeCorners corners = vectorLayer.TransformationCorners;

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

@@ -25,7 +25,6 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
 
 
     public override Type[]? SupportedLayerTypes { get; } = [];
     public override Type[]? SupportedLayerTypes { get; } = [];
     public override LocalizedString Tooltip => new LocalizedString("RECTANGLE_TOOL_TOOLTIP", Shortcut);
     public override LocalizedString Tooltip => new LocalizedString("RECTANGLE_TOOL_TOOLTIP", Shortcut);
-    public bool DrawSquare { get; private set; }
 
 
     public override string DefaultIcon => PixiPerfectIcons.Square;
     public override string DefaultIcon => PixiPerfectIcons.Square;
 
 
@@ -36,12 +35,12 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
     {
     {
         if (shiftIsDown)
         if (shiftIsDown)
         {
         {
-            DrawSquare = true;
+            DrawEven = true;
             ActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT";
             ActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT";
         }
         }
         else
         else
         {
         {
-            DrawSquare = false;
+            DrawEven = false;
             ActionDisplay = defaultActionDisplay;
             ActionDisplay = defaultActionDisplay;
         }
         }
     }
     }

+ 84 - 23
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -78,8 +78,9 @@ internal class TransformOverlay : Overlay
         AvaloniaProperty.Register<TransformOverlay, TransformState>(nameof(InternalState),
         AvaloniaProperty.Register<TransformOverlay, TransformState>(nameof(InternalState),
             defaultValue: default(TransformState));
             defaultValue: default(TransformState));
 
 
-    public static readonly StyledProperty<ICommand> PassthroughPointerPressedCommandProperty = AvaloniaProperty.Register<TransformOverlay, ICommand>(
-        nameof(PassthroughPointerPressedCommand));
+    public static readonly StyledProperty<ICommand> PassthroughPointerPressedCommandProperty =
+        AvaloniaProperty.Register<TransformOverlay, ICommand>(
+            nameof(PassthroughPointerPressedCommand));
 
 
     public ICommand PassthroughPointerPressedCommand
     public ICommand PassthroughPointerPressedCommand
     {
     {
@@ -260,7 +261,7 @@ internal class TransformOverlay : Overlay
     private InfoBox infoBox;
     private InfoBox infoBox;
     private VecD lastSize;
     private VecD lastSize;
     private bool actuallyMoved = false;
     private bool actuallyMoved = false;
-    
+
     public TransformOverlay()
     public TransformOverlay()
     {
     {
         topLeftHandle = new AnchorHandle(this);
         topLeftHandle = new AnchorHandle(this);
@@ -313,7 +314,7 @@ internal class TransformOverlay : Overlay
 
 
         moveHandle.OnPress += OnMoveHandlePressed;
         moveHandle.OnPress += OnMoveHandlePressed;
         moveHandle.OnRelease += OnMoveHandleReleased;
         moveHandle.OnRelease += OnMoveHandleReleased;
-        
+
         infoBox = new InfoBox();
         infoBox = new InfoBox();
     }
     }
 
 
@@ -494,7 +495,7 @@ internal class TransformOverlay : Overlay
         {
         {
             return;
             return;
         }
         }
-        
+
         args.Pointer.Capture(this);
         args.Pointer.Capture(this);
         args.Handled = true;
         args.Handled = true;
     }
     }
@@ -627,17 +628,22 @@ internal class TransformOverlay : Overlay
         SnappingController.HighlightedXAxis = snapDeltaResult.SnapAxisXName;
         SnappingController.HighlightedXAxis = snapDeltaResult.SnapAxisXName;
         SnappingController.HighlightedYAxis = snapDeltaResult.SnapAxisYName;
         SnappingController.HighlightedYAxis = snapDeltaResult.SnapAxisYName;
 
 
-        Corners = new ShapeCorners()
-        {
-            BottomLeft = cornersOnStartMove.BottomLeft + delta + snapDelta,
-            BottomRight = cornersOnStartMove.BottomRight + delta + snapDelta,
-            TopLeft = cornersOnStartMove.TopLeft + delta + snapDelta,
-            TopRight = cornersOnStartMove.TopRight + delta + snapDelta,
-        };
+        Corners = ApplyCornersWithDelta(cornersOnStartMove, delta, snapDelta);
 
 
         InternalState = InternalState with { Origin = originOnStartMove + delta + snapDelta };
         InternalState = InternalState with { Origin = originOnStartMove + delta + snapDelta };
     }
     }
 
 
+    private ShapeCorners ApplyCornersWithDelta(ShapeCorners corners, VecD delta, VecD snapDelta)
+    {
+        return new ShapeCorners()
+        {
+            BottomLeft = corners.BottomLeft + delta + snapDelta,
+            BottomRight = corners.BottomRight + delta + snapDelta,
+            TopLeft = corners.TopLeft + delta + snapDelta,
+            TopRight = corners.TopRight + delta + snapDelta,
+        };
+    }
+
     private SnapData TrySnapCorners(ShapeCorners rawCorners)
     private SnapData TrySnapCorners(ShapeCorners rawCorners)
     {
     {
         if (SnappingController is null)
         if (SnappingController is null)
@@ -709,23 +715,23 @@ internal class TransformOverlay : Overlay
             VecD targetPos = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, (Anchor)capturedAnchor) + pos -
             VecD targetPos = TransformHelper.GetAnchorPosition(cornersOnStartAnchorDrag, (Anchor)capturedAnchor) + pos -
                              mousePosOnStartAnchorDrag;
                              mousePosOnStartAnchorDrag;
 
 
-            var snapped = TrySnapAnchor(targetPos);
-
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromCorner
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromCorner
             ((Anchor)capturedAnchor, CornerFreedom, InternalState.ProportionalAngle1,
             ((Anchor)capturedAnchor, CornerFreedom, InternalState.ProportionalAngle1,
-                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos + snapped.Delta);
-
-            HighlightSnappedAxis(snapped.SnapAxisXName, snapped.SnapAxisYName);
+                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos, SnappingController, out string snapX, out string snapY);
+            
+            HighlightSnappedAxis(snapX, snapY);
 
 
             if (newCorners is not null)
             if (newCorners is not null)
             {
             {
                 bool shouldAlign =
                 bool shouldAlign =
                     (CornerFreedom is TransformCornerFreedom.ScaleProportionally or TransformCornerFreedom.Scale) &&
                     (CornerFreedom is TransformCornerFreedom.ScaleProportionally or TransformCornerFreedom.Scale) &&
                     Corners.IsAlignedToPixels;
                     Corners.IsAlignedToPixels;
-
-                Corners = shouldAlign
+                
+                newCorners = shouldAlign
                     ? TransformHelper.AlignToPixels((ShapeCorners)newCorners)
                     ? TransformHelper.AlignToPixels((ShapeCorners)newCorners)
                     : (ShapeCorners)newCorners;
                     : (ShapeCorners)newCorners;
+
+                Corners = (ShapeCorners)newCorners;
             }
             }
 
 
             UpdateOriginPos();
             UpdateOriginPos();
@@ -747,7 +753,7 @@ internal class TransformOverlay : Overlay
             VecD anchorRelativeDelta = projected - originalAnchorPos;
             VecD anchorRelativeDelta = projected - originalAnchorPos;
 
 
             var adjacentAnchors = TransformHelper.GetAdjacentAnchors((Anchor)capturedAnchor);
             var adjacentAnchors = TransformHelper.GetAdjacentAnchors((Anchor)capturedAnchor);
-            SnapData snapped;
+            SnapData snapped = new SnapData();
 
 
             if (SideFreedom is TransformSideFreedom.Shear or TransformSideFreedom.Free)
             if (SideFreedom is TransformSideFreedom.Shear or TransformSideFreedom.Free)
             {
             {
@@ -766,7 +772,7 @@ internal class TransformOverlay : Overlay
                     snapped = TrySnapAnchor(adjacentPos + rawDelta);
                     snapped = TrySnapAnchor(adjacentPos + rawDelta);
                 }
                 }
             }
             }
-            else
+            else if(SideFreedom is not TransformSideFreedom.ScaleProportionally)
             {
             {
                 // If rotation is almost cardinal, projecting snapping points result in extreme values when perpendicular to the axis
                 // If rotation is almost cardinal, projecting snapping points result in extreme values when perpendicular to the axis
                 if (!TransformHelper.RotationIsAlmostCardinal(cornersOnStartAnchorDrag.RectRotation))
                 if (!TransformHelper.RotationIsAlmostCardinal(cornersOnStartAnchorDrag.RectRotation))
@@ -791,9 +797,13 @@ internal class TransformOverlay : Overlay
 
 
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromSide
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromSide
             ((Anchor)capturedAnchor, SideFreedom, InternalState.ProportionalAngle1,
             ((Anchor)capturedAnchor, SideFreedom, InternalState.ProportionalAngle1,
-                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos + snapped.Delta);
+                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos + snapped.Delta,
+                SnappingController, out string snapX, out string snapY);
 
 
-            HighlightSnappedAxis(snapped.SnapAxisXName, snapped.SnapAxisYName);
+            string finalSnapX = snapped.SnapAxisXName ?? snapX;
+            string finalSnapY = snapped.SnapAxisYName ?? snapY;
+            
+            HighlightSnappedAxis(finalSnapX, finalSnapY);
 
 
             if (newCorners is not null)
             if (newCorners is not null)
             {
             {
@@ -816,6 +826,57 @@ internal class TransformOverlay : Overlay
         Refresh();
         Refresh();
     }
     }
 
 
+    private ShapeCorners SnapAnchorInCorners(Anchor anchor, ShapeCorners corners, VecD delta)
+    {
+        var newCorners = SnapSelectedAnchorsCorners(corners, delta, anchor);
+
+        return newCorners;
+    }
+
+    private static ShapeCorners SnapSelectedAnchorsCorners(ShapeCorners corners, VecD delta, Anchor anchor)
+    {
+        VecD anchorPos = TransformHelper.GetAnchorPosition(corners, anchor);
+        VecD targetAnchorPos = anchorPos + delta;
+
+        VecD topLeftPos = corners.TopLeft;
+        VecD topRightPos = corners.TopRight;
+        VecD bottomLeftPos = corners.BottomLeft;
+        VecD bottomRightPos = corners.BottomRight;
+
+        if (anchor == Anchor.TopLeft)
+        {
+            topLeftPos = targetAnchorPos;
+            topRightPos = new VecD(topRightPos.X, topRightPos.Y + delta.Y);
+            bottomLeftPos = new VecD(bottomLeftPos.X + delta.X, bottomLeftPos.Y);
+        }
+        else if (anchor == Anchor.TopRight)
+        {
+            topRightPos = targetAnchorPos;
+            topLeftPos = new VecD(topLeftPos.X, topLeftPos.Y + delta.Y);
+            bottomRightPos = new VecD(bottomRightPos.X + delta.X, bottomRightPos.Y);
+        }
+        else if (anchor == Anchor.BottomLeft)
+        {
+            bottomLeftPos = targetAnchorPos;
+            topLeftPos = new VecD(topLeftPos.X + delta.X, topLeftPos.Y);
+            bottomRightPos = new VecD(bottomRightPos.X, bottomRightPos.Y + delta.Y);
+        }
+        else if (anchor == Anchor.BottomRight)
+        {
+            bottomRightPos = targetAnchorPos;
+            topRightPos = new VecD(topRightPos.X + delta.X, topRightPos.Y);
+            bottomLeftPos = new VecD(bottomLeftPos.X, bottomLeftPos.Y + delta.Y);
+        }
+
+        return new ShapeCorners()
+        {
+            TopLeft = topLeftPos,
+            TopRight = topRightPos,
+            BottomLeft = bottomLeftPos,
+            BottomRight = bottomRightPos,
+        };
+    }
+
     private SnapData FindAdjacentCornersSnap((Anchor, Anchor) adjacentAnchors, VecD anchorRelativeDelta)
     private SnapData FindAdjacentCornersSnap((Anchor, Anchor) adjacentAnchors, VecD anchorRelativeDelta)
     {
     {
         VecD adjacentAnchorPos =
         VecD adjacentAnchorPos =

+ 66 - 16
src/PixiEditor/Views/Overlays/TransformOverlay/TransformUpdateHelper.cs

@@ -1,20 +1,27 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.Models.Controllers.InputDevice;
 
 
 namespace PixiEditor.Views.Overlays.TransformOverlay;
 namespace PixiEditor.Views.Overlays.TransformOverlay;
 #nullable enable
 #nullable enable
 internal static class TransformUpdateHelper
 internal static class TransformUpdateHelper
 {
 {
     private const double epsilon = 0.00001;
     private const double epsilon = 0.00001;
+
     public static ShapeCorners? UpdateShapeFromCorner
     public static ShapeCorners? UpdateShapeFromCorner
-        (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners, VecD desiredPos)
+    (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
+        VecD desiredPos,
+        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");
 
 
         if (freedom == TransformCornerFreedom.Locked)
         if (freedom == TransformCornerFreedom.Locked)
+        {
+            snapX = snapY = "";
             return corners;
             return corners;
+        }
 
 
         if (freedom is TransformCornerFreedom.ScaleProportionally or TransformCornerFreedom.Scale)
         if (freedom is TransformCornerFreedom.ScaleProportionally or TransformCornerFreedom.Scale)
         {
         {
@@ -23,15 +30,36 @@ internal static class TransformUpdateHelper
             Anchor opposite = TransformHelper.GetOpposite(targetCorner);
             Anchor opposite = TransformHelper.GetOpposite(targetCorner);
             VecD oppositePos = TransformHelper.GetAnchorPosition(corners, opposite);
             VecD oppositePos = TransformHelper.GetAnchorPosition(corners, opposite);
 
 
+            snapX = snapY = "";
+
             // 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)
             {
             {
                 double correctAngle = targetCorner is Anchor.TopLeft or Anchor.BottomRight ? propAngle1 : propAngle2;
                 double correctAngle = targetCorner is Anchor.TopLeft or Anchor.BottomRight ? propAngle1 : propAngle2;
-                desiredPos = desiredPos.ProjectOntoLine(oppositePos, oppositePos + VecD.FromAngleAndLength(correctAngle, 1));
+                VecD direction = VecD.FromAngleAndLength(correctAngle, 1);
+                desiredPos = desiredPos.ProjectOntoLine(oppositePos, oppositePos + direction);
+
+                if (snappingController is not null)
+                {
+                    desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
+                }
             }
             }
             else if (freedom == TransformCornerFreedom.ScaleProportionally)
             else if (freedom == TransformCornerFreedom.ScaleProportionally)
             {
             {
                 desiredPos = desiredPos.ProjectOntoLine(oppositePos, targetPos);
                 desiredPos = desiredPos.ProjectOntoLine(oppositePos, targetPos);
+                VecD direction = (targetPos - oppositePos);
+
+                if (snappingController is not null)
+                {
+                    desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
+                }
+            }
+            else
+            {
+                if (snappingController is not null)
+                {
+                    desiredPos = snappingController.GetSnapPoint(desiredPos, out snapX, out snapY);
+                }
             }
             }
 
 
             // find neighboring corners
             // find neighboring corners
@@ -59,8 +87,10 @@ internal static class TransformUpdateHelper
             }
             }
             else
             else
             {
             {
-                VecD? newLeftPos = TransformHelper.TwoLineIntersection(VecD.Zero, leftNeighTrans, targetTrans + delta, leftNeighTrans + delta);
-                VecD? newRightPos = TransformHelper.TwoLineIntersection(VecD.Zero, rightNeighTrans, targetTrans + delta, rightNeighTrans + delta);
+                VecD? newLeftPos = TransformHelper.TwoLineIntersection(VecD.Zero, leftNeighTrans, targetTrans + delta,
+                    leftNeighTrans + delta);
+                VecD? newRightPos = TransformHelper.TwoLineIntersection(VecD.Zero, rightNeighTrans, targetTrans + delta,
+                    rightNeighTrans + delta);
                 if (newLeftPos is null || newRightPos is null)
                 if (newLeftPos is null || newRightPos is null)
                     return null;
                     return null;
                 leftNeighDelta = newLeftPos.Value - leftNeighTrans;
                 leftNeighDelta = newLeftPos.Value - leftNeighTrans;
@@ -83,7 +113,7 @@ internal static class TransformUpdateHelper
             {
             {
                 rightNeighDelta = TransferZeros(SwapAxes(leftNeighTrans), delta);
                 rightNeighDelta = TransferZeros(SwapAxes(leftNeighTrans), delta);
             }
             }
-            
+
             // move the corners, while reapplying the transform rotation
             // move the corners, while reapplying the transform rotation
             corners = TransformHelper.UpdateCorner(corners, targetCorner,
             corners = TransformHelper.UpdateCorner(corners, targetCorner,
                 (targetTrans + delta).Rotate(angle) + oppositePos);
                 (targetTrans + delta).Rotate(angle) + oppositePos);
@@ -100,9 +130,11 @@ internal static class TransformUpdateHelper
 
 
         if (freedom == TransformCornerFreedom.Free)
         if (freedom == TransformCornerFreedom.Free)
         {
         {
+            snapX = snapY = "";
             ShapeCorners newCorners = TransformHelper.UpdateCorner(corners, targetCorner, desiredPos);
             ShapeCorners newCorners = TransformHelper.UpdateCorner(corners, targetCorner, desiredPos);
             return newCorners.IsLegal ? newCorners : null;
             return newCorners.IsLegal ? newCorners : null;
         }
         }
+
         throw new ArgumentException($"Freedom degree {freedom} is not supported");
         throw new ArgumentException($"Freedom degree {freedom} is not supported");
     }
     }
 
 
@@ -118,11 +150,14 @@ internal static class TransformUpdateHelper
     }
     }
 
 
     public static ShapeCorners? UpdateShapeFromSide
     public static ShapeCorners? UpdateShapeFromSide
-        (Anchor targetSide, TransformSideFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners, VecD desiredPos)
+    (Anchor targetSide, TransformSideFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
+        VecD desiredPos, SnappingController? snappingController, out string snapX, out string snapY)
     {
     {
         if (!TransformHelper.IsSide(targetSide))
         if (!TransformHelper.IsSide(targetSide))
             throw new ArgumentException($"{targetSide} is not a side");
             throw new ArgumentException($"{targetSide} is not a side");
 
 
+        snapX = snapY = "";
+
         if (freedom == TransformSideFreedom.Locked)
         if (freedom == TransformSideFreedom.Locked)
             return corners;
             return corners;
 
 
@@ -134,9 +169,15 @@ internal static class TransformUpdateHelper
 
 
             desiredPos = desiredPos.ProjectOntoLine(targetPos, oppositePos);
             desiredPos = desiredPos.ProjectOntoLine(targetPos, oppositePos);
 
 
-            VecD thing = targetPos - oppositePos;
-            thing = VecD.FromAngleAndLength(thing.Angle, 1 / thing.Length);
-            double scalingFactor = (desiredPos - oppositePos) * thing;
+            VecD direction = targetPos - oppositePos;
+            direction = VecD.FromAngleAndLength(direction.Angle, 1 / direction.Length);
+            
+            if (snappingController is not null)
+            {
+                desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
+            }
+
+            double scalingFactor = (desiredPos - oppositePos) * direction;
             if (!double.IsNormal(scalingFactor))
             if (!double.IsNormal(scalingFactor))
                 return corners;
                 return corners;
 
 
@@ -154,12 +195,18 @@ internal static class TransformUpdateHelper
                 var leftOppCornPos = TransformHelper.GetAnchorPosition(corners, leftOppCorn);
                 var leftOppCornPos = TransformHelper.GetAnchorPosition(corners, leftOppCorn);
                 var rightOppCornPos = TransformHelper.GetAnchorPosition(corners, rightOppCorn);
                 var rightOppCornPos = TransformHelper.GetAnchorPosition(corners, rightOppCorn);
 
 
-                var (leftAngle, rightAngle) = leftCorn is Anchor.TopLeft or Anchor.BottomRight ? (propAngle1, propAngle2) : (propAngle2, propAngle1);
+                var (leftAngle, rightAngle) = leftCorn is Anchor.TopLeft or Anchor.BottomRight
+                    ? (propAngle1, propAngle2)
+                    : (propAngle2, propAngle1);
 
 
-                var updLeftCorn = TransformHelper.TwoLineIntersection(leftCornPos + delta, rightCornPos + delta, center, center + VecD.FromAngleAndLength(leftAngle, 1));
-                var updRightCorn = TransformHelper.TwoLineIntersection(leftCornPos + delta, rightCornPos + delta, center, center + VecD.FromAngleAndLength(rightAngle, 1));
-                var updLeftOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center, center + VecD.FromAngleAndLength(rightAngle, 1));
-                var updRightOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center, center + VecD.FromAngleAndLength(leftAngle, 1));
+                var updLeftCorn = TransformHelper.TwoLineIntersection(leftCornPos + delta, rightCornPos + delta, center,
+                    center + VecD.FromAngleAndLength(leftAngle, 1));
+                var updRightCorn = TransformHelper.TwoLineIntersection(leftCornPos + delta, rightCornPos + delta,
+                    center, center + VecD.FromAngleAndLength(rightAngle, 1));
+                var updLeftOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
+                    center + VecD.FromAngleAndLength(rightAngle, 1));
+                var updRightOppCorn = TransformHelper.TwoLineIntersection(leftOppCornPos, rightOppCornPos, center,
+                    center + VecD.FromAngleAndLength(leftAngle, 1));
 
 
                 if (updLeftCorn is null || updRightCorn is null || updLeftOppCorn is null || updRightOppCorn is null)
                 if (updLeftCorn is null || updRightCorn is null || updLeftOppCorn is null || updRightOppCorn is null)
                     goto fallback;
                     goto fallback;
@@ -171,7 +218,8 @@ internal static class TransformUpdateHelper
 
 
                 return corners;
                 return corners;
             }
             }
-fallback:
+
+            fallback:
             corners.TopLeft = (corners.TopLeft - oppositePos) * scalingFactor + oppositePos;
             corners.TopLeft = (corners.TopLeft - oppositePos) * scalingFactor + oppositePos;
             corners.BottomRight = (corners.BottomRight - oppositePos) * scalingFactor + oppositePos;
             corners.BottomRight = (corners.BottomRight - oppositePos) * scalingFactor + oppositePos;
             corners.TopRight = (corners.TopRight - oppositePos) * scalingFactor + oppositePos;
             corners.TopRight = (corners.TopRight - oppositePos) * scalingFactor + oppositePos;
@@ -207,7 +255,8 @@ fallback:
                 if ((targetPos - oppPos).TaxicabLength > epsilon)
                 if ((targetPos - oppPos).TaxicabLength > epsilon)
                     desiredPos = desiredPos.ProjectOntoLine(targetPos, oppPos);
                     desiredPos = desiredPos.ProjectOntoLine(targetPos, oppPos);
                 else
                 else
-                    desiredPos = desiredPos.ProjectOntoLine(targetPos, (leftCornerPos - targetPos).Rotate(Math.PI / 2) + targetPos);
+                    desiredPos = desiredPos.ProjectOntoLine(targetPos,
+                        (leftCornerPos - targetPos).Rotate(Math.PI / 2) + targetPos);
             }
             }
 
 
             var delta = desiredPos - targetPos;
             var delta = desiredPos - targetPos;
@@ -216,6 +265,7 @@ fallback:
 
 
             return newCorners.IsLegal ? newCorners : null;
             return newCorners.IsLegal ? newCorners : null;
         }
         }
+
         throw new ArgumentException($"Freedom degree {freedom} is not supported");
         throw new ArgumentException($"Freedom degree {freedom} is not supported");
     }
     }