Browse Source

Reworked path overlay

flabbet 8 months ago
parent
commit
031c0e401f

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 96dfe25513f74a09b88341dd1d06991520466d47
+Subproject commit 46b16a26646f37460869263d55a9ecaecb58c89f

+ 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;
+                }
+            }
+        }
+    }
+}

+ 140 - 529
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -57,6 +57,8 @@ public class VectorPathOverlay : Overlay
     private VecD posOnStartDrag;
     private VectorPath pathOnStartDrag;
 
+    private EditableVectorPath editableVectorPath;
+
     static VectorPathOverlay()
     {
         AffectsOverlayRender(PathProperty);
@@ -103,111 +105,75 @@ public class VectorPathOverlay : Overlay
     private void RenderHandles(Canvas context)
     {
         bool anySelected = false;
-        int globalAnchor = 0;
-        int controlPoint = 0;
-        int subPath = 1;
-        int anchorCount = GetAnchorCount();
-        int closeAtIndexInSubPath = GetCloseIndexInSubPath(Path, subPath);
 
-        foreach (var verb in Path)
+        EditableVectorPath editablePath = new EditableVectorPath(Path);
+
+        int anchorIndex = 0;
+        int controlPointIndex = 0;
+        for (int i = 0; i < editablePath.SubShapes.Count; i++)
         {
-            if (globalAnchor == anchorCount - 1 && !anySelected)
+            var subPath = editablePath.SubShapes[i];
+
+            if (subPath.Points.Count == 0)
             {
-                GetHandleAt(globalAnchor).IsSelected = true;
+                continue;
             }
 
-            anySelected = anySelected || GetHandleAt(globalAnchor).IsSelected;
-
-            VecF verbPointPos = GetVerbPointPos(verb);
-
-            if (verb.verb == PathVerb.Cubic)
+            foreach (var point in subPath.Points)
             {
-                VecD controlPoint1 = (VecD)verb.points[1];
-                VecD controlPoint2 = (VecD)verb.points[2];
-
-                var controlPointHandle1 = controlPointHandles[controlPoint];
-                var controlPointHandle2 = controlPointHandles[controlPoint + 1];
-
-                controlPointHandle1.HitTestVisible = controlPoint1 != controlPointHandle1.ConnectedTo.Position;
-                controlPointHandle2.HitTestVisible = controlPoint2 != controlPointHandle2.ConnectedTo.Position;
-
-                controlPointHandle1.Position = controlPoint1;
-
-                if (controlPointHandle1.HitTestVisible)
-                {
-                    controlPointHandle1.Draw(context);
-                }
-
-                controlPointHandle2.Position = controlPoint2;
+                var handle = anchorHandles[anchorIndex];
+                handle.Position = (VecD)point.Position;
 
-                if (controlPointHandle2.HitTestVisible)
+                if (point.Verb.ControlPoint1 != null || point.Verb.ControlPoint2 != null)
                 {
-                    controlPointHandle2.Draw(context);
+                    DrawControlPoints(context, point, ref controlPointIndex);
                 }
 
-                controlPoint += 2;
-            }
-            else if (verb.verb == PathVerb.Close)
-            {
-                subPath++;
-                closeAtIndexInSubPath = GetCloseIndexInSubPath(Path, subPath);
-                continue;
-            }
-
-            if (globalAnchor == closeAtIndexInSubPath - 1)
-            {
-                continue;
-            }
-
-            if (globalAnchor == anchorCount)
-            {
-                break;
+                handle.Draw(context);
+                anySelected |= handle.IsSelected;
+                anchorIndex++;
             }
-
-            anchorHandles[globalAnchor].Position = new VecD(verbPointPos.X, verbPointPos.Y);
-            anchorHandles[globalAnchor].Draw(context);
-
-            globalAnchor++;
         }
 
         transformHandle.Position = Path.TightBounds.BottomRight + new VecD(1, 1);
         transformHandle.Draw(context);
     }
 
-    private int GetAnchorCount()
+    private void DrawControlPoints(Canvas context, ShapePoint point, ref int controlPointIndex)
     {
-        int closeVerbs = Path.Count(x => x.verb == PathVerb.Close);
-        return Path.VerbCount - closeVerbs * 2;
-    }
+        if (point.Verb.VerbType != PathVerb.Cubic) return;
 
-    private int GetCloseIndexInSubPath(VectorPath path, int subPath)
-    {
-        int closeIndex = 0;
-        int subPathIndex = 1;
-
-        foreach (var data in path)
+        if (point.Verb.ControlPoint1 != null)
         {
-            if (data.verb == PathVerb.Close)
+            var controlPoint1 = controlPointHandles[controlPointIndex];
+            controlPoint1.HitTestVisible = controlPoint1.Position != controlPoint1.ConnectedTo.Position;
+            controlPoint1.Position = (VecD)point.Verb.ControlPoint1;
+            if (controlPoint1.HitTestVisible)
             {
-                if (subPathIndex == subPath)
-                {
-                    return closeIndex;
-                }
-
-                subPathIndex++;
+                controlPoint1.Draw(context);
             }
-            else
+
+            controlPointIndex++;
+        }
+
+        if (point.Verb.ControlPoint2 != null)
+        {
+            var controlPoint2 = controlPointHandles[controlPointIndex];
+            controlPoint2.Position = (VecD)point.Verb.ControlPoint2;
+            controlPoint2.HitTestVisible = controlPoint2.Position != controlPoint2.ConnectedTo.Position;
+
+            if (controlPoint2.HitTestVisible)
             {
-                closeIndex++;
+                controlPoint2.Draw(context);
             }
-        }
 
-        return -1;
+            controlPointIndex++;
+        }
     }
 
