Browse Source

Merge pull request #694 from PixiEditor/vector-features

Vector features
Krzysztof Krysiński 8 months ago
parent
commit
7c25c64b45
30 changed files with 1314 additions and 402 deletions
  1. 1 1
      src/Drawie
  2. 17 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  3. 11 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  4. 14 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  5. 13 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  6. 16 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  7. 4 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  8. 131 25
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  9. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  10. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  11. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectEllipse_UpdateableChange.cs
  12. 2 1
      src/PixiEditor/Data/Localization/Languages/en.json
  13. 4 2
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  14. 11 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  15. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs
  16. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs
  17. 11 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorEllipseToolExecutor.cs
  18. 18 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs
  19. 16 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorRectangleToolExecutor.cs
  20. 4 2
      src/PixiEditor/Models/Handlers/Tools/IVectorPathToolHandler.cs
  21. 5 2
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  22. 1 0
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/FillableShapeToolbar.cs
  23. 8 0
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  24. 1 1
      src/PixiEditor/Views/Overlays/Handles/AnchorHandle.cs
  25. 258 0
      src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs
  26. 131 0
      src/PixiEditor/Views/Overlays/PathOverlay/ShapePoint.cs
  27. 72 0
      src/PixiEditor/Views/Overlays/PathOverlay/SubShape.cs
  28. 162 341
      src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs
  29. 3 0
      tests/PixiEditor.Tests/AvaloniaTestRunner.cs
  30. 384 0
      tests/PixiEditor.Tests/EditableVectorPathTests.cs

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 53075856f14518af3f0b59910a9b9d8339368347
+Subproject commit 46b16a26646f37460869263d55a9ecaecb58c89f

+ 17 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs

@@ -3,6 +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.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -48,11 +49,15 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
             ApplyTransformTo(drawingSurface);
             ApplyTransformTo(drawingSurface);
         }
         }
 
 
-        using Paint shapePaint = new Paint() { IsAntiAliased = true };
+        using Paint shapePaint = new Paint();
+        shapePaint.IsAntiAliased = true;
 
 
-        shapePaint.Color = FillColor;
-        shapePaint.Style = PaintStyle.Fill;
-        drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+        if (Fill)
+        {
+            shapePaint.Color = FillColor;
+            shapePaint.Style = PaintStyle.Fill;
+            drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+        }
 
 
         if (StrokeWidth > 0)
         if (StrokeWidth > 0)
         {
         {
@@ -93,4 +98,12 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
             TransformationMatrix = TransformationMatrix
             TransformationMatrix = TransformationMatrix
         };
         };
     }
     }
+
+    public override VectorPath ToPath()
+    {
+        // TODO: Apply transformation matrix
+        VectorPath path = new VectorPath();
+        path.AddOval(RectD.FromCenterAndSize(Center, Radius * 2));
+        return path;
+    }
 }
 }

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

@@ -4,6 +4,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.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -107,4 +108,14 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
             StrokeColor = StrokeColor, StrokeWidth = StrokeWidth, TransformationMatrix = TransformationMatrix
             StrokeColor = StrokeColor, StrokeWidth = StrokeWidth, TransformationMatrix = TransformationMatrix
         };
         };
     }
     }
+
+    public override VectorPath ToPath()
+    {
+        // TODO: Apply transformation matrix
+        
+        VectorPath path = new VectorPath();
+        path.MoveTo((VecF)Start);
+        path.LineTo((VecF)End);
+        return path;
+    }
 }
 }

+ 14 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -46,7 +46,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             IsAntiAliased = true, StrokeJoin = StrokeJoin.Round, StrokeCap = StrokeCap.Round
             IsAntiAliased = true, StrokeJoin = StrokeJoin.Round, StrokeCap = StrokeCap.Round
         };
         };
 
 
-        if (FillColor.A > 0)
+        if (Fill && FillColor.A > 0)
         {
         {
             paint.Color = FillColor;
             paint.Color = FillColor;
             paint.Style = PaintStyle.Fill;
             paint.Style = PaintStyle.Fill;
@@ -54,11 +54,14 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             drawingSurface.Canvas.DrawPath(Path, paint);
             drawingSurface.Canvas.DrawPath(Path, paint);
         }
         }
 
 
-        paint.Color = StrokeColor;
-        paint.Style = PaintStyle.Stroke;
-        paint.StrokeWidth = StrokeWidth;
-
-        drawingSurface.Canvas.DrawPath(Path, paint);
+        if (StrokeWidth > 0)
+        {
+            paint.Color = StrokeColor;
+            paint.Style = PaintStyle.Stroke;
+            paint.StrokeWidth = StrokeWidth;
+            
+            drawingSurface.Canvas.DrawPath(Path, paint);
+        }
 
 
         if (applyTransform)
         if (applyTransform)
         {
         {
@@ -91,4 +94,9 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             TransformationMatrix = TransformationMatrix
             TransformationMatrix = TransformationMatrix
         };
         };
     }
     }
+
+    public override VectorPath ToPath()
+    {
+        return new VectorPath(Path);
+    }
 }
 }

+ 13 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs

@@ -1,6 +1,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.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -77,4 +78,16 @@ public class PointsVectorData : ShapeVectorData
             StrokeColor = StrokeColor, FillColor = FillColor, StrokeWidth = StrokeWidth
             StrokeColor = StrokeColor, FillColor = FillColor, StrokeWidth = StrokeWidth
         };
         };
     }
     }
+
+    public override VectorPath ToPath()
+    {
+        VectorPath path = new VectorPath();
+        
+        foreach (VecD point in Points)
+        {
+            path.LineTo((VecF)point);
+        }
+        
+        return path;
+    }
 }
 }

+ 16 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs

@@ -3,6 +3,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.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -53,11 +54,15 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
             ApplyTransformTo(drawingSurface);
             ApplyTransformTo(drawingSurface);
         }
         }
 
 
-        using Paint paint = new Paint() { IsAntiAliased = true };
+        using Paint paint = new Paint();
+        paint.IsAntiAliased = true;
 
 
-        paint.Color = FillColor;
-        paint.Style = PaintStyle.Fill;
-        drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+        if (Fill && FillColor.A > 0)
+        {
+            paint.Color = FillColor;
+            paint.Style = PaintStyle.Fill;
+            drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+        }
 
 
         if (StrokeWidth > 0)
         if (StrokeWidth > 0)
         {
         {
@@ -99,4 +104,11 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
             TransformationMatrix = TransformationMatrix
             TransformationMatrix = TransformationMatrix
         };
         };
     }
     }
+
+    public override VectorPath ToPath()
+    {
+        VectorPath path = new VectorPath();
+        path.AddRect(RectD.FromCenterAndSize(Center, Size));
+        return path;
+    }
 }
 }

+ 4 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs

@@ -4,6 +4,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.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -15,6 +16,7 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
     public Color StrokeColor { get; set; } = Colors.White;
     public Color StrokeColor { get; set; } = Colors.White;
     public Color FillColor { get; set; } = Colors.White;
     public Color FillColor { get; set; } = Colors.White;
     public float StrokeWidth { get; set; } = 1;
     public float StrokeWidth { get; set; } = 1;
+    public bool Fill { get; set; } = true;
     public abstract RectD GeometryAABB { get; } 
     public abstract RectD GeometryAABB { get; } 
     public abstract RectD VisualAABB { get; }
     public abstract RectD VisualAABB { get; }
     public RectD TransformedAABB => new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix).AABBBounds;
     public RectD TransformedAABB => new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix).AABBBounds;
@@ -41,4 +43,6 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
     {
     {
         return CalculateHash();
         return CalculateHash();
     }
     }
+
+    public abstract VectorPath ToPath();
 }
 }

+ 131 - 25
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -5,9 +5,12 @@ using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 
@@ -20,6 +23,8 @@ internal class CombineStructureMembersOnto_Change : Change
     private Guid targetLayerGuid;
     private Guid targetLayerGuid;
     private Dictionary<int, CommittedChunkStorage> originalChunks = new();
     private Dictionary<int, CommittedChunkStorage> originalChunks = new();
     
     
+    private Dictionary<int, VectorPath> originalPaths = new();
+
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
     public CombineStructureMembersOnto_Change(HashSet<Guid> membersToMerge, Guid targetLayer)
     public CombineStructureMembersOnto_Change(HashSet<Guid> membersToMerge, Guid targetLayer)
@@ -69,7 +74,6 @@ internal class CombineStructureMembersOnto_Change : Change
         List<IChangeInfo> changes = new();
         List<IChangeInfo> changes = new();
         var targetLayer = target.FindMemberOrThrow<LayerNode>(targetLayerGuid);
         var targetLayer = target.FindMemberOrThrow<LayerNode>(targetLayerGuid);
 
 
-        // TODO: add merging similar layers (vector -> vector)
         int maxFrame = GetMaxFrame(target, targetLayer);
         int maxFrame = GetMaxFrame(target, targetLayer);
 
 
         for (int frame = 0; frame < maxFrame || frame == 0; frame++)
         for (int frame = 0; frame < maxFrame || frame == 0; frame++)
@@ -82,6 +86,26 @@ internal class CombineStructureMembersOnto_Change : Change
         return changes;
         return changes;
     }
     }
 
 
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var toDrawOn = target.FindMemberOrThrow<LayerNode>(targetLayerGuid);
+
+        List<IChangeInfo> changes = new();
+
+        int maxFrame = GetMaxFrame(target, toDrawOn);
+
+        for (int frame = 0; frame < maxFrame || frame == 0; frame++)
+        {
+            changes.Add(RevertFrame(toDrawOn, frame));
+        }
+
+        target.AnimationData.RemoveKeyFrame(targetLayerGuid);
+        originalChunks.Clear();
+        changes.Add(new DeleteKeyFrame_ChangeInfo(targetLayerGuid));
+
+        return changes;
+    }
+
     private List<IChangeInfo> ApplyToFrame(Document target, LayerNode targetLayer, int frame)
     private List<IChangeInfo> ApplyToFrame(Document target, LayerNode targetLayer, int frame)
     {
     {
         var chunksToCombine = new HashSet<VecI>();
         var chunksToCombine = new HashSet<VecI>();
@@ -94,7 +118,7 @@ internal class CombineStructureMembersOnto_Change : Change
             var layer = target.FindMemberOrThrow<LayerNode>(guid);
             var layer = target.FindMemberOrThrow<LayerNode>(guid);
 
 
             AddMissingKeyFrame(targetLayer, frame, layer, changes, target);
             AddMissingKeyFrame(targetLayer, frame, layer, changes, target);
-            
+
             if (layer is not IRasterizable or ImageLayerNode)
             if (layer is not IRasterizable or ImageLayerNode)
                 continue;
                 continue;
 
 
@@ -109,6 +133,75 @@ internal class CombineStructureMembersOnto_Change : Change
             }
             }
         }
         }
 
 
