Browse Source

Merge pull request #860 from PixiEditor/fixes/24.03

Fixes
Krzysztof Krysiński 4 months ago
parent
commit
f3c18861bf
23 changed files with 414 additions and 158 deletions
  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();
         float scaleMult = (float)targetChunk.Resolution.Multiplier();
         VecD trans = -chunkPos * ChunkPool.FullChunkSize;
         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);
         var finalMatrix = Matrix3X3.Concat(scaleTrans, transformMatrix);
 
 
         using var snapshot = toPaint.DrawingSurface.Snapshot();
         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.Save();
         targetChunk.Surface.DrawingSurface.Canvas.SetMatrix(finalMatrix);
         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();
         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 float correctionFactor;
     private readonly int strokeWidth;
     private readonly int strokeWidth;
     private readonly List<VecI> positions = new();
     private readonly List<VecI> positions = new();
-    private bool ignoreUpdate = false;
     private readonly bool repeat;
     private readonly bool repeat;
     private int frame;
     private int frame;
+    private int lastAppliedPointIndex = -1;
 
 
     private List<VecI> ellipseLines;
     private List<VecI> ellipseLines;
-    
+
     private CommittedChunkStorage? savedChunks;
     private CommittedChunkStorage? savedChunks;
 
 
     [GenerateUpdateableChangeActions]
     [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.layerGuid = layerGuid;
         this.correctionFactor = correctionFactor;
         this.correctionFactor = correctionFactor;
         this.strokeWidth = strokeWidth;
         this.strokeWidth = strokeWidth;
         this.repeat = repeat;
         this.repeat = repeat;
         this.frame = frame;
         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]
     [UpdateChangeMethod]
     public void Update(VecI pos)
     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)
     public override bool InitializeAndValidate(Document target)
     {
     {
         if (!DrawingChangeHelper.IsValidForDrawing(target, layerGuid, false))
         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)
     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);
         ImageLayerNode node = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
 
 
         var layerImage = node.GetLayerImageAtFrame(frame);
         var layerImage = node.GetLayerImageAtFrame(frame);
         int queueLength = layerImage.QueueLength;
         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);
         var affected = layerImage.FindAffectedArea(queueLength);
-        
+
+        lastAppliedPointIndex = positions.Count - 1;
+
         return new LayerImageArea_ChangeInfo(layerGuid, affected);
         return new LayerImageArea_ChangeInfo(layerGuid, affected);
     }
     }
-    
+
     private static void ChangeBrightness(
     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
         // 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 left = circleLines[i];
             VecI right = circleLines[i + 1];
             VecI right = circleLines[i + 1];
             int y = left.Y;
             int y = left.Y;
-            
+
             for (VecI pos = new VecI(left.X, y); pos.X <= right.X; pos.X++)
             for (VecI pos = new VecI(left.X, y); pos.X <= right.X; pos.X++)
             {
             {
                 layerImage.EnqueueDrawPixel(
                 layerImage.EnqueueDrawPixel(
                     pos + offset,
                     pos + offset,
                     (commitedColor, upToDateColor) =>
                     (commitedColor, upToDateColor) =>
                     {
                     {
-                        Color newColor = ColorHelper.ChangeColorBrightness(repeat ? upToDateColor : commitedColor, correctionFactor);
+                        Color newColor = ColorHelper.ChangeColorBrightness(repeat ? upToDateColor : commitedColor,
+                            correctionFactor);
                         return ColorHelper.ChangeColorBrightness(newColor, correctionFactor);
                         return ColorHelper.ChangeColorBrightness(newColor, correctionFactor);
                     },
                     },
                     BlendMode.Src);
                     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);
         var layer = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
         ignoreInUndo = false;
         ignoreInUndo = false;
 
 
         if (savedChunks is not null)
         if (savedChunks is not null)
             throw new InvalidOperationException("Trying to apply while there are saved chunks");
             throw new InvalidOperationException("Trying to apply while there are saved chunks");
-        
+
         var layerImage = layer.GetLayerImageAtFrame(frame);
         var layerImage = layer.GetLayerImageAtFrame(frame);
-        
+
         if (!firstApply)
         if (!firstApply)
         {
         {
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, layerImage, layerGuid, false);
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, layerImage, layerGuid, false);
             foreach (VecI pos in positions)
             foreach (VecI pos in positions)
             {
             {
-                ChangeBrightness(ellipseLines, strokeWidth, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat, layerImage);
+                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)
     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);
         return new LayerImageArea_ChangeInfo(layerGuid, affected);
     }
     }
 }
 }

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

@@ -16,8 +16,8 @@ public class SnappingController
     /// </summary>
     /// </summary>
     public double SnapDistance { get; set; } = DefaultSnapDistance;
     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
     public string HighlightedXAxis
     {
     {
@@ -84,12 +84,12 @@ public class SnappingController
         }
         }
 
 
         snapAxis = HorizontalSnapPoints.First().Key;
         snapAxis = HorizontalSnapPoints.First().Key;
-        double closest = HorizontalSnapPoints.First().Value();
+        double closest = HorizontalSnapPoints.First().Value().X;
         foreach (var snapPoint in HorizontalSnapPoints)
         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;
                 snapAxis = snapPoint.Key;
             }
             }
         }
         }
@@ -118,12 +118,12 @@ public class SnappingController
         }
         }
 
 
         snapAxisKey = VerticalSnapPoints.First().Key;
         snapAxisKey = VerticalSnapPoints.First().Key;
