Browse Source

Merge pull request #651 from PixiEditor/path-tool

Added Vector Path tool
Krzysztof Krysiński 8 months ago
parent
commit
f36200341d
87 changed files with 2036 additions and 323 deletions
  1. 1 1
      src/ChunkyImageLib/ChunkyImage.cs
  2. 1 1
      src/ChunkyImageLib/Operations/ClearPathOperation.cs
  3. 1 1
      src/ChunkyImageLib/Operations/EllipseHelper.cs
  4. 1 1
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  5. 1 1
      src/ChunkyImageLib/Operations/PathOperation.cs
  6. 1 1
      src/Drawie
  7. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/Selection_ChangeInfo.cs
  8. 8 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyPathData.cs
  9. 0 38
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  10. 0 49
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  11. 87 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  12. 0 32
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  13. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  14. 6 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  15. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlySelection.cs
  16. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Selection.cs
  17. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  18. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  19. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PathBasedPen_UpdateableChange.cs
  20. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/SelectionToMask_Change.cs
  21. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  22. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/ClearSelection_Change.cs
  23. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs
  24. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWand_Change.cs
  25. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectEllipse_UpdateableChange.cs
  26. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectLasso_UpdateableChange.cs
  27. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectRectangle_UpdateableChange.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectionChangeHelper.cs
  29. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Selection/SetSelection_Change.cs
  30. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/TransformSelectionPath_UpdateableChange.cs
  31. 1 1
      src/PixiEditor.ChangeableDocument/Enums/SelectionModeEx.cs
  32. 8 0
      src/PixiEditor.SVG/Elements/SvgPath.cs
  33. 0 1
      src/PixiEditor/Animation/Animators.cs
  34. 1 1
      src/PixiEditor/Animation/IDashPathEffect.cs
  35. 0 1
      src/PixiEditor/Animation/SelectionDashAnimator.cs
  36. 2 1
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  37. 3 1
      src/PixiEditor/Data/Localization/Languages/en.json
  38. 1 1
      src/PixiEditor/Helpers/Converters/VectorPathToVisibleConverter.cs
  39. 1 0
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  40. 6 0
      src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs
  41. 9 0
      src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs
  42. 1 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  43. 7 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs
  44. 15 15
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  45. 8 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/IPathExecutorFeature.cs
  46. 5 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  47. 0 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs
  48. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs
  49. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs
  50. 2 6
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs
  51. 0 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  52. 3 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorEllipseToolExecutor.cs
  53. 251 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs
  54. 3 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorRectangleToolExecutor.cs
  55. 2 1
      src/PixiEditor/Models/Handlers/IDocument.cs
  56. 3 1
      src/PixiEditor/Models/Handlers/IDocumentOperations.cs
  57. 15 0
      src/PixiEditor/Models/Handlers/IPathOverlayHandler.cs
  58. 1 0
      src/PixiEditor/Models/Handlers/ISnappingHandler.cs
  59. 6 0
      src/PixiEditor/Models/Handlers/Tools/IVectorPathToolHandler.cs
  60. 9 0
      src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs
  61. 15 1
      src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs
  62. 33 0
      src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs
  63. 1 0
      src/PixiEditor/Styles/PixiEditor.Handles.axaml
  64. 33 6
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  65. 10 2
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  66. 5 0
      src/PixiEditor/ViewModels/Document/SnappingViewModel.cs
  67. 81 0
      src/PixiEditor/ViewModels/Document/TransformOverlays/PathOverlayUndoStack.cs
  68. 118 0
      src/PixiEditor/ViewModels/Document/TransformOverlays/PathOverlayViewModel.cs
  69. 1 3
      src/PixiEditor/ViewModels/Document/TransformOverlays/TransformOverlayUndoStack.cs
  70. 1 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/Toolbar.cs
  71. 0 1
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs
  72. 72 0
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  73. 1 1
      src/PixiEditor/Views/Main/Tools/ToolPickerButton.axaml
  74. 1 0
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml
  75. 28 19
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  76. 1 1
      src/PixiEditor/Views/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs
  77. 52 0
      src/PixiEditor/Views/Overlays/Drawables/DashedStroke.cs
  78. 9 1
      src/PixiEditor/Views/Overlays/Handles/AnchorHandle.cs
  79. 36 0
      src/PixiEditor/Views/Overlays/Handles/ControlPointHandle.cs
  80. 62 19
      src/PixiEditor/Views/Overlays/Handles/Handle.cs
  81. 52 63
      src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs
  82. 62 8
      src/PixiEditor/Views/Overlays/Overlay.cs
  83. 829 0
      src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs
  84. 1 1
      src/PixiEditor/Views/Overlays/SelectionOverlay/SelectionOverlay.cs
  85. 1 1
      src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs
  86. 6 6
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  87. 34 4
      tests/PixiEditor.Backend.Tests/NodeSystemTests.cs

+ 1 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -11,7 +11,7 @@ using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 [assembly: InternalsVisibleTo("ChunkyImageLibTest")]
 [assembly: InternalsVisibleTo("ChunkyImageLibTest")]

+ 1 - 1
src/ChunkyImageLib/Operations/ClearPathOperation.cs

@@ -1,6 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace ChunkyImageLib.Operations;
 namespace ChunkyImageLib.Operations;

+ 1 - 1
src/ChunkyImageLib/Operations/EllipseHelper.cs

@@ -1,6 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace ChunkyImageLib.Operations;
 namespace ChunkyImageLib.Operations;

+ 1 - 1
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -3,7 +3,7 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 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.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace ChunkyImageLib.Operations;
 namespace ChunkyImageLib.Operations;

+ 1 - 1
src/ChunkyImageLib/Operations/PathOperation.cs

@@ -3,7 +3,7 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 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.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace ChunkyImageLib.Operations;
 namespace ChunkyImageLib.Operations;

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit e95540d2b9d4636824dc55e3c9f65f14381dbcdc
+Subproject commit 47a57ed9ed23a0343cd7a7328cbfb8d6a111e14a

+ 1 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/Selection_ChangeInfo.cs

@@ -1,4 +1,4 @@
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 
 

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyPathData.cs

@@ -0,0 +1,8 @@
+using Drawie.Backend.Core.Vector;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+
+public interface IReadOnlyPathData : IReadOnlyShapeVectorData
+{
+    public VectorPath Path { get; }
+}

+ 0 - 38
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs

@@ -3,7 +3,6 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 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.Surfaces.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -61,43 +60,6 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
         {
         {
             drawingSurface.Canvas.RestoreToCount(saved);
             drawingSurface.Canvas.RestoreToCount(saved);
         }
         }
-
-        // Do not remove below, it might be used (directly or as a reference) for pixelated rendering
-        /*var imageSize = (VecI)(Radius * 2);
-
-        using ChunkyImage img = new ChunkyImage((VecI)GeometryAABB.Size);
-
-        RectD rotated = new ShapeCorners(RectD.FromTwoPoints(VecD.Zero, imageSize)).AABBBounds;
-
-        VecI shift = new VecI((int)Math.Floor(-rotated.Left), (int)Math.Floor(-rotated.Top));
-        RectI drawRect = new(shift, imageSize);
-
-        img.EnqueueDrawEllipse(drawRect, StrokeColor, FillColor, StrokeWidth);
-        img.CommitChanges();
-
-        VecI topLeft = new VecI((int)Math.Round(Center.X - Radius.X), (int)Math.Round(Center.Y - Radius.Y)) - shift;
-        topLeft = (VecI)(topLeft * resolution.Multiplier());
-
-        RectI region = new(VecI.Zero, (VecI)GeometryAABB.Size);
-
-        int num = 0;
-        if (applyTransform)
-        {
-            num = drawingSurface.Canvas.Save();
-            Matrix3X3 final = TransformationMatrix with
-            {
-                TransX = TransformationMatrix.TransX * (float)resolution.Multiplier(),
-                TransY = TransformationMatrix.TransY * (float)resolution.Multiplier()
-            };
-            drawingSurface.Canvas.SetMatrix(final);
-        }
-
-        img.DrawMostUpToDateRegionOn(region, resolution, drawingSurface, topLeft, paint);
-
-        if (applyTransform)
-        {
-            drawingSurface.Canvas.RestoreToCount(num);
-        }*/
     }
     }
     
     
     public override bool IsValid()
     public override bool IsValid()

+ 0 - 49
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs

@@ -67,55 +67,6 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
         {
         {
             drawingSurface.Canvas.RestoreToCount(num);
             drawingSurface.Canvas.RestoreToCount(num);
         }
         }
-
-        /*
-        RectD adjustedAABB = GeometryAABB.RoundOutwards();
-        adjustedAABB = adjustedAABB with { Size = adjustedAABB.Size + new VecD(1, 1) };
-        var imageSize = (VecI)adjustedAABB.Size;
-
-        using ChunkyImage img = new ChunkyImage(imageSize);
-
-        if (StrokeWidth == 1)
-        {
-            VecD adjustment = new VecD(0.5, 0.5);
-
-            img.EnqueueDrawBresenhamLine(
-                (VecI)(Start - adjustedAABB.TopLeft - adjustment),
-                (VecI)(End - adjustedAABB.TopLeft - adjustment), StrokeColor, BlendMode.SrcOver);
-        }
-        else
-        {
-            img.EnqueueDrawSkiaLine(
-                (VecI)Start.Round() - (VecI)adjustedAABB.TopLeft,
-                (VecI)End.Round() - (VecI)adjustedAABB.TopLeft, StrokeCap.Butt, StrokeWidth, StrokeColor, BlendMode.SrcOver);
-        }
-
-        img.CommitChanges();
-
-        VecI topLeft = (VecI)(adjustedAABB.TopLeft * resolution.Multiplier());
-
-        RectI region = new(VecI.Zero, imageSize);
-
-        int num = 0;
-
-        if (applyTransform)
-        {
-            num = drawingSurface.Canvas.Save();
-            Matrix3X3 final = TransformationMatrix with
-            {
-                TransX = TransformationMatrix.TransX * (float)resolution.Multiplier(),
-                TransY = TransformationMatrix.TransY * (float)resolution.Multiplier()
-            };
-            drawingSurface.Canvas.SetMatrix(final);
-        }
-
-        img.DrawMostUpToDateRegionOn(region, resolution, drawingSurface, topLeft, paint);
-
-        if (applyTransform)
-        {
-            drawingSurface.Canvas.RestoreToCount(num);
-        }
-    */
     }
     }
 
 
     public override bool IsValid()
     public override bool IsValid()

+ 87 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -0,0 +1,87 @@
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public class PathVectorData : ShapeVectorData, IReadOnlyPathData
+{
+    public VectorPath Path { get; }
+    public override RectD GeometryAABB => Path.TightBounds.Inflate(StrokeWidth);
+
+    public override ShapeCorners TransformationCorners =>
+        new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
+
+    public PathVectorData(VectorPath path)
+    {
+        Path = path;
+    }
+
+    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    {
+        Rasterize(drawingSurface, false);
+    }
+
+    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    {
+        Rasterize(drawingSurface, true);
+    }
+
+    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    {
+        int num = 0;
+        if (applyTransform)
+        {
+            num = drawingSurface.Canvas.Save();
+            ApplyTransformTo(drawingSurface);
+        }
+
+        using Paint paint = new Paint()
+        {
+            IsAntiAliased = true, StrokeJoin = StrokeJoin.Round, StrokeCap = StrokeCap.Round
+        };
+
+        if (FillColor.A > 0)
+        {
+            paint.Color = FillColor;
+            paint.Style = PaintStyle.Fill;
+
+            drawingSurface.Canvas.DrawPath(Path, paint);
+        }
+
+        paint.Color = StrokeColor;
+        paint.Style = PaintStyle.Stroke;
+        paint.StrokeWidth = StrokeWidth;
+
+        drawingSurface.Canvas.DrawPath(Path, paint);
+
+        if (applyTransform)
+        {
+            drawingSurface.Canvas.RestoreToCount(num);
+        }
+    }
+
+    public override bool IsValid()
+    {
+        return Path is { IsEmpty: false };
+    }
+
+    public override int GetCacheHash()
+    {
+        return Path.GetHashCode();
+    }
+
+    public override int CalculateHash()
+    {
+        return Path.GetHashCode();
+    }
+
+    public override object Clone()
+    {
+        return new PathVectorData(new VectorPath(Path));
+    }
+}

+ 0 - 32
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs

@@ -3,7 +3,6 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 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.Surfaces.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -59,37 +58,6 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         {
         {
             drawingSurface.Canvas.RestoreToCount(saved);
             drawingSurface.Canvas.RestoreToCount(saved);
         }
         }
-        /*var imageSize = (VecI)Size; 
-
-        using ChunkyImage img = new ChunkyImage(imageSize);
-
-        RectI drawRect = (RectI)RectD.FromTwoPoints(VecD.Zero, Size).RoundOutwards();
-
-        ShapeData data = new ShapeData(drawRect.Center, drawRect.Size, 0, StrokeWidth, StrokeColor, FillColor);
-        img.EnqueueDrawRectangle(data);
-        img.CommitChanges();
-        
-        VecI topLeft = (VecI)((Center - Size / 2) * resolution.Multiplier());
-        RectI region = new(VecI.Zero, (VecI)GeometryAABB.Size);
-
-        int num = 0;
-        if (applyTransform)
-        {
-            num = drawingSurface.Canvas.Save();
-            Matrix3X3 final = TransformationMatrix with
-            {
-                TransX = TransformationMatrix.TransX * (float)resolution.Multiplier(),
-                TransY = TransformationMatrix.TransY * (float)resolution.Multiplier()
-            };
-            drawingSurface.Canvas.SetMatrix(final);
-        }
-
-        img.DrawMostUpToDateRegionOn(region, resolution, drawingSurface, topLeft, paint);
-        
-        if (applyTransform)
-        {
-            drawingSurface.Canvas.RestoreToCount(num);
-        }*/
     }
     }
 
 
     public override bool IsValid()
     public override bool IsValid()

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -129,7 +129,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         int renderSaved = renderTarget.Canvas.Save();
         int renderSaved = renderTarget.Canvas.Save();
         VecD scenePos = GetScenePosition(context.FrameTime);
         VecD scenePos = GetScenePosition(context.FrameTime);
         VecD sceneSize = GetSceneSize(context.FrameTime);
         VecD sceneSize = GetSceneSize(context.FrameTime);
-        renderTarget.Canvas.ClipRect(new RectD(scenePos - (sceneSize / 2f), sceneSize));
+        //renderTarget.Canvas.ClipRect(new RectD(scenePos - (sceneSize / 2f), sceneSize));
 
 
         Render(renderObjectContext);
         Render(renderObjectContext);
 
 

+ 6 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs

@@ -124,6 +124,12 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
     {
     {
         base.DeserializeAdditionalData(target, data);
         base.DeserializeAdditionalData(target, data);
         ShapeData = (ShapeVectorData)data["ShapeData"];
         ShapeData = (ShapeVectorData)data["ShapeData"];
+
+        if (ShapeData == null)
+        {
+            return new None();
+        }
+        
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
             (RectI)ShapeData.TransformedAABB, ChunkyImage.FullChunkSize));
             (RectI)ShapeData.TransformedAABB, ChunkyImage.FullChunkSize));
 
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlySelection.cs

@@ -1,4 +1,4 @@
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Selection.cs

@@ -1,6 +1,6 @@
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables;
 namespace PixiEditor.ChangeableDocument.Changeables;
 
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -6,7 +6,7 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs

@@ -1,6 +1,6 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/PathBasedPen_UpdateableChange.cs

@@ -2,7 +2,7 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 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.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/SelectionToMask_Change.cs

@@ -3,7 +3,7 @@ using PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using BlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
 using BlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
 
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -7,7 +7,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 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.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/ClearSelection_Change.cs

@@ -1,4 +1,4 @@
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs

@@ -4,7 +4,7 @@ using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 using PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection.MagicWand;
 namespace PixiEditor.ChangeableDocument.Changes.Selection.MagicWand;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWand_Change.cs

@@ -1,7 +1,7 @@
 using PixiEditor.ChangeableDocument.Changes.Drawing;
 using PixiEditor.ChangeableDocument.Changes.Drawing;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection.MagicWand;
 namespace PixiEditor.ChangeableDocument.Changes.Selection.MagicWand;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectEllipse_UpdateableChange.cs

@@ -1,6 +1,6 @@
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 namespace PixiEditor.ChangeableDocument.Changes.Selection;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectLasso_UpdateableChange.cs

@@ -1,6 +1,6 @@
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 namespace PixiEditor.ChangeableDocument.Changes.Selection;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectRectangle_UpdateableChange.cs

@@ -1,6 +1,6 @@
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 namespace PixiEditor.ChangeableDocument.Changes.Selection;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectionChangeHelper.cs

@@ -1,6 +1,6 @@
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 namespace PixiEditor.ChangeableDocument.Changes.Selection;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Selection/SetSelection_Change.cs

@@ -1,5 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Interfaces;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 internal class SetSelection_Change : Change
 internal class SetSelection_Change : Change

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/TransformSelectionPath_UpdateableChange.cs