+        bool allVector = layersToCombine.All(x => target.FindMember(x) is VectorLayerNode);
+
+        AffectedArea affArea = new();
+
+        // TODO: add custom layer merge
+        if (!allVector)
+        {
+            affArea = RasterMerge(target, targetLayer, frame);
+        }
+        else
+        {
+            affArea = VectorMerge(target, targetLayer, frame, layersToCombine);
+        }
+
+        changes.Add(new LayerImageArea_ChangeInfo(targetLayerGuid, affArea));
+        return changes;
+    }
+
+    private AffectedArea VectorMerge(Document target, LayerNode targetLayer, int frame, HashSet<Guid> toCombine)
+    {
+        if (targetLayer is not VectorLayerNode vectorLayer)
+            throw new InvalidOperationException("Target layer is not a vector layer");
+
+        ShapeVectorData targetData = vectorLayer.ShapeData ?? null;
+        VectorPath? targetPath = targetData?.ToPath();
+
+        var reversed = toCombine.Reverse().ToHashSet();
+        
+        foreach (var guid in reversed)
+        {
+            if (target.FindMember(guid) is not VectorLayerNode vectorNode)
+                continue;
+
+            if (vectorNode.ShapeData == null)
+                continue;
+
+            VectorPath path = vectorNode.ShapeData.ToPath();
+
+            if (targetData == null)
+            {
+                targetData = vectorNode.ShapeData;
+                targetPath = path;
+                
+                if(originalPaths.ContainsKey(frame))
+                    originalPaths[frame].Dispose();
+                
+                originalPaths[frame] = new VectorPath(path);
+            }
+            else
+            {
+                targetPath.AddPath(path, AddPathMode.Append);
+                path.Dispose();
+            }
+        }
+
+        var pathData = new PathVectorData(targetPath)
+        {
+            StrokeWidth = targetData.StrokeWidth,
+            StrokeColor = targetData.StrokeColor,
+            FillColor = targetData.FillColor
+        };
+
+        vectorLayer.ShapeData = pathData;
+
+        return new AffectedArea(new HashSet<VecI>());
+    }
+
+    private AffectedArea RasterMerge(Document target, LayerNode targetLayer, int frame)
+    {
         var toDrawOnImage = ((ImageLayerNode)targetLayer).GetLayerImageAtFrame(frame);
         var toDrawOnImage = ((ImageLayerNode)targetLayer).GetLayerImageAtFrame(frame);
         toDrawOnImage.EnqueueClear();
         toDrawOnImage.EnqueueClear();
 
 
@@ -136,7 +229,7 @@ internal class CombineStructureMembersOnto_Change : Change
                         }
                         }
                     }
                     }
                 }
                 }
-                
+
                 renderer.RenderLayers(tempTexture.DrawingSurface, layersToRender, frame, ChunkResolution.Full);
                 renderer.RenderLayers(tempTexture.DrawingSurface, layersToRender, frame, ChunkResolution.Full);
             }
             }
 
 
@@ -148,11 +241,9 @@ internal class CombineStructureMembersOnto_Change : Change
 
 
             tempTexture.Dispose();
             tempTexture.Dispose();
         });
         });
-
-        changes.Add(new LayerImageArea_ChangeInfo(targetLayerGuid, affArea));
-        return changes;
+        return affArea;
     }
     }
-    
+
     private HashSet<Guid> OrderLayers(HashSet<Guid> layersToCombine, Document document)
     private HashSet<Guid> OrderLayers(HashSet<Guid> layersToCombine, Document document)
     {
     {
         HashSet<Guid> ordered = new();
         HashSet<Guid> ordered = new();
@@ -182,9 +273,9 @@ internal class CombineStructureMembersOnto_Change : Change
             return;
             return;
 
 
         var clonedData = keyFrameData.Clone(true);
         var clonedData = keyFrameData.Clone(true);
-        
+
         targetLayer.AddFrame(keyFrameData.KeyFrameGuid, clonedData);
         targetLayer.AddFrame(keyFrameData.KeyFrameGuid, clonedData);
-        
+
         changes.Add(new CreateRasterKeyFrame_ChangeInfo(targetLayerGuid, frame, clonedData.KeyFrameGuid, true));
         changes.Add(new CreateRasterKeyFrame_ChangeInfo(targetLayerGuid, frame, clonedData.KeyFrameGuid, true));
         changes.Add(new KeyFrameLength_ChangeInfo(targetLayerGuid, clonedData.StartFrame, clonedData.Duration));
         changes.Add(new KeyFrameLength_ChangeInfo(targetLayerGuid, clonedData.StartFrame, clonedData.Duration));
 
 
@@ -193,6 +284,9 @@ internal class CombineStructureMembersOnto_Change : Change
 
 
     private int GetMaxFrame(Document target, LayerNode targetLayer)
     private int GetMaxFrame(Document target, LayerNode targetLayer)
     {
     {
+        if (targetLayer.KeyFrames.Count == 0)
+            return 0;
+
         int maxFrame = targetLayer.KeyFrames.Max(x => x.StartFrame + x.Duration);
         int maxFrame = targetLayer.KeyFrames.Max(x => x.StartFrame + x.Duration);
         foreach (var toMerge in membersToMerge)
         foreach (var toMerge in membersToMerge)
         {
         {
@@ -224,27 +318,21 @@ internal class CombineStructureMembersOnto_Change : Change
         }
         }
     }
     }
 
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    private IChangeInfo RevertFrame(LayerNode targetLayer, int frame)
     {
     {
-        var toDrawOn = target.FindMemberOrThrow<ImageLayerNode>(targetLayerGuid);
-
-        List<IChangeInfo> changes = new();
-
-        int maxFrame = GetMaxFrame(target, toDrawOn);
-
-        for (int frame = 0; frame < maxFrame || frame == 0; frame++)
+        if (targetLayer is ImageLayerNode imageLayerNode)
         {
         {
-            changes.Add(RevertFrame(toDrawOn, frame));
+            return RasterRevert(imageLayerNode, frame);
+        }
+        else if (targetLayer is VectorLayerNode vectorLayerNode)
+        {
+            return VectorRevert(vectorLayerNode, frame);
         }
         }
         
         
-        target.AnimationData.RemoveKeyFrame(targetLayerGuid);
-        originalChunks.Clear();
-        changes.Add(new DeleteKeyFrame_ChangeInfo(targetLayerGuid));
-
-        return changes;
+        throw new InvalidOperationException("Layer type not supported");
     }
     }
 
 
-    private IChangeInfo RevertFrame(ImageLayerNode targetLayer, int frame)
+    private IChangeInfo RasterRevert(ImageLayerNode targetLayer, int frame)
     {
     {
         var toDrawOnImage = targetLayer.GetLayerImageAtFrame(frame);
         var toDrawOnImage = targetLayer.GetLayerImageAtFrame(frame);
         toDrawOnImage.EnqueueClear();
         toDrawOnImage.EnqueueClear();
@@ -255,10 +343,19 @@ internal class CombineStructureMembersOnto_Change : Change
             DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(
             DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(
                 targetLayer.GetLayerImageAtFrame(frame),
                 targetLayer.GetLayerImageAtFrame(frame),
                 ref storedChunks);
                 ref storedChunks);
-        
+
         toDrawOnImage.CommitChanges();
         toDrawOnImage.CommitChanges();
         return new LayerImageArea_ChangeInfo(targetLayerGuid, affectedArea);
         return new LayerImageArea_ChangeInfo(targetLayerGuid, affectedArea);
     }
     }
+    
+    private IChangeInfo VectorRevert(VectorLayerNode targetLayer, int frame)
+    {
+        if (!originalPaths.TryGetValue(frame, out var path))
+            throw new InvalidOperationException("Original path not found");
+
+        targetLayer.ShapeData = new PathVectorData(path);
+        return new VectorShape_ChangeInfo(targetLayer.Id, new AffectedArea(new HashSet<VecI>()));
+    }
 
 
     public override void Dispose()
     public override void Dispose()
     {
     {
@@ -266,5 +363,14 @@ internal class CombineStructureMembersOnto_Change : Change
         {
         {
             originalChunk.Value.Dispose();
             originalChunk.Value.Dispose();
         }
         }
+        
+        originalChunks.Clear();
+        
+        foreach (var originalPath in originalPaths)
+        {
+            originalPath.Value.Dispose();
+        }
+        
+        originalPaths.Clear();
     }
     }
 }
 }

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

@@ -201,7 +201,7 @@ public static class FloodFillHelper
         Surface surface = new Surface(document.Size);
         Surface surface = new Surface(document.Size);
 
 
         var inverse = new VectorPath();
         var inverse = new VectorPath();
-        inverse.AddRect(new RectI(new(0, 0), document.Size));
+        inverse.AddRect((RectD)new RectI(new(0, 0), document.Size));
 
 
         surface.DrawingSurface.Canvas.Clear(new Color(255, 255, 255, 255));
         surface.DrawingSurface.Canvas.Clear(new Color(255, 255, 255, 255));
         surface.DrawingSurface.Canvas.Flush();
         surface.DrawingSurface.Canvas.Flush();
@@ -220,7 +220,7 @@ public static class FloodFillHelper
         if (selection is null)
         if (selection is null)
         {
         {
             selection = new VectorPath();
             selection = new VectorPath();
-            selection.AddRect(globalBounds);
+            selection.AddRect((RectD)globalBounds);
         }
         }
 
 
         RectI localBounds = globalBounds.Offset(-chunkPos * chunkSize).Intersect(new(0, 0, chunkSize, chunkSize));
         RectI localBounds = globalBounds.Offset(-chunkPos * chunkSize).Intersect(new(0, 0, chunkSize, chunkSize));

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

@@ -147,7 +147,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
         {
         {
             RectD tightBounds = layer.GetTightBounds(frame).GetValueOrDefault();
             RectD tightBounds = layer.GetTightBounds(frame).GetValueOrDefault();
             pathToExtract = new VectorPath();
             pathToExtract = new VectorPath();
-            pathToExtract.AddRect((RectI)tightBounds);
+            pathToExtract.AddRect((RectD)(RectI)tightBounds);
         }
         }
 
 
         member.OriginalPath = pathToExtract;
         member.OriginalPath = pathToExtract;

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

@@ -29,7 +29,7 @@ internal class SelectEllipse_UpdateableChange : UpdateableChange
     {
     {
         originalPath = new VectorPath(target.Selection.SelectionPath);
         originalPath = new VectorPath(target.Selection.SelectionPath);
         documentConstraint = new VectorPath();
         documentConstraint = new VectorPath();
-        documentConstraint.AddRect(new RectI(VecI.Zero, target.Size));
+        documentConstraint.AddRect((RectD)new RectI(VecI.Zero, target.Size));
         return true;
         return true;
     }
     }
 
 

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

