Explorar o código

Merge pull request #860 from PixiEditor/fixes/24.03

Fixes
Krzysztof Krysiński hai 4 meses
pai
achega
f3c18861bf
Modificáronse 23 ficheiros con 414 adicións e 158 borrados
  1. 19 5
      src/ChunkyImageLib/Operations/ImageOperation.cs
  2. 1 1
      src/Drawie
  3. 40 26
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs
  4. 48 24
      src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs
  5. 8 0
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  6. 14 8
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  7. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  8. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  9. 49 17
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs
  10. 29 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  11. 5 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs
  12. 1 0
      src/PixiEditor/Models/Handlers/INodeHandler.cs
  13. 56 22
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  14. 1 1
      src/PixiEditor/Views/Overlays/Handles/AnchorHandle.cs
  15. 1 0
      src/PixiEditor/Views/Overlays/Handles/Handle.cs
  16. 1 1
      src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs
  17. 1 0
      src/PixiEditor/Views/Overlays/Overlay.cs
  18. 4 4
      src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs
  19. 36 4
      src/PixiEditor/Views/Overlays/SnappingOverlay.cs
  20. 31 4
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformHelper.cs
  21. 57 34
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  22. 7 1
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformUpdateHelper.cs
  23. 3 0
      src/PixiEditor/Views/Rendering/Scene.cs

+ 19 - 5
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -84,16 +84,30 @@ internal class ImageOperation : IMirroredDrawOperation
         float scaleMult = (float)targetChunk.Resolution.Multiplier();
         VecD trans = -chunkPos * ChunkPool.FullChunkSize;
 
-        var scaleTrans = Matrix3X3.CreateScaleTranslation(scaleMult, scaleMult, (float)trans.X * scaleMult, (float)trans.Y * scaleMult);
+        var scaleTrans = Matrix3X3.CreateScaleTranslation(scaleMult, scaleMult, (float)trans.X * scaleMult,
+            (float)trans.Y * scaleMult);
         var finalMatrix = Matrix3X3.Concat(scaleTrans, transformMatrix);
 
         using var snapshot = toPaint.DrawingSurface.Snapshot();
-        ShapeCorners chunkCorners = new ShapeCorners(new RectD(VecD.Zero, targetChunk.PixelSize));
-        RectD rect = chunkCorners.WithMatrix(finalMatrix.Invert()).AABBBounds;
-
         targetChunk.Surface.DrawingSurface.Canvas.Save();
         targetChunk.Surface.DrawingSurface.Canvas.SetMatrix(finalMatrix);
-        targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, rect, rect, customPaint);
+
+        bool hasPerspective = Math.Abs(finalMatrix.Persp0) > 0.0001 || Math.Abs(finalMatrix.Persp1) > 0.0001;
+
+        // More optimized, but works badly with perspective transformation
+        if (!hasPerspective)
+        {
+            ShapeCorners chunkCorners = new ShapeCorners(new RectD(VecD.Zero, targetChunk.PixelSize));
+            RectD rect = chunkCorners.WithMatrix(finalMatrix.Invert()).AABBBounds;
+
+            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, rect, rect, customPaint);
+        }
+        else
+        {
+            // Slower, but works with perspective transformation
+            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, customPaint);
+        }
+
         targetChunk.Surface.DrawingSurface.Canvas.Restore();
     }
 

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit ea188f49d32efa4875d5ae292d80759e85af5945
+Subproject commit 2bae841cdf55369fc483bc8d007d1d950a838ec3

+ 40 - 26
src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs

@@ -13,35 +13,40 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
     private readonly float correctionFactor;
     private readonly int strokeWidth;
     private readonly List<VecI> positions = new();
-    private bool ignoreUpdate = false;
     private readonly bool repeat;
     private int frame;
+    private int lastAppliedPointIndex = -1;
 
     private List<VecI> ellipseLines;
-    
+
     private CommittedChunkStorage? savedChunks;
 
     [GenerateUpdateableChangeActions]
-    public ChangeBrightness_UpdateableChange(Guid layerGuid, VecI pos, float correctionFactor, int strokeWidth, bool repeat, int frame)
+    public ChangeBrightness_UpdateableChange(Guid layerGuid, VecI pos, float correctionFactor, int strokeWidth,
+        bool repeat, int frame)
     {
         this.layerGuid = layerGuid;
         this.correctionFactor = correctionFactor;
         this.strokeWidth = strokeWidth;
         this.repeat = repeat;
         this.frame = frame;
-        // TODO: pos is unused, check if it should be added to positions
-        
-        ellipseLines = EllipseHelper.SplitEllipseIntoLines((EllipseHelper.GenerateEllipseFromRect(new RectI(0, 0, strokeWidth, strokeWidth), 0)));
+        positions.Add(pos);
+
+        ellipseLines =
+            EllipseHelper.SplitEllipseIntoLines(
+                (EllipseHelper.GenerateEllipseFromRect(new RectI(0, 0, strokeWidth, strokeWidth), 0)));
     }
 
     [UpdateChangeMethod]
     public void Update(VecI pos)
     {
-        ignoreUpdate = positions.Count > 0 && positions[^1] == pos;
-        if (!ignoreUpdate)
-            positions.Add(pos);
+        if (positions.Count > 0)
+        {
+            var bresenham = BresenhamLineHelper.GetBresenhamLine(positions[^1], pos);
+            positions.AddRange(bresenham);
+        }
     }
-    
+
     public override bool InitializeAndValidate(Document target)
     {
         if (!DrawingChangeHelper.IsValidForDrawing(target, layerGuid, false))
@@ -54,23 +59,28 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
-        if (ignoreUpdate)
-            return new None();
-        VecI pos = positions[^1];
         ImageLayerNode node = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
 
         var layerImage = node.GetLayerImageAtFrame(frame);
         int queueLength = layerImage.QueueLength;
-        
-        ChangeBrightness(ellipseLines, strokeWidth, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat, layerImage);
-        
+
+        for (int i = Math.Max(lastAppliedPointIndex, 0); i < positions.Count; i++)
+        {
+            VecI pos = positions[i];
+            ChangeBrightness(ellipseLines, strokeWidth, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
+                layerImage);
+        }
+
         var affected = layerImage.FindAffectedArea(queueLength);
-        
+
+        lastAppliedPointIndex = positions.Count - 1;
+
         return new LayerImageArea_ChangeInfo(layerGuid, affected);
     }