@@ -1,5 +1,5 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 namespace PixiEditor.ChangeableDocument.Changes.Selection;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Enums/SelectionModeEx.cs

@@ -1,4 +1,4 @@
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 
 
 namespace PixiEditor.ChangeableDocument.Enums;
 namespace PixiEditor.ChangeableDocument.Enums;
 internal static class SelectionModeEx
 internal static class SelectionModeEx

+ 8 - 0
src/PixiEditor.SVG/Elements/SvgPath.cs

@@ -0,0 +1,8 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgPath() : SvgPrimitive("path")
+{
+    public SvgProperty<SvgStringUnit> PathData { get; } = new("d");
+}

+ 0 - 1
src/PixiEditor/Animation/Animators.cs

@@ -1,6 +1,5 @@
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Styling;
 using Avalonia.Styling;
-using Drawie.Backend.Core.Surfaces.Vector;
 
 
 namespace PixiEditor.Animation;
 namespace PixiEditor.Animation;
 
 

+ 1 - 1
src/PixiEditor/Animation/IDashPathEffect.cs

@@ -1,4 +1,4 @@
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 
 
 namespace PixiEditor.Animation;
 namespace PixiEditor.Animation;
 
 

+ 0 - 1
src/PixiEditor/Animation/SelectionDashAnimator.cs

@@ -1,6 +1,5 @@
 using Avalonia.Animation;
 using Avalonia.Animation;
 using Avalonia.Media;
 using Avalonia.Media;
-using Drawie.Backend.Core.Surfaces.Vector;
 
 
 namespace PixiEditor.Animation;
 namespace PixiEditor.Animation;
 
 

+ 2 - 1
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -101,7 +101,8 @@
       "Move",
       "Move",
       "VectorLine",
       "VectorLine",
       "VectorEllipse",
       "VectorEllipse",
-      "VectorRectangle"
+      "VectorRectangle",
+      "VectorPath"
     ]
     ]
   }
   }
 ]
 ]

+ 3 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -763,5 +763,7 @@
   "TOGGLE_SNAPPING": "Toggle snapping",
   "TOGGLE_SNAPPING": "Toggle snapping",
   "HIGH_RES_PREVIEW": "High Resolution Preview",
   "HIGH_RES_PREVIEW": "High Resolution Preview",
   "LOW_RES_PREVIEW": "Document Resolution Preview",
   "LOW_RES_PREVIEW": "Document Resolution Preview",
-  "TOGGLE_HIGH_RES_PREVIEW": "Toggle high resolution preview"
+  "TOGGLE_HIGH_RES_PREVIEW": "Toggle high resolution preview",
+  "FACTOR": "Factor",
+  "PATH_TOOL": "Path"
 }
 }

+ 1 - 1
src/PixiEditor/Helpers/Converters/VectorPathToVisibleConverter.cs

@@ -1,6 +1,6 @@
 using System.Globalization;
 using System.Globalization;
 using Avalonia.Data.Converters;
 using Avalonia.Data.Converters;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 
 
 namespace PixiEditor.Helpers.Converters;
 namespace PixiEditor.Helpers.Converters;
 
 

+ 1 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -98,6 +98,7 @@ internal static class ServiceCollectionHelpers
             .AddTool<IVectorEllipseToolHandler, VectorEllipseToolViewModel>()
             .AddTool<IVectorEllipseToolHandler, VectorEllipseToolViewModel>()
             .AddTool<IVectorRectangleToolHandler, VectorRectangleToolViewModel>()
             .AddTool<IVectorRectangleToolHandler, VectorRectangleToolViewModel>()
             .AddTool<IVectorLineToolHandler, VectorLineToolViewModel>()
             .AddTool<IVectorLineToolHandler, VectorLineToolViewModel>()
+            .AddTool<IVectorPathToolHandler, VectorPathToolViewModel>()
             .AddTool<ZoomToolViewModel>()
             .AddTool<ZoomToolViewModel>()
             // File types
             // File types
             .AddSingleton<IoFileType, PixiFileType>()
             .AddSingleton<IoFileType, PixiFileType>()

+ 6 - 0
src/PixiEditor/Models/Controllers/InputDevice/SnappingController.cs

@@ -349,4 +349,10 @@ public class SnappingController
         yAxis = string.Empty;
         yAxis = string.Empty;
         return pos;
         return pos;
     }
     }
+
+    public void AddXYAxis(string identifier, Func<VecD> pointFunc)
+    {
+        HorizontalSnapPoints[identifier] = () => pointFunc().X;
+        VerticalSnapPoints[identifier] = () => pointFunc().Y;
+    }
 }
 }

+ 9 - 0
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -3,6 +3,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
@@ -280,4 +281,12 @@ internal class ChangeExecutionController
         
         
         return default;
         return default;
     }
     }
+
+    public void PathOverlayChangedInlet(VectorPath path)
+    {
+        if (currentSession is IPathExecutorFeature vectorPathToolExecutor)
+        {
+            vectorPathToolExecutor.OnPathChanged(path);
+        }
+    }
 }
 }

+ 1 - 1
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -8,7 +8,7 @@ using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Models.Clipboard;
 using PixiEditor.Models.Clipboard;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;

+ 7 - 1
src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs

@@ -58,13 +58,19 @@ internal class DocumentToolsModule
         bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
         bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
         Internals.ChangeController.TryStartExecutor<VectorRectangleToolExecutor>(force);
         Internals.ChangeController.TryStartExecutor<VectorRectangleToolExecutor>(force);
     }
     }
-    
+
     public void UseVectorLineTool()
     public void UseVectorLineTool()
     {
     {
         bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
         bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
         Internals.ChangeController.TryStartExecutor<VectorLineToolExecutor>(force);
         Internals.ChangeController.TryStartExecutor<VectorLineToolExecutor>(force);
     }
     }
 
 
+    public void UseVectorPathTool()
+    {
+        bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
+        Internals.ChangeController.TryStartExecutor<VectorPathToolExecutor>(force);
+    }
+
     public void UseSelectTool() => Internals.ChangeController.TryStartExecutor<SelectToolExecutor>();
     public void UseSelectTool() => Internals.ChangeController.TryStartExecutor<SelectToolExecutor>();
 
 
     public void UseBrightnessTool() => Internals.ChangeController.TryStartExecutor<BrightnessToolExecutor>();
     public void UseBrightnessTool() => Internals.ChangeController.TryStartExecutor<BrightnessToolExecutor>();

+ 15 - 15
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ComplexShapeToolExecutor.cs → src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs

@@ -9,12 +9,13 @@ using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
 
 #nullable enable
 #nullable enable
 
 
-internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor where T : IShapeToolHandler
+internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor where T : IShapeToolHandler
 {
 {
     protected int StrokeWidth => toolbar.ToolSize;
     protected int StrokeWidth => toolbar.ToolSize;
 
 
@@ -67,25 +68,19 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
             return ExecutionState.Success;
             return ExecutionState.Success;
         }
         }
 
 
-        if (member is IVectorLayerHandler)
+        if (member is IVectorLayerHandler vectorLayerHandler)
         {
         {
-            var node = (VectorLayerNode)internals.Tracker.Document.FindMember(member.Id);
-
-            if (node == null)
-            {
-                return ExecutionState.Error;
-            }
-
-            if (node.ShapeData == null || !InitShapeData(node.ShapeData))
+            var shapeData = vectorLayerHandler.GetShapeData(document.AnimationHandler.ActiveFrameTime);
+            if (shapeData == null || !InitShapeData(shapeData))
             {
             {
                 ActiveMode = ShapeToolMode.Preview;
                 ActiveMode = ShapeToolMode.Preview;
                 return ExecutionState.Success;
                 return ExecutionState.Success;
             }
             }
 
 
-            toolbar.StrokeColor = node.ShapeData.StrokeColor.ToColor();
-            toolbar.FillColor = node.ShapeData.FillColor.ToColor();
-            toolbar.ToolSize = node.ShapeData.StrokeWidth;
-            toolbar.Fill = node.ShapeData.FillColor != Colors.Transparent;
+            toolbar.StrokeColor = shapeData.StrokeColor.ToColor();
+            toolbar.FillColor = shapeData.FillColor.ToColor();
+            toolbar.ToolSize = shapeData.StrokeWidth;
+            toolbar.Fill = shapeData.FillColor != Colors.Transparent;
             ActiveMode = ShapeToolMode.Transform;
             ActiveMode = ShapeToolMode.Transform;
         }
         }
         else
         else
@@ -99,7 +94,7 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
     protected abstract void DrawShape(VecI currentPos, double rotationRad, bool firstDraw);
     protected abstract void DrawShape(VecI currentPos, double rotationRad, bool firstDraw);
     protected abstract IAction SettingsChangedAction();
     protected abstract IAction SettingsChangedAction();
     protected abstract IAction TransformMovedAction(ShapeData data, ShapeCorners corners);
     protected abstract IAction TransformMovedAction(ShapeData data, ShapeCorners corners);
-    protected virtual bool InitShapeData(ShapeVectorData data) { return true; }
+    protected virtual bool InitShapeData(IReadOnlyShapeVectorData data) { return true; }
     protected abstract IAction EndDrawAction();
     protected abstract IAction EndDrawAction();
     protected virtual DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective;
     protected virtual DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective;
 
 
@@ -301,4 +296,9 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
         base.ForceStop();
         base.ForceStop();
         internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
         internals!.ActionAccumulator.AddFinishedActions(EndDrawAction());
     }
     }
+
+    protected override void StopTransformMode()
+    {
+        document!.TransformHandler.HideTransform();
+    }
 }
 }

+ 8 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/IPathExecutorFeature.cs

@@ -0,0 +1,8 @@
+using Drawie.Backend.Core.Vector;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+
+public interface IPathExecutorFeature : IExecutorFeature
+{
+    public void OnPathChanged(VectorPath path);
+}

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

@@ -72,7 +72,6 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
                 return ExecutionState.Success;
                 return ExecutionState.Success;
             }
             }
 
 
-
             if (!InitShapeData(data))
             if (!InitShapeData(data))
             {
             {
                 ActiveMode = ShapeToolMode.Preview;
                 ActiveMode = ShapeToolMode.Preview;
@@ -208,4 +207,9 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         var endDrawAction = EndDraw();
         var endDrawAction = EndDraw();
         internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
         internals!.ActionAccumulator.AddFinishedActions(endDrawAction);
     }
     }
+
+    protected override void StopTransformMode()
+    {
+        document!.LineToolOverlayHandler.Hide();
+    }
 }
 }

+ 0 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -3,7 +3,6 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
 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;

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

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

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

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

+ 2 - 6
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs

@@ -102,11 +102,7 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
         }
         }
     }
     }
 
 