@@ -778,5 +778,6 @@
   "COPY_COLOR_TO_CLIPBOARD": "Copy color to clipboard",
   "COPY_COLOR_TO_CLIPBOARD": "Copy color to clipboard",
   "VIEWPORT_ROTATION": "Viewport rotation",
   "VIEWPORT_ROTATION": "Viewport rotation",
   "NEXT_TOOL_SET": "Next tool set",
   "NEXT_TOOL_SET": "Next tool set",
-  "PREVIOUS_TOOL_SET": "Previous tool set"
+  "PREVIOUS_TOOL_SET": "Previous tool set",
+  "FILL_MODE": "Fill mode"
 }
 }

+ 4 - 2
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -515,8 +515,10 @@ internal class DocumentOperationsModule : IDocumentOperations
         Guid newGuid = Guid.NewGuid();
         Guid newGuid = Guid.NewGuid();
 
 
         //make a new layer, put combined image onto it, delete layers that were merged
         //make a new layer, put combined image onto it, delete layers that were merged
+        bool allVectorNodes = members.All(x => Document.StructureHelper.Find(x) is IVectorLayerHandler);
+        Type layerToCreate = allVectorNodes ? typeof(VectorLayerNode) : typeof(ImageLayerNode);
         Internals.ActionAccumulator.AddActions(
         Internals.ActionAccumulator.AddActions(
-            new CreateStructureMember_Action(parent.Id, newGuid, typeof(ImageLayerNode)),
+            new CreateStructureMember_Action(parent.Id, newGuid, layerToCreate),
             new StructureMemberName_Action(newGuid, node.NodeNameBindable),
             new StructureMemberName_Action(newGuid, node.NodeNameBindable),
             new CombineStructureMembersOnto_Action(members.ToHashSet(), newGuid));
             new CombineStructureMembersOnto_Action(members.ToHashSet(), newGuid));
         foreach (var member in members)
         foreach (var member in members)
@@ -783,7 +785,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         
         
         var selection = Document.SelectionPathBindable;
         var selection = Document.SelectionPathBindable;
         var inverse = new VectorPath();
         var inverse = new VectorPath();
-        inverse.AddRect(new RectI(new(0, 0), Document.SizeBindable));
+        inverse.AddRect(new RectD(new(0, 0), Document.SizeBindable));
 
 
         Internals.ActionAccumulator.AddFinishedActions(
         Internals.ActionAccumulator.AddFinishedActions(
             new SetSelection_Action(inverse.Op(selection, VectorPathOp.Difference)));
             new SetSelection_Action(inverse.Op(selection, VectorPathOp.Difference)));

+ 11 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs

@@ -77,7 +77,8 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         if (member is IVectorLayerHandler vectorLayerHandler)
         if (member is IVectorLayerHandler vectorLayerHandler)
         {
         {
             var shapeData = vectorLayerHandler.GetShapeData(document.AnimationHandler.ActiveFrameTime);
             var shapeData = vectorLayerHandler.GetShapeData(document.AnimationHandler.ActiveFrameTime);
-            if (shapeData == null || !InitShapeData(shapeData))
+            bool shapeIsValid = InitShapeData(shapeData);
+            if (shapeData == null || !shapeIsValid)
             {
             {
                 ActiveMode = ShapeToolMode.Preview;
                 ActiveMode = ShapeToolMode.Preview;
                 return ExecutionState.Success;
                 return ExecutionState.Success;
@@ -103,6 +104,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     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(IReadOnlyShapeVectorData data) { return true; }
     protected virtual bool InitShapeData(IReadOnlyShapeVectorData data) { return true; }
+    protected abstract bool CanEditShape(IStructureMemberHandler layer);
     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;
 
 
@@ -297,7 +299,14 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
 
     public override void OnSettingsChanged(string name, object value)
     public override void OnSettingsChanged(string name, object value)
     {
     {
-        internals!.ActionAccumulator.AddActions(SettingsChangedAction());
+        var layer = document.StructureHelper.Find(memberId);
+        if (layer is null)
+            return;
+        
+        if (CanEditShape(layer))
+        {
+            internals!.ActionAccumulator.AddActions(SettingsChangedAction());
+        }
     }
     }
 
 
     public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)
     public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)

+ 6 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs

@@ -6,6 +6,7 @@ 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.Models.Handlers;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
 #nullable enable
@@ -46,5 +47,10 @@ internal class RasterEllipseToolExecutor : DrawableShapeToolExecutor<IRasterElli
             FillColor, (float)StrokeWidth, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
             FillColor, (float)StrokeWidth, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
     }
 
 
+    protected override bool CanEditShape(IStructureMemberHandler layer)
+    {
+        return true;
+    }
+
     protected override IAction EndDrawAction() => new EndDrawRasterEllipse_Action();
     protected override IAction EndDrawAction() => new EndDrawRasterEllipse_Action();
 }
 }

+ 6 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs

@@ -5,6 +5,7 @@ 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.Models.Handlers;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
 #nullable enable
@@ -57,5 +58,10 @@ internal class RasterRectangleToolExecutor : DrawableShapeToolExecutor<IRasterRe
             document!.AnimationHandler.ActiveFrameBindable);
             document!.AnimationHandler.ActiveFrameBindable);
     }
     }
 
 
+    protected override bool CanEditShape(IStructureMemberHandler layer)
+    {
+        return true;
+    }
+
     protected override IAction EndDrawAction() => new EndDrawRasterRectangle_Action();
     protected override IAction EndDrawAction() => new EndDrawRasterRectangle_Action();
 }
 }

+ 11 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorEllipseToolExecutor.cs

@@ -8,6 +8,7 @@ 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;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.Models.Handlers;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
 
@@ -35,6 +36,16 @@ internal class VectorEllipseToolExecutor : DrawableShapeToolExecutor<IVectorElli
         return true;
         return true;
     }
     }
 
 
+    protected override bool CanEditShape(IStructureMemberHandler layer)
+    {
+        IVectorLayerHandler vectorLayer = layer as IVectorLayerHandler;
+        if (vectorLayer is null)
+            return false;
+        
+        var shapeData = vectorLayer.GetShapeData(document.AnimationHandler.ActiveFrameTime);
+        return shapeData is EllipseVectorData;
+    }
+
     protected override void DrawShape(VecD curPos, double rotationRad, bool firstDraw)
     protected override void DrawShape(VecD curPos, double rotationRad, bool firstDraw)
     {
     {
         RectD rect;
         RectD rect;

+ 18 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs

@@ -15,6 +15,7 @@ using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
+using PixiEditor.ViewModels.Tools.Tools;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
 
 
@@ -60,6 +61,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
             if (shapeData is PathVectorData pathData)
             if (shapeData is PathVectorData pathData)
             {
             {
                 startingPath = new VectorPath(pathData.Path);
                 startingPath = new VectorPath(pathData.Path);
+                ApplySettings(pathData);
                 startingPath.Transform(pathData.TransformationMatrix);
                 startingPath.Transform(pathData.TransformationMatrix);
             }
             }
             else if (shapeData is null)
             else if (shapeData is null)
@@ -183,7 +185,10 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
 
 
     public override void OnSettingsChanged(string name, object value)
     public override void OnSettingsChanged(string name, object value)
     {
     {
-        internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData()));
+        if (document.PathOverlayHandler.IsActive)
+        {
+            internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData()));
+        }
     }
     }
 
 
     public override void ForceStop()
     public override void ForceStop()
@@ -198,7 +203,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     {
     {
         if(startingPath == null)
         if(startingPath == null)
         {
         {
-            return new PathVectorData(new VectorPath())
+            return new PathVectorData(new VectorPath() { FillType = vectorPathToolHandler.FillMode })
             {
             {
                 StrokeWidth = (float)toolbar.ToolSize,
                 StrokeWidth = (float)toolbar.ToolSize,
                 StrokeColor = toolbar.StrokeColor.ToColor(),
                 StrokeColor = toolbar.StrokeColor.ToColor(),
@@ -206,7 +211,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
             };
             };
         }
         }
         
         
-        return new PathVectorData(new VectorPath(startingPath))
+        return new PathVectorData(new VectorPath(startingPath) { FillType = vectorPathToolHandler.FillMode })
         {
         {
             StrokeWidth = (float)toolbar.ToolSize,
             StrokeWidth = (float)toolbar.ToolSize,
             StrokeColor = toolbar.StrokeColor.ToColor(),
             StrokeColor = toolbar.StrokeColor.ToColor(),
@@ -261,4 +266,14 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
 
 
         return shapeData is not IReadOnlyPathData pathData || pathData.Path.IsClosed;
         return shapeData is not IReadOnlyPathData pathData || pathData.Path.IsClosed;
     }
     }
+    
+    private void ApplySettings(PathVectorData pathData)
+    {
+        toolbar.ToolSize = pathData.StrokeWidth;
+        toolbar.StrokeColor = pathData.StrokeColor.ToColor();
+        toolbar.ToolSize = pathData.StrokeWidth;
+        toolbar.Fill = pathData.Fill;
+        toolbar.FillColor = pathData.FillColor.ToColor();
+        toolbar.GetSetting(nameof(VectorPathToolViewModel.FillMode)).Value = pathData.Path.FillType;
+    }
 }
 }

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

@@ -8,6 +8,7 @@ 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;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.Models.Handlers;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
 
@@ -34,6 +35,16 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
         return true;
         return true;
     }
     }
 
 
+    protected override bool CanEditShape(IStructureMemberHandler layer)
+    {
+        IVectorLayerHandler vectorLayer = layer as IVectorLayerHandler;
+        if (vectorLayer is null)
+            return false;
+
+        var shapeData = vectorLayer.GetShapeData(document.AnimationHandler.ActiveFrameTime);
+        return shapeData is RectangleVectorData;
+    }
+
     protected override void DrawShape(VecD curPos, double rotationRad, bool firstDraw)
     protected override void DrawShape(VecD curPos, double rotationRad, bool firstDraw)
     {
     {
         RectD rect;
         RectD rect;
@@ -81,7 +92,7 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
         }
         }
 
 
         Matrix3X3 matrix = Matrix3X3.Identity;
         Matrix3X3 matrix = Matrix3X3.Identity;
