Browse Source

Layers snapping

flabbet 11 months ago
parent
commit
ddd5b03621

+ 82 - 10
src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs

@@ -11,8 +11,8 @@ public class SnappingController
     /// </summary>
     /// </summary>
     public double SnapDistance { get; set; } = DefaultSnapDistance;
     public double SnapDistance { get; set; } = DefaultSnapDistance;
 
 
-    public Dictionary<string, double> HorizontalSnapPoints { get; } = new();
-    public Dictionary<string, double> VerticalSnapPoints { get; } = new();
+    public Dictionary<string, Func<double>> HorizontalSnapPoints { get; } = new();
+    public Dictionary<string, Func<double>> VerticalSnapPoints { get; } = new();
     
     
     public string HighlightedXAxis { get; set; } = string.Empty;
     public string HighlightedXAxis { get; set; } = string.Empty;
     public string HighlightedYAxis { get; set; } = string.Empty;
     public string HighlightedYAxis { get; set; } = string.Empty;
@@ -27,12 +27,12 @@ public class SnappingController
         }
         }
         
         
         snapAxis = HorizontalSnapPoints.First().Key;
         snapAxis = HorizontalSnapPoints.First().Key;
-        double closest = HorizontalSnapPoints.First().Value;
+        double closest = HorizontalSnapPoints.First().Value();
         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() - xPos) < Math.Abs(closest - xPos))
             {
             {
-                closest = snapPoint.Value;
+                closest = snapPoint.Value();
                 snapAxis = snapPoint.Key;
                 snapAxis = snapPoint.Key;
             }
             }
         }
         }
@@ -55,12 +55,12 @@ public class SnappingController
         }
         }
 
 
         snapAxisKey = VerticalSnapPoints.First().Key;
         snapAxisKey = VerticalSnapPoints.First().Key;
-        double closest = VerticalSnapPoints.First().Value;
+        double closest = VerticalSnapPoints.First().Value();
         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() - yPos) < Math.Abs(closest - yPos))
             {
             {
-                closest = snapPoint.Value;
+                closest = snapPoint.Value();
                 snapAxisKey = snapPoint.Key;
                 snapAxisKey = snapPoint.Key;
             }
             }
         }
         }
@@ -76,7 +76,79 @@ 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.X;
+        VerticalSnapPoints[identifier] = () => axisVector.Y;
+    }
+    
+    public void AddBounds(string identifier, Func<RectD> tightBounds)
+    {
+        HorizontalSnapPoints[$"{identifier}.center"] = () => tightBounds().Center.X;
+        VerticalSnapPoints[$"{identifier}.center"] = () => tightBounds().Center.Y;
+        
+        HorizontalSnapPoints[$"{identifier}.left"] = () => tightBounds().Left;
+        VerticalSnapPoints[$"{identifier}.top"] = () => tightBounds().Top;
+        
+        HorizontalSnapPoints[$"{identifier}.right"] = () => tightBounds().Right;
+        VerticalSnapPoints[$"{identifier}.bottom"] = () => tightBounds().Bottom;
+    }
+    
+    /// <summary>
+    ///     Removes all snap points with root identifier. All identifiers that start with root will be removed.
+    /// </summary>
+    /// <param name="id">Root identifier of snap points to remove.</param>
+    public void RemoveAll(string id)
+    {
+       var toRemoveHorizontal = HorizontalSnapPoints.Keys.Where(x => x.StartsWith(id)).ToList();
+       var toRemoveVertical = VerticalSnapPoints.Keys.Where(x => x.StartsWith(id)).ToList();
+        
+       foreach (var key in toRemoveHorizontal)
+       {
+           HorizontalSnapPoints.Remove(key);
+       }
+       
+       foreach (var key in toRemoveVertical)
+       {
+           VerticalSnapPoints.Remove(key);
+       }
+    }
+
+    public VecD GetSnapDeltaForPoints(VecD[] points, out string xAxis, out string yAxis)
+    {
+        bool hasXSnap = false;
+        bool hasYSnap = false;
+        VecD snapDelta = VecD.Zero;
+
+        string snapAxisX = string.Empty;
+        string snapAxisY = string.Empty;
+
+        foreach (var point in points)
+        {
+            double? snapX = SnapToHorizontal(point.X, out string newSnapAxisX);
+            double? snapY = SnapToVertical(point.Y, out string newSnapAxisY);
+
+            if (snapX is not null && !hasXSnap)
+            {
+                snapDelta += new VecD(snapX.Value - point.X, 0);
+                snapAxisX = newSnapAxisX;
+                hasXSnap = true;
+            }
+
+            if (snapY is not null && !hasYSnap)
+            {
+                snapDelta += new VecD(0, snapY.Value - point.Y);
+                snapAxisY = newSnapAxisY;
+                hasYSnap = true;
+            }
+
+            if (hasXSnap && hasYSnap)
+            {
+                break;
+            }
+        }
+
+        xAxis = snapAxisX;
+        yAxis = snapAxisY;
+        
+        return snapDelta;
     }
     }
 }
 }