-        double closest = VerticalSnapPoints.First().Value();
+        double closest = VerticalSnapPoints.First().Value().Y;
         foreach (var snapPoint in VerticalSnapPoints)
         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;
                 snapAxisKey = snapPoint.Key;
             }
             }
         }
         }
@@ -139,20 +139,20 @@ public class SnappingController
 
 
     public void AddXYAxis(string identifier, VecD axisVector)
     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)
     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>
     /// <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)
         if (!SnappingEnabled)
         {
         {
             xAxis = string.Empty;
             xAxis = string.Empty;
             yAxis = string.Empty;
             yAxis = string.Empty;
+            snapSource = null;
             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;
+        snapSource = null;
 
 
         string snapAxisX = string.Empty;
         string snapAxisX = string.Empty;
         string snapAxisY = string.Empty;
         string snapAxisY = string.Empty;
@@ -198,6 +200,7 @@ public class SnappingController
 
 
             if (snapX is not null && !hasXSnap)
             if (snapX is not null && !hasXSnap)
             {
             {
+                snapSource = new VecD(point.X, point.Y);
                 snapDelta += new VecD(snapX.Value - point.X, 0);
                 snapDelta += new VecD(snapX.Value - point.X, 0);
                 snapAxisX = newSnapAxisX;
                 snapAxisX = newSnapAxisX;
                 hasXSnap = true;
                 hasXSnap = true;
@@ -205,6 +208,7 @@ public class SnappingController
 
 
             if (snapY is not null && !hasYSnap)
             if (snapY is not null && !hasYSnap)
             {
             {
+                snapSource = new VecD(snapSource?.X ?? point.X, point.Y);
                 snapDelta += new VecD(0, snapY.Value - point.Y);
                 snapDelta += new VecD(0, snapY.Value - point.Y);
                 snapAxisY = newSnapAxisY;
                 snapAxisY = newSnapAxisY;
                 hasYSnap = true;
                 hasYSnap = true;
@@ -286,7 +290,7 @@ public class SnappingController
 
 
         double? closestX = closestXAxis != string.Empty ? snapDelta.X : null;
         double? closestX = closestXAxis != string.Empty ? snapDelta.X : null;
         double? closestY = closestYAxis != string.Empty ? snapDelta.Y : null;
         double? closestY = closestYAxis != string.Empty ? snapDelta.Y : null;
-        
+
         VecD? xIntersect = null;
         VecD? xIntersect = null;
         if (closestX != null)
         if (closestX != null)
         {
         {
@@ -316,13 +320,13 @@ public class SnappingController
             if (Math.Abs(xIntersect.Value.X - yIntersect.Value.X) < float.Epsilon
             if (Math.Abs(xIntersect.Value.X - yIntersect.Value.X) < float.Epsilon
                 && Math.Abs(xIntersect.Value.Y - yIntersect.Value.Y) < float.Epsilon)
                 && Math.Abs(xIntersect.Value.Y - yIntersect.Value.Y) < float.Epsilon)
             {
             {
-                if(IsWithinSnapDistance(xIntersect.Value, pos))
+                if (IsWithinSnapDistance(xIntersect.Value, pos))
                 {
                 {
                     xAxis = closestXAxis;
                     xAxis = closestXAxis;
                     yAxis = closestYAxis;
                     yAxis = closestYAxis;
                     return xIntersect.Value;
                     return xIntersect.Value;
                 }
                 }
-                
+
                 xAxis = string.Empty;
                 xAxis = string.Empty;
                 yAxis = string.Empty;
                 yAxis = string.Empty;
                 return pos;
                 return pos;
@@ -344,7 +348,7 @@ public class SnappingController
                 yAxis = closestYAxis;
                 yAxis = closestYAxis;
                 return yIntersect.Value;
                 return yIntersect.Value;
             }
             }
-            
+
             xAxis = string.Empty;
             xAxis = string.Empty;
             yAxis = string.Empty;
             yAxis = string.Empty;
             return pos;
             return pos;
@@ -373,10 +377,30 @@ public class SnappingController
 
 
     public void AddXYAxis(string identifier, Func<VecD> pointFunc)
     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)
     private bool IsWithinSnapDistance(VecD snapPoint, VecD pos)
     {
     {
         return (snapPoint - pos).LengthSquared < SnapDistance * SnapDistance;
         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);
         IStructureMemberHandler? memberVM = doc.StructureHelper.FindOrThrow(info.Id);
         memberVM.SetIsVisible(info.IsVisible);
         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)
     private void ProcessUpdateStructureMemberName(StructureMemberName_ChangeInfo info)

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

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 
 
 namespace PixiEditor.Models.DocumentModels.Public;
 namespace PixiEditor.Models.DocumentModels.Public;
@@ -99,16 +100,21 @@ internal class DocumentStructureModule
         return result;
         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)
     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)
             if (noMovement)
             {
             {
                 internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
                 internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
-                AddMemberToSnapping();
+                AddMembersToSnapping();
 
 
                 base.OnLeftMouseButtonUp(argsPositionOnCanvas);
                 base.OnLeftMouseButtonUp(argsPositionOnCanvas);
                 onEnded?.Invoke(this);
                 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)
         if (!startedDrawing)
         {
         {
             internals.ActionAccumulator.AddFinishedActions(EndDraw());
             internals.ActionAccumulator.AddFinishedActions(EndDraw());
-            AddMemberToSnapping();
+            AddMembersToSnapping();
             
             
             base.OnLeftMouseButtonUp(argsPositionOnCanvas);
             base.OnLeftMouseButtonUp(argsPositionOnCanvas);
             ActiveMode = ShapeToolMode.Preview;
             ActiveMode = ShapeToolMode.Preview;

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

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Utils;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
@@ -26,7 +27,7 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 ///         - Transform -> Preview (when user applies the transform)
 ///         - Transform -> Preview (when user applies the transform)
 ///         - Transform -> Drawing (when user clicks outside of shape transform bounds)
 ///         - Transform -> Drawing (when user clicks outside of shape transform bounds)
 /// </summary>
 /// </summary>
-internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor, 
+internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
     ITransformableExecutor, IMidChangeUndoableExecutor, IDelayedColorSwapFeature
     ITransformableExecutor, IMidChangeUndoableExecutor, IDelayedColorSwapFeature
 {
 {
     private ShapeToolMode activeMode;
     private ShapeToolMode activeMode;
@@ -41,11 +42,14 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
             StartMode(activeMode);
             StartMode(activeMode);
         }
         }
     }
     }
+
     protected virtual bool AlignToPixels { get; } = true;
     protected virtual bool AlignToPixels { get; } = true;
-    
+
     protected Guid memberId;
     protected Guid memberId;
     protected VecD startDrawingPos;
     protected VecD startDrawingPos;
 
 
+    private IDisposable restoreSnapping;
+
     public override bool BlocksOtherActions => ActiveMode == ShapeToolMode.Drawing;
     public override bool BlocksOtherActions => ActiveMode == ShapeToolMode.Drawing;
 
 
     public override ExecutionState Start()
     public override ExecutionState Start()
@@ -66,7 +70,7 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
             ActiveMode = ShapeToolMode.Preview;
             ActiveMode = ShapeToolMode.Preview;
         }
         }
 
 
-        document.SnappingHandler.Remove(memberId.ToString()); // This disables self-snapping
+        restoreSnapping = DisableSelfSnapping(memberId, document);
 
 
         return ExecutionState.Success;
         return ExecutionState.Success;
     }
     }
@@ -144,17 +148,16 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
         ActiveMode = ShapeToolMode.Transform;
         ActiveMode = ShapeToolMode.Transform;
     }
     }
 
 