-        
+
         if (!corners.IsRect)
         if (!corners.IsRect)
         {
         {
             RectD firstRect = RectD.FromCenterAndSize(firstCenter, firstSize);
             RectD firstRect = RectD.FromCenterAndSize(firstCenter, firstSize);
@@ -93,9 +104,10 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
         {
         {
             firstCenter = data.Center;
             firstCenter = data.Center;
             firstSize = data.Size;
             firstSize = data.Size;
-            
-            if(corners.RectRotation != 0)
-                matrix = Matrix3X3.CreateRotation((float)corners.RectRotation, (float)firstCenter.X, (float)firstCenter.Y);
+
+            if (corners.RectRotation != 0)
+                matrix = Matrix3X3.CreateRotation((float)corners.RectRotation, (float)firstCenter.X,
+                    (float)firstCenter.Y);
         }
         }
 
 
         RectangleVectorData newData = new RectangleVectorData(firstCenter, firstSize)
         RectangleVectorData newData = new RectangleVectorData(firstCenter, firstSize)

+ 4 - 2
src/PixiEditor/Models/Handlers/Tools/IVectorPathToolHandler.cs

@@ -1,6 +1,8 @@
-namespace PixiEditor.Models.Handlers.Tools;
+using Drawie.Backend.Core.Vector;
+
+namespace PixiEditor.Models.Handlers.Tools;
 
 
 internal interface IVectorPathToolHandler : IToolHandler
 internal interface IVectorPathToolHandler : IToolHandler
 {
 {
-    
+    public PathFillType FillMode { get; }
 }
 }

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

@@ -256,8 +256,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             if (args.LayerChangeType == LayerAction.Add)
             if (args.LayerChangeType == LayerAction.Add)
             {
             {
                 IReadOnlyStructureNode layer = Internals.Tracker.Document.FindMember(args.LayerAffectedGuid);
                 IReadOnlyStructureNode layer = Internals.Tracker.Document.FindMember(args.LayerAffectedGuid);
-                SnappingViewModel.AddFromBounds(layer.Id.ToString(),
-                    () => layer.GetTightBounds(AnimationDataViewModel.ActiveFrameTime) ?? RectD.Empty);
+                if (layer is not null)
+                {
+                    SnappingViewModel.AddFromBounds(layer.Id.ToString(),
+                        () => layer.GetTightBounds(AnimationDataViewModel.ActiveFrameTime) ?? RectD.Empty);
+                }
             }
             }
             else if (args.LayerChangeType == LayerAction.Remove)
             else if (args.LayerChangeType == LayerAction.Remove)
             {
             {

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

@@ -34,5 +34,6 @@ internal class FillableShapeToolbar : ShapeToolbar, IFillableShapeToolbar
     {
     {
         AddSetting(new BoolSettingViewModel(nameof(Fill), "FILL_SHAPE_LABEL") { Value = true });
         AddSetting(new BoolSettingViewModel(nameof(Fill), "FILL_SHAPE_LABEL") { Value = true });
         AddSetting(new ColorSettingViewModel(nameof(FillColor), "FILL_COLOR_LABEL"));
         AddSetting(new ColorSettingViewModel(nameof(FillColor), "FILL_COLOR_LABEL"));
+        GetSetting<SizeSettingViewModel>(nameof(ToolSize)).Value = 0;
     }
     }
 }
 }

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

@@ -1,4 +1,5 @@
 using Avalonia.Input;
 using Avalonia.Input;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
@@ -30,8 +31,15 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
     private LocalizedString actionDisplayCtrl;
     private LocalizedString actionDisplayCtrl;
     private LocalizedString actionDisplayAlt;
     private LocalizedString actionDisplayAlt;
 
 
+    [Settings.Enum("FILL_MODE", PathFillType.Winding)]
+    public PathFillType FillMode
+    {
+        get => GetValue<PathFillType>();
+    }
+
     public VectorPathToolViewModel()
     public VectorPathToolViewModel()
     {
     {
+        Toolbar = ToolbarFactory.Create<VectorPathToolViewModel, FillableShapeToolbar>(this);
         var fillSetting = Toolbar.GetSetting(nameof(FillableShapeToolbar.Fill));
         var fillSetting = Toolbar.GetSetting(nameof(FillableShapeToolbar.Fill));
         if (fillSetting != null)
         if (fillSetting != null)
         {
         {

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

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

+ 258 - 0
src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs

@@ -0,0 +1,258 @@
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+
+namespace PixiEditor.Views.Overlays.PathOverlay;
+
+public class EditableVectorPath
+{
+    private VectorPath path;
+
+    public VectorPath Path
+    {
+        get => path;
+        set
+        {
+            UpdatePathFrom(value);
+            path = value;            
+        }
+    }
+
+    private List<SubShape> subShapes = new List<SubShape>();
+
+    public IReadOnlyList<SubShape> SubShapes => subShapes;
+    
+    public int TotalPoints => subShapes.Sum(x => x.Points.Count);
+
+    public int ControlPointsCount
+    {
+        get
+        {
+            // count verbs with control points
+            return subShapes.Sum(x => CountControlPoints(x.Points));
+        }
+    }
+
+    public EditableVectorPath(VectorPath path)
+    {
+        if (path != null)
+        {
+            Path = new VectorPath(path);
+            UpdatePathFrom(Path);
+        }
+        else
+        {
+            this.path = null;
+        }
+    }
+
+    public VectorPath ToVectorPath()
+    {
+        VectorPath newPath = new VectorPath(Path);
+        newPath.Reset(); // preserve fill type and other properties
+        foreach (var subShape in subShapes)
+        {
+            AddVerbToPath(CreateMoveToVerb(subShape), newPath);
+            for (int i = 0; i < subShape.Points.Count; i++)
+            {
+                AddVerbToPath(subShape.Points[i].Verb, newPath);
+            }
+
+            if (subShape.IsClosed)
+            {
+                newPath.Close();
+            }
+        }
+
+        return newPath;
+    }
+    
+    private static Verb CreateMoveToVerb(SubShape subShape)
+    {
+        VecF[] points = new VecF[4];
+        points[0] = subShape.Points[0].Position;
+        
+        return new Verb((PathVerb.Move, points, 0));
+    }
+
+    private void UpdatePathFrom(VectorPath from)
+    {
+        subShapes.Clear();
+        if (from == null)
+        {
+            path = null;
+            return;
+        }
+
+        int currentSubShapeStartingIndex = 0;
+        bool isSubShapeClosed = false;
+        int globalVerbIndex = 0;
+
+        List<ShapePoint> currentSubShapePoints = new List<ShapePoint>();
+
+        foreach(var data in from)
+        {
+            if (data.verb == PathVerb.Done)
+            {
+                if (!isSubShapeClosed)
+                {
+                    subShapes.Add(new SubShape(currentSubShapePoints, isSubShapeClosed));
+                }
+            }
+            else if (data.verb == PathVerb.Close)
+            {
+                isSubShapeClosed = true;
+                VecF[] verbData = data.points.ToArray();
+                if (currentSubShapePoints[^1].Verb.IsEmptyVerb())
+                {
+                    verbData[0] = currentSubShapePoints[^2].Verb.To;
+                    verbData[1] = currentSubShapePoints[0].Verb.From;
+                    if (verbData[0] != verbData[1])
+                    {
+                        AddVerb((PathVerb.Line, verbData, 0), currentSubShapePoints);
+                    }
+
+                    currentSubShapePoints.RemoveAt(currentSubShapePoints.Count - 1);
+                }
+
+                subShapes.Add(new SubShape(currentSubShapePoints, isSubShapeClosed));
+
+                currentSubShapePoints.Clear();
+
+                currentSubShapeStartingIndex = globalVerbIndex;
+            }
+            else
+            {
+                isSubShapeClosed = false;
+                if (data.verb == PathVerb.Move)
+                {
+                    currentSubShapePoints.Clear();
+                }
+                else
+                {
+                    AddVerb(data, currentSubShapePoints);
+                }
+            }
+
+            globalVerbIndex++;
+        }
+    }
+    
+    private void AddVerbToPath(Verb verb, VectorPath newPath)
+    {
+        if (verb.IsEmptyVerb())
+        {
+            return;
+        }
+
+        switch (verb.VerbType)
+        {
+            case PathVerb.Move:
+                newPath.MoveTo(verb.From);
+                break;
+            case PathVerb.Line:
+                newPath.LineTo(verb.To);
+                break;
+            case PathVerb.Quad:
+                newPath.QuadTo(verb.ControlPoint1.Value, verb.To);
+                break;
+            case PathVerb.Cubic:
+                newPath.CubicTo(verb.ControlPoint1.Value, verb.ControlPoint2.Value, verb.To);
+                break;
+            case PathVerb.Conic:
+                newPath.ConicTo(verb.ControlPoint1.Value, verb.To, verb.ConicWeight);
+                break;
+            case PathVerb.Close:
+                newPath.Close();
+                break;
+        }
+    }
+
+    private static void AddVerb((PathVerb verb, VecF[] points, float conicWeight) data,
+        List<ShapePoint> currentSubShapePoints)
+    {
+        VecF point = data.points[0];
+        int atIndex = Math.Max(0, currentSubShapePoints.Count - 1);
+        bool indexExists = currentSubShapePoints.Count > atIndex;
+        ShapePoint toAdd = new ShapePoint(point, atIndex, new Verb(data));
+        if (!indexExists)
+        {
+            currentSubShapePoints.Add(toAdd);
+        }
+        else
+        {
+            currentSubShapePoints[atIndex] = toAdd;
+        }
+
+
+        VecF to = Verb.GetPointFromVerb(data);
+        currentSubShapePoints.Add(new ShapePoint(to, atIndex + 1, new Verb()));
+    }
+
+    public SubShape GetSubShapeContainingIndex(int index)
+    {
+        int currentIndex = 0;
+        foreach (var subShape in subShapes)
+        {
+            if (currentIndex + subShape.Points.Count > index)
+            {
+                return subShape;
+            }
+
+            currentIndex += subShape.Points.Count;
+        }
+
+        return null;
+    }
+    
+    private int CountControlPoints(IReadOnlyList<ShapePoint> points)
+    {
+        int count = 0;
+        foreach (var point in points)
+        {
+            if(point.Verb.VerbType != PathVerb.Cubic) continue; // temporarily only cubic is supported for control points
+            if (point.Verb.ControlPoint1 != null)
+            {
+                count++;
+            }
+
+            if (point.Verb.ControlPoint2 != null)
+            {
+                count++;
+            }
+        }
+
+        return count;
+    }
+
+    public int GetSubShapePointIndex(int globalIndex, SubShape subShapeContainingIndex)
+    {
+        int currentIndex = 0;
+        foreach (var subShape in subShapes)
+        {
+            if (subShape == subShapeContainingIndex)
+            {
+                return globalIndex - currentIndex;
+            }
+
+            currentIndex += subShape.Points.Count;
+        }
+
+        return -1;
+    }
+
+    public int GetGlobalIndex(SubShape subShape, int pointIndex)
+    {
+        int currentIndex = 0;
+        foreach (var shape in subShapes)
+        {
+            if (shape == subShape)
+            {
+                return currentIndex + pointIndex;
+            }
+
+            currentIndex += shape.Points.Count;
+        }
+
+        return -1;
+    }
+}

+ 131 - 0
src/PixiEditor/Views/Overlays/PathOverlay/ShapePoint.cs

@@ -0,0 +1,131 @@
+using System.Diagnostics;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+
+namespace PixiEditor.Views.Overlays.PathOverlay;
+
+[DebuggerDisplay($"Position: {{{nameof(Position)}}}, Index: {{{nameof(Index)}}}")]
+public class ShapePoint
+{
+    public VecF Position { get; set; }
+
+    public int Index { get; }
+    public Verb Verb { get; set; }
+
+    public ShapePoint(VecF position, int index, Verb verb)
+    {
+        Position = position;
+        Index = index;
+        Verb = verb;
+    }
+
+    public void ConvertVerbToCubic()
+    {
+        if(Verb.IsEmptyVerb()) return;
+        
+        VecF[] points = ConvertVerbToCubicPoints();
+        Verb = new Verb((PathVerb.Cubic, points, Verb.ConicWeight));
+    }
+    
+    private VecF[] ConvertVerbToCubicPoints()
+    {
+        if (Verb.VerbType == PathVerb.Line)
+        {
+            return [Verb.From, Verb.ControlPoint1 ?? Verb.From, Verb.ControlPoint2 ?? Verb.To, Verb.To];
+        }
+
+        if (Verb.VerbType == PathVerb.Conic)
+        {
+            VecF mid1 = Verb.ControlPoint1 ?? Verb.From;
+            
+            float factor = 2 * Verb.ConicWeight / (1 + Verb.ConicWeight);
+
+            VecF control1 = mid1;// + new VecF((mid1.X - Verb.From.X) * factor, (mid1.Y - Verb.From.Y) * factor);
+            VecF control2 = Verb.To + new VecF((mid1.X - Verb.To.X) * factor, (mid1.Y - Verb.To.Y) * factor);
+            
+            return [Verb.From, control1, control2, Verb.To];
+        }
+        
+        //TODO: Implement Quad to Cubic conversion
+        return [Verb.From, Verb.ControlPoint1 ?? Verb.From, Verb.ControlPoint2 ?? Verb.To, Verb.To];
+    }
+}
+
+[DebuggerDisplay($"{{{nameof(VerbType)}}}")]
+public class Verb
+{
+    public PathVerb? VerbType { get; }
+
+    public VecF From { get; set; }
+    public VecF To { get; set; }
+    public VecF? ControlPoint1 { get; set; }
+    public VecF? ControlPoint2 { get; set; }
+    public float ConicWeight { get; set; }
+
+    public Verb()
+    {
+        VerbType = null;
+    }
+    
+    public Verb((PathVerb verb, VecF[] points, float conicWeight) verbData)
+    {
+        VerbType = verbData.verb;
+        From = verbData.points[0];
+        To = GetPointFromVerb(verbData);
+        ControlPoint1 = GetControlPoint(verbData, true);
+        ControlPoint2 = GetControlPoint(verbData, false);
+        ConicWeight = verbData.conicWeight;
+    }
+    
+    public bool IsEmptyVerb()
+    {
+        return VerbType == null;
+    }
+    
+    public static VecF GetPointFromVerb((PathVerb verb, VecF[] points, float conicWeight) 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();
+        }
+    }
+    
+    public static VecF? GetControlPoint((PathVerb verb, VecF[] points, float conicWeight) data, bool first)
+    {
+        int index = first ? 1 : 2;
+        switch (data.verb)
+        {
+            case PathVerb.Move:
+                return null;
+            case PathVerb.Line:
+                return null;
+            case PathVerb.Quad:
+                return data.points[index];
+            case PathVerb.Cubic:
+                return data.points[index];
+            case PathVerb.Conic:
+                return data.points[index];
+            case PathVerb.Close:
+                return null;
+            case PathVerb.Done:
+                return null;
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+    }
+}

+ 72 - 0
src/PixiEditor/Views/Overlays/PathOverlay/SubShape.cs

@@ -0,0 +1,72 @@
+using System.Diagnostics;
+using Drawie.Numerics;
+
+namespace PixiEditor.Views.Overlays.PathOverlay;
+
+[DebuggerDisplay($"Points: {{{nameof(Points)}}}, Closed: {{{nameof(IsClosed)}}}")]
+public class SubShape
+{
+    private List<ShapePoint> points;
+
+    public IReadOnlyList<ShapePoint> Points => points;
+    public bool IsClosed { get; }
+
+    public ShapePoint? GetNextPoint(int nextToIndex)
+    {
+        if (nextToIndex + 1 < points.Count)
+        {
+            return points[nextToIndex + 1];
+        }
+
+        return IsClosed ? points[0] : null;
+    }
+
+    public ShapePoint? GetPreviousPoint(int previousToIndex)
+    {
+        if (previousToIndex - 1 >= 0)
+        {
+            return points[previousToIndex - 1];
+        }
+
+        return IsClosed ? points[^1] : null;
+    }
+
+    public SubShape(List<ShapePoint> points, bool isClosed)
+    {
+        this.points = new List<ShapePoint>(points);
+        IsClosed = isClosed;
+    }
+
+    public void SetPointPosition(int i, VecF newPos, bool updateControlPoints)
+    {
+        var shapePoint = points[i];
+        var oldPos = shapePoint.Position;
+        VecF delta = newPos - oldPos;
+        shapePoint.Position = newPos;
+        shapePoint.Verb.From = newPos;
+
+        if (updateControlPoints)
+        {
+            if (shapePoint.Verb.ControlPoint1 != null)
+            {
+                shapePoint.Verb.ControlPoint1 = shapePoint.Verb.ControlPoint1.Value + delta;
+            }
+            
+        }
+
+        var previousPoint = GetPreviousPoint(i);
+
+        if (previousPoint?.Verb != null && previousPoint.Verb.To == oldPos)
+        {
+            previousPoint.Verb.To = newPos;
+            
+            if (updateControlPoints)
+            {
+                if (previousPoint.Verb.ControlPoint2 != null)
+                {
+                    previousPoint.Verb.ControlPoint2 = previousPoint.Verb.ControlPoint2.Value + delta;
+                }
+            }
+        }
+    }
+}

+ 162 - 341
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -1,7 +1,10 @@
 using System.Windows.Input;
 using System.Windows.Input;
 using Avalonia;
 using Avalonia;
 using Avalonia.Input;
 using Avalonia.Input;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
@@ -54,6 +57,8 @@ public class VectorPathOverlay : Overlay
     private VecD posOnStartDrag;
     private VecD posOnStartDrag;
     private VectorPath pathOnStartDrag;
     private VectorPath pathOnStartDrag;
 
 
+    private EditableVectorPath editableVectorPath;
+
     static VectorPathOverlay()
     static VectorPathOverlay()
     {
     {
         AffectsOverlayRender(PathProperty);
         AffectsOverlayRender(PathProperty);
@@ -68,7 +73,7 @@ public class VectorPathOverlay : Overlay
 
 
         AddHandle(transformHandle);
         AddHandle(transformHandle);
     }
     }
-    
+
     protected override void ZoomChanged(double newZoom)
     protected override void ZoomChanged(double newZoom)
     {
     {
         dashedStroke.UpdateZoom((float)newZoom);
         dashedStroke.UpdateZoom((float)newZoom);
@@ -100,74 +105,75 @@ public class VectorPathOverlay : Overlay
     private void RenderHandles(Canvas context)
     private void RenderHandles(Canvas context)
     {
     {
         bool anySelected = false;
         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;
+        EditableVectorPath editablePath = new EditableVectorPath(Path);
 
 
-            VecF verbPointPos = GetVerbPointPos(verb);
+        int anchorIndex = 0;
+        int controlPointIndex = 0;
+        for (int i = 0; i < editablePath.SubShapes.Count; i++)
+        {
+            var subPath = editablePath.SubShapes[i];
 
 
-            if (verb.verb == PathVerb.Cubic)
+            if (subPath.Points.Count == 0)
             {
             {
-                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;
+                continue;
+            }
 
 
-                controlPointHandle1.Position = controlPoint1;
+            foreach (var point in subPath.Points)
+            {
+                var handle = anchorHandles[anchorIndex];
+                handle.Position = (VecD)point.Position;
 
 
-                if (controlPointHandle1.HitTestVisible)
+                if (point.Verb.ControlPoint1 != null || point.Verb.ControlPoint2 != null)
                 {
                 {
-                    controlPointHandle1.Draw(context);
+                    DrawControlPoints(context, point, ref controlPointIndex);
                 }
                 }
 
 
-                controlPointHandle2.Position = controlPoint2;
+                handle.Draw(context);
+                anySelected |= handle.IsSelected;
+                anchorIndex++;
+            }
+        }
 
 
-                if (controlPointHandle2.HitTestVisible)
-                {
-                    controlPointHandle2.Draw(context);
-                }
+        transformHandle.Position = Path.TightBounds.BottomRight + new VecD(1, 1);
+        transformHandle.Draw(context);
+    }
 
 
-                controlPoint += 2;
-            }
-            else if (verb.verb == PathVerb.Close)
-            {
-                continue;
-            }
+    private void DrawControlPoints(Canvas context, ShapePoint point, ref int controlPointIndex)
+    {
+        if (point.Verb.VerbType != PathVerb.Cubic) return;
 
 
-            if (anchor == anchorCount)
+        if (point.Verb.ControlPoint1 != null)
+        {
+            var controlPoint1 = controlPointHandles[controlPointIndex];
+            controlPoint1.HitTestVisible = controlPoint1.Position != controlPoint1.ConnectedTo.Position;
+            controlPoint1.Position = (VecD)point.Verb.ControlPoint1;
+            if (controlPoint1.HitTestVisible)
             {
             {
-                continue;
+                controlPoint1.Draw(context);
             }
             }
 
 
-            anchorHandles[anchor].Position = new VecD(verbPointPos.X, verbPointPos.Y);
-            anchorHandles[anchor].Draw(context);
-
-            anchor++;
+            controlPointIndex++;
         }
         }
 
 
-        transformHandle.Position = Path.TightBounds.BottomRight + new VecD(1, 1);
-        transformHandle.Draw(context);
-    }
+        if (point.Verb.ControlPoint2 != null)
+        {
+            var controlPoint2 = controlPointHandles[controlPointIndex];
+            controlPoint2.Position = (VecD)point.Verb.ControlPoint2;
+            controlPoint2.HitTestVisible = controlPoint2.Position != controlPoint2.ConnectedTo.Position;
 
 
-    private int GetAnchorCount()
-    {
-        return Path.VerbCount - (Path.IsClosed ? 2 : 0);
+            if (controlPoint2.HitTestVisible)
+            {
+                controlPoint2.Draw(context);
+            }
+
+            controlPointIndex++;
+        }
     }
     }
 
 
-    private void AdjustHandles(int pointsCount)
+    private void AdjustHandles(EditableVectorPath path)
     {
     {
+        int pointsCount = path.TotalPoints + path.ControlPointsCount;
         int anchorCount = anchorHandles.Count;
         int anchorCount = anchorHandles.Count;
         int totalHandles = anchorCount + controlPointHandles.Count;
         int totalHandles = anchorCount + controlPointHandles.Count;
         if (totalHandles != pointsCount)
         if (totalHandles != pointsCount)
@@ -177,8 +183,8 @@ public class VectorPathOverlay : Overlay
                 RemoveAllHandles();
                 RemoveAllHandles();
             }
             }
 
 
-            int missingControlPoints = CalculateMissingControlPoints(controlPointHandles.Count);
-            int missingAnchors = GetAnchorCount() - anchorHandles.Count;
+            int missingControlPoints = path.ControlPointsCount - controlPointHandles.Count;
+            int missingAnchors = path.TotalPoints - anchorHandles.Count;
             for (int i = 0; i < missingAnchors; i++)
             for (int i = 0; i < missingAnchors; i++)
             {
             {
                 CreateHandle(anchorHandles.Count);
                 CreateHandle(anchorHandles.Count);
@@ -194,59 +200,43 @@ public class VectorPathOverlay : Overlay
 
 
             ConnectControlPointsToAnchors();
             ConnectControlPointsToAnchors();
         }
         }
+
         Refresh();
         Refresh();
     }
     }
 
 
     private void ConnectControlPointsToAnchors()
     private void ConnectControlPointsToAnchors()
     {
     {
+        if (controlPointHandles.Count == 0)
+        {
+            return;
+        }
+
         int controlPointIndex = 0;
         int controlPointIndex = 0;
-        int anchorIndex = 0;
-        foreach (var data in Path)
+        foreach (var subShape in editableVectorPath.SubShapes)
         {
         {
-            if (data.verb == PathVerb.Cubic)
+            foreach (var point in subShape.Points)
             {
             {
-                int targetAnchorIndex1 = anchorIndex - 1;
-                if (targetAnchorIndex1 < 0)
-                {
-                    targetAnchorIndex1 = anchorHandles.Count - 1;
-                }
-
-                AnchorHandle previousAnchor = anchorHandles.ElementAtOrDefault(targetAnchorIndex1);
-
-                int targetAnchorIndex2 = anchorIndex;
-                if (targetAnchorIndex2 >= anchorHandles.Count)
+                if (point.Verb.VerbType == PathVerb.Cubic)
                 {
                 {
-                    targetAnchorIndex2 = 0;
-                }
-
-                AnchorHandle nextAnchor = anchorHandles.ElementAtOrDefault(targetAnchorIndex2);
+                    var controlPoint1 = controlPointHandles[controlPointIndex];
+                    var controlPoint2 = controlPointHandles[controlPointIndex + 1];
 
 
-                if (previousAnchor != null)
-                {
-                    controlPointHandles[controlPointIndex].ConnectedTo = previousAnchor;
-                }
+                    var nextPoint = subShape.GetNextPoint(point.Index);
 
 
-                controlPointHandles[controlPointIndex + 1].ConnectedTo = nextAnchor;
-                controlPointIndex += 2;
-            }
+                    int globalIndex = editableVectorPath.GetGlobalIndex(subShape, point.Index);
 
 
-            anchorIndex++;
-        }
-    }
+                    controlPoint1.ConnectedTo = GetHandleAt(globalIndex);
 
 
-    private int CalculateMissingControlPoints(int handleCount)
-    {
-        int totalControlPoints = 0;
+                    if (nextPoint != null)
+                    {
+                        int globalNextIndex = editableVectorPath.GetGlobalIndex(subShape, nextPoint.Index);
+                        controlPoint2.ConnectedTo = GetHandleAt(globalNextIndex);
+                    }
 
 
-        foreach (var point in Path)
-        {
-            if (point.verb == PathVerb.Cubic)
-            {
-                totalControlPoints += 2;
+                    controlPointIndex += 2;
+                }
             }
             }
         }
         }
-
-        return totalControlPoints - handleCount;
     }
     }
 
 
     private void RemoveAllHandles()
     private void RemoveAllHandles()
@@ -356,7 +346,7 @@ public class VectorPathOverlay : Overlay
             return;
             return;
         }
         }
 
 
-        if (IsFirstHandle(anchorHandle))
+        if (anchorHandles.IndexOf(anchorHandle) == 0)
         {
         {
             newPath.LineTo((VecF)anchorHandle.Position);
             newPath.LineTo((VecF)anchorHandle.Position);
             newPath.Close();
             newPath.Close();
@@ -370,11 +360,6 @@ public class VectorPathOverlay : Overlay
         Path = newPath;
         Path = newPath;
     }
     }
 
 
-    private bool IsFirstHandle(AnchorHandle handle)
-    {
-        return anchorHandles.IndexOf(handle) == 0;
-    }
-
     private void SelectAnchor(AnchorHandle handle)
     private void SelectAnchor(AnchorHandle handle)
     {
     {
         foreach (var anchorHandle in anchorHandles)
         foreach (var anchorHandle in anchorHandles)
@@ -392,64 +377,35 @@ public class VectorPathOverlay : Overlay
 
 
             if (!args.Modifiers.HasFlag(KeyModifiers.Control)) return;
             if (!args.Modifiers.HasFlag(KeyModifiers.Control)) return;
 
 
-            var newPath = ConvertTouchingLineVerbsToCubic(anchorHandle);
+            var newPath = ConvertTouchingVerbsToCubic(anchorHandle);
+
+            int index = anchorHandles.IndexOf(anchorHandle);
+            SubShape subShapeContainingIndex = editableVectorPath.GetSubShapeContainingIndex(index);
+            int localIndex = editableVectorPath.GetSubShapePointIndex(index, subShapeContainingIndex);
+
+            HandleContinousCubicDrag(anchorHandle.Position, anchorHandle, subShapeContainingIndex, localIndex);
 
 
-            Path = newPath;
+            Path = newPath.ToVectorPath();
         }
         }
     }
     }
 
 
     // To have continous spline, verb before and after a point must be a cubic with proper control points
     // To have continous spline, verb before and after a point must be a cubic with proper control points
-    private VectorPath ConvertTouchingLineVerbsToCubic(AnchorHandle anchorHandle)
+    private EditableVectorPath ConvertTouchingVerbsToCubic(AnchorHandle anchorHandle)
     {
     {
-        bool convertNextToCubic = false;
-        int i = -1;
-        VectorPath newPath = new VectorPath();
         int index = anchorHandles.IndexOf(anchorHandle);
         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);
-            }
+        SubShape subShapeContainingIndex = editableVectorPath.GetSubShapeContainingIndex(index);
 
 
-            i++;
-        }
+        int localIndex = editableVectorPath.GetSubShapePointIndex(index, subShapeContainingIndex);
+
+        var previousPoint = subShapeContainingIndex.GetPreviousPoint(localIndex);
+        var nextPoint = subShapeContainingIndex.Points[localIndex];
+
+        previousPoint?.ConvertVerbToCubic();
 
 
-        return newPath;
+        nextPoint.ConvertVerbToCubic();
+
+        return editableVectorPath;
     }
     }
 
 
     private void OnHandleDrag(Handle source, OverlayPointerArgs args)
     private void OnHandleDrag(Handle source, OverlayPointerArgs args)