+ 3 - 3
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -15,6 +15,7 @@ using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
@@ -429,7 +430,7 @@ internal class DocumentUpdater
         /*doc.OnPropertyChanged(nameof(doc.SelectedStructureMember));
         /*doc.OnPropertyChanged(nameof(doc.SelectedStructureMember));
         doc.OnPropertyChanged(nameof(memberVM.Selection));*/
         doc.OnPropertyChanged(nameof(memberVM.Selection));*/
 
 
-        //doc.InternalRaiseLayersChanged(new LayersChangedEventArgs(info.Id, LayerAction.Add));
+        doc.InternalRaiseLayersChanged(new LayersChangedEventArgs(info.Id, LayerAction.Add));
     }
     }
 
 
     private void ProcessDeleteStructureMember(DeleteStructureMember_ChangeInfo info)
     private void ProcessDeleteStructureMember(DeleteStructureMember_ChangeInfo info)
@@ -439,8 +440,7 @@ internal class DocumentUpdater
         if (doc.SelectedStructureMember == memberVM)
         if (doc.SelectedStructureMember == memberVM)
             doc.SetSelectedMember(null);
             doc.SetSelectedMember(null);
         doc.ClearSoftSelectedMembers();
         doc.ClearSoftSelectedMembers();
-        // TODO: Make sure property changed events are raised internally
-        //doc.InternalRaiseLayersChanged(new LayersChangedEventArgs(info.Id, LayerAction.Remove));
+        doc.InternalRaiseLayersChanged(new LayersChangedEventArgs(info.Id, LayerAction.Remove));
     }
     }
 
 
     private void ProcessUpdateStructureMemberIsVisible(StructureMemberIsVisible_ChangeInfo info)
     private void ProcessUpdateStructureMemberIsVisible(StructureMemberIsVisible_ChangeInfo info)

+ 10 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -74,6 +74,8 @@ internal abstract class LineExecutor<T> : UpdateableChangeExecutor where T : ILi
             }
             }
         }
         }
 
 
+        document.SnappingHandler.Remove(memberGuid.ToString());
+        
         return ExecutionState.Success;
         return ExecutionState.Success;
     }
     }
 
 
@@ -166,6 +168,7 @@ internal abstract class LineExecutor<T> : UpdateableChangeExecutor where T : ILi
         document!.LineToolOverlayHandler.Hide();
         document!.LineToolOverlayHandler.Hide();
         var endDrawAction = EndDraw();
         var endDrawAction = EndDraw();
         internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
         internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
+        AddMemberToSnapping();
         onEnded!(this);
         onEnded!(this);
 
 
         colorsVM.AddSwatch(new PaletteColor(StrokeColor.R, StrokeColor.G, StrokeColor.B));
         colorsVM.AddSwatch(new PaletteColor(StrokeColor.R, StrokeColor.G, StrokeColor.B));
@@ -178,5 +181,12 @@ internal abstract class LineExecutor<T> : UpdateableChangeExecutor where T : ILi
 
 
         var endDrawAction = EndDraw();
         var endDrawAction = EndDraw();
         internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
         internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
+        AddMemberToSnapping();
+    }
+    
+    private void AddMemberToSnapping()
+    {
+        var member = document.StructureHelper.Find(memberGuid);
+        document!.SnappingHandler.AddFromBounds(memberGuid.ToString(), () => member!.TightBounds ?? RectD.Empty);
     }
     }
 }
 }

+ 40 - 18
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShapeToolExecutor.cs