-    
+
     private static void ChangeBrightness(
-        List<VecI> circleLines, int circleDiameter, VecI offset, float correctionFactor, bool repeat, ChunkyImage layerImage)
+        List<VecI> circleLines, int circleDiameter, VecI offset, float correctionFactor, bool repeat,
+        ChunkyImage layerImage)
     {
         // TODO: Circle diameter is unused, check if it should be used
 
@@ -79,14 +89,15 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
             VecI left = circleLines[i];
             VecI right = circleLines[i + 1];
             int y = left.Y;
-            
+
             for (VecI pos = new VecI(left.X, y); pos.X <= right.X; pos.X++)
             {
                 layerImage.EnqueueDrawPixel(
                     pos + offset,
                     (commitedColor, upToDateColor) =>
                     {
-                        Color newColor = ColorHelper.ChangeColorBrightness(repeat ? upToDateColor : commitedColor, correctionFactor);
+                        Color newColor = ColorHelper.ChangeColorBrightness(repeat ? upToDateColor : commitedColor,
+                            correctionFactor);
                         return ColorHelper.ChangeColorBrightness(newColor, correctionFactor);
                     },
                     BlendMode.Src);
@@ -94,22 +105,24 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
         }
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         var layer = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
         ignoreInUndo = false;
 
         if (savedChunks is not null)
             throw new InvalidOperationException("Trying to apply while there are saved chunks");
-        
+
         var layerImage = layer.GetLayerImageAtFrame(frame);
-        
+
         if (!firstApply)
         {
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, layerImage, layerGuid, false);
             foreach (VecI pos in positions)
             {
-                ChangeBrightness(ellipseLines, strokeWidth, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat, layerImage);
+                ChangeBrightness(ellipseLines, strokeWidth, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
+                    layerImage);
             }
         }
 
@@ -123,7 +136,8 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, layerGuid, false, frame, ref savedChunks);
+        var affected =
+            DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, layerGuid, false, frame, ref savedChunks);
         return new LayerImageArea_ChangeInfo(layerGuid, affected);
     }
 }

+ 48 - 24
src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs

@@ -16,8 +16,8 @@ public class SnappingController
     /// </summary>
     public double SnapDistance { get; set; } = DefaultSnapDistance;
 
-    public Dictionary<string, Func<double>> HorizontalSnapPoints { get; } = new();
-    public Dictionary<string, Func<double>> VerticalSnapPoints { get; } = new();
+    public Dictionary<string, Func<VecD>> HorizontalSnapPoints { get; } = new();
+    public Dictionary<string, Func<VecD>> VerticalSnapPoints { get; } = new();
 
     public string HighlightedXAxis
     {
@@ -84,12 +84,12 @@ public class SnappingController
         }
 
         snapAxis = HorizontalSnapPoints.First().Key;
-        double closest = HorizontalSnapPoints.First().Value();
+        double closest = HorizontalSnapPoints.First().Value().X;
         foreach (var snapPoint in HorizontalSnapPoints)
         {
-            if (Math.Abs(snapPoint.Value() - xPos) < Math.Abs(closest - xPos))
+            if (Math.Abs(snapPoint.Value().X - xPos) < Math.Abs(closest - xPos))
             {
-                closest = snapPoint.Value();
+                closest = snapPoint.Value().X;
                 snapAxis = snapPoint.Key;
             }
         }
@@ -118,12 +118,12 @@ public class SnappingController
         }
 
         snapAxisKey = VerticalSnapPoints.First().Key;
-        double closest = VerticalSnapPoints.First().Value();
+        double closest = VerticalSnapPoints.First().Value().Y;
         foreach (var snapPoint in VerticalSnapPoints)
         {
-            if (Math.Abs(snapPoint.Value() - yPos) < Math.Abs(closest - yPos))
+            if (Math.Abs(snapPoint.Value().Y - yPos) < Math.Abs(closest - yPos))
             {
-                closest = snapPoint.Value();
+                closest = snapPoint.Value().Y;
                 snapAxisKey = snapPoint.Key;
             }
         }
@@ -139,20 +139,20 @@ public class SnappingController
 
     public void AddXYAxis(string identifier, VecD axisVector)
     {
-        HorizontalSnapPoints[identifier] = () => axisVector.X;
-        VerticalSnapPoints[identifier] = () => axisVector.Y;
+        HorizontalSnapPoints[identifier] = () => axisVector;
+        VerticalSnapPoints[identifier] = () => axisVector;
     }
 
     public void AddBounds(string identifier, Func<RectD> tightBounds)
     {
-        HorizontalSnapPoints[$"{identifier}.center"] = () => tightBounds().Center.X;
-        VerticalSnapPoints[$"{identifier}.center"] = () => tightBounds().Center.Y;
+        HorizontalSnapPoints[$"{identifier}.center"] = () => tightBounds().Center;
+        VerticalSnapPoints[$"{identifier}.center"] = () => tightBounds().Center;
 
-        HorizontalSnapPoints[$"{identifier}.left"] = () => tightBounds().Left;
-        VerticalSnapPoints[$"{identifier}.top"] = () => tightBounds().Top;
+        HorizontalSnapPoints[$"{identifier}.left"] = () => tightBounds().TopLeft;
+        VerticalSnapPoints[$"{identifier}.top"] = () => tightBounds().TopRight;
 
-        HorizontalSnapPoints[$"{identifier}.right"] = () => tightBounds().Right;
-        VerticalSnapPoints[$"{identifier}.bottom"] = () => tightBounds().Bottom;
+        HorizontalSnapPoints[$"{identifier}.right"] = () => tightBounds().BottomRight;
+        VerticalSnapPoints[$"{identifier}.bottom"] = () => tightBounds().BottomLeft;
     }
 
     /// <summary>
@@ -175,18 +175,20 @@ public class SnappingController
         }
     }
 