-    private void StopTransformMode()
-    {
-        document!.TransformHandler.HideTransform();
-        document!.LineToolOverlayHandler.Hide();
-    }
+    protected abstract void StopTransformMode();
 
 
     public override void OnLeftMouseButtonDown(MouseOnCanvasEventArgs args)
     public override void OnLeftMouseButtonDown(MouseOnCanvasEventArgs args)
     {
     {
@@ -211,7 +207,7 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
     public abstract bool CanUndo { get; } 
     public abstract bool CanUndo { get; } 
     public abstract bool CanRedo { get; }
     public abstract bool CanRedo { get; }
 
 
-    public bool IsFeatureEnabled(IExecutorFeature feature)
+    public virtual bool IsFeatureEnabled(IExecutorFeature feature)
     {
     {
         if (feature is ITransformableExecutor)
         if (feature is ITransformableExecutor)
         {
         {

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

@@ -4,7 +4,6 @@ using Avalonia.Input;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;

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

@@ -7,10 +7,11 @@ using Drawie.Backend.Core.Numerics;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
 
-internal class VectorEllipseToolExecutor : ComplexShapeToolExecutor<IVectorEllipseToolHandler>
+internal class VectorEllipseToolExecutor : DrawableShapeToolExecutor<IVectorEllipseToolHandler>
 {
 {
     public override ExecutorType Type => ExecutorType.ToolLinked;
     public override ExecutorType Type => ExecutorType.ToolLinked;
     protected override DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_Shear_NoPerspective;
     protected override DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_Shear_NoPerspective;
@@ -20,7 +21,7 @@ internal class VectorEllipseToolExecutor : ComplexShapeToolExecutor<IVectorEllip
     
     
     private Matrix3X3 lastMatrix = Matrix3X3.Identity;
     private Matrix3X3 lastMatrix = Matrix3X3.Identity;
 
 
-    protected override bool InitShapeData(ShapeVectorData data)
+    protected override bool InitShapeData(IReadOnlyShapeVectorData data)
     {
     {
         if (data is not EllipseVectorData ellipseData)
         if (data is not EllipseVectorData ellipseData)
             return false;
             return false;

+ 251 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs

@@ -0,0 +1,251 @@
+using Avalonia.Media;
+using ChunkyImageLib.DataHolders;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.Models.Handlers;
+using PixiEditor.Models.Handlers.Tools;
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+using PixiEditor.Models.Handlers.Toolbars;
+using PixiEditor.Models.Tools;
+using Color = Drawie.Backend.Core.ColorsImpl.Color;
+using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+
+internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorFeature, IMidChangeUndoableExecutor
+{
+    private IStructureMemberHandler member;
+    private VectorPath startingPath;
+    private IVectorPathToolHandler vectorPathToolHandler;
+    private IBasicShapeToolbar toolbar;
+    private IColorsHandler colorHandler;
+
+    public override ExecutorType Type => ExecutorType.ToolLinked;
+
+    public bool CanUndo => document.PathOverlayHandler.HasUndo;
+    public bool CanRedo => document.PathOverlayHandler.HasRedo;
+
+    public override bool BlocksOtherActions => false;
+
+    private bool mouseDown;
+
+    public override ExecutionState Start()
+    {
+        vectorPathToolHandler = GetHandler<IVectorPathToolHandler>();
+
+        member = document.SelectedStructureMember;
+
+        if (member is null)
+        {
+            return ExecutionState.Error;
+        }
+
+        toolbar = (IBasicShapeToolbar)vectorPathToolHandler.Toolbar;
+        colorHandler = GetHandler<IColorsHandler>();
+
+        if (member is IVectorLayerHandler vectorLayerHandler)
+        {
+            var shapeData = vectorLayerHandler.GetShapeData(document.AnimationHandler.ActiveFrameTime);
+            bool wasNull = false;
+            if (shapeData is PathVectorData pathData)
+            {
+                startingPath = new VectorPath(pathData.Path);
+                startingPath.Transform(pathData.TransformationMatrix);
+            }
+            else if (shapeData is null)
+            {
+                wasNull = true;
+                startingPath = new VectorPath();
+            }
+            else
+            {
+                return ExecutionState.Error;
+            }
+
+            document.PathOverlayHandler.Show(startingPath, false);
+            if (controller.LeftMousePressed)
+            {
+                var snapped =
+                    document.SnappingHandler.SnappingController.GetSnapPoint(controller.LastPrecisePosition, out _,
+                        out _);
+                if (wasNull)
+                {
+                    startingPath.MoveTo((VecF)snapped);
+                }
+                else
+                {
+                    startingPath.LineTo((VecF)snapped);
+                }
+
+                if (toolbar.SyncWithPrimaryColor)
+                {
+                    toolbar.StrokeColor = colorHandler.PrimaryColor.ToColor();
+                    toolbar.FillColor = colorHandler.PrimaryColor.ToColor();
+                }
+
+                //below forces undo before starting new path
+                internals.ActionAccumulator.AddFinishedActions(new EndSetShapeGeometry_Action());
+
+                internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData()));
+            }
+        }
+        else
+        {
+            return ExecutionState.Error;
+        }
+
+        document.SnappingHandler.Remove(member.Id.ToString()); // This disables self-snapping
+        return ExecutionState.Success;
+    }
+
+    public override void OnPrecisePositionChange(VecD pos)
+    {
+        if (mouseDown)
+        {
+            return;
+        }
+
+        VecD mouseSnap =
+            document.SnappingHandler.SnappingController.GetSnapPoint(pos, out string snapXAxis,
+                out string snapYAxis);
+        HighlightSnapping(snapXAxis, snapYAxis);
+
+        if (!string.IsNullOrEmpty(snapXAxis) || !string.IsNullOrEmpty(snapYAxis))
+        {
+            document.SnappingHandler.SnappingController.HighlightedPoint = mouseSnap;
+        }
+        else
+        {
+            document.SnappingHandler.SnappingController.HighlightedPoint = null;
+        }
+    }
+
+    public override void OnLeftMouseButtonDown(MouseOnCanvasEventArgs args)
+    {
+        if (startingPath.IsClosed)
+        {
+            if (NeedsNewLayer(document.SelectedStructureMember, document.AnimationHandler.ActiveFrameTime))
+            {
+                Guid? created =
+                    document.Operations.CreateStructureMember(typeof(VectorLayerNode), ActionSource.Automated);
+
+                if (created is null) return;
+
+                document.Operations.SetSelectedMember(created.Value);
+            }
+
+            return;
+        }
+
+        VecD mouseSnap =
+            document.SnappingHandler.SnappingController.GetSnapPoint(args.PositionOnCanvas, out _,
+                out _);
+
+        if (startingPath.Points.Count > 0 && startingPath.Points[0] == (VecF)mouseSnap)
+        {
+            startingPath.Close();
+        }
+        else
+        {
+            startingPath.LineTo((VecF)mouseSnap);
+        }
+
+        PathVectorData vectorData = ConstructShapeData();
+
+        internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, vectorData));
+        mouseDown = true;
+    }
+
+    public override void OnLeftMouseButtonUp(VecD pos)
+    {
+        mouseDown = false;
+    }
+
+    public override void OnColorChanged(Color color, bool primary)
+    {
+        if (primary && toolbar.SyncWithPrimaryColor)
+        {
+            toolbar.StrokeColor = color.ToColor();
+            toolbar.FillColor = color.ToColor();
+        }
+    }
+
+    public override void OnSettingsChanged(string name, object value)
+    {
+        internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData()));
+    }
+
+    public override void ForceStop()
+    {
+        document.PathOverlayHandler.Hide();
+        document.SnappingHandler.AddFromBounds(member.Id.ToString(), () => member.TightBounds ?? RectD.Empty);
+        HighlightSnapping(null, null);
+        internals.ActionAccumulator.AddFinishedActions(new EndSetShapeGeometry_Action());
+    }
+
+    private PathVectorData ConstructShapeData()
+    {
+        return new PathVectorData(new VectorPath(startingPath))
+        {
+            StrokeWidth = toolbar.ToolSize,
+            StrokeColor = toolbar.StrokeColor.ToColor(),
+            FillColor = toolbar.Fill ? toolbar.FillColor.ToColor() : Colors.Transparent,
+        };
+    }
+
+    public void OnPathChanged(VectorPath path)
+    {
+        if (document.PathOverlayHandler.IsActive)
+        {
+            startingPath = path;
+            internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData()));
+        }
+    }
+
+    public bool IsFeatureEnabled(IExecutorFeature feature)
+    {
+        return feature switch
+        {
+            IPathExecutorFeature _ => true,
+            IMidChangeUndoableExecutor _ => true,
+            ITransformableExecutor _ => true,
+            _ => false
+        };
+    }
+
+    public void OnMidChangeUndo()
+    {
+        document.PathOverlayHandler.Undo();
+    }
+
+    public void OnMidChangeRedo()
+    {
+        document.PathOverlayHandler.Redo();
+    }
+
+    protected void HighlightSnapping(string? snapX, string? snapY)
+    {
+        document!.SnappingHandler.SnappingController.HighlightedXAxis = snapX;
+        document!.SnappingHandler.SnappingController.HighlightedYAxis = snapY;
+        document.SnappingHandler.SnappingController.HighlightedPoint = null;
+    }
+
+    private bool NeedsNewLayer(IStructureMemberHandler? member, KeyFrameTime frameTime)
+    {
+        var shapeData = (member as IVectorLayerHandler).GetShapeData(frameTime);
+        if (shapeData is null)
+        {
+            return false;
+        }
+
+        return shapeData is not IReadOnlyPathData pathData || pathData.Path.IsClosed;
+    }
+}

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

@@ -7,10 +7,11 @@ using Drawie.Backend.Core.Numerics;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
 
-internal class VectorRectangleToolExecutor : ComplexShapeToolExecutor<IVectorRectangleToolHandler>
+internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRectangleToolHandler>
 {
 {
     public override ExecutorType Type => ExecutorType.ToolLinked;
     public override ExecutorType Type => ExecutorType.ToolLinked;
     protected override DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_Shear_NoPerspective;
     protected override DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_Shear_NoPerspective;
@@ -20,7 +21,7 @@ internal class VectorRectangleToolExecutor : ComplexShapeToolExecutor<IVectorRec
 
 
     private Matrix3X3 lastMatrix = Matrix3X3.Identity;
     private Matrix3X3 lastMatrix = Matrix3X3.Identity;
 
 
-    protected override bool InitShapeData(ShapeVectorData data)
+    protected override bool InitShapeData(IReadOnlyShapeVectorData data)
     {
     {
         if (data is not RectangleVectorData rectData)
         if (data is not RectangleVectorData rectData)
             return false;
             return false;

+ 2 - 1
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -6,7 +6,7 @@ using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
@@ -34,6 +34,7 @@ internal interface IDocument : IHandler
     public string CoordinatesString { get; set; }
     public string CoordinatesString { get; set; }
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }
     public ITransformHandler TransformHandler { get; }
     public ITransformHandler TransformHandler { get; }
+    public IPathOverlayHandler PathOverlayHandler { get; }
     public bool Busy { get; set; }
     public bool Busy { get; set; }
     public ILineOverlayHandler LineToolOverlayHandler { get; }
     public ILineOverlayHandler LineToolOverlayHandler { get; }
     public bool HorizontalSymmetryAxisEnabledBindable { get; }
     public bool HorizontalSymmetryAxisEnabledBindable { get; }

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

@@ -1,4 +1,5 @@
-using PixiEditor.Models.Layers;
+using PixiEditor.ChangeableDocument;
+using PixiEditor.Models.Layers;
 
 
 namespace PixiEditor.Models.Handlers;
 namespace PixiEditor.Models.Handlers;
 
 
@@ -10,4 +11,5 @@ internal interface IDocumentOperations
     public void MoveStructureMember(Guid memberGuidValue, Guid target, StructureMemberPlacement placement);
     public void MoveStructureMember(Guid memberGuidValue, Guid target, StructureMemberPlacement placement);
     public void SetSelectedMember(Guid memberId);
     public void SetSelectedMember(Guid memberId);
     public void ClearSoftSelectedMembers();
     public void ClearSoftSelectedMembers();
+    public Guid? CreateStructureMember(Type type, ActionSource source, string? name = null);
 }
 }

+ 15 - 0
src/PixiEditor/Models/Handlers/IPathOverlayHandler.cs

@@ -0,0 +1,15 @@
+using Drawie.Backend.Core.Vector;
+
+namespace PixiEditor.Models.Handlers;
+
+public interface IPathOverlayHandler : IHandler
+{
+    public void Show(VectorPath path, bool showApplyButton);
+    public void Hide();
+    public event Action<VectorPath> PathChanged;
+    public bool IsActive { get; }
+    public bool HasUndo { get; }
+    public bool HasRedo { get; }
+    public void Undo();
+    public void Redo();
+}

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

@@ -8,4 +8,5 @@ public interface ISnappingHandler
     public SnappingController SnappingController { get; }
     public SnappingController SnappingController { get; }
     public void Remove(string id);
     public void Remove(string id);
     public void AddFromBounds(string id, Func<RectD> tightBounds);
     public void AddFromBounds(string id, Func<RectD> tightBounds);
+    public void AddFromPoint(string id, Func<VecD> func);
 }
 }

+ 6 - 0
src/PixiEditor/Models/Handlers/Tools/IVectorPathToolHandler.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.Handlers.Tools;
+
+internal interface IVectorPathToolHandler : IToolHandler
+{
+    
+}

+ 9 - 0
src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs

@@ -66,4 +66,13 @@ public class ByteBuilder
             AddVecD(point);
             AddVecD(point);
         }
         }
     }
     }
+
+    public void AddString(string str)
+    {
+        AddInt(str.Length);
+        foreach (var c in str)
+        {
+            AddInt(c);
+        }
+    }
 }
 }

+ 15 - 1
src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs

@@ -1,4 +1,5 @@
-using Drawie.Backend.Core.ColorsImpl;
+using System.Text;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
@@ -81,4 +82,17 @@ public class ByteExtractor
         
         
         return value;
         return value;
     }
     }
+
+    public string GetString()
+    {
+        int length = GetInt();
+        StringBuilder builder = new StringBuilder();
+        
+        for (int i = 0; i < length; i++)
+        {
+            builder.Append((char)GetInt());
+        }
+        
+        return builder.ToString();
+    }
 }
 }

+ 33 - 0
src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs

@@ -0,0 +1,33 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.Serialization.Factories;
+
+internal class VectorPathSerializationFactory : VectorShapeSerializationFactory<PathVectorData> 
+{
+    public override string DeserializationId { get; } = "PixiEditor.PathData";
+
+    protected override void AddSpecificData(ByteBuilder builder, PathVectorData original)
+    {
+        builder.AddString(original.Path.ToSvgPathData());
+    }
+
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
+        int strokeWidth, out PathVectorData original)
+    {
+        string path = extractor.GetString();
+
+        original = new PathVectorData(VectorPath.FromSvgPath(path))
+        {
+            StrokeColor = strokeColor,
+            FillColor = fillColor,
+            StrokeWidth = strokeWidth,
+            TransformationMatrix = matrix
+        };
+
+        return true;
+    }
+}

+ 1 - 0
src/PixiEditor/Styles/PixiEditor.Handles.axaml

@@ -61,6 +61,7 @@
 
 
             <system:String x:Key="RotateHandle">M -1.26 -0.455 Q 0 0.175 1.26 -0.455 L 1.12 -0.735 L 2.1 -0.7 L 1.54 0.105 L 1.4 -0.175 Q 0 0.525 -1.4 -0.175 L -1.54 0.105 L -2.1 -0.7 L -1.12 -0.735 Z</system:String>
             <system:String x:Key="RotateHandle">M -1.26 -0.455 Q 0 0.175 1.26 -0.455 L 1.12 -0.735 L 2.1 -0.7 L 1.54 0.105 L 1.4 -0.175 Q 0 0.525 -1.4 -0.175 L -1.54 0.105 L -2.1 -0.7 L -1.12 -0.735 Z</system:String>
             <SolidColorBrush x:Key="HandleBrush" Color="{DynamicResource GlyphColor}"/>
             <SolidColorBrush x:Key="HandleBrush" Color="{DynamicResource GlyphColor}"/>
+            <SolidColorBrush x:Key="SelectedHandleBrush" Color="{DynamicResource ThemeAccent2Color}"/>
             <SolidColorBrush x:Key="HandleBackgroundBrush" Color="{DynamicResource GlyphBackground}"/>
             <SolidColorBrush x:Key="HandleBackgroundBrush" Color="{DynamicResource GlyphBackground}"/>
 
 
             <system:Double x:Key="HandleSize">20</system:Double>
             <system:Double x:Key="HandleSize">20</system:Double>

+ 33 - 6
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -14,6 +14,7 @@ using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -87,7 +88,7 @@ internal partial class DocumentViewModel
         float resizeFactorY = (float)exportSize.Y / Height;
         float resizeFactorY = (float)exportSize.Y / Height;
         VecD resizeFactor = new VecD(resizeFactorX, resizeFactorY);
         VecD resizeFactor = new VecD(resizeFactorX, resizeFactorY);
 
 
-        AddElements(NodeGraph.StructureTree.Members, svgDocument, atTime, resizeFactor, vectorExportConfig);
+        AddElements(NodeGraph.StructureTree.Members.Reverse().ToList(), svgDocument, atTime, resizeFactor, vectorExportConfig);
 
 
         return svgDocument;
         return svgDocument;
     }
     }
@@ -101,7 +102,7 @@ internal partial class DocumentViewModel
             {
             {
                 var group = new SvgGroup();
                 var group = new SvgGroup();
 
 
-                AddElements(folderNodeViewModel.Children, group, atTime, resizeFactor, vectorExportConfig);
+                AddElements(folderNodeViewModel.Children.Reverse().ToList(), group, atTime, resizeFactor, vectorExportConfig);
                 elementContainer.Children.Add(group);
                 elementContainer.Children.Add(group);
             }
             }
 
 
@@ -137,6 +138,10 @@ internal partial class DocumentViewModel
         {
         {
             elementToAdd = AddLine(resizeFactor, lineData);
             elementToAdd = AddLine(resizeFactor, lineData);
         }
         }
+        else if (vectorNode.ShapeData is IReadOnlyPathData shapeData)
+        {
+            elementToAdd = AddVectorPath(resizeFactor, shapeData);
+        }
 
 
         if (elementToAdd != null)
         if (elementToAdd != null)
         {
         {
@@ -151,12 +156,12 @@ internal partial class DocumentViewModel
         line.Y1.Unit = SvgNumericUnit.FromUserUnits(lineData.Start.Y * resizeFactor.Y);
         line.Y1.Unit = SvgNumericUnit.FromUserUnits(lineData.Start.Y * resizeFactor.Y);
         line.X2.Unit = SvgNumericUnit.FromUserUnits(lineData.End.X * resizeFactor.X);
         line.X2.Unit = SvgNumericUnit.FromUserUnits(lineData.End.X * resizeFactor.X);
         line.Y2.Unit = SvgNumericUnit.FromUserUnits(lineData.End.Y * resizeFactor.Y);
         line.Y2.Unit = SvgNumericUnit.FromUserUnits(lineData.End.Y * resizeFactor.Y);
-        
+
         line.Stroke.Unit = SvgColorUnit.FromRgba(lineData.StrokeColor.R, lineData.StrokeColor.G,
         line.Stroke.Unit = SvgColorUnit.FromRgba(lineData.StrokeColor.R, lineData.StrokeColor.G,
             lineData.StrokeColor.B, lineData.StrokeColor.A);
             lineData.StrokeColor.B, lineData.StrokeColor.A);
         line.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(lineData.StrokeWidth * resizeFactor.X);
         line.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(lineData.StrokeWidth * resizeFactor.X);
         line.Transform.Unit = new SvgTransformUnit(lineData.TransformationMatrix);
         line.Transform.Unit = new SvgTransformUnit(lineData.TransformationMatrix);
-        
+
         return line;
         return line;
     }
     }
 
 
@@ -194,10 +199,31 @@ internal partial class DocumentViewModel
             rectangleData.StrokeColor.B, rectangleData.StrokeColor.A);
             rectangleData.StrokeColor.B, rectangleData.StrokeColor.A);
         rect.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(rectangleData.StrokeWidth);
         rect.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(rectangleData.StrokeWidth);
         rect.Transform.Unit = new SvgTransformUnit(rectangleData.TransformationMatrix);
         rect.Transform.Unit = new SvgTransformUnit(rectangleData.TransformationMatrix);
-        
+
         return rect;
         return rect;
     }
     }
 
 