@@ -17,7 +17,10 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T : IShapeToolHandler
 internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T : IShapeToolHandler
 {
 {
     protected int StrokeWidth => toolbar.ToolSize;
     protected int StrokeWidth => toolbar.ToolSize;
-    protected Color FillColor => toolbar.Fill ? toolbar.FillColor.ToColor() : DrawingApi.Core.ColorsImpl.Colors.Transparent;
+
+    protected Color FillColor =>
+        toolbar.Fill ? toolbar.FillColor.ToColor() : DrawingApi.Core.ColorsImpl.Colors.Transparent;
+
     protected Color StrokeColor => toolbar.StrokeColor.ToColor();
     protected Color StrokeColor => toolbar.StrokeColor.ToColor();
     protected Guid memberGuid;
     protected Guid memberGuid;
     protected bool drawOnMask;
     protected bool drawOnMask;
@@ -27,11 +30,11 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
     protected VecI startPos;
     protected VecI startPos;
     protected RectI lastRect;
     protected RectI lastRect;
     protected double lastRadians;
     protected double lastRadians;
-    
+
     private bool noMovement = true;
     private bool noMovement = true;
     private IBasicShapeToolbar toolbar;
     private IBasicShapeToolbar toolbar;
     private IColorsHandler? colorsVM;
     private IColorsHandler? colorsVM;
-    
+
     public override ExecutionState Start()
     public override ExecutionState Start()
     {
     {
         colorsVM = GetHandler<IColorsHandler>();
         colorsVM = GetHandler<IColorsHandler>();
@@ -47,7 +50,7 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
             return ExecutionState.Error;
             return ExecutionState.Error;
 
 
         memberGuid = member.Id;
         memberGuid = member.Id;
-        
+
         if (controller.LeftMousePressed || member is not IVectorLayerHandler)
         if (controller.LeftMousePressed || member is not IVectorLayerHandler)
         {
         {
             startPos = controller!.LastPixelPosition;
             startPos = controller!.LastPixelPosition;
@@ -65,15 +68,16 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
                     document.TransformHandler.HideTransform();
                     document.TransformHandler.HideTransform();
                     return ExecutionState.Error;
                     return ExecutionState.Error;
                 }
                 }
-                
+
                 toolbar.StrokeColor = node.ShapeData.StrokeColor.ToColor();
                 toolbar.StrokeColor = node.ShapeData.StrokeColor.ToColor();
                 toolbar.FillColor = node.ShapeData.FillColor.ToColor();
                 toolbar.FillColor = node.ShapeData.FillColor.ToColor();
                 toolbar.ToolSize = node.ShapeData.StrokeWidth;
                 toolbar.ToolSize = node.ShapeData.StrokeWidth;
                 toolbar.Fill = node.ShapeData.FillColor != Colors.Transparent;
                 toolbar.Fill = node.ShapeData.FillColor != Colors.Transparent;
             }
             }
         }
         }
-        
-        
+
+        document.SnappingHandler.Remove(member.Id.ToString());
+
         return ExecutionState.Success;
         return ExecutionState.Success;
     }
     }
 
 
@@ -88,10 +92,14 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
     {
     {
         Span<VecI> positions = stackalloc VecI[]
         Span<VecI> positions = stackalloc VecI[]
         {
         {
-            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, 1)) - new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
-            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, -1)) - new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
-            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, 0)) - new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
-            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(0, 1)) - new VecD(0.25).Multiply((curPos - startPos).Signs())).Round()
+            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, 1)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
+            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, -1)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
+            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, 0)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round(),
+            (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(0, 1)) -
+                   new VecD(0.25).Multiply((curPos - startPos).Signs())).Round()
         };
         };
         VecI max = positions[0];
         VecI max = positions[0];
         double maxLength = double.MaxValue;
         double maxLength = double.MaxValue;
@@ -104,13 +112,16 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
                 max = pos;
                 max = pos;
             }
             }
         }
         }
+
         return max;
         return max;
     }
     }
 
 
     public static VecI GetSquaredPosition(VecI startPos, VecI curPos)
     public static VecI GetSquaredPosition(VecI startPos, VecI curPos)
     {
     {
-        VecI pos1 = (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, 1)) - new VecD(0.25).Multiply((curPos - startPos).Signs())).Round();
-        VecI pos2 = (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, -1)) - new VecD(0.25).Multiply((curPos - startPos).Signs())).Round();
+        VecI pos1 = (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, 1)) -
+                           new VecD(0.25).Multiply((curPos - startPos).Signs())).Round();
+        VecI pos2 = (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, -1)) -
+                           new VecD(0.25).Multiply((curPos - startPos).Signs())).Round();
         if ((pos1 - curPos).LengthSquared > (pos2 - curPos).LengthSquared)
         if ((pos1 - curPos).LengthSquared > (pos2 - curPos).LengthSquared)
             return (VecI)pos2;
             return (VecI)pos2;
         return (VecI)pos1;
         return (VecI)pos1;