-    public VecD GetSnapDeltaForPoints(VecD[] points, out string xAxis, out string yAxis)
+    public VecD GetSnapDeltaForPoints(VecD[] points, out string xAxis, out string yAxis, out VecD? snapSource)
     {
         if (!SnappingEnabled)
         {
             xAxis = string.Empty;
             yAxis = string.Empty;
+            snapSource = null;
             return VecD.Zero;
         }
 
         bool hasXSnap = false;
         bool hasYSnap = false;
         VecD snapDelta = VecD.Zero;
+        snapSource = null;
 
         string snapAxisX = string.Empty;
         string snapAxisY = string.Empty;
@@ -198,6 +200,7 @@ public class SnappingController
 
             if (snapX is not null && !hasXSnap)
             {
+                snapSource = new VecD(point.X, point.Y);
                 snapDelta += new VecD(snapX.Value - point.X, 0);
                 snapAxisX = newSnapAxisX;
                 hasXSnap = true;
@@ -205,6 +208,7 @@ public class SnappingController
 
             if (snapY is not null && !hasYSnap)
             {
+                snapSource = new VecD(snapSource?.X ?? point.X, point.Y);
                 snapDelta += new VecD(0, snapY.Value - point.Y);
                 snapAxisY = newSnapAxisY;
                 hasYSnap = true;
@@ -286,7 +290,7 @@ public class SnappingController
 
         double? closestX = closestXAxis != string.Empty ? snapDelta.X : null;
         double? closestY = closestYAxis != string.Empty ? snapDelta.Y : null;
-        
+
         VecD? xIntersect = null;
         if (closestX != null)
         {
@@ -316,13 +320,13 @@ public class SnappingController
             if (Math.Abs(xIntersect.Value.X - yIntersect.Value.X) < float.Epsilon
                 && Math.Abs(xIntersect.Value.Y - yIntersect.Value.Y) < float.Epsilon)
             {
-                if(IsWithinSnapDistance(xIntersect.Value, pos))
+                if (IsWithinSnapDistance(xIntersect.Value, pos))
                 {
                     xAxis = closestXAxis;
                     yAxis = closestYAxis;
                     return xIntersect.Value;
                 }
-                
+
                 xAxis = string.Empty;
                 yAxis = string.Empty;
                 return pos;
@@ -344,7 +348,7 @@ public class SnappingController
                 yAxis = closestYAxis;
                 return yIntersect.Value;
             }
-            
+
             xAxis = string.Empty;
             yAxis = string.Empty;
             return pos;
@@ -373,10 +377,30 @@ public class SnappingController
 
     public void AddXYAxis(string identifier, Func<VecD> pointFunc)
     {
-        HorizontalSnapPoints[identifier] = () => pointFunc().X;
-        VerticalSnapPoints[identifier] = () => pointFunc().Y;
+        HorizontalSnapPoints[identifier] = pointFunc;
+        VerticalSnapPoints[identifier] = pointFunc;
+    }
+
+    public VecD? GetSnapAxisXPoint(string snapAxisX)
+    {
+        if (HorizontalSnapPoints.TryGetValue(snapAxisX, out Func<VecD> snapPoint))
+        {
+            return snapPoint();
+        }
+
+        return null;
+    }
+
+    public VecD? GetSnapAxisYPoint(string snapAxisY)
+    {
+        if (VerticalSnapPoints.TryGetValue(snapAxisY, out Func<VecD> snapPoint))
+        {
+            return snapPoint();
+        }
+
+        return null;
     }
-    
+
     private bool IsWithinSnapDistance(VecD snapPoint, VecD pos)
     {
         return (snapPoint - pos).LengthSquared < SnapDistance * SnapDistance;

+ 8 - 0
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -446,6 +446,14 @@ internal class DocumentUpdater
     {
         IStructureMemberHandler? memberVM = doc.StructureHelper.FindOrThrow(info.Id);
         memberVM.SetIsVisible(info.IsVisible);
+        if (info.IsVisible)
+        {
+            doc.SnappingHandler.AddFromBounds(memberVM.Id.ToString(), () => memberVM.TightBounds ?? RectD.Empty);
+        }
+        else
+        {
+            doc.SnappingHandler.Remove(memberVM.Id.ToString());
+        }
     }
 
     private void ProcessUpdateStructureMemberName(StructureMemberName_ChangeInfo info)

+ 14 - 8
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.Models.DocumentModels.Public;
@@ -99,16 +100,21 @@ internal class DocumentStructureModule
         return result;
     }
 
-    public (IStructureMemberHandler?, INodeHandler?) FindChildAndParent(Guid childGuid)
+    public List<IStructureMemberHandler> GetParents(Guid child)
     {
-        List<IStructureMemberHandler>? path = FindPath(childGuid);
-        return path.Count switch
+        var childNode = FindNode<IStructureMemberHandler>(child);
+        if (childNode == null)
+            return new List<IStructureMemberHandler>();
+
+        List<IStructureMemberHandler> parents = new List<IStructureMemberHandler>();
+        childNode.TraverseForwards((node, previous, output, input) =>
         {
-            0 => (null, null),
-            1 => (path[0], null),
-            >= 2 => (path[0], path[1]),
-            _ => (null, null),
-        };
+            if (node is IStructureMemberHandler parent && input is { PropertyName: FolderNode.ContentInternalName })
+                parents.Add(parent);
+            return true;
+        });
+
+        return parents;
     }
 
     public (IStructureMemberHandler, IFolderHandler) FindChildAndParentOrThrow(Guid childGuid)

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

@@ -358,7 +358,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             if (noMovement)
             {
                 internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
-                AddMemberToSnapping();
+                AddMembersToSnapping();
 
                 base.OnLeftMouseButtonUp(argsPositionOnCanvas);
                 onEnded?.Invoke(this);

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

@@ -170,7 +170,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         if (!startedDrawing)
         {
             internals.ActionAccumulator.AddFinishedActions(EndDraw());
-            AddMemberToSnapping();
+            AddMembersToSnapping();
             
             base.OnLeftMouseButtonUp(argsPositionOnCanvas);
             ActiveMode = ShapeToolMode.Preview;

+ 49 - 17
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Utils;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Tools;
@@ -26,7 +27,7 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 ///         - Transform -> Preview (when user applies the transform)
 ///         - Transform -> Drawing (when user clicks outside of shape transform bounds)
 /// </summary>
-internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor, 
+internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
     ITransformableExecutor, IMidChangeUndoableExecutor, IDelayedColorSwapFeature
 {
     private ShapeToolMode activeMode;
@@ -41,11 +42,14 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
             StartMode(activeMode);
         }
     }
+
     protected virtual bool AlignToPixels { get; } = true;
-    
+
     protected Guid memberId;
     protected VecD startDrawingPos;
 
+    private IDisposable restoreSnapping;
+
     public override bool BlocksOtherActions => ActiveMode == ShapeToolMode.Drawing;
 
     public override ExecutionState Start()
@@ -66,7 +70,7 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
             ActiveMode = ShapeToolMode.Preview;
         }
 
-        document.SnappingHandler.Remove(memberId.ToString()); // This disables self-snapping
+        restoreSnapping = DisableSelfSnapping(memberId, document);
 
         return ExecutionState.Success;
     }
@@ -144,17 +148,16 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
         ActiveMode = ShapeToolMode.Transform;
     }
 
-    public bool IsTransforming => ActiveMode == ShapeToolMode.Transform; 
+    public bool IsTransforming => ActiveMode == ShapeToolMode.Transform;
 
     public virtual void OnTransformChanged(ShapeCorners corners)
     {
-        
     }
 
     public virtual void OnTransformApplied()
     {
         ActiveMode = ShapeToolMode.Preview;
-        AddMemberToSnapping();
+        AddMembersToSnapping();
         HighlightSnapping(null, null);
     }
 
@@ -169,7 +172,7 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
     public override void ForceStop()
     {
         StopMode(activeMode);
-        AddMemberToSnapping();
+        AddMembersToSnapping();
         HighlightSnapping(null, null);
     }
 
@@ -180,15 +183,15 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
         document.SnappingHandler.SnappingController.HighlightedPoint = null;
     }
 
-    protected void AddMemberToSnapping()
+    protected void AddMembersToSnapping()
     {
-        var member = document.StructureHelper.Find(memberId);
-        document!.SnappingHandler.AddFromBounds(memberId.ToString(), () => member?.TightBounds ?? RectD.Empty);
+        restoreSnapping?.Dispose();
     }