+    private SvgPath AddVectorPath(VecD resizeFactor, IReadOnlyPathData data)
+    {
+        var path = new SvgPath();
+        if (data.Path != null)
+        {
+            string pathData = data.Path.ToSvgPathData();
+            path.PathData.Unit = new SvgStringUnit(pathData);
+            Matrix3X3 transform = data.TransformationMatrix;
+            transform = transform.PostConcat(Matrix3X3.CreateScale((float)resizeFactor.X, (float)resizeFactor.Y));
+            path.Transform.Unit = new SvgTransformUnit?(new SvgTransformUnit(transform));
+
+            path.Fill.Unit =
+                SvgColorUnit.FromRgba(data.FillColor.R, data.FillColor.G, data.FillColor.B, data.FillColor.A);
+            path.Stroke.Unit = SvgColorUnit.FromRgba(data.StrokeColor.R, data.StrokeColor.G, data.StrokeColor.B,
+                data.StrokeColor.A);
+            path.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(data.StrokeWidth);
+        }
+
+        return path;
+    }
+
     private void AddSvgImage(IElementContainer elementContainer, KeyFrameTime atTime, INodeHandler member,
     private void AddSvgImage(IElementContainer elementContainer, KeyFrameTime atTime, INodeHandler member,
         VecD resizeFactor, bool useNearestNeighborForImageUpscaling)
         VecD resizeFactor, bool useNearestNeighborForImageUpscaling)
     {
     {
@@ -210,7 +236,7 @@ internal partial class DocumentViewModel
         Image toSave = null;
         Image toSave = null;
         DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
         DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
         {
         {
-            using Surface surface = new Surface(SizeBindable); 
+            using Surface surface = new Surface(SizeBindable);
             Renderer.RenderLayer(surface.DrawingSurface, imageNode.Id, ChunkResolution.Full, atTime.Frame);
             Renderer.RenderLayer(surface.DrawingSurface, imageNode.Id, ChunkResolution.Full, atTime.Frame);
 
 
             toSave = surface.DrawingSurface.Snapshot((RectI)tightBounds.Value);
             toSave = surface.DrawingSurface.Snapshot((RectI)tightBounds.Value);
@@ -221,6 +247,7 @@ internal partial class DocumentViewModel
         elementContainer.Children.Add(image);
         elementContainer.Children.Add(image);
     }
     }
 
 
+
     private static SvgImage CreateImageElement(VecD resizeFactor, RectD tightBounds,
     private static SvgImage CreateImageElement(VecD resizeFactor, RectD tightBounds,
         Image toSerialize, bool useNearestNeighborForImageUpscaling)
         Image toSerialize, bool useNearestNeighborForImageUpscaling)
     {
     {

+ 10 - 2
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -31,7 +31,7 @@ using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
@@ -201,6 +201,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     ISnappingHandler IDocument.SnappingHandler => SnappingViewModel;
     ISnappingHandler IDocument.SnappingHandler => SnappingViewModel;
     public IReadOnlyCollection<Guid> SelectedMembers => GetSelectedMembers().AsReadOnly();
     public IReadOnlyCollection<Guid> SelectedMembers => GetSelectedMembers().AsReadOnly();
     public DocumentTransformViewModel TransformViewModel { get; }
     public DocumentTransformViewModel TransformViewModel { get; }
+    public PathOverlayViewModel PathOverlayViewModel { get; }
     public ReferenceLayerViewModel ReferenceLayerViewModel { get; }
     public ReferenceLayerViewModel ReferenceLayerViewModel { get; }
     public LineToolOverlayViewModel LineToolOverlayViewModel { get; }
     public LineToolOverlayViewModel LineToolOverlayViewModel { get; }
     public AnimationDataViewModel AnimationDataViewModel { get; }
     public AnimationDataViewModel AnimationDataViewModel { get; }
@@ -210,6 +211,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     INodeGraphHandler IDocument.NodeGraphHandler => NodeGraph;
     INodeGraphHandler IDocument.NodeGraphHandler => NodeGraph;
     IDocumentOperations IDocument.Operations => Operations;
     IDocumentOperations IDocument.Operations => Operations;
     ITransformHandler IDocument.TransformHandler => TransformViewModel;
     ITransformHandler IDocument.TransformHandler => TransformViewModel;
+    IPathOverlayHandler IDocument.PathOverlayHandler => PathOverlayViewModel;
     ILineOverlayHandler IDocument.LineToolOverlayHandler => LineToolOverlayViewModel;
     ILineOverlayHandler IDocument.LineToolOverlayHandler => LineToolOverlayViewModel;
     IReferenceLayerHandler IDocument.ReferenceLayerHandler => ReferenceLayerViewModel;
     IReferenceLayerHandler IDocument.ReferenceLayerHandler => ReferenceLayerViewModel;
     IAnimationHandler IDocument.AnimationHandler => AnimationDataViewModel;
     IAnimationHandler IDocument.AnimationHandler => AnimationDataViewModel;
@@ -232,7 +234,13 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
         TransformViewModel = new(this);
         TransformViewModel = new(this);
         TransformViewModel.TransformMoved += (_, args) => Internals.ChangeController.TransformMovedInlet(args);
         TransformViewModel.TransformMoved += (_, args) => Internals.ChangeController.TransformMovedInlet(args);
-
+        
+        PathOverlayViewModel = new(this, Internals);
+        PathOverlayViewModel.PathChanged += path =>
+        {
+            Internals.ChangeController.PathOverlayChangedInlet(path);
+        };
+        
         LineToolOverlayViewModel = new();
         LineToolOverlayViewModel = new();
         LineToolOverlayViewModel.LineMoved += (_, args) =>
         LineToolOverlayViewModel.LineMoved += (_, args) =>
             Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
             Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);

+ 5 - 0
src/PixiEditor/ViewModels/Document/SnappingViewModel.cs

@@ -29,6 +29,11 @@ public class SnappingViewModel : PixiObservableObject, ISnappingHandler
         SnappingController.AddBounds(id, tightBounds);
         SnappingController.AddBounds(id, tightBounds);
     }
     }
 
 
+    public void AddFromPoint(string id, Func<VecD> func)
+    {
+        SnappingController.AddXYAxis(id, func);
+    }
+
     public void Remove(string id)
     public void Remove(string id)
     {
     {
         SnappingController.RemoveAll(id);
         SnappingController.RemoveAll(id);

+ 81 - 0
src/PixiEditor/ViewModels/Document/TransformOverlays/PathOverlayUndoStack.cs

@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+
+namespace PixiEditor.ViewModels.Document.TransformOverlays;
+
+internal class PathOverlayUndoStack<TState> : IDisposable where TState : class
+{
+    private struct StackItem<TState>
+    {
+        public TState State { get; set; }
+
+        public StackItem(TState state)
+        {
+            State = state;
+        }
+    }
+
+    private Stack<StackItem<TState>> undoStack = new();
+    private Stack<StackItem<TState>> redoStack = new();
+    private StackItem<TState>? current;
+
+    public void AddState(TState state)
+    {
+        foreach (var item in redoStack)
+            (item.State as IDisposable)?.Dispose();
+        redoStack.Clear();
+        
+        if (current is not null)
+            undoStack.Push(current.Value);
+
+        current = new(state);
+    }
+
+    public TState? PeekCurrent() => current?.State;
+
+    public int UndoCount => undoStack.Count;
+    public int RedoCount => redoStack.Count;
+
+    public TState? Undo()
+    {
+        if (current is null || undoStack.Count == 0)
+            return null;
+
+        DoUndoStep();
+        return current.Value.State;
+    }
+
+    public TState? Redo()
+    {
+        if (current is null || redoStack.Count == 0)
+            return null;
+
+        DoRedoStep();
+
+        return current.Value.State;
+    }
+
+    private void DoUndoStep()
+    {
+        redoStack.Push(current.Value);
+        current = undoStack.Pop();
+    }
+
+    private void DoRedoStep()
+    {
+        undoStack.Push(current.Value);
+        current = redoStack.Pop();
+    }
+
+    public void Dispose()
+    {
+        foreach (var item in undoStack)
+            (item.State as IDisposable)?.Dispose();
+        foreach (var item in redoStack)
+            (item.State as IDisposable)?.Dispose();
+        (current?.State as IDisposable)?.Dispose();
+
+        undoStack.Clear();
+        redoStack.Clear();
+        current = null;
+    }
+}

+ 118 - 0
src/PixiEditor/ViewModels/Document/TransformOverlays/PathOverlayViewModel.cs

@@ -0,0 +1,118 @@
+using System.Windows.Input;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Drawie.Backend.Core.Vector;
+using PixiEditor.Models.DocumentModels;
+using PixiEditor.Models.Handlers;
+
+namespace PixiEditor.ViewModels.Document.TransformOverlays;
+
+internal class PathOverlayViewModel : ObservableObject, IPathOverlayHandler
+{
+    private DocumentViewModel documentViewModel;
+    private DocumentInternalParts internals;
+
+    private PathOverlayUndoStack<VectorPath>? undoStack = null;
+
+    private VectorPath path;
+
+    public VectorPath Path
+    {
+        get => path;
+        set
+        {
+            var old = path;
+            if (SetProperty(ref path, value))
+            {
+                if (old != null)
+                {
+                    old.Changed -= PathDataChanged;
+                }
+
+                if (value != null)
+                {
+                    value.Changed += PathDataChanged;
+                }
+
+                PathChanged?.Invoke(value);
+            }
+        }
+    }
+
+    public event Action<VectorPath>? PathChanged;
+    public bool IsActive { get; set; }
+    public bool HasUndo => undoStack.UndoCount > 0;
+    public bool HasRedo => undoStack.RedoCount > 0;
+
+    public RelayCommand<VectorPath> AddToUndoCommand { get; }
+    
+    private bool showApplyButton;
+
+    public bool ShowApplyButton
+    {
+        get => showApplyButton;
+        set => SetProperty(ref showApplyButton, value);
+    }
+
+    private bool suppressUndo = false;
+
+    public PathOverlayViewModel(DocumentViewModel documentViewModel, DocumentInternalParts internals)
+    {
+        this.documentViewModel = documentViewModel;
+        this.internals = internals;
+
+        AddToUndoCommand = new RelayCommand<VectorPath>(AddToUndo);
+        undoStack = new PathOverlayUndoStack<VectorPath>();
+    }
+
+    public void Show(VectorPath newPath, bool showApplyButton)
+    {
+        if (IsActive)
+        {
+            return;
+        }
+
+        undoStack?.Dispose();
+        undoStack = new PathOverlayUndoStack<VectorPath>();
+        undoStack.AddState(new VectorPath(newPath));
+        Path = newPath;
+        IsActive = true;
+        ShowApplyButton = showApplyButton;
+    }
+
+    public void Hide()
+    {
+        IsActive = false;
+        Path = null;
+        ShowApplyButton = false;
+    }
+
+    public void Undo()
+    {
+        suppressUndo = true;
+        Path = new VectorPath(undoStack?.Undo());
+        suppressUndo = false;
+    }
+
+    public void Redo()
+    {
+        suppressUndo = true;
+        Path = new VectorPath(undoStack?.Redo());
+        suppressUndo = false;
+    }
+
+    private void AddToUndo(VectorPath toAdd)
+    {
+        if (suppressUndo)
+        {
+            return;
+        }
+
+        undoStack?.AddState(new VectorPath(path));
+    }
+
+    private void PathDataChanged(VectorPath path)
+    {
+        AddToUndo(path);
+    }
+}

+ 1 - 3
src/PixiEditor/ViewModels/Document/TransformOverlays/TransformOverlayUndoStack.cs

@@ -1,6 +1,4 @@
-using System.Collections.Generic;
-
-namespace PixiEditor.ViewModels.Document.TransformOverlays;
+namespace PixiEditor.ViewModels.Document.TransformOverlays;
 internal class TransformOverlayUndoStack<TState> where TState : struct
 internal class TransformOverlayUndoStack<TState> where TState : struct
 {
 {
     private struct StackItem<TState>
     private struct StackItem<TState>

+ 1 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/Toolbar.cs

@@ -29,7 +29,7 @@ internal abstract class Toolbar : ObservableObject, IToolbar
     /// </summary>
     /// </summary>
     /// <param name="name">Setting name, non case sensitive.</param>
     /// <param name="name">Setting name, non case sensitive.</param>
     /// <returns>Generic Setting.</returns>
     /// <returns>Generic Setting.</returns>
-    public virtual Setting GetSetting(string name)
+    public virtual Setting? GetSetting(string name)
     {
     {
         return Settings.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.CurrentCultureIgnoreCase));
         return Settings.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.CurrentCultureIgnoreCase));
     }
     }

+ 0 - 1
src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -3,7 +3,6 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.Vector;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;

+ 72 - 0
src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs

@@ -0,0 +1,72 @@
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Handlers;
+using PixiEditor.Models.Handlers.Tools;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Document;
+using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
+
+namespace PixiEditor.ViewModels.Tools.Tools;
+
+internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
+{
+    public override string ToolNameLocalizationKey => "PATH_TOOL";
+    public override Type[]? SupportedLayerTypes { get; } = [typeof(IVectorLayerHandler)];
+    public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
+    public override LocalizedString Tooltip => new LocalizedString("PATH_TOOL_TOOLTIP", Shortcut);
+
+    public override string DefaultIcon => PixiPerfectIcons.VectorPen; 
+    public override bool StopsLinkedToolOnUse => false;
+
+    private bool isActivated;
+
+    public VectorPathToolViewModel()
+    {
+        var fillSetting = Toolbar.GetSetting(nameof(BasicShapeToolbar.Fill));
+        if (fillSetting != null)
+        {
+            fillSetting.Value = false;
+        }
+    }
+
+    public override void UseTool(VecD pos)
+    {
+        var doc =
+            ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
+
+        if (doc is null || isActivated) return;
+        
+        if (!doc.PathOverlayViewModel.IsActive)
+        {
+            doc?.Tools.UseVectorPathTool();
+            isActivated = true;
+        }
+    }
+
+    public override void OnSelected(bool restoring)
+    {
+        if (restoring) return;
+
+        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorPathTool();
+        isActivated = true;
+    }
+
+    public override void OnDeselecting(bool transient)
+    {
+        if (!transient)
+        {
+            ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TryStopToolLinkedExecutor();
+            isActivated = false;
+        }
+    }
+
+    protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
+    {
+        OnDeselecting(false);
+        OnSelected(false);
+    }
+}

+ 1 - 1
src/PixiEditor/Views/Main/Tools/ToolPickerButton.axaml

@@ -17,7 +17,7 @@
     <Button Command="{xaml:Command PixiEditor.Tools.SelectTool, UseProvided=true}"
     <Button Command="{xaml:Command PixiEditor.Tools.SelectTool, UseProvided=true}"
             CommandParameter="{Binding}"
             CommandParameter="{Binding}"
             Width="44" Height="34"
             Width="44" Height="34"
-            ui:Translator.TooltipKey="{Binding DisplayName}"
+            ui:Translator.TooltipLocalizedString="{Binding Tooltip}"
             Background="{DynamicResource ThemeBackgroundBrush1}">
             Background="{DynamicResource ThemeBackgroundBrush1}">
         <Button.Template>
         <Button.Template>
             <ControlTemplate>
             <ControlTemplate>

+ 1 - 0
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml

@@ -182,6 +182,7 @@
                     <MultiBinding.Bindings>
                     <MultiBinding.Bindings>
                         <Binding ElementName="vpUc" Path="Document.TransformViewModel.ShowTransformControls" />
                         <Binding ElementName="vpUc" Path="Document.TransformViewModel.ShowTransformControls" />
                         <Binding ElementName="vpUc" Path="Document.LineToolOverlayViewModel.ShowApplyButton" />
                         <Binding ElementName="vpUc" Path="Document.LineToolOverlayViewModel.ShowApplyButton" />
+                        <Binding ElementName="vpUc" Path="Document.PathOverlayViewModel.ShowApplyButton" />
                     </MultiBinding.Bindings>
                     </MultiBinding.Bindings>
                 </MultiBinding>
                 </MultiBinding>
             </Button.IsVisible>
             </Button.IsVisible>

+ 28 - 19
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -10,6 +10,7 @@ using PixiEditor.ViewModels;
 using PixiEditor.Views.Overlays;
 using PixiEditor.Views.Overlays;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 using PixiEditor.Views.Overlays.LineToolOverlay;
 using PixiEditor.Views.Overlays.LineToolOverlay;
+using PixiEditor.Views.Overlays.PathOverlay;
 using PixiEditor.Views.Overlays.Pointers;
 using PixiEditor.Views.Overlays.Pointers;
 using PixiEditor.Views.Overlays.SelectionOverlay;
 using PixiEditor.Views.Overlays.SelectionOverlay;
 using PixiEditor.Views.Overlays.SymmetryOverlay;
 using PixiEditor.Views.Overlays.SymmetryOverlay;
@@ -29,6 +30,7 @@ internal class ViewportOverlays
     private ReferenceLayerOverlay referenceLayerOverlay;
     private ReferenceLayerOverlay referenceLayerOverlay;
     private SnappingOverlay snappingOverlay;
     private SnappingOverlay snappingOverlay;
     private BrushShapeOverlay brushShapeOverlay;
     private BrushShapeOverlay brushShapeOverlay;
+    private VectorPathOverlay vectorPathOverlay;
 
 
     public void Init(Viewport viewport)
     public void Init(Viewport viewport)
     {
     {
@@ -56,6 +58,9 @@ internal class ViewportOverlays
 
 
         brushShapeOverlay = new BrushShapeOverlay();
         brushShapeOverlay = new BrushShapeOverlay();
         BindMouseOverlayPointer();
         BindMouseOverlayPointer();
+        
+        vectorPathOverlay = new VectorPathOverlay();
+        BindVectorPathOverlay();
 
 
         Viewport.ActiveOverlays.Add(gridLinesOverlay);
         Viewport.ActiveOverlays.Add(gridLinesOverlay);
         Viewport.ActiveOverlays.Add(referenceLayerOverlay);
         Viewport.ActiveOverlays.Add(referenceLayerOverlay);
@@ -63,6 +68,7 @@ internal class ViewportOverlays
         Viewport.ActiveOverlays.Add(symmetryOverlay);
         Viewport.ActiveOverlays.Add(symmetryOverlay);
         Viewport.ActiveOverlays.Add(lineToolOverlay);
         Viewport.ActiveOverlays.Add(lineToolOverlay);
         Viewport.ActiveOverlays.Add(transformOverlay);
         Viewport.ActiveOverlays.Add(transformOverlay);
+        Viewport.ActiveOverlays.Add(vectorPathOverlay);
         Viewport.ActiveOverlays.Add(snappingOverlay);
         Viewport.ActiveOverlays.Add(snappingOverlay);
         Viewport.ActiveOverlays.Add(brushShapeOverlay);
         Viewport.ActiveOverlays.Add(brushShapeOverlay);
     }
     }
@@ -333,6 +339,28 @@ internal class ViewportOverlays
         transformOverlay.Bind(TransformOverlay.ShowHandlesProperty, showHandlesBinding);
         transformOverlay.Bind(TransformOverlay.ShowHandlesProperty, showHandlesBinding);
         transformOverlay.Bind(TransformOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
         transformOverlay.Bind(TransformOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
     }
     }
+    
+    private void BindVectorPathOverlay()
+    {
+        Binding pathBinding = new()
+        {
+            Source = Viewport, Path = "Document.PathOverlayViewModel.Path", Mode = BindingMode.TwoWay
+        };
+        
+        Binding addToUndoCommandBinding = new()
+        {
+            Source = Viewport, Path = "Document.PathOverlayViewModel.AddToUndoCommand", Mode = BindingMode.OneWay
+        };
+        
+        Binding snappingBinding = new()
+        {
+            Source = Viewport, Path = "Document.SnappingViewModel.SnappingController", Mode = BindingMode.OneWay
+        };
+
+        vectorPathOverlay.Bind(VectorPathOverlay.PathProperty, pathBinding);
+        vectorPathOverlay.Bind(VectorPathOverlay.AddToUndoCommandProperty, addToUndoCommandBinding);
+        vectorPathOverlay.Bind(VectorPathOverlay.SnappingControllerProperty, snappingBinding);
+    }
 
 
     private void BindSnappingOverlay()
     private void BindSnappingOverlay()
     {
     {
@@ -344,25 +372,6 @@ internal class ViewportOverlays
         snappingOverlay.Bind(SnappingOverlay.SnappingControllerProperty, snappingControllerBinding);
         snappingOverlay.Bind(SnappingOverlay.SnappingControllerProperty, snappingControllerBinding);
     }
     }
 
 
-/**  <brushShapeOverlay:BrushShapeOverlay
-               DataContext="{Binding ElementName=vpUc}"
-               RenderTransform="{Binding #scene.CanvasTransform}"
-               RenderTransformOrigin="0, 0"
-               Name="brushShapeOverlay"
-               Focusable="False" ZIndex="6"
-               IsHitTestVisible="False"
-               ZoomScale="{Binding #scene.Scale}"
-               Scene="{Binding #scene, Mode=OneTime}"
-               BrushSize="{Binding ToolsSubViewModel.ActiveBasicToolbar.ToolSize, Source={viewModels:MainVM}}"
-               BrushShape="{Binding ToolsSubViewModel.ActiveTool.BrushShape, Source={viewModels:MainVM}, FallbackValue={x:Static brushShapeOverlay:BrushShape.Hidden}}"
-               FlowDirection="LeftToRight">
-               <brushShapeOverlay:BrushShapeOverlay.IsVisible>
-                   <MultiBinding Converter="{converters:AllTrueConverter}">
-                       <Binding Path="!Document.TransformViewModel.TransformActive" />
-                       <Binding Path="IsOverCanvas" />
-                   </MultiBinding>
-               </brushShapeOverlay:BrushShapeOverlay.IsVisible>
-           </brushShapeOverlay:BrushShapeOverlay>*/
     private void BindMouseOverlayPointer()
     private void BindMouseOverlayPointer()
     {
     {
         Binding isTransformingBinding = new()
         Binding isTransformingBinding = new()

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

@@ -2,7 +2,7 @@
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.Views.Rendering;
 using PixiEditor.Views.Rendering;

+ 52 - 0
src/PixiEditor/Views/Overlays/Drawables/DashedStroke.cs

@@ -0,0 +1,52 @@
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+
+namespace PixiEditor.Views.Overlays.Drawables;
+
+public class DashedStroke
+{
+    private Paint blackPaint = new Paint()
+    {
+        Color = Colors.Black, StrokeWidth = 1, Style = PaintStyle.Stroke, IsAntiAliased = true
+    };
+
+    private Paint whiteDashPaint = new Paint()
+    {
+        Color = Colors.White,
+        StrokeWidth = 1,
+        Style = PaintStyle.Stroke,
+        PathEffect = PathEffect.CreateDash(
+            [2, 2], 2),
+        IsAntiAliased = true
+    };
+
+    public void UpdateZoom(float newZoom)
+    {
+        blackPaint.StrokeWidth = (float)(1.0 / newZoom);
+
+        whiteDashPaint.StrokeWidth = (float)(2.0 / newZoom);
+        whiteDashPaint?.PathEffect?.Dispose();
+
+        float[] dashes = [whiteDashPaint.StrokeWidth * 4, whiteDashPaint.StrokeWidth * 3];
+
+        dashes[0] = whiteDashPaint.StrokeWidth * 4;
+        dashes[1] = whiteDashPaint.StrokeWidth * 3;
+
+        whiteDashPaint.PathEffect = PathEffect.CreateDash(dashes, 2);
+    }
+
+    public void Draw(Canvas canvas, VecD start, VecD end)
+    {
+        canvas.DrawLine(start, end, blackPaint);
+        canvas.DrawLine(start, end, whiteDashPaint);
+    }
+
+    public void Draw(Canvas canvas, VectorPath path)
+    {
+        canvas.DrawPath(path, blackPaint);
+        canvas.DrawPath(path, whiteDashPaint);
+    }
+}

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

@@ -11,17 +11,25 @@ namespace PixiEditor.Views.Overlays.Handles;
 public class AnchorHandle : RectangleHandle
 public class AnchorHandle : RectangleHandle
 {
 {
     private Paint paint;
     private Paint paint;
+    private Paint selectedPaint;
+    
+    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"));
         paint = GetPaint("HandleBrush");
         paint = GetPaint("HandleBrush");
+        selectedPaint = GetPaint("SelectedHandleBrush");
         StrokePaint = paint;
         StrokePaint = paint;
-        StrokePaint.Style = PaintStyle.Stroke;
     }
     }
 
 
     public override void Draw(Canvas context)
     public override void Draw(Canvas context)
     {
     {
         paint.StrokeWidth = (float)(1.0 / ZoomScale);
         paint.StrokeWidth = (float)(1.0 / ZoomScale);
+        selectedPaint.StrokeWidth = (float)(2.5 / ZoomScale);
+        
+        StrokePaint = IsSelected ? selectedPaint : paint;
+        StrokePaint.Style = PaintStyle.Stroke;
         base.Draw(context);
         base.Draw(context);
     }
     }
 }
 }

+ 36 - 0
src/PixiEditor/Views/Overlays/Handles/ControlPointHandle.cs

@@ -0,0 +1,36 @@
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+using PixiEditor.Extensions.UI.Overlays;
+using Canvas = Drawie.Backend.Core.Surfaces.Canvas;
+
+namespace PixiEditor.Views.Overlays.Handles;
+
+public class ControlPointHandle : Handle
+{
+    public Handle ConnectedTo { get; set; }
+
+    public ControlPointHandle(IOverlay owner) : base(owner)
+    {
+        Size = new VecD(GetResource<double>("AnchorHandleSize"));
+    }
+
+    public override void Draw(Canvas target)
+    {
+        float radius = (float)(Size.X / 2);
+        radius /= (float)ZoomScale;
+        if (FillPaint != null)
+        {
+            target.DrawCircle(Position, radius, FillPaint);
+        }
+
+        if (StrokePaint != null)
+        {
+            target.DrawCircle(Position, radius, StrokePaint);
+        }
+
+        if (ConnectedTo != null)
+        {
+            target.DrawLine(Position, ConnectedTo.Position, FillPaint);
+        }
+    }
+}

+ 62 - 19
src/PixiEditor/Views/Overlays/Handles/Handle.cs

@@ -6,7 +6,7 @@ using Avalonia.Media;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
-using Drawie.Backend.Core.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
@@ -16,36 +16,39 @@ using Path = Avalonia.Controls.Shapes.Path;
 
 
 namespace PixiEditor.Views.Overlays.Handles;
 namespace PixiEditor.Views.Overlays.Handles;
 
 
-public delegate void HandleEvent(Handle source, VecD position);
+public delegate void HandleEvent(Handle source, OverlayPointerArgs args);
+
 public abstract class Handle : IHandle
 public abstract class Handle : IHandle
 {
 {
     public Paint? FillPaint { get; set; } = GetPaint("HandleBackgroundBrush");
     public Paint? FillPaint { get; set; } = GetPaint("HandleBackgroundBrush");
-    public Paint? StrokePaint { get; set; } = GetPaint("HandleStrokeBrush", 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;
     public IOverlay Owner { get; set; } = null!;
     public IOverlay Owner { get; set; } = null!;
     public VecD Position { get; set; }
     public VecD Position { get; set; }
     public VecD Size { get; set; }
     public VecD Size { get; set; }
     public RectD HandleRect => new(Position, Size);
     public RectD HandleRect => new(Position, Size);
+    public bool HitTestVisible { get; set; } = true;
+    public bool IsHovered => isHovered;
 
 
     public event HandleEvent OnPress;
     public event HandleEvent OnPress;
     public event HandleEvent OnDrag;
     public event HandleEvent OnDrag;
-    public event Action<Handle> OnRelease;
-    public event Action<Handle> OnHover;
-    public event Action<Handle> OnExit;
+    public event HandleEvent OnRelease;
+    public event HandleEvent OnHover;
+    public event HandleEvent OnExit;
+    public event HandleEvent OnTap;
     public Cursor? Cursor { get; set; }
     public Cursor? Cursor { get; set; }
 
 
     private bool isPressed;
     private bool isPressed;
     private bool isHovered;
     private bool isHovered;
+    private bool moved;
 
 
     public Handle(IOverlay owner)
     public Handle(IOverlay owner)
     {
     {
         Owner = owner;
         Owner = owner;
         Position = VecD.Zero;
         Position = VecD.Zero;
-        Size = Application.Current.TryGetResource("HandleSize", out object size) ? new VecD((double)size) : new VecD(16);
-
-        Owner.PointerPressedOverlay += OnPointerPressed;
-        Owner.PointerMovedOverlay += OnPointerMoved;
-        Owner.PointerReleasedOverlay += OnPointerReleased;
+        Size = Application.Current.TryGetResource("HandleSize", out object size)
+            ? new VecD((double)size)
+            : new VecD(16);
     }
     }
 
 
     public abstract void Draw(Canvas target);
     public abstract void Draw(Canvas target);
@@ -59,7 +62,7 @@ public abstract class Handle : IHandle
 
 
     public static T? GetResource<T>(string key)
     public static T? GetResource<T>(string key)
     {
     {
-       return ResourceLoader.GetResource<T>(key); 
+        return ResourceLoader.GetResource<T>(key);
     }
     }
 
 
     public static VectorPath GetHandleGeometry(string handleName)
     public static VectorPath GetHandleGeometry(string handleName)
@@ -77,7 +80,22 @@ public abstract class Handle : IHandle
 
 
     protected static Paint? GetPaint(string key, PaintStyle style = PaintStyle.Fill)
     protected static Paint? GetPaint(string key, PaintStyle style = PaintStyle.Fill)
     {
     {
-       return ResourceLoader.GetPaint(key, style);
+        return ResourceLoader.GetPaint(key, style);
+    }
+
+    public void InvokePress(OverlayPointerArgs args)
+    {
+        OnPointerPressed(args);
+    }
+
+    public void InvokeMove(OverlayPointerArgs args)
+    {
+        OnPointerMoved(args);
+    }
+
+    public void InvokeRelease(OverlayPointerArgs args)
+    {
+        OnPointerReleased(args);
     }
     }
 
 
     private void OnPointerPressed(OverlayPointerArgs args)
     private void OnPointerPressed(OverlayPointerArgs args)
@@ -87,13 +105,19 @@ public abstract class Handle : IHandle
             return;
             return;
         }
         }
 
 
+        if (args.Handled)
+        {
+            return;
+        }
+
         VecD handlePos = Position;
         VecD handlePos = Position;
 
 
-        if (IsWithinHandle(handlePos, args.Point, ZoomScale))
+        if (IsWithinHandle(handlePos, args.Point, ZoomScale) && HitTestVisible)
         {
         {
             args.Handled = true;
             args.Handled = true;
             OnPressed(args);
             OnPressed(args);
-            OnPress?.Invoke(this, args.Point);
+            moved = false;
+            OnPress?.Invoke(this, args);
             isPressed = true;
             isPressed = true;
             args.Pointer.Capture(Owner);
             args.Pointer.Capture(Owner);
         }
         }
@@ -103,6 +127,11 @@ public abstract class Handle : IHandle
     {
     {
         VecD handlePos = Position;
         VecD handlePos = Position;
 
 
+        if (args.Handled || !HitTestVisible)
+        {
+            return;
+        }
+
         bool isWithinHandle = IsWithinHandle(handlePos, args.Point, ZoomScale);
         bool isWithinHandle = IsWithinHandle(handlePos, args.Point, ZoomScale);
 
 
         if (!isHovered && isWithinHandle)
         if (!isHovered && isWithinHandle)
@@ -113,13 +142,13 @@ public abstract class Handle : IHandle
                 Owner.Cursor = Cursor;
                 Owner.Cursor = Cursor;
             }
             }
 
 
-            OnHover?.Invoke(this);
+            OnHover?.Invoke(this, args);
         }
         }
         else if (isHovered && !isWithinHandle)
         else if (isHovered && !isWithinHandle)
         {
         {
             isHovered = false;
             isHovered = false;
             Owner.Cursor = null;
             Owner.Cursor = null;
-            OnExit?.Invoke(this);
+            OnExit?.Invoke(this, args);
         }
         }
 
 
         if (!isPressed)
         if (!isPressed)