@@ -139,12 +150,14 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
     {
     {
         internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
         internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
         document!.TransformHandler.HideTransform();
         document!.TransformHandler.HideTransform();
+
+        AddToSnapController();
         onEnded?.Invoke(this);
         onEnded?.Invoke(this);
-        
+
         colorsVM.AddSwatch(StrokeColor.ToPaletteColor());
         colorsVM.AddSwatch(StrokeColor.ToPaletteColor());
         colorsVM.AddSwatch(FillColor.ToPaletteColor());
         colorsVM.AddSwatch(FillColor.ToPaletteColor());
     }
     }
-    
+
     public override void OnColorChanged(Color color, bool primary)
     public override void OnColorChanged(Color color, bool primary)
     {
     {
         if (primary && toolbar.SyncWithPrimaryColor)
         if (primary && toolbar.SyncWithPrimaryColor)
@@ -192,15 +205,16 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
     {
     {
         if (transforming)
         if (transforming)
             return;
             return;
-        
+
         if (noMovement)
         if (noMovement)
         {
         {
             internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
             internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
+            AddToSnapController();
+            
             onEnded?.Invoke(this);
             onEnded?.Invoke(this);
             return;
             return;
         }
         }
-        
-        
+
         document!.TransformHandler.HideTransform();
         document!.TransformHandler.HideTransform();
         document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), true);
         document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), true);
         transforming = true;
         transforming = true;
@@ -211,5 +225,13 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
         if (transforming)
         if (transforming)
             document!.TransformHandler.HideTransform();
             document!.TransformHandler.HideTransform();
         internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
         internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
+
+        AddToSnapController();
+    }
+
+    private void AddToSnapController()
+    {
+        var member = document!.StructureHelper.Find(memberGuid);
+        document.SnappingHandler.AddFromBounds(member.Id.ToString(), () => member.TightBounds ?? RectD.Empty);
     }
     }
 }
 }

+ 27 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -76,6 +76,12 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor
         DocumentTransformMode mode = allRaster
         DocumentTransformMode mode = allRaster
             ? DocumentTransformMode.Scale_Rotate_Shear_Perspective
             ? DocumentTransformMode.Scale_Rotate_Shear_Perspective
             : DocumentTransformMode.Scale_Rotate_Shear_NoPerspective;
             : DocumentTransformMode.Scale_Rotate_Shear_NoPerspective;
+        
+        foreach (var structureMemberHandler in members)
+        {
+            document.SnappingHandler.Remove(structureMemberHandler.Id.ToString());
+        }
+        
         document.TransformHandler.ShowTransform(mode, true, masterCorners, Type == ExecutorType.Regular);
         document.TransformHandler.ShowTransform(mode, true, masterCorners, Type == ExecutorType.Regular);
         internals!.ActionAccumulator.AddActions(
         internals!.ActionAccumulator.AddActions(
             new TransformSelected_Action(masterCorners, tool.KeepOriginalImage, memberCorners, false,
             new TransformSelected_Action(masterCorners, tool.KeepOriginalImage, memberCorners, false,
@@ -91,6 +97,8 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor
         {
         {
             internals.ActionAccumulator.AddActions(new EndTransformSelected_Action());
             internals.ActionAccumulator.AddActions(new EndTransformSelected_Action());
             document!.TransformHandler.HideTransform();
             document!.TransformHandler.HideTransform();
+            AddSnappingForMembers(memberGuids);
+            
             memberCorners.Clear();
             memberCorners.Clear();
             isInProgress = false;
             isInProgress = false;
         }
         }
@@ -129,6 +137,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor
         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());
         onEnded!.Invoke(this);
         onEnded!.Invoke(this);
 
 
         if (Type == ExecutorType.ToolLinked)
         if (Type == ExecutorType.ToolLinked)
@@ -149,7 +158,25 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor
         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());
 
 
         isInProgress = false;
         isInProgress = false;
     }
     }
+    
+    private void AddSnappingForMembers(List<Guid> memberGuids)
+    {
+        foreach (Guid memberGuid in memberGuids)
+        {
+            IStructureMemberHandler? member = document!.StructureHelper.Find(memberGuid);
+            if (member is null)
+            {
+                continue;
+            }
+
+            if (member is ILayerHandler layer)
+            {
+                document!.SnappingHandler.AddFromBounds(layer.Id.ToString(), () => layer.TightBounds ?? RectD.Empty);
+            }
+        }
+    }
 }
 }

+ 4 - 0
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -9,6 +9,7 @@ using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.Structures;
 using PixiEditor.Models.Structures;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
@@ -40,6 +41,7 @@ internal interface IDocument : IHandler
     public double VerticalSymmetryAxisXBindable { get; }
     public double VerticalSymmetryAxisXBindable { get; }
     public IDocumentOperations Operations { get; }
     public IDocumentOperations Operations { get; }
     public DocumentRenderer Renderer { get; }
     public DocumentRenderer Renderer { get; }