@@ -460,168 +416,87 @@ public class VectorPathOverlay : Overlay
         }
         }
 
 
         var index = anchorHandles.IndexOf(handle);
         var index = anchorHandles.IndexOf(handle);
-        VectorPath newPath = new VectorPath();
 
 
-        bool pointHandled = false;
-        int i = 0;
+        var targetPos = ApplySnapping(args.Point);
 
 
-        VecF previousDelta = new VecF();
+        SubShape subShapeContainingIndex = editableVectorPath.GetSubShapeContainingIndex(index);
 
 
-        int controlPointIndex = 0;
-        var connectedControlPoints = controlPointHandles.Where(x => x.ConnectedTo == handle).ToList();
-        int targetControlPointIndex = controlPointHandles.IndexOf(connectedControlPoints.FirstOrDefault());
-        int symmetricControlPointIndex = controlPointHandles.IndexOf(connectedControlPoints.LastOrDefault());
+        int localIndex = editableVectorPath.GetSubShapePointIndex(index, subShapeContainingIndex);
 
 
-        VecD targetPos = ApplySymmetry(args.Point);
-        VecD targetSymmetryPos = GetMirroredControlPoint((VecF)targetPos, (VecF)handle.Position);
+        bool isDraggingControlPoints = args.Modifiers.HasFlag(KeyModifiers.Control);
 
 
-        bool ctrlPressed = args.Modifiers.HasFlag(KeyModifiers.Control);
-        foreach (var data in Path)
+        if (isDraggingControlPoints)
         {
         {
-            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++;
+            HandleContinousCubicDrag(targetPos, handle, subShapeContainingIndex, localIndex);
+        }
+        else
+        {
+            subShapeContainingIndex.SetPointPosition(localIndex, (VecF)targetPos, true);
         }
         }
 
 