@@ -127,7 +156,9 @@ public abstract class Handle : IHandle
             return;
             return;
         }
         }
 
 
-        OnDrag?.Invoke(this, args.Point);
+        OnDrag?.Invoke(this, args);
+        args.Handled = true;
+        moved = true;
     }
     }
 
 
     private void OnPointerReleased(OverlayPointerArgs args)
     private void OnPointerReleased(OverlayPointerArgs args)
@@ -137,11 +168,23 @@ public abstract class Handle : IHandle
             return;
             return;
         }
         }
 
 
+        if (args.Handled || !HitTestVisible)
+        {
+            isPressed = false;
+            return;
+        }
+
         if (isPressed)
         if (isPressed)
         {
         {
             isPressed = false;
             isPressed = false;
-            OnRelease?.Invoke(this);
+            if (!moved)
+            {
+                OnTap?.Invoke(this, args);
+            }
+
+            OnRelease?.Invoke(this, args);
             args.Pointer.Capture(null);
             args.Pointer.Capture(null);
+            args.Handled = true;
         }
         }
     }
     }
 }
 }

+ 52 - 63
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -7,7 +7,6 @@ using PixiEditor.Models.Controllers.InputDevice;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 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.Surfaces.Vector;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.Views.Overlays.Drawables;
 using PixiEditor.Views.Overlays.Drawables;
@@ -17,6 +16,7 @@ using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
 using Point = Avalonia.Point;
 using Point = Avalonia.Point;
 
 
 namespace PixiEditor.Views.Overlays.LineToolOverlay;
 namespace PixiEditor.Views.Overlays.LineToolOverlay;
+
 internal class LineToolOverlay : Overlay
 internal class LineToolOverlay : Overlay
 {
 {
     public static readonly StyledProperty<VecD> LineStartProperty =
     public static readonly StyledProperty<VecD> LineStartProperty =
@@ -46,8 +46,9 @@ 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 static readonly StyledProperty<SnappingController> SnappingControllerProperty =
+        AvaloniaProperty.Register<LineToolOverlay, SnappingController>(
+            nameof(SnappingController));
 
 
     public SnappingController SnappingController
     public SnappingController SnappingController
     {
     {
@@ -64,30 +65,34 @@ internal class LineToolOverlay : Overlay
         set => SetValue(ShowHandlesProperty, value);
         set => SetValue(ShowHandlesProperty, value);
     }
     }
 
 
-    public static readonly StyledProperty<bool> IsSizeBoxEnabledProperty = AvaloniaProperty.Register<LineToolOverlay, bool>(
-        nameof(IsSizeBoxEnabled));
+    public static readonly StyledProperty<bool> IsSizeBoxEnabledProperty =
+        AvaloniaProperty.Register<LineToolOverlay, bool>(
+            nameof(IsSizeBoxEnabled));
 
 
     public bool IsSizeBoxEnabled
     public bool IsSizeBoxEnabled
     {
     {
         get => GetValue(IsSizeBoxEnabledProperty);
         get => GetValue(IsSizeBoxEnabledProperty);
         set => SetValue(IsSizeBoxEnabledProperty, value);
         set => SetValue(IsSizeBoxEnabledProperty, value);
     }
     }
-    
+
     static LineToolOverlay()
     static LineToolOverlay()
     {
     {
         LineStartProperty.Changed.Subscribe(RenderAffectingPropertyChanged);
         LineStartProperty.Changed.Subscribe(RenderAffectingPropertyChanged);
         LineEndProperty.Changed.Subscribe(RenderAffectingPropertyChanged);
         LineEndProperty.Changed.Subscribe(RenderAffectingPropertyChanged);
     }
     }
-    
 
 
-    private Paint blackPaint = new Paint() { Color = Colors.Black, StrokeWidth = 1, Style = PaintStyle.Stroke, IsAntiAliased = true };
-    private Paint whiteDashPaint = new Paint() { Color = Colors.White, StrokeWidth = 1, Style = PaintStyle.Stroke, PathEffect = PathEffect.CreateDash(
-        [2, 2], 2), IsAntiAliased = true };
+
+    private DashedStroke dashedStroke = new DashedStroke();
+
+    private Paint blackPaint = new Paint()
+    {
+        Color = Colors.Black, StrokeWidth = 1, Style = PaintStyle.Stroke, IsAntiAliased = true
+    };
 
 
     private VecD mouseDownPos = VecD.Zero;
     private VecD mouseDownPos = VecD.Zero;
     private VecD lineStartOnMouseDown = VecD.Zero;
     private VecD lineStartOnMouseDown = VecD.Zero;
     private VecD lineEndOnMouseDown = VecD.Zero;
     private VecD lineEndOnMouseDown = VecD.Zero;
-    
+
     private VecD lastMousePos = VecD.Zero;
     private VecD lastMousePos = VecD.Zero;
 
 
     private bool movedWhileMouseDown = false;
     private bool movedWhileMouseDown = false;
@@ -95,7 +100,7 @@ internal class LineToolOverlay : Overlay
     private RectangleHandle startHandle;
     private RectangleHandle startHandle;
     private RectangleHandle endHandle;
     private RectangleHandle endHandle;
     private TransformHandle moveHandle;
     private TransformHandle moveHandle;
-    
+
     private bool isDraggingHandle = false;
     private bool isDraggingHandle = false;
     private InfoBox infoBox;
     private InfoBox infoBox;
 
 
@@ -109,7 +114,7 @@ internal class LineToolOverlay : Overlay
         startHandle = new AnchorHandle(this);
         startHandle = new AnchorHandle(this);
         startHandle.StrokePaint = blackPaint;
         startHandle.StrokePaint = blackPaint;
         startHandle.OnDrag += StartHandleOnDrag;
         startHandle.OnDrag += StartHandleOnDrag;
-        startHandle.OnHover += handle => Refresh();
+        startHandle.OnHover += (handle, _) => Refresh();
         startHandle.OnRelease += OnHandleRelease;
         startHandle.OnRelease += OnHandleRelease;
         startHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         startHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         AddHandle(startHandle);
         AddHandle(startHandle);
@@ -118,7 +123,7 @@ internal class LineToolOverlay : Overlay
         endHandle.StrokePaint = blackPaint;
         endHandle.StrokePaint = blackPaint;
         endHandle.OnDrag += EndHandleOnDrag;
         endHandle.OnDrag += EndHandleOnDrag;
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
-        endHandle.OnHover += handle => Refresh();
+        endHandle.OnHover += (handle, _) => Refresh();
         endHandle.OnRelease += OnHandleRelease;
         endHandle.OnRelease += OnHandleRelease;
         AddHandle(endHandle);
         AddHandle(endHandle);
 
 
@@ -126,10 +131,10 @@ internal class LineToolOverlay : Overlay
         moveHandle.StrokePaint = blackPaint;
         moveHandle.StrokePaint = blackPaint;
         moveHandle.OnDrag += MoveHandleOnDrag;
         moveHandle.OnDrag += MoveHandleOnDrag;
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
-        moveHandle.OnHover += handle => Refresh();
+        moveHandle.OnHover += (handle, _)=> Refresh();
         moveHandle.OnRelease += OnHandleRelease;
         moveHandle.OnRelease += OnHandleRelease;
         AddHandle(moveHandle);
         AddHandle(moveHandle);
-        
+
         infoBox = new InfoBox();
         infoBox = new InfoBox();
     }
     }
 
 