+    public ISnappingHandler SnappingHandler { get; }
     public void RemoveSoftSelectedMember(IStructureMemberHandler member);
     public void RemoveSoftSelectedMember(IStructureMemberHandler member);
     public void ClearSoftSelectedMembers();
     public void ClearSoftSelectedMembers();
     public void AddSoftSelectedMember(IStructureMemberHandler member);
     public void AddSoftSelectedMember(IStructureMemberHandler member);
@@ -53,4 +55,6 @@ internal interface IDocument : IHandler
     public Color PickColor(VecD controllerLastPrecisePosition, DocumentScope scope, bool includeReference, bool includeCanvas, int frame, bool isTopMost);
     public Color PickColor(VecD controllerLastPrecisePosition, DocumentScope scope, bool includeReference, bool includeCanvas, int frame, bool isTopMost);
     public List<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false);
     public List<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false);
     public void UpdateSavedState();
     public void UpdateSavedState();
+
+    internal void InternalRaiseLayersChanged(LayersChangedEventArgs e);
 }
 }

+ 9 - 0
src/PixiEditor/Models/Handlers/ISnappingHandler.cs

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

+ 18 - 4
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -40,6 +40,7 @@ using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
+using PixiEditor.Models.Layers;
 using PixiEditor.Models.Serialization;
 using PixiEditor.Models.Serialization;
 using PixiEditor.Models.Serialization.Factories;
 using PixiEditor.Models.Serialization.Factories;
 using PixiEditor.Models.Structures;
 using PixiEditor.Models.Structures;
@@ -201,6 +202,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public ObservableCollection<PaletteColor> Swatches { get; set; } = new();
     public ObservableCollection<PaletteColor> Swatches { get; set; } = new();
     public ObservableRangeCollection<PaletteColor> Palette { get; set; } = new();
     public ObservableRangeCollection<PaletteColor> Palette { get; set; } = new();
     public SnappingViewModel SnappingViewModel { get; }
     public SnappingViewModel SnappingViewModel { get; }
+    ISnappingHandler IDocument.SnappingHandler => SnappingViewModel;
     public DocumentTransformViewModel TransformViewModel { get; }
     public DocumentTransformViewModel TransformViewModel { get; }
     public ReferenceLayerViewModel ReferenceLayerViewModel { get; }
     public ReferenceLayerViewModel ReferenceLayerViewModel { get; }
     public LineToolOverlayViewModel LineToolOverlayViewModel { get; }
     public LineToolOverlayViewModel LineToolOverlayViewModel { get; }
@@ -221,7 +223,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         var serviceProvider = ViewModelMain.Current.Services;
         var serviceProvider = ViewModelMain.Current.Services;
         Internals = new DocumentInternalParts(this, serviceProvider);
         Internals = new DocumentInternalParts(this, serviceProvider);
         Internals.ChangeController.ToolSessionFinished += () => ToolSessionFinished?.Invoke();
         Internals.ChangeController.ToolSessionFinished += () => ToolSessionFinished?.Invoke();
-        
+
         Tools = new DocumentToolsModule(this, Internals);
         Tools = new DocumentToolsModule(this, Internals);
         StructureHelper = new DocumentStructureModule(this);
         StructureHelper = new DocumentStructureModule(this);
         EventInlet = new DocumentEventsModule(this, Internals);
         EventInlet = new DocumentEventsModule(this, Internals);
@@ -237,13 +239,26 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         LineToolOverlayViewModel = new();
         LineToolOverlayViewModel = new();
         LineToolOverlayViewModel.LineMoved += (_, args) =>
         LineToolOverlayViewModel.LineMoved += (_, args) =>
             Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
             Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
-        
+
         SnappingViewModel = new();
         SnappingViewModel = new();
         SnappingViewModel.AddFromDocumentSize(SizeBindable);
         SnappingViewModel.AddFromDocumentSize(SizeBindable);
         SizeChanged += (_, args) =>
         SizeChanged += (_, args) =>
         {
         {
             SnappingViewModel.AddFromDocumentSize(args.NewSize);
             SnappingViewModel.AddFromDocumentSize(args.NewSize);
         };
         };