-    public bool IsTransforming => ActiveMode == ShapeToolMode.Transform; 
+    public bool IsTransforming => ActiveMode == ShapeToolMode.Transform;
 
 
     public virtual void OnTransformChanged(ShapeCorners corners)
     public virtual void OnTransformChanged(ShapeCorners corners)
     {
     {
-        
     }
     }
 
 
     public virtual void OnTransformApplied()
     public virtual void OnTransformApplied()
     {
     {
         ActiveMode = ShapeToolMode.Preview;
         ActiveMode = ShapeToolMode.Preview;
-        AddMemberToSnapping();
+        AddMembersToSnapping();
         HighlightSnapping(null, null);
         HighlightSnapping(null, null);
     }
     }
 
 
@@ -169,7 +172,7 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
     public override void ForceStop()
     public override void ForceStop()
     {
     {
         StopMode(activeMode);
         StopMode(activeMode);
-        AddMemberToSnapping();
+        AddMembersToSnapping();
         HighlightSnapping(null, null);
         HighlightSnapping(null, null);
     }
     }
 
 
@@ -180,15 +183,15 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
         document.SnappingHandler.SnappingController.HighlightedPoint = null;
         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)
     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);
         HighlightSnapping(snapX, snapY);
         return snapped;
         return snapped;
     }
     }
@@ -209,12 +212,41 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
             document.SnappingHandler.SnappingController.HighlightedPoint = null;
             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 PrecisePositionChangeDrawingMode(VecD pos) { }
     protected virtual void PrecisePositionChangeTransformMode(VecD pos) { }
     protected virtual void PrecisePositionChangeTransformMode(VecD pos) { }
     public abstract void OnMidChangeUndo();
     public abstract void OnMidChangeUndo();
     public abstract void OnMidChangeRedo();
     public abstract void OnMidChangeRedo();
-    public abstract bool CanUndo { get; } 
+    public abstract bool CanUndo { get; }
     public abstract bool CanRedo { get; }
     public abstract bool CanRedo { get; }
 
 
     public virtual bool IsFeatureEnabled(IExecutorFeature feature)
     public virtual bool IsFeatureEnabled(IExecutorFeature feature)
@@ -223,17 +255,17 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
         {
         {
             return IsTransforming;
             return IsTransforming;
         }
         }
-        
+
         if (feature is IMidChangeUndoableExecutor)
         if (feature is IMidChangeUndoableExecutor)
         {
         {
             return ActiveMode == ShapeToolMode.Transform;
             return ActiveMode == ShapeToolMode.Transform;
         }
         }
-        
+
         if (feature is IDelayedColorSwapFeature)
         if (feature is IDelayedColorSwapFeature)
         {
         {
             return true;
             return true;
         }
         }
-        
+
         return false;
         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 movedOnce;
     private bool duplicateOnStop = false;
     private bool duplicateOnStop = false;
 
 