-    
+
     protected VecD SnapAndHighlight(VecD pos)
     {
-        VecD snapped = document.SnappingHandler.SnappingController.GetSnapPoint(pos, out string snapX, out string snapY);
+        VecD snapped =
+            document.SnappingHandler.SnappingController.GetSnapPoint(pos, out string snapX, out string snapY);
         HighlightSnapping(snapX, snapY);
         return snapped;
     }
@@ -209,12 +212,41 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
             document.SnappingHandler.SnappingController.HighlightedPoint = null;
         }
     }
-    
+
+    public static IDisposable DisableSelfSnapping(Guid memberId, IDocument document)
+    {
+        List<Guid> disabledSnappingMembers = new();
+        disabledSnappingMembers.Add(memberId);
+        document.SnappingHandler.Remove(memberId.ToString());
+
+        Guid child = memberId;
+
+        var parents = document.StructureHelper.GetParents(child);
+
+        foreach (var parent in parents)
+        {
+            disabledSnappingMembers.Add(parent.Id);
+            document.SnappingHandler.Remove(parent.Id.ToString());
+        }
+
+        return Disposable.Create(() =>
+        {
+            foreach (var id in disabledSnappingMembers)
+            {
+                var member = document.StructureHelper.Find(id);
+                if (member != null && member.IsVisibleBindable)
+                {
+                    document.SnappingHandler.AddFromBounds(id.ToString(), () => member?.TightBounds ?? RectD.Empty);
+                }
+            }
+        });
+    }
+
     protected virtual void PrecisePositionChangeDrawingMode(VecD pos) { }
     protected virtual void PrecisePositionChangeTransformMode(VecD pos) { }
     public abstract void OnMidChangeUndo();
     public abstract void OnMidChangeRedo();
-    public abstract bool CanUndo { get; } 
+    public abstract bool CanUndo { get; }
     public abstract bool CanRedo { get; }
 
     public virtual bool IsFeatureEnabled(IExecutorFeature feature)
@@ -223,17 +255,17 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
         {
             return IsTransforming;
         }
-        
+
         if (feature is IMidChangeUndoableExecutor)
         {
             return ActiveMode == ShapeToolMode.Transform;
         }
-        
+
         if (feature is IDelayedColorSwapFeature)
         {
             return true;
         }
-        
+
         return false;
     }
 }

+ 29 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -35,6 +35,8 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
     private bool movedOnce;
     private bool duplicateOnStop = false;
 
+    private List<Guid> disabledSnappingMembers = new();
+
     public TransformSelectedExecutor(bool toolLinked)
     {
         Type = toolLinked ? ExecutorType.ToolLinked : ExecutorType.Regular;
@@ -117,6 +119,17 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         foreach (var structureMemberHandler in members)
         {
             document.SnappingHandler.Remove(structureMemberHandler.Id.ToString());
+            disabledSnappingMembers.Add(structureMemberHandler.Id);
+            var parents = document.StructureHelper.GetParents(structureMemberHandler.Id);
+
+            foreach (var parent in parents)
+            {
+                document.SnappingHandler.Remove(parent.Id.ToString());
+                if (!disabledSnappingMembers.Contains(parent.Id))
+                {
+                    disabledSnappingMembers.Add(parent.Id);
+                }
+            }
         }
 
         selectedMembers = members.Select(m => m.Id).ToList();
@@ -426,7 +439,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
-        AddSnappingForMembers(memberCorners.Keys.ToList());
+        RestoreSnapping();
         onEnded!.Invoke(this);
 
         if (Type == ExecutorType.ToolLinked)
@@ -449,7 +462,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
-        AddSnappingForMembers(memberCorners.Keys.ToList());
+        RestoreSnapping();
 
         isInProgress = false;
         document.TransformHandler.PassthroughPointerPressed -= OnLeftMouseButtonDown;
@@ -481,6 +494,20 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         }
     }
 
+    private void RestoreSnapping()
+    {
+        foreach (var id in disabledSnappingMembers)
+        {
+            var member = document!.StructureHelper.Find(id);
+            if (member is null)
+            {
+                continue;
+            }
+
+            document!.SnappingHandler.AddFromBounds(id.ToString(), () => member?.TightBounds ?? RectD.Empty);
+        }
+    }
+
     public bool IsFeatureEnabled(IExecutorFeature feature)
     {
         return feature is ITransformableExecutor && IsTransforming || feature is IMidChangeUndoableExecutor ||

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

@@ -34,6 +34,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     private IFillableShapeToolbar toolbar;
     private IColorsHandler colorHandler;
     private bool isValidPathLayer;
+    private IDisposable restoreSnapping;
 
     public override ExecutorType Type => ExecutorType.ToolLinked;
 
@@ -107,7 +108,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
             return ExecutionState.Error;
         }
 
-        document.SnappingHandler.Remove(member.Id.ToString()); // This disables self-snapping
+        restoreSnapping = SimpleShapeToolExecutor.DisableSelfSnapping(member.Id, document);
         return ExecutionState.Success;
     }
 
@@ -182,7 +183,9 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     public override void ForceStop()
     {
         document.PathOverlayHandler.Hide();
-        document.SnappingHandler.AddFromBounds(member.Id.ToString(), () => member.TightBounds ?? RectD.Empty);
+
+        restoreSnapping?.Dispose();
+
         HighlightSnapping(null, null);
         internals.ActionAccumulator.AddFinishedActions(new EndSetShapeGeometry_Action());
     }

+ 1 - 0
src/PixiEditor/Models/Handlers/INodeHandler.cs

@@ -30,4 +30,5 @@ public interface INodeHandler : INotifyPropertyChanged
     public void TraverseForwards(Func<INodeHandler, bool> func);
     public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func);
     public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func);
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func);
 }

+ 56 - 22
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -47,7 +47,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             }
         }
     }
-    
+
     public string Category { get; }
 
     public string NodeNameBindable
@@ -60,7 +60,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 Internals.ActionAccumulator.AddFinishedActions(
                     new SetNodeName_Action(Id, value));
             }
-        } 
+        }
     }
 
     public string InternalName { get; private set; }
@@ -71,11 +71,12 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         {
             if (categoryBrush == null)
             {
-                if (!string.IsNullOrWhiteSpace(Category) && Application.Current.Styles.TryGetResource($"{Stylize(Category)}CategoryBackgroundBrush", App.Current.ActualThemeVariant, out var brushObj) && brushObj is IBrush brush)
+                if (!string.IsNullOrWhiteSpace(Category) &&
+                    Application.Current.Styles.TryGetResource($"{Stylize(Category)}CategoryBackgroundBrush",
+                        App.Current.ActualThemeVariant, out var brushObj) && brushObj is IBrush brush)
                 {
                     categoryBrush = brush;
                 }
-
             }
 
             return categoryBrush;
@@ -83,7 +84,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             string Stylize(string input) => string.Concat(input[0].ToString().ToUpper(), input.ToLower().AsSpan(1));
         }
     }