+        LayersChanged += (sender, args) =>
+        {
+            if (args.LayerChangeType == LayerAction.Add)
+            {
+                IReadOnlyStructureNode layer = Internals.Tracker.Document.FindMember(args.LayerAffectedGuid);
+                SnappingViewModel.AddFromBounds(layer.Id.ToString(),
+                    () => layer.GetTightBounds(AnimationDataViewModel.ActiveFrameTime) ?? RectD.Empty);
+            }
+            else if (args.LayerChangeType == LayerAction.Remove)
+            {
+                SnappingViewModel.Remove(args.LayerAffectedGuid.ToString());
+            }
+        };
 
 
         VecI previewSize = StructureMemberViewModel.CalculatePreviewSize(SizeBindable);
         VecI previewSize = StructureMemberViewModel.CalculatePreviewSize(SizeBindable);
         PreviewSurface = new Texture(new VecI(previewSize.X, previewSize.Y));
         PreviewSurface = new Texture(new VecI(previewSize.X, previewSize.Y));
@@ -692,7 +707,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
     // these are intended to only be called from DocumentUpdater
     // these are intended to only be called from DocumentUpdater
 
 
-    public void RaiseLayersChanged(LayersChangedEventArgs args) => LayersChanged?.Invoke(this, args);
+    public void InternalRaiseLayersChanged(LayersChangedEventArgs args) => LayersChanged?.Invoke(this, args);
 
 
     public void RaiseSizeChanged(DocumentSizeChangedEventArgs args) => SizeChanged?.Invoke(this, args);
     public void RaiseSizeChanged(DocumentSizeChangedEventArgs args) => SizeChanged?.Invoke(this, args);
 
 
@@ -969,5 +984,4 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         Internals.Tracker.Dispose();
         Internals.Tracker.Dispose();
         Internals.Tracker.Document.Dispose();
         Internals.Tracker.Document.Dispose();
     }
     }
-
 }
 }

+ 13 - 2
src/PixiEditor/ViewModels/Document/SnappingViewModel.cs

@@ -1,10 +1,11 @@
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Models.Handlers;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.ViewModels.Document;
 namespace PixiEditor.ViewModels.Document;
 
 
-public class SnappingViewModel : PixiObservableObject
+public class SnappingViewModel : PixiObservableObject, ISnappingHandler
 {
 {
     public SnappingController SnappingController { get; } = new SnappingController();
     public SnappingController SnappingController { get; } = new SnappingController();
 
 
@@ -12,10 +13,20 @@ public class SnappingViewModel : PixiObservableObject
     {
     {
         SnappingController.AddXYAxis("Root", VecD.Zero);
         SnappingController.AddXYAxis("Root", VecD.Zero);
     }
     }
-    
+
     public void AddFromDocumentSize(VecD documentSize)
     public void AddFromDocumentSize(VecD documentSize)
     {
     {
         SnappingController.AddXYAxis("DocumentSize", documentSize);
         SnappingController.AddXYAxis("DocumentSize", documentSize);
         SnappingController.AddXYAxis("DocumentCenter", documentSize / 2);
         SnappingController.AddXYAxis("DocumentCenter", documentSize / 2);
     }
     }
+
+    public void AddFromBounds(string id, Func<RectD> tightBounds)
+    {
+        SnappingController.AddBounds(id, tightBounds);
+    }
+
+    public void Remove(string id)
+    {
+        SnappingController.RemoveAll(id);
+    }
 }
 }

+ 8 - 0
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -188,6 +188,13 @@ internal class ViewportOverlays
             Path = "Document.LineToolOverlayViewModel.IsEnabled",
             Path = "Document.LineToolOverlayViewModel.IsEnabled",
             Mode = BindingMode.OneWay
             Mode = BindingMode.OneWay
         };
         };
+        
+        Binding snappingBinding = new()
+        {
+            Source = Viewport,
+            Path = "Document.SnappingViewModel.SnappingController",
+            Mode = BindingMode.OneWay
+        };
 
 
         Binding actionCompletedBinding = new()
         Binding actionCompletedBinding = new()
         {
         {
@@ -211,6 +218,7 @@ internal class ViewportOverlays
         };
         };
 
 
         lineToolOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         lineToolOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
+        lineToolOverlay.Bind(LineToolOverlay.SnappingControllerProperty, snappingBinding);
         lineToolOverlay.Bind(LineToolOverlay.ActionCompletedProperty, actionCompletedBinding);
         lineToolOverlay.Bind(LineToolOverlay.ActionCompletedProperty, actionCompletedBinding);
         lineToolOverlay.Bind(LineToolOverlay.LineStartProperty, lineStartBinding);
         lineToolOverlay.Bind(LineToolOverlay.LineStartProperty, lineStartBinding);
         lineToolOverlay.Bind(LineToolOverlay.LineEndProperty, lineEndBinding);
         lineToolOverlay.Bind(LineToolOverlay.LineEndProperty, lineEndBinding);