-        Path = newPath;
+        Path = editableVectorPath.ToVectorPath();
     }
     }
 
 
-    private void OnControlPointDrag(Handle source, OverlayPointerArgs args)
+    private void HandleContinousCubicDrag(VecD targetPos, AnchorHandle handle, SubShape subShapeContainingIndex,
+        int localIndex, bool swapOrder = false)
     {
     {
-        if (source is not ControlPointHandle controlPointHandle)
+        var symmetricPos = GetMirroredControlPoint((VecF)targetPos, (VecF)handle.Position);
+
+        if (swapOrder)
         {
         {
-            return;
+            (targetPos, symmetricPos) = (symmetricPos, targetPos);
         }
         }
 
 
-        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();
+        subShapeContainingIndex.Points[localIndex].Verb.ControlPoint1 = (VecF)targetPos;
+
+        var previousPoint = subShapeContainingIndex.GetPreviousPoint(localIndex);
 
 
-        if (args.Modifiers.HasFlag(KeyModifiers.Alt))
+        if (previousPoint != null)
         {
         {
-            symmetricIndex = -1;
+            previousPoint.Verb.ControlPoint2 = (VecF)symmetricPos;
         }
         }
+    }
 
 
-        int i = 0;
-
-        foreach (var data in Path)
+    private void OnControlPointDrag(Handle source, OverlayPointerArgs args)
+    {
+        if (source is not ControlPointHandle controlPointHandle ||
+            controlPointHandle.ConnectedTo is not AnchorHandle to)
         {
         {
-            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;
-            }
+            return;
         }
         }
 
 
-        Path = newPath;
-    }
+        var targetPos = ApplySnapping(args.Point);
 
 
-    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;
+        var globalIndex = anchorHandles.IndexOf(to);
+        var subShapeContainingIndex = editableVectorPath.GetSubShapeContainingIndex(globalIndex);
 
 
-        bool isFirstSymmetricControlPoint = i == symmetricIndex;
-        bool isSecondSymmetricControlPoint = i + 1 == symmetricIndex;
+        int localIndex = editableVectorPath.GetSubShapePointIndex(globalIndex, subShapeContainingIndex);
 
 
-        VecF controlPoint1 = data.points[1];
-        VecF controlPoint2 = data.points[2];
-        VecF endPoint = data.points[3];
+        bool dragOnlyOne = args.Modifiers.HasFlag(KeyModifiers.Alt);
 
 
-        if (isFirstControlPoint)
-        {
-            controlPoint1 = (VecF)targetPos;
-        }
-        else if (isSecondControlPoint)
+        if (!dragOnlyOne)
         {
         {
-            controlPoint2 = (VecF)targetPos;
+            bool isDraggingFirst = controlPointHandles.IndexOf(controlPointHandle) % 2 == 0;
+            HandleContinousCubicDrag(targetPos, to, subShapeContainingIndex, localIndex, !isDraggingFirst);
         }
         }