@@ -139,7 +144,7 @@ internal class LineToolOverlay : Overlay
         lastMousePos = args.Point;
         lastMousePos = args.Point;
     }
     }
 
 
-    private void OnHandleRelease(Handle obj)
+    private void OnHandleRelease(Handle obj, OverlayPointerArgs args)
     {
     {
         if (SnappingController != null)
         if (SnappingController != null)
         {
         {
@@ -147,25 +152,15 @@ internal class LineToolOverlay : Overlay
             SnappingController.HighlightedYAxis = null;
             SnappingController.HighlightedYAxis = null;
             Refresh();
             Refresh();
         }
         }
-        
+
         isDraggingHandle = false;
         isDraggingHandle = false;
         IsSizeBoxEnabled = false;
         IsSizeBoxEnabled = false;
     }
     }
 
 
     protected override void ZoomChanged(double newZoom)
     protected override void ZoomChanged(double newZoom)
     {
     {
-        blackPaint.StrokeWidth = (float)(1.0 / newZoom);
-        
-        whiteDashPaint.StrokeWidth = (float)(2.0 / newZoom);
-        whiteDashPaint?.PathEffect?.Dispose();
-        
-        float[] dashes = new float[] { whiteDashPaint.StrokeWidth * 4, whiteDashPaint.StrokeWidth * 3 }; 
-
-        dashes[0] = whiteDashPaint.StrokeWidth * 4;
-        dashes[1] = whiteDashPaint.StrokeWidth * 3;
-        
-        whiteDashPaint.PathEffect = PathEffect.CreateDash(dashes, 2);
-        
+        blackPaint.StrokeWidth = 1 / (float)newZoom;
+        dashedStroke.UpdateZoom((float)newZoom);
         infoBox.ZoomScale = newZoom;
         infoBox.ZoomScale = newZoom;
     }
     }
 
 
@@ -173,17 +168,16 @@ internal class LineToolOverlay : Overlay
     {
     {
         VecD mappedStart = LineStart;
         VecD mappedStart = LineStart;
         VecD mappedEnd = LineEnd;
         VecD mappedEnd = LineEnd;
-        
+
         startHandle.Position = mappedStart;
         startHandle.Position = mappedStart;
-        endHandle.Position = mappedEnd; 
-        
+        endHandle.Position = mappedEnd;
+
         VecD center = (mappedStart + mappedEnd) / 2;
         VecD center = (mappedStart + mappedEnd) / 2;
         VecD size = mappedEnd - mappedStart;
         VecD size = mappedEnd - mappedStart;
-        
+
         moveHandle.Position = TransformHelper.GetHandlePos(new ShapeCorners(center, size), ZoomScale, moveHandle.Size);
         moveHandle.Position = TransformHelper.GetHandlePos(new ShapeCorners(center, size), ZoomScale, moveHandle.Size);
 
 
-        context.DrawLine(new VecD(mappedStart.X, mappedStart.Y), new VecD(mappedEnd.X, mappedEnd.Y), blackPaint);
-        context.DrawLine(new VecD(mappedStart.X, mappedStart.Y), new VecD(mappedEnd.X, mappedEnd.Y), whiteDashPaint);
+        dashedStroke.Draw(context, mappedStart, mappedEnd);
 
 
         if (ShowHandles)
         if (ShowHandles)
         {
         {
@@ -212,27 +206,27 @@ internal class LineToolOverlay : Overlay
         args.Pointer.Capture(this);
         args.Pointer.Capture(this);
     }
     }
 
 
-    private void StartHandleOnDrag(Handle source, VecD position)
+    private void StartHandleOnDrag(Handle source, OverlayPointerArgs args)
     {
     {
-        VecD delta = position - mouseDownPos;
+        VecD delta = args.Point - mouseDownPos;
         LineStart = SnapAndHighlight(lineStartOnMouseDown + delta);
         LineStart = SnapAndHighlight(lineStartOnMouseDown + delta);
         movedWhileMouseDown = true;
         movedWhileMouseDown = true;
-        
-        lastMousePos = position;
+
+        lastMousePos = args.Point;
         isDraggingHandle = true;
         isDraggingHandle = true;
         IsSizeBoxEnabled = true;
         IsSizeBoxEnabled = true;
     }
     }
 
 
-    private void EndHandleOnDrag(Handle source, VecD position)
+    private void EndHandleOnDrag(Handle source, OverlayPointerArgs args)
     {
     {
-        VecD delta = position - mouseDownPos;
+        VecD delta = args.Point - mouseDownPos;
         VecD final = SnapAndHighlight(lineEndOnMouseDown + delta);
         VecD final = SnapAndHighlight(lineEndOnMouseDown + delta);
-        
+
         LineEnd = final;
         LineEnd = final;
         movedWhileMouseDown = true;
         movedWhileMouseDown = true;
-        
+
         isDraggingHandle = true;
         isDraggingHandle = true;
-        lastMousePos = position;
+        lastMousePos = args.Point;
         IsSizeBoxEnabled = true;
         IsSizeBoxEnabled = true;
     }
     }
 
 
@@ -243,17 +237,17 @@ internal class LineToolOverlay : Overlay
         {
         {
             double? x = SnappingController.SnapToHorizontal(position.X, out string snapAxisX);
             double? x = SnappingController.SnapToHorizontal(position.X, out string snapAxisX);
             double? y = SnappingController.SnapToVertical(position.Y, out string snapAxisY);
             double? y = SnappingController.SnapToVertical(position.Y, out string snapAxisY);
-            
+
             if (x.HasValue)
             if (x.HasValue)
             {
             {
                 final = new VecD(x.Value, final.Y);
                 final = new VecD(x.Value, final.Y);
             }
             }
-            
+
             if (y.HasValue)
             if (y.HasValue)
             {
             {
                 final = new VecD(final.X, y.Value);
                 final = new VecD(final.X, y.Value);
             }
             }
-            
+
             SnappingController.HighlightedXAxis = snapAxisX;
             SnappingController.HighlightedXAxis = snapAxisX;
             SnappingController.HighlightedYAxis = snapAxisY;
             SnappingController.HighlightedYAxis = snapAxisY;
         }
         }
@@ -261,10 +255,10 @@ internal class LineToolOverlay : Overlay
         return final;
         return final;
     }
     }
 
 
-    private void MoveHandleOnDrag(Handle source, VecD position)
+    private void MoveHandleOnDrag(Handle source, OverlayPointerArgs args)
     {
     {
-        var delta = position - mouseDownPos;
-        
+        var delta = args.Point - mouseDownPos;
+
         VecD mappedStart = lineStartOnMouseDown;
         VecD mappedStart = lineStartOnMouseDown;
         VecD mappedEnd = lineEndOnMouseDown;
         VecD mappedEnd = lineEndOnMouseDown;
 
 
@@ -275,7 +269,7 @@ internal class LineToolOverlay : Overlay
             SnappingController.HighlightedXAxis = snapDeltaResult.Item1.Item1;
             SnappingController.HighlightedXAxis = snapDeltaResult.Item1.Item1;
             SnappingController.HighlightedYAxis = snapDeltaResult.Item1.Item2;
             SnappingController.HighlightedYAxis = snapDeltaResult.Item1.Item2;
         }
         }
-        
+
         LineStart = lineStartOnMouseDown + delta + snapDeltaResult.Item2;
         LineStart = lineStartOnMouseDown + delta + snapDeltaResult.Item2;
         LineEnd = lineEndOnMouseDown + delta + snapDeltaResult.Item2;
         LineEnd = lineEndOnMouseDown + delta + snapDeltaResult.Item2;
 
 
@@ -289,7 +283,6 @@ 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)
     private ((string, string), VecD) TrySnapLine(VecD originalStart, VecD originalEnd, VecD delta)
@@ -298,21 +291,17 @@ internal class LineToolOverlay : Overlay
         {
         {
             return ((string.Empty, string.Empty), delta);
             return ((string.Empty, string.Empty), delta);
         }
         }
-        
+
         VecD center = (originalStart + originalEnd) / 2f;
         VecD center = (originalStart + originalEnd) / 2f;
-        VecD[] pointsToTest = new VecD[]
-        {
-            center + delta,
-            originalStart + delta,
-            originalEnd + delta,
-        };
+        VecD[] pointsToTest = new VecD[] { center + delta, originalStart + delta, originalEnd + delta, };
 
 
-        VecD snapDelta = SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX, out string snapAxisY);
+        VecD snapDelta =
+            SnappingController.GetSnapDeltaForPoints(pointsToTest, out string snapAxisX, out string snapAxisY);
 
 
         return ((snapAxisX, snapAxisY), snapDelta);
         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)

+ 62 - 8
src/PixiEditor/Views/Overlays/Overlay.cs

@@ -19,6 +19,15 @@ using Canvas = Drawie.Backend.Core.Surfaces.Canvas;
 
 
 namespace PixiEditor.Views.Overlays;
 namespace PixiEditor.Views.Overlays;
 
 
+enum HandleEventType
+{
+    PointerEnteredOverlay,
+    PointerExitedOverlay,
+    PointerMovedOverlay,
+    PointerPressedOverlay,
+    PointerReleasedOverlay
+}
+
 public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not avalonia element
 public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not avalonia element
 {
 {
     public List<Handle> Handles { get; } = new();
     public List<Handle> Handles { get; } = new();
@@ -40,6 +49,8 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     public event PointerEvent? PointerMovedOverlay;
     public event PointerEvent? PointerMovedOverlay;
     public event PointerEvent? PointerPressedOverlay;
     public event PointerEvent? PointerPressedOverlay;
     public event PointerEvent? PointerReleasedOverlay;
     public event PointerEvent? PointerReleasedOverlay;
+    
+    public Handle? CapturedHandle { get; set; } = null!;
 
 
     private readonly Dictionary<AvaloniaProperty, OverlayTransition> activeTransitions = new();
     private readonly Dictionary<AvaloniaProperty, OverlayTransition> activeTransitions = new();
 
 
@@ -57,7 +68,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     }
     }
 
 
     public virtual bool CanRender() => true;
     public virtual bool CanRender() => true;
-    
+
     public abstract void RenderOverlay(Canvas context, RectD canvasBounds);
     public abstract void RenderOverlay(Canvas context, RectD canvasBounds);
 
 
     public void Refresh()
     public void Refresh()
@@ -66,34 +77,46 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
         InvalidateVisual(); // For elements in visual tree
         InvalidateVisual(); // For elements in visual tree
     }
     }
 
 
+    public void CaptureHandle(Handle handle)
+    {
+        CapturedHandle = handle;
+    }
+    
     public void EnterPointer(OverlayPointerArgs args)
     public void EnterPointer(OverlayPointerArgs args)
     {
     {
         OnOverlayPointerEntered(args);
         OnOverlayPointerEntered(args);
+        InvokeHandleEvent(HandleEventType.PointerEnteredOverlay, args);
         PointerEnteredOverlay?.Invoke(args);
         PointerEnteredOverlay?.Invoke(args);
     }
     }
 
 
     public void ExitPointer(OverlayPointerArgs args)
     public void ExitPointer(OverlayPointerArgs args)
     {
     {
         OnOverlayPointerExited(args);
         OnOverlayPointerExited(args);
+        InvokeHandleEvent(HandleEventType.PointerExitedOverlay, args);
         PointerExitedOverlay?.Invoke(args);
         PointerExitedOverlay?.Invoke(args);
     }
     }
 
 
     public void MovePointer(OverlayPointerArgs args)
     public void MovePointer(OverlayPointerArgs args)
     {
     {
         OnOverlayPointerMoved(args);
         OnOverlayPointerMoved(args);
+        InvokeHandleEvent(HandleEventType.PointerMovedOverlay, args);
         PointerMovedOverlay?.Invoke(args);
         PointerMovedOverlay?.Invoke(args);
     }
     }
 
 
     public void PressPointer(OverlayPointerArgs args)
     public void PressPointer(OverlayPointerArgs args)
     {
     {
         OnOverlayPointerPressed(args);
         OnOverlayPointerPressed(args);
+        InvokeHandleEvent(HandleEventType.PointerPressedOverlay, args);
         PointerPressedOverlay?.Invoke(args);
         PointerPressedOverlay?.Invoke(args);
     }
     }
 
 
     public void ReleasePointer(OverlayPointerArgs args)
     public void ReleasePointer(OverlayPointerArgs args)
     {
     {
         OnOverlayPointerReleased(args);
         OnOverlayPointerReleased(args);
+        InvokeHandleEvent(HandleEventType.PointerReleasedOverlay, args);
         PointerReleasedOverlay?.Invoke(args);
         PointerReleasedOverlay?.Invoke(args);
+        
+        CaptureHandle(null);
     }
     }
 
 
     public virtual bool TestHit(VecD point)
     public virtual bool TestHit(VecD point)
@@ -106,6 +129,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
         if (Handles.Contains(handle)) return;
         if (Handles.Contains(handle)) return;
 
 
         Handles.Add(handle);
         Handles.Add(handle);
+        handle.ZoomScale = ZoomScale;
     }
     }
 
 
     public void ForAllHandles(Action<Handle> action)
     public void ForAllHandles(Action<Handle> action)
@@ -126,6 +150,40 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
             }
             }
         }
         }
     }
     }
+    
+    private void InvokeHandleEvent(HandleEventType eventName, OverlayPointerArgs args)
+    {
+        if (CapturedHandle != null)
+        {
+            InvokeHandleEvent(CapturedHandle, args, eventName);
+        }
+        else
+        {
+            var reversedHandles = Handles.Reverse<Handle>();
+            foreach (var handle in reversedHandles)
+            {
+                InvokeHandleEvent(handle, args, eventName);
+            }
+        }
+    }
+    
+    private void InvokeHandleEvent(Handle handle, OverlayPointerArgs args, HandleEventType pointerEvent)
+    {
+        if(pointerEvent == null) return;
+        
+        if (pointerEvent == HandleEventType.PointerMovedOverlay)
+        {
+            handle.InvokeMove(args);
+        }
+        else if (pointerEvent == HandleEventType.PointerPressedOverlay)
+        {
+            handle.InvokePress(args);
+        }
+        else if (pointerEvent == HandleEventType.PointerReleasedOverlay)
+        {
+            handle.InvokeRelease(args);
+        }
+    }
 
 
     protected void TransitionTo(AvaloniaProperty property, double durationSeconds, double to, Easing? easing = null)
     protected void TransitionTo(AvaloniaProperty property, double durationSeconds, double to, Easing? easing = null)
     {
     {
@@ -176,29 +234,25 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     }
     }
 
 
     protected virtual void ZoomChanged(double newZoom) { }
     protected virtual void ZoomChanged(double newZoom) { }