+    private List<Guid> disabledSnappingMembers = new();
+
     public TransformSelectedExecutor(bool toolLinked)
     public TransformSelectedExecutor(bool toolLinked)
     {
     {
         Type = toolLinked ? ExecutorType.ToolLinked : ExecutorType.Regular;
         Type = toolLinked ? ExecutorType.ToolLinked : ExecutorType.Regular;
@@ -117,6 +119,17 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         foreach (var structureMemberHandler in members)
         foreach (var structureMemberHandler in members)
         {
         {
             document.SnappingHandler.Remove(structureMemberHandler.Id.ToString());
             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();
         selectedMembers = members.Select(m => m.Id).ToList();
@@ -426,7 +439,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
         document!.TransformHandler.HideTransform();
-        AddSnappingForMembers(memberCorners.Keys.ToList());
+        RestoreSnapping();
         onEnded!.Invoke(this);
         onEnded!.Invoke(this);
 
 
         if (Type == ExecutorType.ToolLinked)
         if (Type == ExecutorType.ToolLinked)
@@ -449,7 +462,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
         document!.TransformHandler.HideTransform();
-        AddSnappingForMembers(memberCorners.Keys.ToList());
+        RestoreSnapping();
 
 
         isInProgress = false;
         isInProgress = false;
         document.TransformHandler.PassthroughPointerPressed -= OnLeftMouseButtonDown;
         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)
     public bool IsFeatureEnabled(IExecutorFeature feature)
     {
     {
         return feature is ITransformableExecutor && IsTransforming || feature is IMidChangeUndoableExecutor ||
         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 IFillableShapeToolbar toolbar;
     private IColorsHandler colorHandler;
     private IColorsHandler colorHandler;
     private bool isValidPathLayer;
     private bool isValidPathLayer;
+    private IDisposable restoreSnapping;
 
 
     public override ExecutorType Type => ExecutorType.ToolLinked;
     public override ExecutorType Type => ExecutorType.ToolLinked;
 
 
@@ -107,7 +108,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
             return ExecutionState.Error;
             return ExecutionState.Error;
         }
         }
 
 
-        document.SnappingHandler.Remove(member.Id.ToString()); // This disables self-snapping
+        restoreSnapping = SimpleShapeToolExecutor.DisableSelfSnapping(member.Id, document);
         return ExecutionState.Success;
         return ExecutionState.Success;
     }
     }
 
 
@@ -182,7 +183,9 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     public override void ForceStop()
     public override void ForceStop()
     {
     {
         document.PathOverlayHandler.Hide();
         document.PathOverlayHandler.Hide();
-        document.SnappingHandler.AddFromBounds(member.Id.ToString(), () => member.TightBounds ?? RectD.Empty);
+
+        restoreSnapping?.Dispose();
+
         HighlightSnapping(null, null);
         HighlightSnapping(null, null);
         internals.ActionAccumulator.AddFinishedActions(new EndSetShapeGeometry_Action());
         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, bool> func);
     public void TraverseForwards(Func<INodeHandler, 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, 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 Category { get; }
 
 
     public string NodeNameBindable
     public string NodeNameBindable
@@ -60,7 +60,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 Internals.ActionAccumulator.AddFinishedActions(
                 Internals.ActionAccumulator.AddFinishedActions(
                     new SetNodeName_Action(Id, value));
                     new SetNodeName_Action(Id, value));
             }
             }
-        } 
+        }
     }
     }
 
 
     public string InternalName { get; private set; }
     public string InternalName { get; private set; }
@@ -71,11 +71,12 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         {
         {
             if (categoryBrush == null)
             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;
                     categoryBrush = brush;
                 }
                 }
-
             }
             }
 
 
             return categoryBrush;
             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));
             string Stylize(string input) => string.Concat(input[0].ToString().ToUpper(), input.ToLower().AsSpan(1));
         }
         }
     }
     }
-    
+
     public NodeMetadata? Metadata { get; set; }
     public NodeMetadata? Metadata { get; set; }
 
 
     public VecD PositionBindable
     public VecD PositionBindable
@@ -117,7 +118,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         get => resultPainter;
         get => resultPainter;
         set => SetProperty(ref resultPainter, value);
         set => SetProperty(ref resultPainter, value);
     }
     }
-    
+
     public bool IsNodeSelected
     public bool IsNodeSelected
     {
     {
         get => isSelected;
         get => isSelected;
@@ -134,18 +135,19 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         Document = document;
         Document = document;
         Internals = internals;
         Internals = internals;
     }
     }
-    
+
     public virtual void OnInitialized() { }
     public virtual void OnInitialized() { }
-    
+
     public NodeViewModel()
     public NodeViewModel()
     {
     {
         var attribute = GetType().GetCustomAttribute<NodeViewModelAttribute>();
         var attribute = GetType().GetCustomAttribute<NodeViewModelAttribute>();
-        
+
         displayName = attribute.DisplayName;
         displayName = attribute.DisplayName;
         Category = attribute.Category;
         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.nodeNameBindable = nodeNameBindable;
         this.id = id;
         this.id = id;
@@ -153,13 +155,13 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         Document = document;
         Document = document;
         Internals = internals;
         Internals = internals;
     }
     }
-    
+
     public void SetPosition(VecD newPosition)
     public void SetPosition(VecD newPosition)
     {
     {
         position = newPosition;
         position = newPosition;
         OnPropertyChanged(nameof(PositionBindable));
         OnPropertyChanged(nameof(PositionBindable));
     }
     }
-    
+
     public void SetName(string newName)
     public void SetName(string newName)
     {
     {
         nodeNameBindable = new LocalizedString(newName);
         nodeNameBindable = new LocalizedString(newName);
@@ -212,7 +214,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
             {
                 continue;
                 continue;
             }
             }
-            
+
             if (!func(node.Item1, node.Item2))
             if (!func(node.Item1, node.Item2))
             {
             {
                 return;
                 return;
@@ -223,7 +225,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 if (inputProperty.ConnectedOutput != null)
                 if (inputProperty.ConnectedOutput != null)
                 {
                 {
                     queueNodes.Enqueue((inputProperty.ConnectedOutput.Node, node.Item1));
                     queueNodes.Enqueue((inputProperty.ConnectedOutput.Node, node.Item1));
-                } 
+                }
             }
             }
         }
         }
     }
     }