-        else if (isFirstSymmetricControlPoint)
-        {
-            controlPoint1 = (VecF)targetSymmetryPos;
-        }
-        else if (isSecondSymmetricControlPoint)
+        else
         {
         {
-            controlPoint2 = (VecF)targetSymmetryPos;
+            bool isFirstControlPoint = controlPointHandles.IndexOf(controlPointHandle) % 2 == 0;
+            if (isFirstControlPoint)
+            {
+                subShapeContainingIndex.Points[localIndex].Verb.ControlPoint1 = (VecF)targetPos;
+            }
+            else
+            {
+                var previousPoint = subShapeContainingIndex.GetPreviousPoint(localIndex);
+                if (previousPoint != null)
+                {
+                    previousPoint.Verb.ControlPoint2 = (VecF)targetPos;
+                }
+            }
         }
         }
 
 
-        newPath.CubicTo(controlPoint1, controlPoint2, endPoint);
+        Path = editableVectorPath.ToVectorPath();
     }
     }
 
 
     private VecD GetMirroredControlPoint(VecF controlPoint, VecF anchor)
     private VecD GetMirroredControlPoint(VecF controlPoint, VecF anchor)
@@ -629,45 +504,7 @@ public class VectorPathOverlay : Overlay
         return new VecD(2 * anchor.X - controlPoint.X, 2 * anchor.Y - controlPoint.Y);
         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)
+    private VecD ApplySnapping(VecD point)
     {
     {
         var snappedPoint = SnappingController.GetSnapPoint(point, out string axisX, out string axisY);
         var snappedPoint = SnappingController.GetSnapPoint(point, out string axisX, out string axisY);
         var snapped = new VecD((float)snappedPoint.X, (float)snappedPoint.Y);
         var snapped = new VecD((float)snappedPoint.X, (float)snappedPoint.Y);
@@ -770,36 +607,16 @@ public class VectorPathOverlay : Overlay
 
 
     private void PathChanged(VectorPath newPath)
     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)
+        if (editableVectorPath == null)
         {
         {
-            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();
+            editableVectorPath = new EditableVectorPath(newPath);
         }
         }
+        else
+        {
+            editableVectorPath.Path = newPath;
+        }
+
+        AdjustHandles(editableVectorPath);
     }
     }
 
 
     private static void OnPathChanged(AvaloniaPropertyChangedEventArgs<VectorPath> args)
     private static void OnPathChanged(AvaloniaPropertyChangedEventArgs<VectorPath> args)
@@ -810,11 +627,14 @@ public class VectorPathOverlay : Overlay
             overlay.SnappingController.RemoveAll("editingPath");
             overlay.SnappingController.RemoveAll("editingPath");
             overlay.ClearAnchorHandles();
             overlay.ClearAnchorHandles();
             overlay.IsVisible = false;
             overlay.IsVisible = false;
+            overlay.editableVectorPath = null;
         }
         }
         else
         else
         {
         {
             var path = args.NewValue.Value;
             var path = args.NewValue.Value;
-            overlay.AdjustHandles(path.PointCount - (path.IsClosed ? 1 : 0));
+            EditableVectorPath editablePath = new EditableVectorPath(path);
+            overlay.editableVectorPath = editablePath;
+            overlay.AdjustHandles(editablePath);
             overlay.IsVisible = true;
             overlay.IsVisible = true;
         }
         }
 
 
@@ -825,6 +645,7 @@ public class VectorPathOverlay : Overlay
 
 
         if (args.NewValue.Value != null)
         if (args.NewValue.Value != null)
         {
         {
+            overlay.editableVectorPath = new EditableVectorPath(args.NewValue.Value);
             args.NewValue.Value.Changed += overlay.PathChanged;
             args.NewValue.Value.Changed += overlay.PathChanged;
         }
         }
     }
     }

+ 3 - 0
tests/PixiEditor.Tests/AvaloniaTestRunner.cs

@@ -2,6 +2,9 @@
 using Avalonia.Headless;
 using Avalonia.Headless;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Threading;
 using Avalonia.Threading;
+using Drawie.Backend.Core.Bridge;
+using Drawie.Skia;
+using DrawiEngine;
 using PixiEditor.Desktop;
 using PixiEditor.Desktop;
 using Xunit.Abstractions;
 using Xunit.Abstractions;
 using Xunit.Sdk;
 using Xunit.Sdk;

+ 384 - 0
tests/PixiEditor.Tests/EditableVectorPathTests.cs