-    
+
     public NodeMetadata? Metadata { get; set; }
 
     public VecD PositionBindable
@@ -117,7 +118,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         get => resultPainter;
         set => SetProperty(ref resultPainter, value);
     }
-    
+
     public bool IsNodeSelected
     {
         get => isSelected;
@@ -134,18 +135,19 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         Document = document;
         Internals = internals;
     }
-    
+
     public virtual void OnInitialized() { }
-    
+
     public NodeViewModel()
     {
         var attribute = GetType().GetCustomAttribute<NodeViewModelAttribute>();
-        
+
         displayName = attribute.DisplayName;
         Category = attribute.Category;
     }
 
-    public NodeViewModel(string nodeNameBindable, Guid id, VecD position, DocumentViewModel document, DocumentInternalParts internals)
+    public NodeViewModel(string nodeNameBindable, Guid id, VecD position, DocumentViewModel document,
+        DocumentInternalParts internals)
     {
         this.nodeNameBindable = nodeNameBindable;
         this.id = id;
@@ -153,13 +155,13 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         Document = document;
         Internals = internals;
     }
-    
+
     public void SetPosition(VecD newPosition)
     {
         position = newPosition;
         OnPropertyChanged(nameof(PositionBindable));
     }
-    
+
     public void SetName(string newName)
     {
         nodeNameBindable = new LocalizedString(newName);
@@ -212,7 +214,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
                 continue;
             }
-            
+
             if (!func(node.Item1, node.Item2))
             {
                 return;
@@ -223,7 +225,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 if (inputProperty.ConnectedOutput != null)
                 {
                     queueNodes.Enqueue((inputProperty.ConnectedOutput.Node, node.Item1));
-                } 
+                }
             }
         }
     }
@@ -242,7 +244,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
                 continue;
             }
-            
+
             if (!func(node.Item1, node.Item2, node.Item3))
             {
                 return;
@@ -253,7 +255,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 if (inputProperty.ConnectedOutput != null)
                 {
                     queueNodes.Enqueue((inputProperty.ConnectedOutput.Node, node.Item1, inputProperty));
-                } 
+                }
             }
         }
     }
@@ -287,7 +289,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             }
         }
     }
-    
+
     public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func)
     {
         var visited = new HashSet<INodeHandler>();
@@ -302,7 +304,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
                 continue;
             }
-            
+
             if (!func(node.Item1, node.Item2))
             {
                 return;
@@ -317,7 +319,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             }
         }
     }
-    
+
     public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func)
     {
         var visited = new HashSet<INodeHandler>();
@@ -332,7 +334,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
                 continue;
             }
-            
+
             if (!func(node.Item1, node.Item2, node.Item3))
             {
                 return;
@@ -348,11 +350,41 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func)
+    {
+        var visited = new HashSet<INodeHandler>();
+        var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler)>();
+        queueNodes.Enqueue((this, null, null, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add(node.Item1))
+            {
+                continue;
+            }
+
+            if (!func(node.Item1, node.Item2, node.Item3, node.Item4))
+            {
+                return;
+            }
+
+            foreach (var outputProperty in node.Item1.Outputs)
+            {
+                foreach (var connection in outputProperty.ConnectedInputs)
+                {
+                    queueNodes.Enqueue((connection.Node, node.Item1, outputProperty, connection));
+                }
+            }
+        }
+    }
+
     public NodePropertyViewModel FindInputProperty(string propName)
     {
         return Inputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel;
     }
-    
+
     public NodePropertyViewModel<T> FindInputProperty<T>(string propName)
     {
         return Inputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel<T>;
@@ -369,4 +401,6 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
     }
 }
 
-internal abstract class NodeViewModel<T> : NodeViewModel where T : Node { }
+internal abstract class NodeViewModel<T> : NodeViewModel where T : Node
+{
+}

+ 1 - 1
src/PixiEditor/Views/Overlays/Handles/AnchorHandle.cs

@@ -14,7 +14,7 @@ public class AnchorHandle : RectangleHandle
     private Paint selectedPaint;
     
     public bool IsSelected { get; set; } = false;
-    
+
     public AnchorHandle(Overlay owner) : base(owner)
     {
         Size = new VecD(GetResource<double>("AnchorHandleSize"));

+ 1 - 0
src/PixiEditor/Views/Overlays/Handles/Handle.cs

@@ -22,6 +22,7 @@ public delegate void HandleEvent(Handle source, OverlayPointerArgs args);
 
 public abstract class Handle : IHandle
 {
+    public string Name { get; set; }
     public Paint? FillPaint { get; set; } = GetPaint("HandleBackgroundBrush");
     public Paint? StrokePaint { get; set; } = GetPaint("HandleBrush", PaintStyle.Stroke);
     public double ZoomScale { get; set; } = 1.0;

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

@@ -330,7 +330,7 @@ internal class LineToolOverlay : Overlay
         VecD[] pointsToTest = new VecD[] { center + delta, originalStart + delta, originalEnd + delta, };
 
         VecD snapDelta =
-            SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX, out string snapAxisY);
+            SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX, out string snapAxisY, out _);
 
         return ((snapAxisX, snapAxisY), snapDelta);
     }

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

@@ -55,6 +55,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     public event KeyEvent? KeyReleasedOverlay;
 
     public Handle? CapturedHandle { get; set; } = null!;
+    public VecD PointerPosition { get; internal set; }
 
     private readonly Dictionary<AvaloniaProperty, OverlayTransition> activeTransitions = new();
 

+ 4 - 4
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -506,7 +506,7 @@ public class VectorPathOverlay : Overlay
         }
         else if (args.Modifiers == KeyModifiers.None)
         {
-            args.Handled = AddNewPointFromClick(args.Point);
+            args.Handled = AddNewPointFromClick(SnappingController.GetSnapPoint(args.Point, out _, out _));
             AddToUndoCommand.Execute(Path);
         }
     }
@@ -733,7 +733,7 @@ public class VectorPathOverlay : Overlay
     {
         var snappedPoint = SnappingController.GetSnapPoint(point, out string axisX, out string axisY);
         var snapped = new VecD((float)snappedPoint.X, (float)snappedPoint.Y);
-        TryHighlightSnap(axisX, axisY);
+        TryHighlightSnap(axisX, axisY, snapped);
         return snapped;
     }
 
@@ -756,11 +756,11 @@ public class VectorPathOverlay : Overlay
         Refresh();
     }
 
-    private void TryHighlightSnap(string axisX, string axisY)
+    private void TryHighlightSnap(string axisX, string axisY, VecD? point = null)
     {
         SnappingController.HighlightedXAxis = axisX;
         SnappingController.HighlightedYAxis = axisY;
-        SnappingController.HighlightedPoint = null;
+        SnappingController.HighlightedPoint = point;
     }
 
     private AnchorHandle? GetHandleAt(int index)

+ 36 - 4
src/PixiEditor/Views/Overlays/SnappingOverlay.cs