-    private void AdjustHandles(VectorPath path)
+    private void AdjustHandles(EditableVectorPath path)
     {
-        int pointsCount = GetPointCount(path);
+        int pointsCount = path.TotalPoints + path.ControlPointsCount;
         int anchorCount = anchorHandles.Count;
         int totalHandles = anchorCount + controlPointHandles.Count;
         if (totalHandles != pointsCount)
@@ -217,8 +183,8 @@ public class VectorPathOverlay : Overlay
                 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++)
             {
                 CreateHandle(anchorHandles.Count);
@@ -240,73 +206,37 @@ public class VectorPathOverlay : Overlay
 
     private void ConnectControlPointsToAnchors()
     {
+        if (controlPointHandles.Count == 0)
+        {
+            return;
+        }
+
         int controlPointIndex = 0;
-        int anchorIndex = 0;
-        int startingIndexInSubPath = 0;
-        int lastAnchorIndexInSubPath = CountAnchorsInSubPath(Path, 1) - 1;
-        int subPath = 1;
-        foreach (var data in Path)
+        foreach (var subShape in editableVectorPath.SubShapes)
         {
-            int localAnchorIndex = anchorIndex - startingIndexInSubPath;
-            if (data.verb == PathVerb.Cubic)
+            foreach (var point in subShape.Points)
             {
-                int targetAnchorIndex1 = anchorIndex - 1;
-                if (targetAnchorIndex1 < startingIndexInSubPath)
-                {
-                    targetAnchorIndex1 = lastAnchorIndexInSubPath;
-                }
-
-                AnchorHandle previousAnchor = anchorHandles.ElementAtOrDefault(targetAnchorIndex1);
-
-                int targetAnchorIndex2 = anchorIndex;
-                if (targetAnchorIndex2 > lastAnchorIndexInSubPath)
+                if (point.Verb.VerbType == PathVerb.Cubic)
                 {
-                    targetAnchorIndex2 = startingIndexInSubPath;
-                }
+                    var controlPoint1 = controlPointHandles[controlPointIndex];
+                    var controlPoint2 = controlPointHandles[controlPointIndex + 1];
 
-                AnchorHandle nextAnchor = anchorHandles.ElementAtOrDefault(targetAnchorIndex2);
+                    var nextPoint = subShape.GetNextPoint(point.Index);
 
-                if (previousAnchor != null)
-                {
-                    controlPointHandles[controlPointIndex].ConnectedTo = previousAnchor;
-                }
-
-                controlPointHandles[controlPointIndex + 1].ConnectedTo = nextAnchor;
-                controlPointIndex += 2;
-            }
-            
-            if (data.verb == PathVerb.Close)
-            {
-                subPath++;
-                anchorIndex--;
-                startingIndexInSubPath = anchorIndex;
-                lastAnchorIndexInSubPath = CountAnchorsInSubPath(Path, subPath) - 1 + startingIndexInSubPath;
-            }
-            else 
-            {
-                anchorIndex++;
-            }
-        }
-    }
+                    int globalIndex = editableVectorPath.GetGlobalIndex(subShape, point.Index);
 
-    private int GetPointCount(VectorPath path)
-    {
-        return path.PointCount - path.Count(x => x.verb == PathVerb.Close);
-    }
+                    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()
@@ -416,7 +346,7 @@ public class VectorPathOverlay : Overlay
             return;
         }
 