@@ -0,0 +1,384 @@
+using Drawie.Backend.Core.Bridge;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using Drawie.Skia;
+using DrawiEngine;
+using PixiEditor.Views.Overlays.PathOverlay;
+
+namespace PixiEditor.Tests;
+
+public class EditableVectorPathTests
+{
+    public EditableVectorPathTests()
+    {
+        if (DrawingBackendApi.HasBackend)
+        {
+            return;
+        }
+        
+        SkiaDrawingBackend skiaDrawingBackend = new SkiaDrawingBackend();
+        DrawingBackendApi.SetupBackend(skiaDrawingBackend, new DrawieRenderingDispatcher());
+    }
+
+    [Fact]
+    public void TestThatRectVectorShapeReturnsCorrectSubShapes()
+    {
+        VectorPath path = new VectorPath();
+        path.AddRect(new RectD(0, 0, 10, 10));
+
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+
+        Assert.Single(editablePath.SubShapes);
+        Assert.True(editablePath.SubShapes[0].IsClosed);
+        Assert.Equal(4, editablePath.SubShapes[0].Points.Count);
+        
+        Assert.Equal(new VecF(0, 0), editablePath.SubShapes[0].Points[0].Position);
+        Assert.Equal(new VecF(10, 0), editablePath.SubShapes[0].Points[1].Position);
+        Assert.Equal(new VecF(10, 10), editablePath.SubShapes[0].Points[2].Position);
+        Assert.Equal(new VecF(0, 10), editablePath.SubShapes[0].Points[3].Position);
+    }
+    
+    [Fact]
+    public void TestThatOvalVectorShapeReturnsCorrectSubShapes()
+    {
+        VectorPath path = new VectorPath();
+        path.AddOval(RectD.FromCenterAndSize(new VecD(5, 5), new VecD(10, 10)));
+
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+
+        Assert.Single(editablePath.SubShapes);
+        Assert.True(editablePath.SubShapes[0].IsClosed);
+        Assert.Equal(4, editablePath.SubShapes[0].Points.Count);
+        
+        Assert.Equal(new VecF(10, 5), editablePath.SubShapes[0].Points[0].Position);
+        Assert.Equal(new VecF(5, 10), editablePath.SubShapes[0].Points[1].Position);
+        Assert.Equal(new VecF(0, 5), editablePath.SubShapes[0].Points[2].Position);
+        Assert.Equal(new VecF(5, 0), editablePath.SubShapes[0].Points[3].Position);
+    }
+    
+    [Fact]
+    public void TestThatLineVectorShapeReturnsCorrectSubShapes()
+    {
+        VectorPath path = new VectorPath();
+        path.LineTo(new VecF(2, 2));
+        path.Close();
+
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+
+        Assert.Single(editablePath.SubShapes);
+        Assert.True(editablePath.SubShapes[0].IsClosed);
+        Assert.Equal(2, editablePath.SubShapes[0].Points.Count);
+        
+        Assert.Equal(new VecF(0, 0), editablePath.SubShapes[0].Points[0].Position);
+        Assert.Equal(new VecF(2, 2), editablePath.SubShapes[0].Points[1].Position);
+    }
+    
+    [Fact]
+    public void TestThatNotClosedPolyReturnsCorrectSubShape()
+    {
+        VectorPath path = new VectorPath();
+        
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(2, 2));
+        path.LineTo(new VecF(4, 4));
+
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+
+        Assert.Single(editablePath.SubShapes);
+        Assert.False(editablePath.SubShapes[0].IsClosed);
+        Assert.Equal(3, editablePath.SubShapes[0].Points.Count);
+        
+        Assert.Equal(new VecF(0, 0), editablePath.SubShapes[0].Points[0].Position);
+        Assert.Equal(new VecF(2, 2), editablePath.SubShapes[0].Points[1].Position);
+        Assert.Equal(new VecF(4, 4), editablePath.SubShapes[0].Points[2].Position);
+    }
+    
+    [Fact]
+    public void TestThatMultipleRectsReturnCorrectSubShapes()
+    {
+        VectorPath path = new VectorPath();
+        path.AddRect(new RectD(0, 0, 10, 10));
+        path.AddRect(new RectD(10, 10, 20, 20));
+
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+
+        Assert.Equal(2, editablePath.SubShapes.Count);
+        
+        Assert.True(editablePath.SubShapes[0].IsClosed);
+        Assert.Equal(4, editablePath.SubShapes[0].Points.Count);
+        
+        Assert.Equal(new VecF(0, 0), editablePath.SubShapes[0].Points[0].Position);
+        Assert.Equal(new VecF(10, 0), editablePath.SubShapes[0].Points[1].Position);
+        Assert.Equal(new VecF(10, 10), editablePath.SubShapes[0].Points[2].Position);
+        Assert.Equal(new VecF(0, 10), editablePath.SubShapes[0].Points[3].Position);
+        
+        Assert.True(editablePath.SubShapes[1].IsClosed);
+        Assert.Equal(4, editablePath.SubShapes[1].Points.Count);
+        
+        Assert.Equal(new VecF(10, 10), editablePath.SubShapes[1].Points[0].Position);
+        Assert.Equal(new VecF(30, 10), editablePath.SubShapes[1].Points[1].Position);
+        Assert.Equal(new VecF(30, 30), editablePath.SubShapes[1].Points[2].Position);
+        Assert.Equal(new VecF(10, 30), editablePath.SubShapes[1].Points[3].Position);
+    }
+
+    [Fact]
+    public void TestThatTwoPolysWithSecondUnclosedReturnsCorrectShapeData()
+    {
+        VectorPath path = new VectorPath();
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(2, 2));
+        path.LineTo(new VecF(4, 4));
+        path.Close();
+        
+        path.MoveTo(new VecF(10, 10));
+        path.LineTo(new VecF(12, 12));
+        path.LineTo(new VecF(14, 14));
+
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+
+        Assert.Equal(2, editablePath.SubShapes.Count);
+        
+        Assert.True(editablePath.SubShapes[0].IsClosed);
+        Assert.Equal(3, editablePath.SubShapes[0].Points.Count);
+        
+        Assert.Equal(new VecF(0, 0), editablePath.SubShapes[0].Points[0].Position);
+        Assert.Equal(new VecF(2, 2), editablePath.SubShapes[0].Points[1].Position);
+        Assert.Equal(new VecF(4, 4), editablePath.SubShapes[0].Points[2].Position);
+        
+        Assert.False(editablePath.SubShapes[1].IsClosed);
+        Assert.Equal(3, editablePath.SubShapes[1].Points.Count);
+        
+        Assert.Equal(new VecF(10, 10), editablePath.SubShapes[1].Points[0].Position);
+        Assert.Equal(new VecF(12, 12), editablePath.SubShapes[1].Points[1].Position);
+        Assert.Equal(new VecF(14, 14), editablePath.SubShapes[1].Points[2].Position);
+    }
+
+    [Fact]
+    public void TestThatStartAndEndIndexesForMultipleShapesAreCorrect()
+    {
+        VectorPath path = new VectorPath();
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(2, 2));
+        path.LineTo(new VecF(4, 4));
+        path.Close();
+        
+        path.MoveTo(new VecF(10, 10));
+        path.LineTo(new VecF(12, 12));
+        path.Close();
+        
+        int expectedFirstShapeStartIndex = 0;
+        int expectedFirstShapeEndIndex = 2;
+        
+        int expectedSecondShapeStartIndex = 3;
+        int expectedSecondShapeEndIndex = 5;
+        
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+        
+        Assert.Equal(2, editablePath.SubShapes.Count);
+    }
+    
+    [Fact]
+    public void TestThatGetNextPointInTriangleShapeReturnsCorrectPoint()
+    {
+        VectorPath path = new VectorPath();
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(2, 2));
+        path.LineTo(new VecF(4, 4));
+        path.Close();
+
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+
+        ShapePoint nextPoint = editablePath.SubShapes[0].GetNextPoint(0);
+        
+        Assert.Equal(new VecF(2, 2), nextPoint.Position);
+        Assert.Equal(1, nextPoint.Index);
+        
+        nextPoint = editablePath.SubShapes[0].GetNextPoint(1);
+        
+        Assert.Equal(new VecF(4, 4), nextPoint.Position);
+        Assert.Equal(2, nextPoint.Index);
+        
+        nextPoint = editablePath.SubShapes[0].GetNextPoint(2);
+        
+        Assert.Equal(new VecF(0, 0), nextPoint.Position);
+        Assert.Equal(0, nextPoint.Index);
+    }
+    
+    [Fact]
+    public void TestThatGetPreviousPointInTriangleShapeReturnsCorrectPoint()
+    {
+        VectorPath path = new VectorPath();
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(2, 2));
+        path.LineTo(new VecF(4, 4));
+        path.Close();
+
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+
+        ShapePoint previousPoint = editablePath.SubShapes[0].GetPreviousPoint(0);
+        
+        Assert.Equal(new VecF(4, 4), previousPoint.Position);
+        Assert.Equal(2, previousPoint.Index);
+        
+        previousPoint = editablePath.SubShapes[0].GetPreviousPoint(1);
+        
+        Assert.Equal(new VecF(0, 0), previousPoint.Position);
+        Assert.Equal(0, previousPoint.Index);
+        
+        previousPoint = editablePath.SubShapes[0].GetPreviousPoint(2);
+        
+        Assert.Equal(new VecF(2, 2), previousPoint.Position);
+        Assert.Equal(1, previousPoint.Index);
+    }
+
+    [Fact]
+    public void TestThatVerbsInTriangleAreCorrect()
+    {
+        VectorPath path = new VectorPath();
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(2, 2));
+        path.LineTo(new VecF(4, 4));
+        path.Close();
+        
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+        
+        Assert.Equal(3, editablePath.SubShapes[0].Points.Count);
+        
+        Assert.Equal(PathVerb.Line, editablePath.SubShapes[0].Points[0].Verb.VerbType);
+        
+        Assert.Equal(new VecF(0, 0), editablePath.SubShapes[0].Points[0].Verb.From);
+        Assert.Equal(new VecF(2, 2), editablePath.SubShapes[0].Points[0].Verb.To);
+        
+        Assert.Equal(PathVerb.Line, editablePath.SubShapes[0].Points[1].Verb.VerbType);
+        
+        Assert.Equal(new VecF(2, 2), editablePath.SubShapes[0].Points[1].Verb.From);
+        Assert.Equal(new VecF(4, 4), editablePath.SubShapes[0].Points[1].Verb.To);
+        
+        Assert.Equal(PathVerb.Line, editablePath.SubShapes[0].Points[2].Verb.VerbType);
+        
+        Assert.Equal(new VecF(4, 4), editablePath.SubShapes[0].Points[2].Verb.From);
+        Assert.Equal(new VecF(0, 0), editablePath.SubShapes[0].Points[2].Verb.To);
+    }
+
+    [Fact]
+    public void TestThatVerbsInOvalAreCorrect()
+    {
+        const float conic = 0.70710769f;
+        const float rangeLower = conic - 0.001f;
+        const float rangeUpper = conic + 0.001f;
+        VectorPath path = new VectorPath();
+        path.AddOval(RectD.FromCenterAndSize(new VecD(5, 5), new VecD(10, 10)));
+        
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+        
+        Assert.Equal(4, editablePath.SubShapes[0].Points.Count);
+        
+        Assert.Equal(PathVerb.Conic, editablePath.SubShapes[0].Points[0].Verb.VerbType);
+        
+        Assert.Equal(new VecF(10, 5), editablePath.SubShapes[0].Points[0].Verb.From);
+        Assert.Equal(new VecF(5, 10), editablePath.SubShapes[0].Points[0].Verb.To);
+        Assert.InRange(editablePath.SubShapes[0].Points[0].Verb.ConicWeight, rangeLower, rangeUpper);
+        
+        Assert.Equal(PathVerb.Conic, editablePath.SubShapes[0].Points[1].Verb.VerbType);
+        Assert.Equal(new VecF(5, 10), editablePath.SubShapes[0].Points[1].Verb.From);
+        Assert.Equal(new VecF(0, 5), editablePath.SubShapes[0].Points[1].Verb.To);
+        Assert.InRange(editablePath.SubShapes[0].Points[1].Verb.ConicWeight, rangeLower, rangeUpper);
+        
+        Assert.Equal(PathVerb.Conic, editablePath.SubShapes[0].Points[2].Verb.VerbType);
+        Assert.Equal(new VecF(0, 5), editablePath.SubShapes[0].Points[2].Verb.From);
+        Assert.Equal(new VecF(5, 0), editablePath.SubShapes[0].Points[2].Verb.To);
+        Assert.InRange(editablePath.SubShapes[0].Points[2].Verb.ConicWeight, rangeLower, rangeUpper);
+        
+        Assert.Equal(PathVerb.Conic, editablePath.SubShapes[0].Points[3].Verb.VerbType);
+        Assert.Equal(new VecF(5, 0), editablePath.SubShapes[0].Points[3].Verb.From);
+        Assert.Equal(new VecF(10, 5), editablePath.SubShapes[0].Points[3].Verb.To);
+        Assert.InRange(editablePath.SubShapes[0].Points[3].Verb.ConicWeight, rangeLower, rangeUpper);
+    }
+
+    [Fact]
+    public void TestThatOverlappingPolyPointsReturnCorrectSubShapePoints()
+    {
+        VectorPath path = new VectorPath();
+        
+        /* 
+         *     |\
+         *     |_\
+         *     |
+         */
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(0, 2));
+        path.LineTo(new VecF(0, 4));
+        path.LineTo(new VecF(2, 2));
+        path.LineTo(new VecF(0, 2));
+        path.Close();
+        
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+        
+        Assert.Equal(5, editablePath.SubShapes[0].Points.Count);
+        
+        Assert.Equal(new VecF(0, 0), editablePath.SubShapes[0].Points[0].Verb.From);
+        Assert.Equal(new VecF(0, 2), editablePath.SubShapes[0].Points[0].Verb.To);
+        
+        Assert.Equal(new VecF(0, 2), editablePath.SubShapes[0].Points[1].Verb.From);
+        Assert.Equal(new VecF(0, 4), editablePath.SubShapes[0].Points[1].Verb.To);
+        
+        Assert.Equal(new VecF(0, 4), editablePath.SubShapes[0].Points[2].Verb.From);
+        Assert.Equal(new VecF(2, 2), editablePath.SubShapes[0].Points[2].Verb.To);
+        
+        Assert.Equal(new VecF(2, 2), editablePath.SubShapes[0].Points[3].Verb.From);
+        Assert.Equal(new VecF(0, 2), editablePath.SubShapes[0].Points[3].Verb.To);
+        
+        Assert.Equal(new VecF(0, 2), editablePath.SubShapes[0].Points[4].Verb.From);
+        Assert.Equal(new VecF(0, 0), editablePath.SubShapes[0].Points[4].Verb.To);
+    }
+
+    [Fact]
+    public void TestThatEditingPointResultsInCorrectVectorPath()
+    {
+        VectorPath path = new VectorPath();
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(2, 2));
+        path.LineTo(new VecF(4, 4));
+        path.Close();
+        
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+        
+        editablePath.SubShapes[0].SetPointPosition(1, new VecF(3, 3), true);
+        
+        VectorPath newPath = editablePath.ToVectorPath();
+        
+        PathVerb[] sequence = [ PathVerb.Move, PathVerb.Line, PathVerb.Line, PathVerb.Line, PathVerb.Close, PathVerb.Done ];
+        VecF[] points = [ new VecF(0, 0), new VecF(3, 3), new VecF(4, 4), new VecF(0, 0) ];
+
+        int i = 0;
+        foreach (var data in newPath)
+        {
+            Assert.Equal(sequence[i], data.verb);
+            if(data.verb != PathVerb.Close && data.verb != PathVerb.Done)
+            {
+                Assert.Equal(points[i], Verb.GetPointFromVerb(data));
+            }
+            i++;
+        }
+    }
+
+    [Theory]
+    [InlineData(0, 0)]
+    [InlineData(1, 0)]
+    [InlineData(2, 0)]
+    [InlineData(3, 0)]
+    [InlineData(4, 1)]
+    [InlineData(5, 1)]
+    [InlineData(6, 1)]
+    [InlineData(7, 1)]
+    public void TestThatGetSubShapeByPointIndexReturnsCorrectSubShapeIndex(int index, int expected)
+    {
+        VectorPath path = new VectorPath();
+        path.AddOval(RectD.FromCenterAndSize(new VecD(5, 5), new VecD(10, 10)));
+        path.AddOval(RectD.FromCenterAndSize(new VecD(15, 15), new VecD(20, 20)));
+        
+        EditableVectorPath editablePath = new EditableVectorPath(path);
+        
+        Assert.Equal(expected, editablePath.SubShapes.ToList().IndexOf(editablePath.GetSubShapeContainingIndex(index)));
+    }
+}