+
     protected virtual void OnOverlayPointerReleased(OverlayPointerArgs args)
     protected virtual void OnOverlayPointerReleased(OverlayPointerArgs args)
     {
     {
-
     }
     }
 
 
     protected virtual void OnOverlayPointerPressed(OverlayPointerArgs args)
     protected virtual void OnOverlayPointerPressed(OverlayPointerArgs args)
     {
     {
-
     }
     }
 
 
     protected virtual void OnOverlayPointerMoved(OverlayPointerArgs args)
     protected virtual void OnOverlayPointerMoved(OverlayPointerArgs args)
     {
     {
-
     }
     }
 
 
     protected virtual void OnOverlayPointerExited(OverlayPointerArgs args)
     protected virtual void OnOverlayPointerExited(OverlayPointerArgs args)
     {
     {
-
     }
     }
 
 
     protected virtual void OnOverlayPointerEntered(OverlayPointerArgs args)
     protected virtual void OnOverlayPointerEntered(OverlayPointerArgs args)
     {
     {
-
     }
     }
 
 
     private static void OnZoomScaleChanged(AvaloniaPropertyChangedEventArgs<double> e)
     private static void OnZoomScaleChanged(AvaloniaPropertyChangedEventArgs<double> e)
@@ -212,7 +266,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
             }
             }
         }
         }
     }
     }
-    
+
     private void OnIsVisibleChanged(AvaloniaPropertyChangedEventArgs<bool> e)
     private void OnIsVisibleChanged(AvaloniaPropertyChangedEventArgs<bool> e)
     {
     {
         if (e.NewValue.Value)
         if (e.NewValue.Value)
@@ -220,7 +274,7 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
             Refresh();
             Refresh();
         }
         }
     }
     }
-    
+
     protected static void AffectsOverlayRender(params AvaloniaProperty[] properties)
     protected static void AffectsOverlayRender(params AvaloniaProperty[] properties)
     {
     {
         foreach (var property in properties)
         foreach (var property in properties)

+ 829 - 0
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -0,0 +1,829 @@
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Input;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.Extensions.UI.Overlays;
+using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Views.Overlays.Drawables;
+using PixiEditor.Views.Overlays.Handles;
+using Canvas = Drawie.Backend.Core.Surfaces.Canvas;
+
+namespace PixiEditor.Views.Overlays.PathOverlay;
+
+// If you need to make any changes in this overlay, I feel sorry for you
+public class VectorPathOverlay : Overlay
+{
+    public static readonly StyledProperty<VectorPath> PathProperty =
+        AvaloniaProperty.Register<VectorPathOverlay, VectorPath>(
+            nameof(Path));
+
+    public static readonly StyledProperty<SnappingController> SnappingControllerProperty =
+        AvaloniaProperty.Register<VectorPathOverlay, SnappingController>(
+            nameof(SnappingController));
+
+    public SnappingController SnappingController
+    {
+        get => GetValue(SnappingControllerProperty);
+        set => SetValue(SnappingControllerProperty, value);
+    }
+
+    public VectorPath Path
+    {
+        get => GetValue(PathProperty);
+        set => SetValue(PathProperty, value);
+    }
+
+    public static readonly StyledProperty<ICommand> AddToUndoCommandProperty =
+        AvaloniaProperty.Register<VectorPathOverlay, ICommand>(
+            nameof(AddToUndoCommand));
+
+    public ICommand AddToUndoCommand
+    {
+        get => GetValue(AddToUndoCommandProperty);
+        set => SetValue(AddToUndoCommandProperty, value);
+    }
+
+    private DashedStroke dashedStroke = new DashedStroke();
+    private TransformHandle transformHandle;
+
+    private List<AnchorHandle> anchorHandles = new();
+    private List<ControlPointHandle> controlPointHandles = new();
+
+    private VecD posOnStartDrag;
+    private VectorPath pathOnStartDrag;
+
+    static VectorPathOverlay()
+    {
+        AffectsOverlayRender(PathProperty);
+        PathProperty.Changed.Subscribe(OnPathChanged);
+    }
+
+    public VectorPathOverlay()
+    {
+        transformHandle = new TransformHandle(this);
+        transformHandle.OnPress += MoveHandlePress;
+        transformHandle.OnDrag += MoveHandleDrag;
+
+        AddHandle(transformHandle);
+    }
+
+    protected override void ZoomChanged(double newZoom)
+    {
+        dashedStroke.UpdateZoom((float)newZoom);
+        transformHandle.ZoomScale = newZoom;
+    }
+
+    public override void RenderOverlay(Canvas context, RectD canvasBounds)
+    {
+        if (Path is null)
+        {
+            return;
+        }
+
+        dashedStroke.Draw(context, Path);
+
+        RenderHandles(context);
+
+        if (IsOverAnyHandle())
+        {
+            TryHighlightSnap(null, null);
+        }
+    }
+
+    public override bool CanRender()
+    {
+        return Path != null;
+    }
+
+    private void RenderHandles(Canvas context)
+    {
+        bool anySelected = false;
+        int anchor = 0;
+        int controlPoint = 0;
+        int anchorCount = GetAnchorCount();
+        foreach (var verb in Path)
+        {
+            if (anchor == anchorCount - 1 && !anySelected)
+            {
+                GetHandleAt(anchor).IsSelected = true;
+            }
+
+            anySelected = anySelected || GetHandleAt(anchor).IsSelected;
+
+            VecF verbPointPos = GetVerbPointPos(verb);
+
+            if (verb.verb == PathVerb.Cubic)
+            {
+                VecD controlPoint1 = (VecD)verb.points[1];
+                VecD controlPoint2 = (VecD)verb.points[2];
+
+                var controlPointHandle1 = controlPointHandles[controlPoint];
+                var controlPointHandle2 = controlPointHandles[controlPoint + 1];
+
+                controlPointHandle1.HitTestVisible = controlPoint1 != controlPointHandle1.ConnectedTo.Position;
+                controlPointHandle2.HitTestVisible = controlPoint2 != controlPointHandle2.ConnectedTo.Position;
+
+                controlPointHandle1.Position = controlPoint1;
+
+                if (controlPointHandle1.HitTestVisible)
+                {
+                    controlPointHandle1.Draw(context);
+                }
+
+                controlPointHandle2.Position = controlPoint2;
+
+                if (controlPointHandle2.HitTestVisible)
+                {
+                    controlPointHandle2.Draw(context);
+                }
+
+                controlPoint += 2;
+            }
+            else if (verb.verb == PathVerb.Close)
+            {
+                continue;
+            }
+
+            if (anchor == anchorCount)
+            {
+                continue;
+            }
+
+            anchorHandles[anchor].Position = new VecD(verbPointPos.X, verbPointPos.Y);
+            anchorHandles[anchor].Draw(context);
+
+            anchor++;
+        }
+
+        transformHandle.Position = Path.TightBounds.BottomRight + new VecD(1, 1);
+        transformHandle.Draw(context);
+    }
+
+    private int GetAnchorCount()
+    {
+        return Path.VerbCount - (Path.IsClosed ? 2 : 0);
+    }
+
+    private void AdjustHandles(int pointsCount)
+    {
+        int anchorCount = anchorHandles.Count;
+        int totalHandles = anchorCount + controlPointHandles.Count;
+        if (totalHandles != pointsCount)
+        {
+            if (totalHandles > pointsCount)
+            {
+                RemoveAllHandles();
+            }
+
+            int missingControlPoints = CalculateMissingControlPoints(controlPointHandles.Count);
+            int missingAnchors = GetAnchorCount() - anchorHandles.Count;
+            for (int i = 0; i < missingAnchors; i++)
+            {
+                CreateHandle(anchorHandles.Count);
+            }
+
+            for (int i = 0; i < missingControlPoints; i++)
+            {
+                CreateHandle(controlPointHandles.Count, true);
+            }
+
+
+            SelectAnchor(GetHandleAt(pointsCount - 1));
+
+            ConnectControlPointsToAnchors();
+        }
+        Refresh();
+    }
+
+    private void ConnectControlPointsToAnchors()
+    {
+        int controlPointIndex = 0;
+        int anchorIndex = 0;
+        foreach (var data in Path)
+        {
+            if (data.verb == PathVerb.Cubic)
+            {
+                int targetAnchorIndex1 = anchorIndex - 1;
+                if (targetAnchorIndex1 < 0)
+                {
+                    targetAnchorIndex1 = anchorHandles.Count - 1;
+                }
+
+                AnchorHandle previousAnchor = anchorHandles.ElementAtOrDefault(targetAnchorIndex1);
+
+                int targetAnchorIndex2 = anchorIndex;
+                if (targetAnchorIndex2 >= anchorHandles.Count)
+                {
+                    targetAnchorIndex2 = 0;
+                }
+
+                AnchorHandle nextAnchor = anchorHandles.ElementAtOrDefault(targetAnchorIndex2);
+
+                if (previousAnchor != null)
+                {
+                    controlPointHandles[controlPointIndex].ConnectedTo = previousAnchor;
+                }
+
+                controlPointHandles[controlPointIndex + 1].ConnectedTo = nextAnchor;
+                controlPointIndex += 2;
+            }
+
+            anchorIndex++;
+        }
+    }
+
+    private int CalculateMissingControlPoints(int handleCount)
+    {
+        int totalControlPoints = 0;
+
+        foreach (var point in Path)
+        {
+            if (point.verb == PathVerb.Cubic)
+            {
+                totalControlPoints += 2;
+            }
+        }
+
+        return totalControlPoints - handleCount;
+    }
+
+    private void RemoveAllHandles()
+    {
+        int previouslySelectedIndex = -1;
+
+        for (int i = anchorHandles.Count - 1; i >= 0; i--)
+        {
+            var handle = anchorHandles[i];
+            handle.OnPress -= OnHandlePress;
+            handle.OnDrag -= OnHandleDrag;
+            handle.OnRelease -= OnHandleRelease;
+            handle.OnTap -= OnHandleTap;
+            if (handle is { IsSelected: true })
+            {
+                previouslySelectedIndex = i;
+            }
+
+            Handles.Remove(handle);
+        }
+
+        for (int i = controlPointHandles.Count - 1; i >= 0; i--)
+        {
+            var handle = controlPointHandles[i];
+            handle.OnDrag -= OnControlPointDrag;
+            handle.OnRelease -= OnHandleRelease;
+
+            Handles.Remove(controlPointHandles[i]);
+        }
+
+        anchorHandles.Clear();
+        controlPointHandles.Clear();
+        SnappingController.RemoveAll("editingPath");
+    }
+
+    private bool IsOverAnyHandle()
+    {
+        return Handles.Any(handle => handle.IsHovered);
+    }
+
+    private void MoveHandlePress(Handle source, OverlayPointerArgs args)
+    {
+        posOnStartDrag = args.Point;
+        pathOnStartDrag?.Dispose();
+        pathOnStartDrag = new VectorPath(Path);
+        TryHighlightSnap(null, null);
+        args.Pointer.Capture(this);
+        args.Handled = true;
+    }
+
+
+    private void MoveHandleDrag(Handle source, OverlayPointerArgs args)
+    {
+        var delta = args.Point - posOnStartDrag;
+
+        VectorPath updatedPath = new VectorPath(pathOnStartDrag);
+
+        delta = TryFindAnySnap(delta, pathOnStartDrag, out string axisX, out string axisY);
+        updatedPath.Transform(Matrix3X3.CreateTranslation((float)delta.X, (float)delta.Y));
+
+        TryHighlightSnap(axisX, axisY);
+
+        Path = updatedPath;
+        args.Handled = true;
+    }
+
+    private void CreateHandle(int atIndex, bool isControlPoint = false)
+    {
+        if (!isControlPoint)
+        {
+            AnchorHandle anchor = new AnchorHandle(this);
+            anchorHandles.Add(anchor);
+
+            anchor.OnPress += OnHandlePress;
+            anchor.OnDrag += OnHandleDrag;
+            anchor.OnRelease += OnHandleRelease;
+            anchor.OnTap += OnHandleTap;
+            AddHandle(anchor);
+            SnappingController.AddXYAxis($"editingPath[{atIndex}]", () => anchor.Position);
+        }
+        else
+        {
+            var controlPoint = new ControlPointHandle(this);
+            controlPoint.OnDrag += OnControlPointDrag;
+            controlPoint.OnRelease += OnHandleRelease;
+            controlPointHandles.Add(controlPoint);
+            AddHandle(controlPoint);
+        }
+    }
+
+    private void OnHandleTap(Handle handle, OverlayPointerArgs args)
+    {
+        if (handle is not AnchorHandle anchorHandle)
+        {
+            return;
+        }
+
+        if (Path.IsClosed)
+        {
+            return;
+        }
+
+        VectorPath newPath = new VectorPath(Path);
+        if (args.Modifiers.HasFlag(KeyModifiers.Control))
+        {
+            SelectAnchor(anchorHandle);
+            return;
+        }
+
+        if (IsFirstHandle(anchorHandle))
+        {
+            newPath.LineTo((VecF)anchorHandle.Position);
+            newPath.Close();
+        }
+        else
+        {
+            VecD pos = anchorHandle.Position;
+            newPath.LineTo(new VecF((float)pos.X, (float)pos.Y));
+        }
+
+        Path = newPath;
+    }
+
+    private bool IsFirstHandle(AnchorHandle handle)
+    {
+        return anchorHandles.IndexOf(handle) == 0;
+    }
+
+    private void SelectAnchor(AnchorHandle handle)
+    {
+        foreach (var anchorHandle in anchorHandles)
+        {
+            anchorHandle.IsSelected = anchorHandle == handle;
+        }
+    }
+
+    private void OnHandlePress(Handle source, OverlayPointerArgs args)
+    {
+        if (source is AnchorHandle anchorHandle)
+        {
+            SnappingController.RemoveAll($"editingPath[{anchorHandles.IndexOf(anchorHandle)}]");
+            CaptureHandle(source);
+
+            if (!args.Modifiers.HasFlag(KeyModifiers.Control)) return;
+
+            var newPath = ConvertTouchingLineVerbsToCubic(anchorHandle);
+
+            Path = newPath;
+        }
+    }
+
+    // To have continous spline, verb before and after a point must be a cubic with proper control points
+    private VectorPath ConvertTouchingLineVerbsToCubic(AnchorHandle anchorHandle)
+    {
+        bool convertNextToCubic = false;
+        int i = -1;
+        VectorPath newPath = new VectorPath();
+        int index = anchorHandles.IndexOf(anchorHandle);
+
+        foreach (var data in Path)
+        {
+            if (data.verb == PathVerb.Line)
+            {
+                if (i == index)
+                {
+                    newPath.CubicTo(data.points[0], data.points[1], data.points[1]);
+                    convertNextToCubic = true;
+                }
+                else if (i + 1 == index)
+                {
+                    newPath.CubicTo(data.points[0], data.points[1], data.points[1]);
+                }
+                else if (i == 0 && index == 0 || (Path.IsClosed && i == Path.PointCount - 2 && index == 0))
+                {
+                    newPath.CubicTo(data.points[0], data.points[1], data.points[1]);
+                }
+                else
+                {
+                    if (convertNextToCubic)
+                    {
+                        newPath.CubicTo(data.points[0], data.points[1], data.points[1]);
+                        convertNextToCubic = false;
+                    }
+                    else
+                    {
+                        newPath.LineTo(data.points[1]);
+                    }
+                }
+            }
+            else if (data.verb == PathVerb.Cubic && i == index)
+            {
+                newPath.CubicTo(data.points[1], data.points[2], data.points[3]);
+                convertNextToCubic = true;
+            }
+            else
+            {
+                DefaultPathVerb(data, newPath);
+            }
+
+            i++;
+        }
+
+        return newPath;
+    }
+
+    private void OnHandleDrag(Handle source, OverlayPointerArgs args)
+    {
+        if (source is not AnchorHandle handle)
+        {
+            return;
+        }
+
+        var index = anchorHandles.IndexOf(handle);
+        VectorPath newPath = new VectorPath();
+
+        bool pointHandled = false;
+        int i = 0;
+
+        VecF previousDelta = new VecF();
+
+        int controlPointIndex = 0;
+        var connectedControlPoints = controlPointHandles.Where(x => x.ConnectedTo == handle).ToList();
+        int targetControlPointIndex = controlPointHandles.IndexOf(connectedControlPoints.FirstOrDefault());
+        int symmetricControlPointIndex = controlPointHandles.IndexOf(connectedControlPoints.LastOrDefault());
+
+        VecD targetPos = ApplySymmetry(args.Point);
+        VecD targetSymmetryPos = GetMirroredControlPoint((VecF)targetPos, (VecF)handle.Position);
+
+        bool ctrlPressed = args.Modifiers.HasFlag(KeyModifiers.Control);
+        foreach (var data in Path)
+        {
+            VecF point;
+            switch (data.verb)
+            {
+                case PathVerb.Move:
+                    if (ctrlPressed)
+                    {
+                        DefaultPathVerb(data, newPath);
+                        break;
+                    }
+
+                    point = data.points[0];
+                    point = TryApplyNewPos(args, i, index, point, Path.IsClosed, data.points[0]);
+
+                    newPath.MoveTo(point);
+                    previousDelta = point - data.points[0];
+                    break;
+                case PathVerb.Line:
+                    if (ctrlPressed)
+                    {
+                        DefaultPathVerb(data, newPath);
+                        break;
+                    }
+
+                    point = data.points[1];
+                    point = TryApplyNewPos(args, i, index, point, Path.IsClosed, newPath.Points[0]);
+
+                    newPath.LineTo(point);
+                    break;
+                case PathVerb.Cubic:
+                    if (ctrlPressed)
+                    {
+                        HandleCubicControlContinousDrag(controlPointIndex, targetControlPointIndex,
+                            symmetricControlPointIndex,
+                            targetPos, targetSymmetryPos, data, newPath);
+                        controlPointIndex += 2;
+                    }
+                    else
+                    {
+                        point = data.points[3];
+                        point = TryApplyNewPos(args, i, index, point, Path.IsClosed, newPath.Points[0]);
+
+                        VecF mid1Delta = previousDelta;
+
+                        VecF mid2Delta = point - data.points[3];
+
+                        newPath.CubicTo(data.points[1] + mid1Delta, data.points[2] + mid2Delta, point);
+
+                        previousDelta = mid2Delta;
+                    }
+
+                    break;
+                default:
+                    DefaultPathVerb(data, newPath);
+                    break;
+            }
+
+            i++;
+        }
+
+        Path = newPath;
+    }
+
+    private void OnControlPointDrag(Handle source, OverlayPointerArgs args)
+    {
+        if (source is not ControlPointHandle controlPointHandle)
+        {
+            return;
+        }
+
+        var targetIndex = controlPointHandles.IndexOf(controlPointHandle);
+        int symmetricIndex = controlPointHandles.IndexOf(controlPointHandles.FirstOrDefault(x =>
+            x.ConnectedTo == controlPointHandle.ConnectedTo && x != controlPointHandle));
+        VecD targetPos = ApplySymmetry(args.Point);
+        VecD targetSymmetryPos =
+            GetMirroredControlPoint((VecF)targetPos, (VecF)controlPointHandle.ConnectedTo.Position);
+        VectorPath newPath = new VectorPath();
+
+        if (args.Modifiers.HasFlag(KeyModifiers.Alt))
+        {
+            symmetricIndex = -1;
+        }
+
+        int i = 0;
+
+        foreach (var data in Path)
+        {
+            VecF point;
+            switch (data.verb)
+            {
+                case PathVerb.Move:
+                    newPath.MoveTo(data.points[0]);
+                    break;
+                case PathVerb.Line:
+                    point = data.points[1];
+                    newPath.LineTo(point);
+                    break;
+                case PathVerb.Cubic:
+                    HandleCubicControlContinousDrag(i, targetIndex, symmetricIndex,
+                        targetPos, targetSymmetryPos, data, newPath);
+                    i += 2;
+                    break;
+                default:
+                    i++;
+                    DefaultPathVerb(data, newPath);
+                    break;
+            }
+        }
+
+        Path = newPath;
+    }
+
+    private void HandleCubicControlContinousDrag(int i, int targetIndex, int symmetricIndex,
+        VecD targetPos, VecD targetSymmetryPos,
+        (PathVerb verb, VecF[] points) data,
+        VectorPath newPath)
+    {
+        bool isFirstControlPoint = i == targetIndex;
+        bool isSecondControlPoint = i + 1 == targetIndex;
+
+        bool isFirstSymmetricControlPoint = i == symmetricIndex;
+        bool isSecondSymmetricControlPoint = i + 1 == symmetricIndex;
+
+        VecF controlPoint1 = data.points[1];
+        VecF controlPoint2 = data.points[2];
+        VecF endPoint = data.points[3];
+
+        if (isFirstControlPoint)
+        {
+            controlPoint1 = (VecF)targetPos;
+        }
+        else if (isSecondControlPoint)
+        {
+            controlPoint2 = (VecF)targetPos;
+        }
+        else if (isFirstSymmetricControlPoint)
+        {
+            controlPoint1 = (VecF)targetSymmetryPos;
+        }
+        else if (isSecondSymmetricControlPoint)
+        {
+            controlPoint2 = (VecF)targetSymmetryPos;
+        }
+
+        newPath.CubicTo(controlPoint1, controlPoint2, endPoint);
+    }
+
+    private VecD GetMirroredControlPoint(VecF controlPoint, VecF anchor)
+    {
+        return new VecD(2 * anchor.X - controlPoint.X, 2 * anchor.Y - controlPoint.Y);
+    }
+
+    private VecF GetVerbPointPos((PathVerb verb, VecF[] points) data)
+    {
+        switch (data.verb)
+        {
+            case PathVerb.Move:
+                return data.points[0];
+            case PathVerb.Line:
+                return data.points[1];
+            case PathVerb.Quad:
+                return data.points[2];
+            case PathVerb.Cubic:
+                return data.points[3];
+            case PathVerb.Conic:
+                return data.points[2];
+            case PathVerb.Close:
+                return data.points[0];
+            case PathVerb.Done:
+                return new VecF();
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+    }
+
+    private VecF TryApplyNewPos(OverlayPointerArgs args, int i, int index, VecF point, bool firstIsLast,
+        VecF firstPoint)
+    {
+        if (i == index)
+        {
+            point = (VecF)ApplySymmetry(args.Point);
+        }
+        else if (firstIsLast && i == GetAnchorCount())
+        {
+            point = firstPoint;
+        }
+
+        return point;
+    }
+
+    private VecD ApplySymmetry(VecD point)
+    {
+        var snappedPoint = SnappingController.GetSnapPoint(point, out string axisX, out string axisY);
+        var snapped = new VecD((float)snappedPoint.X, (float)snappedPoint.Y);
+        TryHighlightSnap(axisX, axisY);
+        return snapped;
+    }
+
+    private void OnHandleRelease(Handle source, OverlayPointerArgs args)
+    {
+        AddToUndoCommand.Execute(Path);
+
+        if (source is AnchorHandle anchorHandle)
+        {
+            SnappingController.AddXYAxis($"editingPath[{anchorHandles.IndexOf(anchorHandle)}]", () => source.Position);
+        }
+
+        TryHighlightSnap(null, null);
+
+        Refresh();
+    }
+
+    private void TryHighlightSnap(string axisX, string axisY)
+    {
+        SnappingController.HighlightedXAxis = axisX;
+        SnappingController.HighlightedYAxis = axisY;
+        SnappingController.HighlightedPoint = null;
+    }
+
+    private AnchorHandle? GetHandleAt(int index)
+    {
+        if (index < 0 || index >= anchorHandles.Count)
+        {
+            return null;
+        }
+
+        return anchorHandles[index];
+    }
+
+    private void ClearAnchorHandles()
+    {
+        foreach (var handle in anchorHandles)
+        {
+            handle.OnPress -= OnHandlePress;
+            handle.OnDrag -= OnHandleDrag;
+            handle.OnRelease -= OnHandleRelease;
+            handle.OnTap -= OnHandleTap;
+            Handles.Remove(handle);
+        }
+
+        anchorHandles.Clear();
+    }
+
+    private VecD TryFindAnySnap(VecD delta, VectorPath path, out string? axisX, out string? axisY)
+    {
+        VecD closestSnapDelta = new VecD(double.PositiveInfinity, double.PositiveInfinity);
+        axisX = null;
+        axisY = null;
+
+        SnappingController.RemoveAll("editingPath");
+
+        foreach (var point in path.Points)
+        {
+            var snap = SnappingController.GetSnapDeltaForPoint((VecD)point + delta, out string x, out string y);
+            if (snap.X < closestSnapDelta.X && !string.IsNullOrEmpty(x))
+            {
+                closestSnapDelta = new VecD(snap.X, closestSnapDelta.Y);
+                axisX = x;
+            }
+
+            if (snap.Y < closestSnapDelta.Y && !string.IsNullOrEmpty(y))
+            {
+                closestSnapDelta = new VecD(closestSnapDelta.X, snap.Y);
+                axisY = y;
+            }
+        }
+
+        AddAllSnaps();
+
+        if (closestSnapDelta.X == double.PositiveInfinity)
+        {
+            closestSnapDelta = new VecD(0, closestSnapDelta.Y);
+        }
+
+        if (closestSnapDelta.Y == double.PositiveInfinity)
+        {
+            closestSnapDelta = new VecD(closestSnapDelta.X, 0);
+        }
+
+        return delta + closestSnapDelta;
+    }
+
+    private void AddAllSnaps()
+    {
+        for (int i = 0; i < anchorHandles.Count; i++)
+        {
+            var i1 = i;
+            SnappingController.AddXYAxis($"editingPath[{i}]", () => anchorHandles[i1].Position);
+        }
+    }
+
+    private void PathChanged(VectorPath newPath)
+    {
+        AdjustHandles(newPath.PointCount - (newPath.IsClosed ? 1 : 0));
+    }
+
+    private static void DefaultPathVerb((PathVerb verb, VecF[] points) data, VectorPath newPath)
+    {
+        switch (data.verb)
+        {
+            case PathVerb.Move:
+                newPath.MoveTo(data.points[0]);
+                break;
+            case PathVerb.Line:
+                newPath.LineTo(data.points[1]);
+                break;
+            case PathVerb.Quad:
+                newPath.QuadTo(data.points[1], data.points[2]);
+                break;
+            case PathVerb.Conic:
+                newPath.ConicTo(data.points[1], data.points[2], data.points[3].X);
+                break;
+            case PathVerb.Cubic:
+                newPath.CubicTo(data.points[1], data.points[2], data.points[3]);
+                break;
+            case PathVerb.Close:
+                newPath.Close();
+                break;
+            case PathVerb.Done:
+                break;
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+    }
+
+    private static void OnPathChanged(AvaloniaPropertyChangedEventArgs<VectorPath> args)
+    {
+        var overlay = args.Sender as VectorPathOverlay;
+        if (args.NewValue.Value == null)
+        {
+            overlay.SnappingController.RemoveAll("editingPath");
+            overlay.ClearAnchorHandles();
+        }
+        else
+        {
+            var path = args.NewValue.Value;
+            overlay.AdjustHandles(path.PointCount - (path.IsClosed ? 1 : 0));
+        }
+
+        if (args.OldValue.Value != null)
+        {
+            args.OldValue.Value.Changed -= overlay.PathChanged;
+        }
+
+        if (args.NewValue.Value != null)
+        {
+            args.NewValue.Value.Changed += overlay.PathChanged;
+        }
+    }
+}

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

@@ -8,7 +8,7 @@ using PixiEditor.Animation;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 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.Surfaces.Vector;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;

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

@@ -5,8 +5,8 @@ using Avalonia.Media;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 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.Surfaces.Vector;
 using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Text;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
 using Drawie.Numerics;
 using Drawie.Numerics;

+ 6 - 6
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -12,8 +12,8 @@ using PixiEditor.Helpers.Extensions;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 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.Surfaces.Vector;
 using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Text;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.Controllers.InputDevice;
@@ -446,7 +446,7 @@ internal class TransformOverlay : Overlay
         return degrees;
         return degrees;
     }
     }
 
 
-    private void OnAnchorHandlePressed(Handle source, VecD position)
+    private void OnAnchorHandlePressed(Handle source, OverlayPointerArgs args)
     {
     {
         capturedAnchor = anchorMap[source];
         capturedAnchor = anchorMap[source];
         cornersOnStartAnchorDrag = Corners;
         cornersOnStartAnchorDrag = Corners;
@@ -461,9 +461,9 @@ internal class TransformOverlay : Overlay
         }
         }
     }
     }
 
 