-        if (IsFirstHandle(anchorHandle))
+        if (anchorHandles.IndexOf(anchorHandle) == 0)
         {
             newPath.LineTo((VecF)anchorHandle.Position);
             newPath.Close();
@@ -430,37 +360,6 @@ public class VectorPathOverlay : Overlay
         Path = newPath;
     }
 
-    private int CountAnchorsInSubPath(VectorPath path, int subPath)
-    {
-        int anchorCount = 0;
-        int subPathIndex = 1;
-
-        foreach (var data in path)
-        {
-            if (data.verb == PathVerb.Close)
-            {
-                if (subPathIndex == subPath)
-                {
-                    return anchorCount - 1;
-                }
-
-                subPathIndex++;
-                anchorCount = 0;
-            }
-            else if (data.verb != PathVerb.Done)
-            {
-                anchorCount++;
-            }
-        }
-
-        return anchorCount;
-    }
-
-    private bool IsFirstHandle(AnchorHandle handle)
-    {
-        return anchorHandles.IndexOf(handle) == 0;
-    }
-
     private void SelectAnchor(AnchorHandle handle)
     {
         foreach (var anchorHandle in anchorHandles)
@@ -480,100 +379,33 @@ public class VectorPathOverlay : Overlay
 
             var newPath = ConvertTouchingVerbsToCubic(anchorHandle);
 
-            Path = newPath;
+            int index = anchorHandles.IndexOf(anchorHandle);
+            SubShape subShapeContainingIndex = editableVectorPath.GetSubShapeContainingIndex(index);
+            int localIndex = editableVectorPath.GetSubShapePointIndex(index, subShapeContainingIndex);
+
+            HandleContinousCubicDrag(anchorHandle.Position, anchorHandle, subShapeContainingIndex, localIndex);
+
+            Path = newPath.ToVectorPath();
         }
     }
 
     // To have continous spline, verb before and after a point must be a cubic with proper control points
-    private VectorPath ConvertTouchingVerbsToCubic(AnchorHandle anchorHandle)
+    private EditableVectorPath ConvertTouchingVerbsToCubic(AnchorHandle anchorHandle)
     {
-        VectorPath newPath = new VectorPath();
         int index = anchorHandles.IndexOf(anchorHandle);
-        
-        int anchorIndex1 = index - 1;
-        int anchorIndex2 = index;
-        
-        int targetSubPath = GetSubPathIndexForAnchor(Path, index);
-        
-        int lastAnchorIndexInSubPath = GetCloseAnchorIndexInSubPath(Path, targetSubPath);
-        int firstAnchorIndexInSubPath = lastAnchorIndexInSubPath - CountAnchorsInSubPath(Path, targetSubPath);
-        
-        if(anchorIndex1 < firstAnchorIndexInSubPath)
-        {
-            anchorIndex1 = lastAnchorIndexInSubPath - 1;
-        }
-        
-        if(anchorIndex2 >= lastAnchorIndexInSubPath)
-        {
-            anchorIndex2 = firstAnchorIndexInSubPath;
-        }
 
-        int anchor = 0;
-        foreach (var data in Path)
-        {
-            if (data.verb == PathVerb.Close)
-            {
-                DefaultPathVerb(data, newPath);
-            }
-            else
-            {
-                if (anchor == anchorIndex1 || anchor == anchorIndex2)
-                {
-                    if (data.verb == PathVerb.Line)
-                    {
-                        newPath.CubicTo(data.points[0], data.points[1], data.points[1]);
-                    }
-                    else if (data.verb is PathVerb.Conic or PathVerb.Quad)
-                    {
-                        newPath.CubicTo(data.points[0], data.points[1], data.points[2]);
-                    }
-                    else
-                    {
-                        DefaultPathVerb(data, newPath);
-                    }
-                }
-                else
-                {
-                    DefaultPathVerb(data, newPath);
-                }
+        SubShape subShapeContainingIndex = editableVectorPath.GetSubShapeContainingIndex(index);
 
-                if (data.verb != PathVerb.Move)
-                {
-                    anchor++;
-                }
-            }
-        }
+        int localIndex = editableVectorPath.GetSubShapePointIndex(index, subShapeContainingIndex);
 
-        return newPath;
-    }
-    
-    private int GetSubPathIndexForAnchor(VectorPath path, int anchorIndex)
-    {
-        int subPath = 1;
-        int anchor = 0;
+        var previousPoint = subShapeContainingIndex.GetPreviousPoint(localIndex);
+        var nextPoint = subShapeContainingIndex.Points[localIndex];
 
-        foreach (var data in path)
-        {
-            if (data.verb == PathVerb.Close)
-            {
-                subPath++;
-                //anchor--;
-            }
-            else
-            {
-                if (anchor == anchorIndex)
-                {
-                    return subPath;
-                }
+        previousPoint?.ConvertVerbToCubic();
 
-                if (data.verb != PathVerb.Move)
-                {
-                    anchor++;
-                }
-            }
-        }
+        nextPoint.ConvertVerbToCubic();
 
-        return -1;
+        return editableVectorPath;
     }
 
     private void OnHandleDrag(Handle source, OverlayPointerArgs args)