@@ -1,10 +1,13 @@
-using Avalonia;
+using System.Globalization;
+using Avalonia;
 using Avalonia.Media;
 using Avalonia.Styling;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Text;
 using PixiEditor.Models.Controllers.InputDevice;
 using Drawie.Numerics;
+using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Helpers;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
 using Point = Avalonia.Point;
@@ -26,6 +29,9 @@ internal class SnappingOverlay : Overlay
     private Paint verticalAxisPen; 
     private Paint previewPointPen;
 
+    private Paint distanceTextPaint;
+    private Font distanceFont = Font.CreateDefault();
+
     private const float startSize = 1;
     
     static SnappingOverlay()
@@ -39,7 +45,9 @@ internal class SnappingOverlay : Overlay
         /*TODO: Theme variant is not present, that's why Dark is hardcoded*/        
         horizontalAxisPen = ResourceLoader.GetPaint("HorizontalSnapAxisBrush", PaintStyle.Stroke, ThemeVariant.Dark) ?? new Paint() { Color = Colors.Red, Style = PaintStyle.Stroke, IsAntiAliased = true, StrokeWidth = startSize};
         verticalAxisPen = ResourceLoader.GetPaint("VerticalSnapAxisBrush", PaintStyle.Stroke, ThemeVariant.Dark) ?? new Paint() { Color = Colors.Green, Style = PaintStyle.Stroke, IsAntiAliased = true, StrokeWidth = startSize}; 
-        previewPointPen = ResourceLoader.GetPaint("SnapPointPreviewBrush", PaintStyle.Fill, ThemeVariant.Dark) ?? new Paint() { Color = Colors.Blue, Style = PaintStyle.Stroke, IsAntiAliased = true, StrokeWidth = startSize}; 
+        previewPointPen = ResourceLoader.GetPaint("SnapPointPreviewBrush", PaintStyle.Fill, ThemeVariant.Dark) ?? new Paint() { Color = Colors.Blue, Style = PaintStyle.Stroke, IsAntiAliased = true, StrokeWidth = startSize};
+        distanceTextPaint = new Paint() { Color = Colors.White, Style = PaintStyle.Fill, IsAntiAliased = true, StrokeWidth = startSize };
+
         IsHitTestVisible = false;
     }
 
@@ -50,13 +58,18 @@ internal class SnappingOverlay : Overlay
             return;
         }
 
+        VecD mousePoint = SnappingController.HighlightedPoint ?? PointerPosition;
+
         if (!string.IsNullOrEmpty(SnappingController.HighlightedXAxis))
         {
             foreach (var snapPoint in SnappingController.HorizontalSnapPoints)
             {
                 if (snapPoint.Key == SnappingController.HighlightedXAxis)
                 {
-                    context.DrawLine(new VecD(snapPoint.Value(), 0), new VecD(snapPoint.Value(), canvasBounds.Height), horizontalAxisPen);
+                    VecD snapPointValue = snapPoint.Value();
+                    context.DrawLine(new VecD(snapPointValue.X, mousePoint.Y), new VecD(snapPointValue.X, snapPointValue.Y), horizontalAxisPen);
+
+                    DrawDistanceText(context, snapPointValue + new VecD(10 / ZoomScale, 0), mousePoint);
                 }
             }
         }
@@ -67,7 +80,10 @@ internal class SnappingOverlay : Overlay
             {
                 if (snapPoint.Key == SnappingController.HighlightedYAxis)
                 {
-                    context.DrawLine(new VecD(0, snapPoint.Value()), new VecD(canvasBounds.Width, snapPoint.Value()), verticalAxisPen);
+                    var snapPointValue = snapPoint.Value();
+                    context.DrawLine(new VecD(mousePoint.X, snapPointValue.Y), new VecD(snapPointValue.X, snapPointValue.Y), verticalAxisPen);
+
+                    DrawDistanceText(context, snapPointValue + new VecD(0, -10 / ZoomScale), mousePoint);
                 }
             }
         }
@@ -78,6 +94,22 @@ internal class SnappingOverlay : Overlay
         }
     }
 
+    private void DrawDistanceText(Canvas context, VecD snapPointValue, VecD mousePoint)
+    {
+        VecD distance = snapPointValue - mousePoint;
+        VecD center = (snapPointValue + mousePoint) / 2;
+        distanceFont.Size = 12 / (float)ZoomScale;
+
+        distanceTextPaint.Color = Colors.Black;
+        distanceTextPaint.Style = PaintStyle.Stroke;
+        distanceTextPaint.StrokeWidth = 2f / (float)ZoomScale;
+
+        context.DrawText($"{distance.Length.ToString("F2", CultureInfo.CurrentCulture)} px", center, distanceFont, distanceTextPaint);
+        distanceTextPaint.Color = Colors.White;
+        distanceTextPaint.Style = PaintStyle.Fill;
+        context.DrawText($"{distance.Length.ToString("F2", CultureInfo.CurrentCulture)} px", center, distanceFont, distanceTextPaint);
+    }
+
     protected override void ZoomChanged(double newZoom)
     {
         horizontalAxisPen.StrokeWidth = startSize / (float)newZoom;

+ 31 - 4
src/PixiEditor/Views/Overlays/TransformOverlay/TransformHelper.cs

@@ -8,6 +8,7 @@ using Drawie.Numerics;
 using Point = Avalonia.Point;
 
 namespace PixiEditor.Views.Overlays.TransformOverlay;
+
 internal static class TransformHelper
 {
     public static RectD ToHandleRect(VecD pos, VecD size, double zoomboxScale)
@@ -77,6 +78,7 @@ internal static class TransformHelper
     {
         return Math.Round(angle * 8 / (Math.PI * 2)) * (Math.PI * 2) / 8;
     }
+
     public static double FindSnappingAngle(ShapeCorners corners, double desiredAngle)
     {
         var desTop = (corners.TopLeft - corners.TopRight).Rotate(desiredAngle).Angle;
@@ -106,7 +108,7 @@ internal static class TransformHelper
             GetAnchorPosition(corners, Anchor.Bottom),
             GetAnchorPosition(corners, Anchor.Left),
             GetAnchorPosition(corners, Anchor.Right)
-            );
+        );
         return maybeOrigin ?? corners.TopLeft.Lerp(corners.BottomRight, 0.5);
     }
 
@@ -238,7 +240,8 @@ internal static class TransformHelper
         };
     }
 