-    private void OnMoveHandlePressed(Handle source, VecD position)
+    private void OnMoveHandlePressed(Handle source, OverlayPointerArgs args)
     {
     {
-        StartMoving(position);
+        StartMoving(args.Point);
     }
     }
 
 
     protected override void OnOverlayPointerExited(OverlayPointerArgs args)
     protected override void OnOverlayPointerExited(OverlayPointerArgs args)
@@ -580,7 +580,7 @@ internal class TransformOverlay : Overlay
         return base.TestHit(point) || Corners.AsScaled(1.25f).IsPointInside(point);
         return base.TestHit(point) || Corners.AsScaled(1.25f).IsPointInside(point);
     }
     }
 
 
-    private void OnMoveHandleReleased(Handle obj)
+    private void OnMoveHandleReleased(Handle obj, OverlayPointerArgs args)
     {
     {
         StopMoving();
         StopMoving();
     }
     }
@@ -1043,7 +1043,7 @@ internal class TransformOverlay : Overlay
         return originOnStartAnchorDrag + pos - mousePosOnStartAnchorDrag;
         return originOnStartAnchorDrag + pos - mousePosOnStartAnchorDrag;
     }
     }
 
 
-    private void OnAnchorHandleReleased(Handle source)
+    private void OnAnchorHandleReleased(Handle source, OverlayPointerArgs args)
     {
     {
         capturedAnchor = null;
         capturedAnchor = null;
 
 

+ 34 - 4
tests/PixiEditor.Backend.Tests/NodeSystemTests.cs

@@ -5,6 +5,7 @@ using Drawie.Interop.VulkanAvalonia;
 using Drawie.Skia;
 using Drawie.Skia;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.Models.Serialization;
 using PixiEditor.Models.Serialization;
@@ -109,11 +110,11 @@ public class NodeSystemTests
         var allFoundFactories = typeof(SerializationFactory).Assembly.GetTypes()
         var allFoundFactories = typeof(SerializationFactory).Assembly.GetTypes()
             .Where(x => x.IsAssignableTo(typeof(SerializationFactory))
             .Where(x => x.IsAssignableTo(typeof(SerializationFactory))
                         && x is { IsAbstract: false, IsInterface: false }).ToList();
                         && x is { IsAbstract: false, IsInterface: false }).ToList();
-        
+
         List<SerializationFactory> factories = new();
         List<SerializationFactory> factories = new();
         QoiEncoder encoder = new QoiEncoder();
         QoiEncoder encoder = new QoiEncoder();
         SerializationConfig config = new SerializationConfig(encoder);
         SerializationConfig config = new SerializationConfig(encoder);
-        
+
         foreach (var factoryType in allFoundFactories)
         foreach (var factoryType in allFoundFactories)
         {
         {
             var factory = (SerializationFactory)Activator.CreateInstance(factoryType);
             var factory = (SerializationFactory)Activator.CreateInstance(factoryType);
@@ -130,16 +131,45 @@ public class NodeSystemTests
             foreach (var input in node.InputProperties)
             foreach (var input in node.InputProperties)
             {
             {
                 if (knownNonSerializableTypes.Contains(input.ValueType)) continue;
                 if (knownNonSerializableTypes.Contains(input.ValueType)) continue;
-                if(input.ValueType.IsAbstract) continue;
+                if (input.ValueType.IsAbstract) continue;
                 if (input.ValueType.IsAssignableTo(typeof(Delegate))) continue;
                 if (input.ValueType.IsAssignableTo(typeof(Delegate))) continue;
                 bool hasFactory = factories.Any(x => x.OriginalType == input.ValueType);
                 bool hasFactory = factories.Any(x => x.OriginalType == input.ValueType);
                 Assert.True(
                 Assert.True(
-                    input.ValueType.IsValueType || input.ValueType == typeof(string) || hasFactory, 
+                    input.ValueType.IsValueType || input.ValueType == typeof(string) || hasFactory,
                     $"{input.ValueType} doesn't have a factory and is not serializable. Property: {input.InternalPropertyName}, NodeType: {node.GetType().Name}");
                     $"{input.ValueType} doesn't have a factory and is not serializable. Property: {input.InternalPropertyName}, NodeType: {node.GetType().Name}");
             }
             }
         }
         }
     }
     }
 
 
+    [Fact]
+    public void TestThatAllVectorDataTypesHaveSerializationFactories()
+    {
+        var allVectorTypes = typeof(ShapeVectorData).Assembly.GetTypes()
+            .Where(x => x.IsAssignableTo(typeof(ShapeVectorData))
+                        && x is { IsAbstract: false, IsInterface: false }).ToList();
+
+        QoiEncoder encoder = new QoiEncoder();
+        SerializationConfig config = new SerializationConfig(encoder);
+
+        var factoryTypes = typeof(SerializationFactory).Assembly.GetTypes()
+            .Where(x => x.IsAssignableTo(typeof(SerializationFactory))
+                        && x is { IsAbstract: false, IsInterface: false }).ToList();
+
+        List<SerializationFactory> factories = new();
+        
+        foreach (var factoryType in factoryTypes)
+        {
+            var factory = (SerializationFactory)Activator.CreateInstance(factoryType);
+            factories.Add(factory);
+        }
+        
+        foreach (var type in allVectorTypes)
+        {
+            bool hasFactory = factories.Any(x => x.OriginalType == type);
+            Assert.True(hasFactory, $"{type} doesn't have a factory.");
+        }
+    }
+
     private static List<Type> GetNodeTypesWithoutPairs()
     private static List<Type> GetNodeTypesWithoutPairs()
     {
     {
         var allNodeTypes = typeof(Node).Assembly.GetTypes()
         var allNodeTypes = typeof(Node).Assembly.GetTypes()