@@ -584,254 +416,87 @@ public class VectorPathOverlay : Overlay
         }
 
         var index = anchorHandles.IndexOf(handle);
-        VectorPath newPath = new VectorPath(Path);
-        newPath.Reset();
 
-        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);
-        int subPathNum = 1;
-
-        VecF firstSubPathPoint = Path.Points.First();
-        bool isSubPathClosed = IsSubPathClosed(subPathNum, Path);
-        int lastAnchorAt = GetCloseAnchorIndexInSubPath(Path, subPathNum);
-        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, isSubPathClosed, data.points[0], lastAnchorAt);
-
-                    newPath.MoveTo(point);
-                    firstSubPathPoint = 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, isSubPathClosed, firstSubPathPoint, lastAnchorAt);
-
-                    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, isSubPathClosed, firstSubPathPoint, lastAnchorAt);
-
-                        VecF mid1Delta = previousDelta;
-
-                        VecF mid2Delta = point - data.points[3];
-
-                        newPath.CubicTo(data.points[1] + mid1Delta, data.points[2] + mid2Delta, point);
-
-                        previousDelta = mid2Delta;
-                    }
-                    break;
-                case PathVerb.Quad:
-                    if (ctrlPressed)
-                    {
-                        DefaultPathVerb(data, newPath);
-                        break;
-                    }
-
-                    point = data.points[2];
-                    point = TryApplyNewPos(args, i, index, point, isSubPathClosed, firstSubPathPoint, lastAnchorAt);
-
-                    newPath.QuadTo(data.points[1], point);
-                    break;
-                case PathVerb.Conic:
-                    if (ctrlPressed)
-                    {
-                        DefaultPathVerb(data, newPath);
-                        break;
-                    }
-
-                    point = data.points[2];
-                    point = TryApplyNewPos(args, i, index, point, isSubPathClosed, firstSubPathPoint, lastAnchorAt);
-
-                    newPath.ConicTo(data.points[1], point, data.conicWeight);
-                    break;
-                default:
-                    DefaultPathVerb(data, newPath);
-                    break;
-            }
-
-            if (data.verb == PathVerb.Close)
-            {
-                subPathNum++;
-                isSubPathClosed = IsSubPathClosed(subPathNum, Path);
-                lastAnchorAt = GetCloseAnchorIndexInSubPath(Path, subPathNum);
-                i--;
-            }
-            else
-            {
-                i++;
-            }
+            HandleContinousCubicDrag(targetPos, handle, subShapeContainingIndex, localIndex);
         }