@@ -242,7 +244,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
             {
                 continue;
                 continue;
             }
             }
-            
+
             if (!func(node.Item1, node.Item2, node.Item3))
             if (!func(node.Item1, node.Item2, node.Item3))
             {
             {
                 return;
                 return;
@@ -253,7 +255,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 if (inputProperty.ConnectedOutput != null)
                 if (inputProperty.ConnectedOutput != null)
                 {
                 {
                     queueNodes.Enqueue((inputProperty.ConnectedOutput.Node, node.Item1, inputProperty));
                     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)
     public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
@@ -302,7 +304,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
             {
                 continue;
                 continue;
             }
             }
-            
+
             if (!func(node.Item1, node.Item2))
             if (!func(node.Item1, node.Item2))
             {
             {
                 return;
                 return;
@@ -317,7 +319,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             }
             }
         }
         }
     }
     }
-    
+
     public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func)
     public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
@@ -332,7 +334,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
             {
                 continue;
                 continue;
             }
             }
-            
+
             if (!func(node.Item1, node.Item2, node.Item3))
             if (!func(node.Item1, node.Item2, node.Item3))
             {
             {
                 return;
                 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)
     public NodePropertyViewModel FindInputProperty(string propName)
     {
     {
         return Inputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel;
         return Inputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel;
     }
     }
-    
+
     public NodePropertyViewModel<T> FindInputProperty<T>(string propName)
     public NodePropertyViewModel<T> FindInputProperty<T>(string propName)
     {
     {
         return Inputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel<T>;
         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;
     private Paint selectedPaint;
     
     
     public bool IsSelected { get; set; } = false;
     public bool IsSelected { get; set; } = false;
-    
+
     public AnchorHandle(Overlay owner) : base(owner)
     public AnchorHandle(Overlay owner) : base(owner)
     {
     {
         Size = new VecD(GetResource<double>("AnchorHandleSize"));
         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 abstract class Handle : IHandle
 {
 {
+    public string Name { get; set; }
     public Paint? FillPaint { get; set; } = GetPaint("HandleBackgroundBrush");
     public Paint? FillPaint { get; set; } = GetPaint("HandleBackgroundBrush");
     public Paint? StrokePaint { get; set; } = GetPaint("HandleBrush", PaintStyle.Stroke);
     public Paint? StrokePaint { get; set; } = GetPaint("HandleBrush", PaintStyle.Stroke);
     public double ZoomScale { get; set; } = 1.0;
     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[] pointsToTest = new VecD[] { center + delta, originalStart + delta, originalEnd + delta, };
 
 
         VecD snapDelta =
         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);
         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 event KeyEvent? KeyReleasedOverlay;
 
 
     public Handle? CapturedHandle { get; set; } = null!;
     public Handle? CapturedHandle { get; set; } = null!;
+    public VecD PointerPosition { get; internal set; }
 
 
     private readonly Dictionary<AvaloniaProperty, OverlayTransition> activeTransitions = new();
     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)
         else if (args.Modifiers == KeyModifiers.None)
         {
         {
-            args.Handled = AddNewPointFromClick(args.Point);
+            args.Handled = AddNewPointFromClick(SnappingController.GetSnapPoint(args.Point, out _, out _));
             AddToUndoCommand.Execute(Path);
             AddToUndoCommand.Execute(Path);
         }
         }
     }
     }
@@ -733,7 +733,7 @@ public class VectorPathOverlay : Overlay
     {
     {
         var snappedPoint = SnappingController.GetSnapPoint(point, out string axisX, out string axisY);
         var snappedPoint = SnappingController.GetSnapPoint(point, out string axisX, out string axisY);
         var snapped = new VecD((float)snappedPoint.X, (float)snappedPoint.Y);
         var snapped = new VecD((float)snappedPoint.X, (float)snappedPoint.Y);
-        TryHighlightSnap(axisX, axisY);
+        TryHighlightSnap(axisX, axisY, snapped);
         return snapped;
         return snapped;
     }
     }
 
 
@@ -756,11 +756,11 @@ public class VectorPathOverlay : Overlay
         Refresh();
         Refresh();
     }
     }
 
 
-    private void TryHighlightSnap(string axisX, string axisY)
+    private void TryHighlightSnap(string axisX, string axisY, VecD? point = null)
     {
     {
         SnappingController.HighlightedXAxis = axisX;
         SnappingController.HighlightedXAxis = axisX;
         SnappingController.HighlightedYAxis = axisY;
         SnappingController.HighlightedYAxis = axisY;
-        SnappingController.HighlightedPoint = null;
+        SnappingController.HighlightedPoint = point;
     }
     }
 
 
     private AnchorHandle? GetHandleAt(int index)
     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.Media;
 using Avalonia.Styling;
 using Avalonia.Styling;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Text;
 using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.Controllers.InputDevice;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
 using Point = Avalonia.Point;
 using Point = Avalonia.Point;
@@ -26,6 +29,9 @@ internal class SnappingOverlay : Overlay
     private Paint verticalAxisPen; 
     private Paint verticalAxisPen; 
     private Paint previewPointPen;
     private Paint previewPointPen;
 
 
+    private Paint distanceTextPaint;
+    private Font distanceFont = Font.CreateDefault();
+
     private const float startSize = 1;
     private const float startSize = 1;
     
     
     static SnappingOverlay()
     static SnappingOverlay()
@@ -39,7 +45,9 @@ internal class SnappingOverlay : Overlay
         /*TODO: Theme variant is not present, that's why Dark is hardcoded*/        
         /*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};
         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}; 
         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;
         IsHitTestVisible = false;
     }
     }
 
 
@@ -50,13 +58,18 @@ internal class SnappingOverlay : Overlay
             return;
             return;
         }
         }
 
 
+        VecD mousePoint = SnappingController.HighlightedPoint ?? PointerPosition;
+
         if (!string.IsNullOrEmpty(SnappingController.HighlightedXAxis))
         if (!string.IsNullOrEmpty(SnappingController.HighlightedXAxis))
         {
         {
             foreach (var snapPoint in SnappingController.HorizontalSnapPoints)
             foreach (var snapPoint in SnappingController.HorizontalSnapPoints)
             {
             {
                 if (snapPoint.Key == SnappingController.HighlightedXAxis)
                 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)
                 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)
     protected override void ZoomChanged(double newZoom)
     {
     {
         horizontalAxisPen.StrokeWidth = startSize / (float)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;
 using Point = Avalonia.Point;
 
 
 namespace PixiEditor.Views.Overlays.TransformOverlay;
 namespace PixiEditor.Views.Overlays.TransformOverlay;
+
 internal static class TransformHelper
 internal static class TransformHelper
 {
 {
     public static RectD ToHandleRect(VecD pos, VecD size, double zoomboxScale)
     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;
         return Math.Round(angle * 8 / (Math.PI * 2)) * (Math.PI * 2) / 8;
     }
     }
+
     public static double FindSnappingAngle(ShapeCorners corners, double desiredAngle)
     public static double FindSnappingAngle(ShapeCorners corners, double desiredAngle)
     {
     {
         var desTop = (corners.TopLeft - corners.TopRight).Rotate(desiredAngle).Angle;
         var desTop = (corners.TopLeft - corners.TopRight).Rotate(desiredAngle).Angle;
@@ -106,7 +108,7 @@ internal static class TransformHelper
             GetAnchorPosition(corners, Anchor.Bottom),
             GetAnchorPosition(corners, Anchor.Bottom),
             GetAnchorPosition(corners, Anchor.Left),
             GetAnchorPosition(corners, Anchor.Left),
             GetAnchorPosition(corners, Anchor.Right)
             GetAnchorPosition(corners, Anchor.Right)
-            );
+        );
         return maybeOrigin ?? corners.TopLeft.Lerp(corners.BottomRight, 0.5);
         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 topLeft = corners.TopLeft;
         VecD topRight = corners.TopRight;
         VecD topRight = corners.TopRight;
@@ -281,8 +284,10 @@ internal static class TransformHelper
     public static VecD GetHandlePos(ShapeCorners corners, double zoomboxScale, VecD size)
     public static VecD GetHandlePos(ShapeCorners corners, double zoomboxScale, VecD size)
     {
     {
         VecD max = new(
         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);
         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 };
         double[] cardinals = { 0, Math.PI / 2, Math.PI, 3 * Math.PI / 2, 2 * Math.PI };
         return cardinals.Any(cardinal => Math.Abs(normalized - cardinal) < threshold);
         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);
         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
     public bool CanAlignToPixels
     {
     {
@@ -180,15 +181,16 @@ internal class TransformOverlay : Overlay
         set => SetValue(LockShearProperty, value);
         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
     public ICommand TransformDraggedCommand
     {
     {
         get => GetValue(TransformDraggedCommandProperty);
         get => GetValue(TransformDraggedCommandProperty);
         set => SetValue(TransformDraggedCommandProperty, value);
         set => SetValue(TransformDraggedCommandProperty, value);
     }
     }
-    
+
     static TransformOverlay()
     static TransformOverlay()
     {
     {
         AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty,
         AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty,
@@ -310,6 +312,7 @@ internal class TransformOverlay : Overlay
         moveHandle.StrokePaint = handlePen;
         moveHandle.StrokePaint = handlePen;
         centerHandle = new RectangleHandle(this);
         centerHandle = new RectangleHandle(this);
         centerHandle.Size = rightHandle.Size;
         centerHandle.Size = rightHandle.Size;
+        centerHandle.HitTestVisible = false;
 
 
         originHandle = new(this) { StrokePaint = blackFreqDashedPen, SecondaryHandlePen = whiteFreqDashedPen, };
         originHandle = new(this) { StrokePaint = blackFreqDashedPen, SecondaryHandlePen = whiteFreqDashedPen, };
 
 
@@ -414,6 +417,7 @@ internal class TransformOverlay : Overlay
         if (ShowHandles)
         if (ShowHandles)
         {
         {
             centerHandle.Position = VecD.Zero;
             centerHandle.Position = VecD.Zero;
+            centerHandle.HitTestVisible = capturedAnchor == Anchor.Origin;
             topLeftHandle.Position = topLeft;
             topLeftHandle.Position = topLeft;
             topRightHandle.Position = topRight;
             topRightHandle.Position = topRight;
             bottomLeftHandle.Position = bottomLeft;
             bottomLeftHandle.Position = bottomLeft;
@@ -457,25 +461,27 @@ internal class TransformOverlay : Overlay
             context.DrawPath(rotateCursorGeometry, whiteFillPen);
             context.DrawPath(rotateCursorGeometry, whiteFillPen);
             context.DrawPath(rotateCursorGeometry, cursorBorderPaint);
             context.DrawPath(rotateCursorGeometry, cursorBorderPaint);
         }
         }
-        
+
         context.RestoreToCount(saved);
         context.RestoreToCount(saved);
-        
+
         saved = context.Save();
         saved = context.Save();
 
 
         if (ShowHandles && shearCursorActive)
         if (ShowHandles && shearCursorActive)
         {
         {
             var matrix = Matrix3X3.CreateTranslation((float)lastPointerPos.X, (float)lastPointerPos.Y);
             var matrix = Matrix3X3.CreateTranslation((float)lastPointerPos.X, (float)lastPointerPos.Y);
-            
+
             matrix = matrix.PostConcat(Matrix3X3.CreateTranslation(
             matrix = matrix.PostConcat(Matrix3X3.CreateTranslation(
                 (float)-shearCursorGeometry.VisualAABB.Center.X,
                 (float)-shearCursorGeometry.VisualAABB.Center.X,
                 (float)-shearCursorGeometry.VisualAABB.Center.Y));
                 (float)-shearCursorGeometry.VisualAABB.Center.Y));
-            
+
             matrix = matrix.PostConcat(Matrix3X3.CreateScale(
             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));
                 (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));
             context.SetMatrix(context.TotalMatrix.Concat(matrix));
 
 
@@ -551,7 +557,7 @@ internal class TransformOverlay : Overlay
     {
     {
         if (args.PointerButton != MouseButton.Left)
         if (args.PointerButton != MouseButton.Left)
             return;
             return;
-        
+
         lastClickCount = args.ClickCount;
         lastClickCount = args.ClickCount;
 
 
         if (Handles.Any(x => x.IsWithinHandle(x.Position, args.Point, ZoomScale))) return;
         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)
     private bool CanShear(VecD mousePos, out Anchor side)
     {
     {
-        if(LockShear)
+        if (LockShear)
         {
         {
             side = default;
             side = default;
             return false;
             return false;
         }
         }
-        
+
         double distance = 20 / ZoomScale;
         double distance = 20 / ZoomScale;
         var sides = new[] { Anchor.Top, Anchor.Bottom, Anchor.Left, Anchor.Right };
         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))
         if (ActionCompleted is not null && ActionCompleted.CanExecute(null))
             ActionCompleted.Execute(null);
             ActionCompleted.Execute(null);
 
 
-        SnappingController.HighlightedXAxis = string.Empty;
-        SnappingController.HighlightedYAxis = string.Empty;
+        HighlightSnappedAxis(null, null);
         IsSizeBoxEnabled = false;
         IsSizeBoxEnabled = false;
     }
     }
 
 
@@ -787,15 +792,14 @@ internal class TransformOverlay : Overlay
 
 
         VecD snapDelta = snapDeltaResult.Delta;
         VecD snapDelta = snapDeltaResult.Delta;
 
 
-        SnappingController.HighlightedXAxis = snapDeltaResult.SnapAxisXName;
-        SnappingController.HighlightedYAxis = snapDeltaResult.SnapAxisYName;
+        HighlightSnappedAxis(snapDeltaResult.SnapAxisXName, snapDeltaResult.SnapAxisYName, snapDeltaResult.SnapSource);
 
 
         VecD from = originOnStartMove;
         VecD from = originOnStartMove;
-        
+
         Corners = ApplyCornersWithDelta(cornersOnStartMove, delta, snapDelta);
         Corners = ApplyCornersWithDelta(cornersOnStartMove, delta, snapDelta);
 
 
         InternalState = InternalState with { Origin = originOnStartMove + delta + snapDelta };
         InternalState = InternalState with { Origin = originOnStartMove + delta + snapDelta };
-        
+
         VecD to = InternalState.Origin;
         VecD to = InternalState.Origin;
         TransformDraggedCommand?.Execute((from, to));
         TransformDraggedCommand?.Execute((from, to));
     }
     }
@@ -821,13 +825,20 @@ internal class TransformOverlay : Overlay
         VecD[] pointsToTest = new VecD[]
         VecD[] pointsToTest = new VecD[]
         {
         {
             rawCorners.RectCenter, rawCorners.TopLeft, rawCorners.TopRight, rawCorners.BottomLeft,
             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,
         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)
     private Cursor HandleRotate(VecD pos)
@@ -891,9 +902,9 @@ internal class TransformOverlay : Overlay
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos,
                 InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos,
                 ScaleFromCenter,
                 ScaleFromCenter,
                 SnappingController,
                 SnappingController,
-                out string snapX, out string snapY);
+                out string snapX, out string snapY, out VecD? snapPoint);
 
 
-            HighlightSnappedAxis(snapX, snapY);
+            HighlightSnappedAxis(snapX, snapY, snapPoint);
 
 
             if (newCorners is not null)
             if (newCorners is not null)
             {
             {
@@ -977,8 +988,13 @@ internal class TransformOverlay : Overlay
 
 
             string finalSnapX = snapped.SnapAxisXName ?? snapX;
             string finalSnapX = snapped.SnapAxisXName ?? snapX;
             string finalSnapY = snapped.SnapAxisYName ?? snapY;
             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)
             if (newCorners is not null)
             {
             {
@@ -1112,7 +1128,7 @@ internal class TransformOverlay : Overlay
         VecD[] pointsToTest = new VecD[] { anchor };
         VecD[] pointsToTest = new VecD[] { anchor };
 
 
         VecD snapDelta = SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX,
         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
         // 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;
         VecD snapPoint = anchor + snapDelta;
@@ -1129,7 +1145,10 @@ internal class TransformOverlay : Overlay
             snapDelta = VecD.Zero;
             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)
     private VecD FindHorizontalIntersection(VecD p1, VecD p2, double y)
@@ -1170,15 +1189,19 @@ internal class TransformOverlay : Overlay
         VecD[] pointsToTest = new VecD[] { anchorPos };
         VecD[] pointsToTest = new VecD[] { anchorPos };
 
 
         VecD snapDelta = SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX,
         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.HighlightedXAxis = snapAxisXName;
         SnappingController.HighlightedYAxis = snapAxisYName;
         SnappingController.HighlightedYAxis = snapAxisYName;
+        SnappingController.HighlightedPoint = snapSource;
     }
     }
 
 
     private void UpdateOriginPos()
     private void UpdateOriginPos()
@@ -1230,8 +1253,7 @@ internal class TransformOverlay : Overlay
 
 
         IsSizeBoxEnabled = false;
         IsSizeBoxEnabled = false;
 
 
-        SnappingController.HighlightedXAxis = string.Empty;
-        SnappingController.HighlightedYAxis = string.Empty;
+        HighlightSnappedAxis(null, null);
     }
     }
 
 
     private Handle? GetSnapHandleOfOrigin()
     private Handle? GetSnapHandleOfOrigin()
@@ -1285,4 +1307,5 @@ struct SnapData
     public VecD Delta { get; set; }
     public VecD Delta { get; set; }
     public string SnapAxisXName { get; set; }
     public string SnapAxisXName { get; set; }
     public string SnapAxisYName { 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
     public static ShapeCorners? UpdateShapeFromCorner
     (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
     (Anchor targetCorner, TransformCornerFreedom freedom, double propAngle1, double propAngle2, ShapeCorners corners,
         VecD desiredPos, bool scaleFromCenter,
         VecD desiredPos, bool scaleFromCenter,
-        SnappingController? snappingController, out string snapX, out string snapY)
+        SnappingController? snappingController, out string snapX, out string snapY, out VecD? snapPoint)
     {
     {
         if (!TransformHelper.IsCorner(targetCorner))
         if (!TransformHelper.IsCorner(targetCorner))
             throw new ArgumentException($"{targetCorner} is not a corner");
             throw new ArgumentException($"{targetCorner} is not a corner");
@@ -21,6 +21,7 @@ internal static class TransformUpdateHelper
         if (freedom == TransformCornerFreedom.Locked)
         if (freedom == TransformCornerFreedom.Locked)
         {
         {
             snapX = snapY = "";
             snapX = snapY = "";
+            snapPoint = null;
             return corners;
             return corners;
         }
         }
 
 
@@ -32,6 +33,7 @@ internal static class TransformUpdateHelper
             VecD oppositePos = TransformHelper.GetAnchorPosition(corners, opposite);
             VecD oppositePos = TransformHelper.GetAnchorPosition(corners, opposite);
 
 
             snapX = snapY = "";
             snapX = snapY = "";
+            snapPoint = null;
 
 
             // constrain desired pos to a "propotional" diagonal line if needed
             // constrain desired pos to a "propotional" diagonal line if needed
             if (freedom == TransformCornerFreedom.ScaleProportionally && corners.IsRect)
             if (freedom == TransformCornerFreedom.ScaleProportionally && corners.IsRect)
@@ -43,6 +45,7 @@ internal static class TransformUpdateHelper
                 if (snappingController is not null)
                 if (snappingController is not null)
                 {
                 {
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
+                    snapPoint = string.IsNullOrEmpty(snapX) && string.IsNullOrEmpty(snapY) ? null : desiredPos;
                 }
                 }
             }
             }
             else if (freedom == TransformCornerFreedom.ScaleProportionally)
             else if (freedom == TransformCornerFreedom.ScaleProportionally)
@@ -53,6 +56,7 @@ internal static class TransformUpdateHelper
                 if (snappingController is not null)
                 if (snappingController is not null)
                 {
                 {
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
                     desiredPos = snappingController.GetSnapPoint(desiredPos, direction, out snapX, out snapY);
+                    snapPoint = string.IsNullOrEmpty(snapX) && string.IsNullOrEmpty(snapY) ? null : desiredPos;
                 }
                 }
             }
             }
             else
             else
@@ -60,6 +64,7 @@ internal static class TransformUpdateHelper
                 if (snappingController is not null)
                 if (snappingController is not null)
                 {
                 {
                     desiredPos = snappingController.GetSnapPoint(desiredPos, out snapX, out snapY);
                     desiredPos = snappingController.GetSnapPoint(desiredPos, out snapX, out snapY);
+                    snapPoint = string.IsNullOrEmpty(snapX) && string.IsNullOrEmpty(snapY) ? null : desiredPos;
                 }
                 }
             }
             }
 
 
@@ -138,6 +143,7 @@ internal static class TransformUpdateHelper
         if (freedom == TransformCornerFreedom.Free)
         if (freedom == TransformCornerFreedom.Free)
         {
         {
             snapX = snapY = "";
             snapX = snapY = "";
+            snapPoint = null;
             ShapeCorners newCorners = TransformHelper.UpdateCorner(corners, targetCorner, desiredPos);
             ShapeCorners newCorners = TransformHelper.UpdateCorner(corners, targetCorner, desiredPos);
             return newCorners.IsLegal ? newCorners : null;
             return newCorners.IsLegal ? newCorners : null;
         }
         }

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

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