+ 83 - 4
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -43,6 +43,15 @@ internal class LineToolOverlay : Overlay
         set => SetValue(ActionCompletedProperty, value);
         set => SetValue(ActionCompletedProperty, value);
     }
     }
 
 
+    public static readonly StyledProperty<SnappingController> SnappingControllerProperty = AvaloniaProperty.Register<LineToolOverlay, SnappingController>(
+        nameof(SnappingController));
+
+    public SnappingController SnappingController
+    {
+        get => GetValue(SnappingControllerProperty);
+        set => SetValue(SnappingControllerProperty, value);
+    }
+
     static LineToolOverlay()
     static LineToolOverlay()
     {
     {
         LineStartProperty.Changed.Subscribe(RenderAffectingPropertyChanged);
         LineStartProperty.Changed.Subscribe(RenderAffectingPropertyChanged);
@@ -70,19 +79,32 @@ internal class LineToolOverlay : Overlay
         startHandle = new AnchorHandle(this);
         startHandle = new AnchorHandle(this);
         startHandle.HandlePen = blackPen;
         startHandle.HandlePen = blackPen;
         startHandle.OnDrag += StartHandleOnDrag;
         startHandle.OnDrag += StartHandleOnDrag;
+        startHandle.OnRelease += OnHandleRelease;
         AddHandle(startHandle);
         AddHandle(startHandle);
 
 
         endHandle = new AnchorHandle(this);
         endHandle = new AnchorHandle(this);
         endHandle.HandlePen = blackPen;
         endHandle.HandlePen = blackPen;
         endHandle.OnDrag += EndHandleOnDrag;
         endHandle.OnDrag += EndHandleOnDrag;
+        endHandle.OnRelease += OnHandleRelease;
         AddHandle(endHandle);
         AddHandle(endHandle);
 
 
         moveHandle = new TransformHandle(this);
         moveHandle = new TransformHandle(this);
         moveHandle.HandlePen = blackPen;
         moveHandle.HandlePen = blackPen;
         moveHandle.OnDrag += MoveHandleOnDrag;
         moveHandle.OnDrag += MoveHandleOnDrag;
+        moveHandle.OnRelease += OnHandleRelease;
         AddHandle(moveHandle);
         AddHandle(moveHandle);
     }
     }
 
 
+    private void OnHandleRelease(Handle obj)
+    {
+        if (SnappingController != null)
+        {
+            SnappingController.HighlightedXAxis = null;
+            SnappingController.HighlightedYAxis = null;
+            Refresh();
+        }
+    }
+
     protected override void ZoomChanged(double newZoom)
     protected override void ZoomChanged(double newZoom)
     {
     {
         blackPen.Thickness = 1.0 / newZoom;
         blackPen.Thickness = 1.0 / newZoom;
@@ -120,22 +142,57 @@ internal class LineToolOverlay : Overlay
 
 
     private void StartHandleOnDrag(Handle source, VecD position)
     private void StartHandleOnDrag(Handle source, VecD position)
     {
     {
-        LineStart = position;
+        LineStart = SnapAndHighlight(position);
         movedWhileMouseDown = true;
         movedWhileMouseDown = true;
     }
     }
 
 
     private void EndHandleOnDrag(Handle source, VecD position)
     private void EndHandleOnDrag(Handle source, VecD position)
     {
     {
-        LineEnd = position;
+        var final = SnapAndHighlight(position);
+        
+        LineEnd = final;
         movedWhileMouseDown = true;
         movedWhileMouseDown = true;
     }
     }
 
 
+    private VecD SnapAndHighlight(VecD position)
+    {
+        VecD final = position;
+        if (SnappingController != null)
+        {
+            double? x = SnappingController.SnapToHorizontal(position.X, out string snapAxisX);
+            double? y = SnappingController.SnapToVertical(position.Y, out string snapAxisY);
+            
+            if (x.HasValue)
+            {
+                final = new VecD(x.Value, final.Y);
+            }
+            
+            if (y.HasValue)
+            {
+                final = new VecD(final.X, y.Value);
+            }
+            
+            SnappingController.HighlightedXAxis = snapAxisX;
+            SnappingController.HighlightedYAxis = snapAxisY;
+        }
+
+        return final;
+    }
+
     private void MoveHandleOnDrag(Handle source, VecD position)
     private void MoveHandleOnDrag(Handle source, VecD position)
     {
     {
         var delta = position - mouseDownPos;
         var delta = position - mouseDownPos;
 
 
-        LineStart = lineStartOnMouseDown + delta;
-        LineEnd = lineEndOnMouseDown + delta;
+        ((string, string), VecD) snapDeltaResult = TrySnapLine(LineStart, LineEnd, delta);
+
+        if (SnappingController != null)
+        {
+            SnappingController.HighlightedXAxis = snapDeltaResult.Item1.Item1;
+            SnappingController.HighlightedYAxis = snapDeltaResult.Item1.Item2;
+        }
+        
+        LineStart = lineStartOnMouseDown + delta + snapDeltaResult.Item2;
+        LineEnd = lineEndOnMouseDown + delta + snapDeltaResult.Item2;
 
 
         movedWhileMouseDown = true;
         movedWhileMouseDown = true;
     }
     }
@@ -147,8 +204,30 @@ internal class LineToolOverlay : Overlay
 
 
         if (movedWhileMouseDown && ActionCompleted is not null && ActionCompleted.CanExecute(null))
         if (movedWhileMouseDown && ActionCompleted is not null && ActionCompleted.CanExecute(null))
             ActionCompleted.Execute(null);
             ActionCompleted.Execute(null);
+        
     }
     }
 
 