-    public static Anchor? GetAnchorInPosition(VecD pos, ShapeCorners corners, VecD origin, double zoomboxScale, VecD size)
+    public static Anchor? GetAnchorInPosition(VecD pos, ShapeCorners corners, VecD origin, double zoomboxScale,
+        VecD size)
     {
         VecD topLeft = corners.TopLeft;
         VecD topRight = corners.TopRight;
@@ -281,8 +284,10 @@ internal static class TransformHelper
     public static VecD GetHandlePos(ShapeCorners corners, double zoomboxScale, VecD size)
     {
         VecD max = new(
-            Math.Max(Math.Max(corners.TopLeft.X, corners.TopRight.X), Math.Max(corners.BottomLeft.X, corners.BottomRight.X)),
-            Math.Max(Math.Max(corners.TopLeft.Y, corners.TopRight.Y), Math.Max(corners.BottomLeft.Y, corners.BottomRight.Y)));
+            Math.Max(Math.Max(corners.TopLeft.X, corners.TopRight.X),
+                Math.Max(corners.BottomLeft.X, corners.BottomRight.X)),
+            Math.Max(Math.Max(corners.TopLeft.Y, corners.TopRight.Y),
+                Math.Max(corners.BottomLeft.Y, corners.BottomRight.Y)));
         return max + new VecD(size.X / zoomboxScale, size.Y / zoomboxScale);
     }
 
@@ -324,4 +329,26 @@ internal static class TransformHelper
         double[] cardinals = { 0, Math.PI / 2, Math.PI, 3 * Math.PI / 2, 2 * Math.PI };
         return cardinals.Any(cardinal => Math.Abs(normalized - cardinal) < threshold);
     }
+
+    public static VecD? GetClosestAnchorToPoint(VecD point, ShapeCorners corners)
+    {
+        var distances = new Dictionary<Anchor, double>
+        {
+            { Anchor.TopLeft, (point - corners.TopLeft).Length },
+            { Anchor.TopRight, (point - corners.TopRight).Length },
+            { Anchor.BottomLeft, (point - corners.BottomLeft).Length },
+            { Anchor.BottomRight, (point - corners.BottomRight).Length },
+            { Anchor.Left, (point - corners.LeftCenter).Length },
+            { Anchor.Right, (point - corners.RightCenter).Length },
+            { Anchor.Top, (point - corners.TopCenter).Length },
+            { Anchor.Bottom, (point - corners.BottomCenter).Length },
+        };
+
+        var ordered = distances.OrderBy(pair => pair.Value).ToList();
+        if (!ordered.Any())
+            return null;
+
+        var anchor = ordered.First().Key;
+        return GetAnchorPosition(corners, anchor);
+    }
 }

+ 57 - 34
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -162,8 +162,9 @@ internal class TransformOverlay : Overlay
         set => SetValue(ScaleFromCenterProperty, value);
     }
 
-    public static readonly StyledProperty<bool> CanAlignToPixelsProperty = AvaloniaProperty.Register<TransformOverlay, bool>(
-        nameof(CanAlignToPixels), defaultValue: true);
+    public static readonly StyledProperty<bool> CanAlignToPixelsProperty =
+        AvaloniaProperty.Register<TransformOverlay, bool>(
+            nameof(CanAlignToPixels), defaultValue: true);
 
     public bool CanAlignToPixels
     {
@@ -180,15 +181,16 @@ internal class TransformOverlay : Overlay
         set => SetValue(LockShearProperty, value);
     }
 
-    public static readonly StyledProperty<ICommand> TransformDraggedCommandProperty = AvaloniaProperty.Register<TransformOverlay, ICommand>(
-        nameof(TransformDraggedCommand));
+    public static readonly StyledProperty<ICommand> TransformDraggedCommandProperty =
+        AvaloniaProperty.Register<TransformOverlay, ICommand>(
+            nameof(TransformDraggedCommand));
 
     public ICommand TransformDraggedCommand
     {
         get => GetValue(TransformDraggedCommandProperty);
         set => SetValue(TransformDraggedCommandProperty, value);
     }
-    
+
     static TransformOverlay()
     {
         AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty,
@@ -310,6 +312,7 @@ internal class TransformOverlay : Overlay
         moveHandle.StrokePaint = handlePen;
         centerHandle = new RectangleHandle(this);
         centerHandle.Size = rightHandle.Size;
+        centerHandle.HitTestVisible = false;
 
         originHandle = new(this) { StrokePaint = blackFreqDashedPen, SecondaryHandlePen = whiteFreqDashedPen, };
 
@@ -414,6 +417,7 @@ internal class TransformOverlay : Overlay
         if (ShowHandles)
         {
             centerHandle.Position = VecD.Zero;
+            centerHandle.HitTestVisible = capturedAnchor == Anchor.Origin;
             topLeftHandle.Position = topLeft;
             topRightHandle.Position = topRight;
             bottomLeftHandle.Position = bottomLeft;
@@ -457,25 +461,27 @@ internal class TransformOverlay : Overlay
             context.DrawPath(rotateCursorGeometry, whiteFillPen);
             context.DrawPath(rotateCursorGeometry, cursorBorderPaint);
         }
-        
+
         context.RestoreToCount(saved);
-        
+
         saved = context.Save();
 
         if (ShowHandles && shearCursorActive)
         {
             var matrix = Matrix3X3.CreateTranslation((float)lastPointerPos.X, (float)lastPointerPos.Y);
-            
+
             matrix = matrix.PostConcat(Matrix3X3.CreateTranslation(
                 (float)-shearCursorGeometry.VisualAABB.Center.X,
                 (float)-shearCursorGeometry.VisualAABB.Center.Y));
-            
+
             matrix = matrix.PostConcat(Matrix3X3.CreateScale(
-                20 / zoomboxScale / (float)shearCursorGeometry.VisualAABB.Size.X, 20 / zoomboxScale / (float)shearCursorGeometry.VisualAABB.Size.Y,
+                20 / zoomboxScale / (float)shearCursorGeometry.VisualAABB.Size.X,
+                20 / zoomboxScale / (float)shearCursorGeometry.VisualAABB.Size.Y,
                 (float)lastPointerPos.X, (float)lastPointerPos.Y));
 
-            if(hoveredAnchor is Anchor.Right or Anchor.Left)
-                matrix = matrix.PostConcat(Matrix3X3.CreateRotationDegrees(90, (float)lastPointerPos.X, (float)lastPointerPos.Y));
+            if (hoveredAnchor is Anchor.Right or Anchor.Left)
+                matrix = matrix.PostConcat(Matrix3X3.CreateRotationDegrees(90, (float)lastPointerPos.X,
+                    (float)lastPointerPos.Y));
 
             context.SetMatrix(context.TotalMatrix.Concat(matrix));
 
@@ -551,7 +557,7 @@ internal class TransformOverlay : Overlay
     {
         if (args.PointerButton != MouseButton.Left)
             return;
-        
+
         lastClickCount = args.ClickCount;
 
         if (Handles.Any(x => x.IsWithinHandle(x.Position, args.Point, ZoomScale))) return;
@@ -716,12 +722,12 @@ internal class TransformOverlay : Overlay
 
     private bool CanShear(VecD mousePos, out Anchor side)
     {
-        if(LockShear)
+        if (LockShear)
         {
             side = default;
             return false;
         }
-        
+
         double distance = 20 / ZoomScale;
         var sides = new[] { Anchor.Top, Anchor.Bottom, Anchor.Left, Anchor.Right };
 
@@ -745,8 +751,7 @@ internal class TransformOverlay : Overlay
         if (ActionCompleted is not null && ActionCompleted.CanExecute(null))
             ActionCompleted.Execute(null);
 
-        SnappingController.HighlightedXAxis = string.Empty;
-        SnappingController.HighlightedYAxis = string.Empty;
+        HighlightSnappedAxis(null, null);
         IsSizeBoxEnabled = false;
     }
 
@@ -787,15 +792,14 @@ internal class TransformOverlay : Overlay
 
         VecD snapDelta = snapDeltaResult.Delta;
 
-        SnappingController.HighlightedXAxis = snapDeltaResult.SnapAxisXName;
-        SnappingController.HighlightedYAxis = snapDeltaResult.SnapAxisYName;
+        HighlightSnappedAxis(snapDeltaResult.SnapAxisXName, snapDeltaResult.SnapAxisYName, snapDeltaResult.SnapSource);
 
         VecD from = originOnStartMove;
-        
+
         Corners = ApplyCornersWithDelta(cornersOnStartMove, delta, snapDelta);
 
         InternalState = InternalState with { Origin = originOnStartMove + delta + snapDelta };
-        
+
         VecD to = InternalState.Origin;
         TransformDraggedCommand?.Execute((from, to));
     }
@@ -821,13 +825,20 @@ internal class TransformOverlay : Overlay
         VecD[] pointsToTest = new VecD[]
         {
             rawCorners.RectCenter, rawCorners.TopLeft, rawCorners.TopRight, rawCorners.BottomLeft,
-            rawCorners.BottomRight
+            rawCorners.BottomRight, rawCorners.TopCenter, rawCorners.BottomCenter, rawCorners.LeftCenter,
+            rawCorners.RightCenter
         };
 
         VecD snapDelta = SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX,
-            out string snapAxisY);
+            out string snapAxisY, out VecD? snapSource);
 