-
-        Path = newPath;
-    }
-    
-    private int GetCloseAnchorIndexInSubPath(VectorPath path, int subPath)
-    {
-        int anchorIndex = 0;
-        int subPathIndex = 1;
-
-        foreach (var data in path)
+        else
         {
-            if (data.verb == PathVerb.Close)
-            {
-                if (subPathIndex == subPath)
-                {
-                    return anchorIndex - 1;
-                }
-
-                subPathIndex++;
-                anchorIndex--;
-            }
-            else
-            {
-                anchorIndex++;
-            }
+            subShapeContainingIndex.SetPointPosition(localIndex, (VecF)targetPos, true);
         }
 
-        return -1;
+        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;
 
-        if (args.Modifiers.HasFlag(KeyModifiers.Alt))
+        var previousPoint = subShapeContainingIndex.GetPreviousPoint(localIndex);
+
+        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, float conicWeight) 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)
-        {
-            controlPoint2 = (VecF)targetPos;
-        }
-        else if (isFirstSymmetricControlPoint)
+        if (!dragOnlyOne)
         {
-            controlPoint1 = (VecF)targetSymmetryPos;
+            bool isDraggingFirst = controlPointHandles.IndexOf(controlPointHandle) % 2 == 0;
+            HandleContinousCubicDrag(targetPos, to, subShapeContainingIndex, localIndex, !isDraggingFirst);
         }
-        else if (isSecondSymmetricControlPoint)
-        {
-            controlPoint2 = (VecF)targetSymmetryPos;
-        }
-
-        newPath.CubicTo(controlPoint1, controlPoint2, endPoint);
-    }
-
-    private bool IsSubPathClosed(int subPathIndex, VectorPath path)
-    {
-        int subPath = 1;
-
-        foreach (var data in path)
+        else
         {
-            if (data.verb == PathVerb.Close)
+            bool isFirstControlPoint = controlPointHandles.IndexOf(controlPointHandle) % 2 == 0;
+            if (isFirstControlPoint)
+            {
+                subShapeContainingIndex.Points[localIndex].Verb.ControlPoint1 = (VecF)targetPos;
+            }
+            else
             {
-                if (subPathIndex == subPath)
+                var previousPoint = subShapeContainingIndex.GetPreviousPoint(localIndex);
+                if (previousPoint != null)
                 {
-                    return true;
+                    previousPoint.Verb.ControlPoint2 = (VecF)targetPos;
                 }
-
-                subPath++;
             }
         }
 
-        return false;
+        Path = editableVectorPath.ToVectorPath();
     }
 
     private VecD GetMirroredControlPoint(VecF controlPoint, VecF anchor)
@@ -839,45 +504,7 @@ public class VectorPathOverlay : Overlay
         return new VecD(2 * anchor.X - controlPoint.X, 2 * anchor.Y - controlPoint.Y);
     }
 
-    private VecF GetVerbPointPos((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();
-        }
-    }
-
-    private VecF TryApplyNewPos(OverlayPointerArgs args, int i, int index, VecF point, bool firstIsLast,
-        VecF firstPoint, int lastAnchorAt)
-    {
-        if (i == index && i != lastAnchorAt)
-        {
-            point = (VecF)ApplySymmetry(args.Point);
-        }
-        else if (firstIsLast && i == lastAnchorAt)
-        {
-            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 snapped = new VecD((float)snappedPoint.X, (float)snappedPoint.Y);
@@ -980,36 +607,16 @@ public class VectorPathOverlay : Overlay
 
     private void PathChanged(VectorPath newPath)
     {
-        AdjustHandles(newPath);
-    }
-
-    private static void DefaultPathVerb((PathVerb verb, VecF[] points, float conicWeight) 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.conicWeight);
-                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)
@@ -1020,11 +627,14 @@ public class VectorPathOverlay : Overlay
             overlay.SnappingController.RemoveAll("editingPath");
             overlay.ClearAnchorHandles();
             overlay.IsVisible = false;
+            overlay.editableVectorPath = null;
         }
         else
         {
             var path = args.NewValue.Value;
-            overlay.AdjustHandles(path);
+            EditableVectorPath editablePath = new EditableVectorPath(path);
+            overlay.editableVectorPath = editablePath;
+            overlay.AdjustHandles(editablePath);
             overlay.IsVisible = true;
         }
 
@@ -1035,6 +645,7 @@ public class VectorPathOverlay : Overlay
 
         if (args.NewValue.Value != null)
         {
+            overlay.editableVectorPath = new EditableVectorPath(args.NewValue.Value);
             args.NewValue.Value.Changed += overlay.PathChanged;
         }
     }

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

@@ -2,6 +2,9 @@
 using Avalonia.Headless;
 using Avalonia.Platform;
 using Avalonia.Threading;
+using Drawie.Backend.Core.Bridge;
+using Drawie.Skia;
+using DrawiEngine;
 using PixiEditor.Desktop;
 using Xunit.Abstractions;
 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));
+        
+        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)));
+    }
+}