+    private ((string, string), VecD) TrySnapLine(VecD originalStart, VecD originalEnd, VecD delta)
+    {
+        if (SnappingController == null)
+        {
+            return ((string.Empty, string.Empty), delta);
+        }
+        
+        VecD center = (originalStart + originalEnd) / 2f;
+        VecD[] pointsToTest = new VecD[]
+        {
+            center + delta,
+            originalStart + delta,
+            originalEnd + delta,
+        };
+
+        VecD snapDelta = SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX, out string snapAxisY);
+
+        return ((snapAxisX, snapAxisY), snapDelta);
+    }
+    
+    
     private static void RenderAffectingPropertyChanged(AvaloniaPropertyChangedEventArgs<VecD> e)
     private static void RenderAffectingPropertyChanged(AvaloniaPropertyChangedEventArgs<VecD> e)
     {
     {
         if (e.Sender is LineToolOverlay overlay)
         if (e.Sender is LineToolOverlay overlay)

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

@@ -41,7 +41,7 @@ internal class SnappingOverlay : Overlay
             {
             {
                 if (snapPoint.Key == SnappingController.HighlightedXAxis)
                 if (snapPoint.Key == SnappingController.HighlightedXAxis)
                 {
                 {
-                    context.DrawLine(horizontalAxisPen, new Point(snapPoint.Value, 0), new Point(snapPoint.Value, canvasBounds.Height));
+                    context.DrawLine(horizontalAxisPen, new Point(snapPoint.Value(), 0), new Point(snapPoint.Value(), canvasBounds.Height));
                 }
                 }
             }
             }
         }
         }
@@ -52,7 +52,7 @@ internal class SnappingOverlay : Overlay
             {
             {
                 if (snapPoint.Key == SnappingController.HighlightedYAxis)
                 if (snapPoint.Key == SnappingController.HighlightedYAxis)
                 {
                 {
-                    context.DrawLine(verticalAxisPen, new Point(0, snapPoint.Value), new Point(canvasBounds.Width, snapPoint.Value));
+                    context.DrawLine(verticalAxisPen, new Point(0, snapPoint.Value()), new Point(canvasBounds.Width, snapPoint.Value()));
                 }
                 }
             }
             }
         }
         }

+ 3 - 32
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -532,38 +532,9 @@ internal class TransformOverlay : Overlay
             rawCorners.BottomRight
             rawCorners.BottomRight
         };
         };
 
 
-        VecD snapDelta = new();
-        bool hasXSnap = false;
-        bool hasYSnap = false;
-
-        string snapAxisX = string.Empty;
-        string snapAxisY = string.Empty;
-
-        foreach (var point in pointsToTest)
-        {
-            double? snapX = SnappingController.SnapToHorizontal(point.X, out string newSnapAxisX);
-            double? snapY = SnappingController.SnapToVertical(point.Y, out string newSnapAxisY);
-
-            if (snapX is not null && !hasXSnap)
-            {
-                snapDelta += new VecD(snapX.Value - point.X, 0);
-                snapAxisX = newSnapAxisX;
-                hasXSnap = true;
-            }
-
-            if (snapY is not null && !hasYSnap)
-            {
-                snapDelta += new VecD(0, snapY.Value - point.Y);
-                snapAxisY = newSnapAxisY;
-                hasYSnap = true;
-            }
-
-            if (hasXSnap && hasYSnap)
-            {
-                break;
-            }
-        }
-
+        VecD snapDelta = SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX,
+            out string snapAxisY);
+        
         return ((snapAxisX, snapAxisY), snapDelta);
         return ((snapAxisX, snapAxisY), snapDelta);
     }
     }