-        return new SnapData() { Delta = snapDelta, SnapAxisXName = snapAxisX, SnapAxisYName = snapAxisY };
+        return new SnapData()
+        {
+            Delta = snapDelta,
+            SnapSource = snapSource + snapDelta,
+            SnapAxisXName = snapAxisX,
+            SnapAxisYName = snapAxisY
+        };
     }
 
     private Cursor HandleRotate(VecD pos)
@@ -891,9 +902,9 @@ internal class TransformOverlay : Overlay
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos,
                 ScaleFromCenter,
                 SnappingController,
-                out string snapX, out string snapY);
+                out string snapX, out string snapY, out VecD? snapPoint);
 
-            HighlightSnappedAxis(snapX, snapY);
+            HighlightSnappedAxis(snapX, snapY, snapPoint);
 
             if (newCorners is not null)
             {
@@ -977,8 +988,13 @@ internal class TransformOverlay : Overlay
 
             string finalSnapX = snapped.SnapAxisXName ?? snapX;
             string finalSnapY = snapped.SnapAxisYName ?? snapY;
+            VecD? finalSnapPoint = null;
+            if (newCorners.HasValue && snapped.Delta != VecD.Zero)
+            {
+                finalSnapPoint = TransformHelper.GetAnchorPosition(newCorners.Value, (Anchor)capturedAnchor);
+            }
 
-            HighlightSnappedAxis(finalSnapX, finalSnapY);
+            HighlightSnappedAxis(finalSnapX, finalSnapY, finalSnapPoint);
 
             if (newCorners is not null)
             {
@@ -1112,7 +1128,7 @@ internal class TransformOverlay : Overlay
         VecD[] pointsToTest = new VecD[] { anchor };
 
         VecD snapDelta = SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX,
-            out string snapAxisY);
+            out string snapAxisY, out VecD? snapSource);
 
         // snap delta is a straight line from the anchor to the snapped point, we need to find intersection between snap point axis and line starting from projectLineStart going through transformed
         VecD snapPoint = anchor + snapDelta;
@@ -1129,7 +1145,10 @@ internal class TransformOverlay : Overlay
             snapDelta = VecD.Zero;
         }
 
-        return new SnapData() { Delta = snapDelta, SnapAxisXName = snapAxisX, SnapAxisYName = snapAxisY };
+        return new SnapData()
+        {
+            Delta = snapDelta, SnapSource = snapSource, SnapAxisXName = snapAxisX, SnapAxisYName = snapAxisY
+        };
     }
 
     private VecD FindHorizontalIntersection(VecD p1, VecD p2, double y)
@@ -1170,15 +1189,19 @@ internal class TransformOverlay : Overlay
         VecD[] pointsToTest = new VecD[] { anchorPos };
 
         VecD snapDelta = SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX,
-            out string snapAxisY);
+            out string snapAxisY, out VecD? snapSource);
 
-        return new SnapData() { Delta = snapDelta, SnapAxisXName = snapAxisX, SnapAxisYName = snapAxisY };
+        return new SnapData()
+        {
+            Delta = snapDelta, SnapSource = snapSource, SnapAxisXName = snapAxisX, SnapAxisYName = snapAxisY
+        };
     }
 
-    private void HighlightSnappedAxis(string snapAxisXName, string snapAxisYName)
+    private void HighlightSnappedAxis(string snapAxisXName, string snapAxisYName, VecD? snapSource = null)
     {
         SnappingController.HighlightedXAxis = snapAxisXName;
         SnappingController.HighlightedYAxis = snapAxisYName;
+        SnappingController.HighlightedPoint = snapSource;
     }
 
     private void UpdateOriginPos()
@@ -1230,8 +1253,7 @@ internal class TransformOverlay : Overlay
 
         IsSizeBoxEnabled = false;
 
-        SnappingController.HighlightedXAxis = string.Empty;
-        SnappingController.HighlightedYAxis = string.Empty;
+        HighlightSnappedAxis(null, null);
     }
 
     private Handle? GetSnapHandleOfOrigin()
@@ -1285,4 +1307,5 @@ struct SnapData
     public VecD Delta { get; set; }
     public string SnapAxisXName { get; set; }
     public string SnapAxisYName { get; set; }
+    public VecD? SnapSource { get; set; }
 }

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

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

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

@@ -140,6 +140,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     private PixelSize lastSize = PixelSize.Empty;
     private Cursor lastCursor;
+    private VecD lastMousePosition;
 
     public static readonly StyledProperty<string> RenderOutputProperty =
         AvaloniaProperty.Register<Scene, string>("RenderOutput");
@@ -299,6 +300,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
                     continue;
                 }
 
+                overlay.PointerPosition = lastMousePosition;
                 overlay.ZoomScale = Scale;
 
                 if (!overlay.CanRender()) continue;
@@ -331,6 +333,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         if (AllOverlays != null)
         {
             OverlayPointerArgs args = ConstructPointerArgs(e);
+            lastMousePosition = args.Point;
 
             Cursor finalCursor = DefaultCursor;