瀏覽代碼

Merge branch 'master' into development

flabbet 9 月之前
父節點
當前提交
4834d5936d
共有 43 個文件被更改,包括 1376 次插入927 次删除
  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. 0 256
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.deps.json
  13. 二進制
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  14. 0 244
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.deps.json
  15. 二進制
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  16. 2 1
      src/PixiEditor/Data/Localization/Languages/en.json
  17. 4 2
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  18. 11 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  19. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs
  20. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs
  21. 11 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorEllipseToolExecutor.cs
  22. 18 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs
  23. 16 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorRectangleToolExecutor.cs
  24. 4 2
      src/PixiEditor/Models/Handlers/Tools/IVectorPathToolHandler.cs
  25. 0 1
      src/PixiEditor/Models/IO/ExportConfig.cs
  26. 2 1
      src/PixiEditor/Models/IO/Exporter.cs
  27. 1 1
      src/PixiEditor/Models/Serialization/Factories/VecD3SerializationFactory.cs
  28. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  29. 5 2
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  30. 2 1
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  31. 1 0
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/FillableShapeToolbar.cs
  32. 8 0
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  33. 3 0
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs
  34. 1 1
      src/PixiEditor/Views/Overlays/Handles/AnchorHandle.cs
  35. 10 5
      src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs
  36. 258 0
      src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs
  37. 131 0
      src/PixiEditor/Views/Overlays/PathOverlay/ShapePoint.cs
  38. 72 0
      src/PixiEditor/Views/Overlays/PathOverlay/SubShape.cs
  39. 162 341
      src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs
  40. 19 14
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  41. 23 0
      tests/PixiEditor.Backend.Tests/NodeSystemTests.cs
  42. 3 0
      tests/PixiEditor.Tests/AvaloniaTestRunner.cs
  43. 384 0
      tests/PixiEditor.Tests/EditableVectorPathTests.cs

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 53075856f14518af3f0b59910a9b9d8339368347
+Subproject commit 0c1d72f544e73128235d0a0112cd571bbb44858f

+ 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.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -48,11 +49,15 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
             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)
         {
@@ -93,4 +98,12 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
             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.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 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
         };
     }
+
+    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
         };
 
-        if (FillColor.A > 0)
+        if (Fill && FillColor.A > 0)
         {
             paint.Color = FillColor;
             paint.Style = PaintStyle.Fill;
@@ -54,11 +54,14 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             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)
         {
@@ -91,4 +94,9 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
             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.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -77,4 +78,16 @@ public class PointsVectorData : ShapeVectorData
             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.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -53,11 +54,15 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
             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)
         {
@@ -99,4 +104,11 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
             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.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 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 FillColor { get; set; } = Colors.White;
     public float StrokeWidth { get; set; } = 1;
+    public bool Fill { get; set; } = true;
     public abstract RectD GeometryAABB { get; } 
     public abstract RectD VisualAABB { get; }
     public RectD TransformedAABB => new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix).AABBBounds;
@@ -41,4 +43,6 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
     {
         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.Bridge;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
@@ -20,6 +23,8 @@ internal class CombineStructureMembersOnto_Change : Change
     private Guid targetLayerGuid;
     private Dictionary<int, CommittedChunkStorage> originalChunks = new();
     
+    private Dictionary<int, VectorPath> originalPaths = new();
+
 
     [GenerateMakeChangeAction]
     public CombineStructureMembersOnto_Change(HashSet<Guid> membersToMerge, Guid targetLayer)
@@ -69,7 +74,6 @@ internal class CombineStructureMembersOnto_Change : Change
         List<IChangeInfo> changes = new();
         var targetLayer = target.FindMemberOrThrow<LayerNode>(targetLayerGuid);
 
-        // TODO: add merging similar layers (vector -> vector)
         int maxFrame = GetMaxFrame(target, targetLayer);
 
         for (int frame = 0; frame < maxFrame || frame == 0; frame++)
@@ -82,6 +86,26 @@ internal class CombineStructureMembersOnto_Change : Change
         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)
     {
         var chunksToCombine = new HashSet<VecI>();
@@ -94,7 +118,7 @@ internal class CombineStructureMembersOnto_Change : Change
             var layer = target.FindMemberOrThrow<LayerNode>(guid);
 
             AddMissingKeyFrame(targetLayer, frame, layer, changes, target);
-            
+
             if (layer is not IRasterizable or ImageLayerNode)
                 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);
         toDrawOnImage.EnqueueClear();
 
@@ -136,7 +229,7 @@ internal class CombineStructureMembersOnto_Change : Change
                         }
                     }
                 }
-                
+
                 renderer.RenderLayers(tempTexture.DrawingSurface, layersToRender, frame, ChunkResolution.Full);
             }
 
@@ -148,11 +241,9 @@ internal class CombineStructureMembersOnto_Change : Change
 
             tempTexture.Dispose();
         });
-
-        changes.Add(new LayerImageArea_ChangeInfo(targetLayerGuid, affArea));
-        return changes;
+        return affArea;
     }
-    
+
     private HashSet<Guid> OrderLayers(HashSet<Guid> layersToCombine, Document document)
     {
         HashSet<Guid> ordered = new();
@@ -182,9 +273,9 @@ internal class CombineStructureMembersOnto_Change : Change
             return;
 
         var clonedData = keyFrameData.Clone(true);
-        
+
         targetLayer.AddFrame(keyFrameData.KeyFrameGuid, clonedData);
-        
+
         changes.Add(new CreateRasterKeyFrame_ChangeInfo(targetLayerGuid, frame, clonedData.KeyFrameGuid, true));
         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)
     {
+        if (targetLayer.KeyFrames.Count == 0)
+            return 0;
+
         int maxFrame = targetLayer.KeyFrames.Max(x => x.StartFrame + x.Duration);
         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);
         toDrawOnImage.EnqueueClear();
@@ -255,10 +343,19 @@ internal class CombineStructureMembersOnto_Change : Change
             DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(
                 targetLayer.GetLayerImageAtFrame(frame),
                 ref storedChunks);
-        
+
         toDrawOnImage.CommitChanges();
         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()
     {
@@ -266,5 +363,14 @@ internal class CombineStructureMembersOnto_Change : Change
         {
             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);
 
         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.Flush();
@@ -220,7 +220,7 @@ public static class FloodFillHelper
         if (selection is null)
         {
             selection = new VectorPath();
-            selection.AddRect(globalBounds);
+            selection.AddRect((RectD)globalBounds);
         }
 
         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();
             pathToExtract = new VectorPath();
-            pathToExtract.AddRect((RectI)tightBounds);
+            pathToExtract.AddRect((RectD)(RectI)tightBounds);
         }
 
         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);
         documentConstraint = new VectorPath();
-        documentConstraint.AddRect(new RectI(VecI.Zero, target.Size));
+        documentConstraint.AddRect((RectD)new RectI(VecI.Zero, target.Size));
         return true;
     }
 

+ 0 - 256
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.deps.json

@@ -1,256 +0,0 @@
-{
-  "runtimeTarget": {
-    "name": ".NETStandard,Version=v2.0/",
-    "signature": ""
-  },
-  "compilationOptions": {},
-  "targets": {
-    ".NETStandard,Version=v2.0": {},
-    ".NETStandard,Version=v2.0/": {
-      "PixiEditor.Api.CGlueMSBuild/1.0.0": {
-        "dependencies": {
-          "Microsoft.Build.Utilities.Core": "17.12.6",
-          "Mono.Cecil": "0.11.6",
-          "NETStandard.Library": "2.0.3",
-          "StyleCop.Analyzers": "1.1.118"
-        },
-        "runtime": {
-          "PixiEditor.Api.CGlueMSBuild.dll": {}
-        }
-      },
-      "Microsoft.Build.Framework/17.12.6": {
-        "dependencies": {
-          "Microsoft.Win32.Registry": "5.0.0",
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "Microsoft.Build.Utilities.Core/17.12.6": {
-        "dependencies": {
-          "Microsoft.Build.Framework": "17.12.6",
-          "Microsoft.NET.StringTools": "17.12.6",
-          "Microsoft.Win32.Registry": "5.0.0",
-          "System.Collections.Immutable": "8.0.0",
-          "System.Configuration.ConfigurationManager": "8.0.0",
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
-          "System.Security.Principal.Windows": "5.0.0",
-          "System.Text.Encoding.CodePages": "7.0.0"
-        }
-      },
-      "Microsoft.NET.StringTools/17.12.6": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "Microsoft.NETCore.Platforms/1.1.0": {},
-      "Microsoft.Win32.Registry/5.0.0": {
-        "dependencies": {
-          "System.Buffers": "4.5.1",
-          "System.Memory": "4.5.5",
-          "System.Security.AccessControl": "5.0.0",
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "Mono.Cecil/0.11.6": {
-        "runtime": {
-          "lib/netstandard2.0/Mono.Cecil.Mdb.dll": {
-            "assemblyVersion": "0.11.6.0",
-            "fileVersion": "0.11.6.0"
-          },
-          "lib/netstandard2.0/Mono.Cecil.Pdb.dll": {
-            "assemblyVersion": "0.11.6.0",
-            "fileVersion": "0.11.6.0"
-          },
-          "lib/netstandard2.0/Mono.Cecil.Rocks.dll": {
-            "assemblyVersion": "0.11.6.0",
-            "fileVersion": "0.11.6.0"
-          },
-          "lib/netstandard2.0/Mono.Cecil.dll": {
-            "assemblyVersion": "0.11.6.0",
-            "fileVersion": "0.11.6.0"
-          }
-        }
-      },
-      "NETStandard.Library/2.0.3": {
-        "dependencies": {
-          "Microsoft.NETCore.Platforms": "1.1.0"
-        }
-      },
-      "StyleCop.Analyzers/1.1.118": {},
-      "System.Buffers/4.5.1": {},
-      "System.Collections.Immutable/8.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "System.Configuration.ConfigurationManager/8.0.0": {
-        "dependencies": {
-          "System.Security.Cryptography.ProtectedData": "8.0.0"
-        }
-      },
-      "System.Memory/4.5.5": {
-        "dependencies": {
-          "System.Buffers": "4.5.1",
-          "System.Numerics.Vectors": "4.4.0",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "System.Numerics.Vectors/4.4.0": {},
-      "System.Runtime.CompilerServices.Unsafe/6.0.0": {},
-      "System.Security.AccessControl/5.0.0": {
-        "dependencies": {
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "System.Security.Cryptography.ProtectedData/8.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5"
-        }
-      },
-      "System.Security.Principal.Windows/5.0.0": {},
-      "System.Text.Encoding.CodePages/7.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      }
-    }
-  },
-  "libraries": {
-    "PixiEditor.Api.CGlueMSBuild/1.0.0": {
-      "type": "project",
-      "serviceable": false,
-      "sha512": ""
-    },
-    "Microsoft.Build.Framework/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-jleteC0seumLGTmTVwob97lcwPj/dfgzL/V3g/VVcMZgo2Ic7jzdy8AYpByPDh8e3uRq0SjCl6HOFCjhy5GzRQ==",
-      "path": "microsoft.build.framework/17.12.6",
-      "hashPath": "microsoft.build.framework.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.Build.Utilities.Core/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-pU3GnHcXp8VRMGKxdJCq+tixfhFn+QwEbpqmZmc/nqFHFyuhlGwjonWZMIWcwuCv/8EHgxoOttFvna1vrN+RrA==",
-      "path": "microsoft.build.utilities.core/17.12.6",
-      "hashPath": "microsoft.build.utilities.core.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.NET.StringTools/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-w8Ehofqte5bJoR+Fa3f6JwkwFEkGtXxqvQHGOVOSHDzgNVySvL5FSNhavbQSZ864el9c3rjdLPLAtBW8dq6fmg==",
-      "path": "microsoft.net.stringtools/17.12.6",
-      "hashPath": "microsoft.net.stringtools.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.NETCore.Platforms/1.1.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
-      "path": "microsoft.netcore.platforms/1.1.0",
-      "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
-    },
-    "Microsoft.Win32.Registry/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
-      "path": "microsoft.win32.registry/5.0.0",
-      "hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512"
-    },
-    "Mono.Cecil/0.11.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==",
-      "path": "mono.cecil/0.11.6",
-      "hashPath": "mono.cecil.0.11.6.nupkg.sha512"
-    },
-    "NETStandard.Library/2.0.3": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
-      "path": "netstandard.library/2.0.3",
-      "hashPath": "netstandard.library.2.0.3.nupkg.sha512"
-    },
-    "StyleCop.Analyzers/1.1.118": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==",
-      "path": "stylecop.analyzers/1.1.118",
-      "hashPath": "stylecop.analyzers.1.1.118.nupkg.sha512"
-    },
-    "System.Buffers/4.5.1": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==",
-      "path": "system.buffers/4.5.1",
-      "hashPath": "system.buffers.4.5.1.nupkg.sha512"
-    },
-    "System.Collections.Immutable/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==",
-      "path": "system.collections.immutable/8.0.0",
-      "hashPath": "system.collections.immutable.8.0.0.nupkg.sha512"
-    },
-    "System.Configuration.ConfigurationManager/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==",
-      "path": "system.configuration.configurationmanager/8.0.0",
-      "hashPath": "system.configuration.configurationmanager.8.0.0.nupkg.sha512"
-    },
-    "System.Memory/4.5.5": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
-      "path": "system.memory/4.5.5",
-      "hashPath": "system.memory.4.5.5.nupkg.sha512"
-    },
-    "System.Numerics.Vectors/4.4.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==",
-      "path": "system.numerics.vectors/4.4.0",
-      "hashPath": "system.numerics.vectors.4.4.0.nupkg.sha512"
-    },
-    "System.Runtime.CompilerServices.Unsafe/6.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
-      "path": "system.runtime.compilerservices.unsafe/6.0.0",
-      "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
-    },
-    "System.Security.AccessControl/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
-      "path": "system.security.accesscontrol/5.0.0",
-      "hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512"
-    },
-    "System.Security.Cryptography.ProtectedData/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg==",
-      "path": "system.security.cryptography.protecteddata/8.0.0",
-      "hashPath": "system.security.cryptography.protecteddata.8.0.0.nupkg.sha512"
-    },
-    "System.Security.Principal.Windows/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==",
-      "path": "system.security.principal.windows/5.0.0",
-      "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512"
-    },
-    "System.Text.Encoding.CodePages/7.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==",
-      "path": "system.text.encoding.codepages/7.0.0",
-      "hashPath": "system.text.encoding.codepages.7.0.0.nupkg.sha512"
-    }
-  }
-}

二進制
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


+ 0 - 244
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.deps.json

@@ -1,244 +0,0 @@
-{
-  "runtimeTarget": {
-    "name": ".NETStandard,Version=v2.0/",
-    "signature": ""
-  },
-  "compilationOptions": {},
-  "targets": {
-    ".NETStandard,Version=v2.0": {},
-    ".NETStandard,Version=v2.0/": {
-      "PixiEditor.Extensions.MSPackageBuilder/1.0.0": {
-        "dependencies": {
-          "Microsoft.Build.Utilities.Core": "17.12.6",
-          "NETStandard.Library": "2.0.3",
-          "Newtonsoft.Json": "13.0.3",
-          "StyleCop.Analyzers": "1.1.118"
-        },
-        "runtime": {
-          "PixiEditor.Extensions.MSPackageBuilder.dll": {}
-        }
-      },
-      "Microsoft.Build.Framework/17.12.6": {
-        "dependencies": {
-          "Microsoft.Win32.Registry": "5.0.0",
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "Microsoft.Build.Utilities.Core/17.12.6": {
-        "dependencies": {
-          "Microsoft.Build.Framework": "17.12.6",
-          "Microsoft.NET.StringTools": "17.12.6",
-          "Microsoft.Win32.Registry": "5.0.0",
-          "System.Collections.Immutable": "8.0.0",
-          "System.Configuration.ConfigurationManager": "8.0.0",
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
-          "System.Security.Principal.Windows": "5.0.0",
-          "System.Text.Encoding.CodePages": "7.0.0"
-        }
-      },
-      "Microsoft.NET.StringTools/17.12.6": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "Microsoft.NETCore.Platforms/1.1.0": {},
-      "Microsoft.Win32.Registry/5.0.0": {
-        "dependencies": {
-          "System.Buffers": "4.5.1",
-          "System.Memory": "4.5.5",
-          "System.Security.AccessControl": "5.0.0",
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "NETStandard.Library/2.0.3": {
-        "dependencies": {
-          "Microsoft.NETCore.Platforms": "1.1.0"
-        }
-      },
-      "Newtonsoft.Json/13.0.3": {
-        "runtime": {
-          "lib/netstandard2.0/Newtonsoft.Json.dll": {
-            "assemblyVersion": "13.0.0.0",
-            "fileVersion": "13.0.3.27908"
-          }
-        }
-      },
-      "StyleCop.Analyzers/1.1.118": {},
-      "System.Buffers/4.5.1": {},
-      "System.Collections.Immutable/8.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "System.Configuration.ConfigurationManager/8.0.0": {
-        "dependencies": {
-          "System.Security.Cryptography.ProtectedData": "8.0.0"
-        }
-      },
-      "System.Memory/4.5.5": {
-        "dependencies": {
-          "System.Buffers": "4.5.1",
-          "System.Numerics.Vectors": "4.4.0",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "System.Numerics.Vectors/4.4.0": {},
-      "System.Runtime.CompilerServices.Unsafe/6.0.0": {},
-      "System.Security.AccessControl/5.0.0": {
-        "dependencies": {
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "System.Security.Cryptography.ProtectedData/8.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5"
-        }
-      },
-      "System.Security.Principal.Windows/5.0.0": {},
-      "System.Text.Encoding.CodePages/7.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      }
-    }
-  },
-  "libraries": {
-    "PixiEditor.Extensions.MSPackageBuilder/1.0.0": {
-      "type": "project",
-      "serviceable": false,
-      "sha512": ""
-    },
-    "Microsoft.Build.Framework/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-jleteC0seumLGTmTVwob97lcwPj/dfgzL/V3g/VVcMZgo2Ic7jzdy8AYpByPDh8e3uRq0SjCl6HOFCjhy5GzRQ==",
-      "path": "microsoft.build.framework/17.12.6",
-      "hashPath": "microsoft.build.framework.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.Build.Utilities.Core/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-pU3GnHcXp8VRMGKxdJCq+tixfhFn+QwEbpqmZmc/nqFHFyuhlGwjonWZMIWcwuCv/8EHgxoOttFvna1vrN+RrA==",
-      "path": "microsoft.build.utilities.core/17.12.6",
-      "hashPath": "microsoft.build.utilities.core.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.NET.StringTools/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-w8Ehofqte5bJoR+Fa3f6JwkwFEkGtXxqvQHGOVOSHDzgNVySvL5FSNhavbQSZ864el9c3rjdLPLAtBW8dq6fmg==",
-      "path": "microsoft.net.stringtools/17.12.6",
-      "hashPath": "microsoft.net.stringtools.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.NETCore.Platforms/1.1.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
-      "path": "microsoft.netcore.platforms/1.1.0",
-      "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
-    },
-    "Microsoft.Win32.Registry/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
-      "path": "microsoft.win32.registry/5.0.0",
-      "hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512"
-    },
-    "NETStandard.Library/2.0.3": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
-      "path": "netstandard.library/2.0.3",
-      "hashPath": "netstandard.library.2.0.3.nupkg.sha512"
-    },
-    "Newtonsoft.Json/13.0.3": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
-      "path": "newtonsoft.json/13.0.3",
-      "hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
-    },
-    "StyleCop.Analyzers/1.1.118": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==",
-      "path": "stylecop.analyzers/1.1.118",
-      "hashPath": "stylecop.analyzers.1.1.118.nupkg.sha512"
-    },
-    "System.Buffers/4.5.1": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==",
-      "path": "system.buffers/4.5.1",
-      "hashPath": "system.buffers.4.5.1.nupkg.sha512"
-    },
-    "System.Collections.Immutable/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==",
-      "path": "system.collections.immutable/8.0.0",
-      "hashPath": "system.collections.immutable.8.0.0.nupkg.sha512"
-    },
-    "System.Configuration.ConfigurationManager/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==",
-      "path": "system.configuration.configurationmanager/8.0.0",
-      "hashPath": "system.configuration.configurationmanager.8.0.0.nupkg.sha512"
-    },
-    "System.Memory/4.5.5": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
-      "path": "system.memory/4.5.5",
-      "hashPath": "system.memory.4.5.5.nupkg.sha512"
-    },
-    "System.Numerics.Vectors/4.4.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==",
-      "path": "system.numerics.vectors/4.4.0",
-      "hashPath": "system.numerics.vectors.4.4.0.nupkg.sha512"
-    },
-    "System.Runtime.CompilerServices.Unsafe/6.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
-      "path": "system.runtime.compilerservices.unsafe/6.0.0",
-      "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
-    },
-    "System.Security.AccessControl/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
-      "path": "system.security.accesscontrol/5.0.0",
-      "hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512"
-    },
-    "System.Security.Cryptography.ProtectedData/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg==",
-      "path": "system.security.cryptography.protecteddata/8.0.0",
-      "hashPath": "system.security.cryptography.protecteddata.8.0.0.nupkg.sha512"
-    },
-    "System.Security.Principal.Windows/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==",
-      "path": "system.security.principal.windows/5.0.0",
-      "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512"
-    },
-    "System.Text.Encoding.CodePages/7.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==",
-      "path": "system.text.encoding.codepages/7.0.0",
-      "hashPath": "system.text.encoding.codepages.7.0.0.nupkg.sha512"
-    }
-  }
-}

二進制
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll


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

@@ -778,5 +778,6 @@
   "COPY_COLOR_TO_CLIPBOARD": "Copy color to clipboard",
   "VIEWPORT_ROTATION": "Viewport rotation",
   "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();
 
         //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(
-            new CreateStructureMember_Action(parent.Id, newGuid, typeof(ImageLayerNode)),
+            new CreateStructureMember_Action(parent.Id, newGuid, layerToCreate),
             new StructureMemberName_Action(newGuid, node.NodeNameBindable),
             new CombineStructureMembersOnto_Action(members.ToHashSet(), newGuid));
         foreach (var member in members)
@@ -783,7 +785,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         
         var selection = Document.SelectionPathBindable;
         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(
             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)
         {
             var shapeData = vectorLayerHandler.GetShapeData(document.AnimationHandler.ActiveFrameTime);
-            if (shapeData == null || !InitShapeData(shapeData))
+            bool shapeIsValid = InitShapeData(shapeData);
+            if (shapeData == null || !shapeIsValid)
             {
                 ActiveMode = ShapeToolMode.Preview;
                 return ExecutionState.Success;
@@ -103,6 +104,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     protected abstract IAction SettingsChangedAction();
     protected abstract IAction TransformMovedAction(ShapeData data, ShapeCorners corners);
     protected virtual bool InitShapeData(IReadOnlyShapeVectorData data) { return true; }
+    protected abstract bool CanEditShape(IStructureMemberHandler layer);
     protected abstract IAction EndDrawAction();
     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)
     {
-        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)

+ 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.Tools;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
@@ -46,5 +47,10 @@ internal class RasterEllipseToolExecutor : DrawableShapeToolExecutor<IRasterElli
             FillColor, (float)StrokeWidth, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
+    protected override bool CanEditShape(IStructureMemberHandler layer)
+    {
+        return true;
+    }
+
     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.Tools;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
@@ -57,5 +58,10 @@ internal class RasterRectangleToolExecutor : DrawableShapeToolExecutor<IRasterRe
             document!.AnimationHandler.ActiveFrameBindable);
     }
 
+    protected override bool CanEditShape(IStructureMemberHandler layer)
+    {
+        return true;
+    }
+
     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 Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
@@ -35,6 +36,16 @@ internal class VectorEllipseToolExecutor : DrawableShapeToolExecutor<IVectorElli
         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)
     {
         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.Handlers.Toolbars;
 using PixiEditor.Models.Tools;
+using PixiEditor.ViewModels.Tools.Tools;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
 
@@ -60,6 +61,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
             if (shapeData is PathVectorData pathData)
             {
                 startingPath = new VectorPath(pathData.Path);
+                ApplySettings(pathData);
                 startingPath.Transform(pathData.TransformationMatrix);
             }
             else if (shapeData is null)
@@ -183,7 +185,10 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
 
     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()
@@ -198,7 +203,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     {
         if(startingPath == null)
         {
-            return new PathVectorData(new VectorPath())
+            return new PathVectorData(new VectorPath() { FillType = vectorPathToolHandler.FillMode })
             {
                 StrokeWidth = (float)toolbar.ToolSize,
                 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,
             StrokeColor = toolbar.StrokeColor.ToColor(),
@@ -261,4 +266,14 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
 
         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 Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
@@ -34,6 +35,16 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
         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)
     {
         RectD rect;
@@ -81,7 +92,7 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
         }
 
         Matrix3X3 matrix = Matrix3X3.Identity;
-        
+
         if (!corners.IsRect)
         {
             RectD firstRect = RectD.FromCenterAndSize(firstCenter, firstSize);
@@ -93,9 +104,10 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
         {
             firstCenter = data.Center;
             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)

+ 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
 {
-    
+    public PathFillType FillMode { get; }
 }

+ 0 - 1
src/PixiEditor/Models/IO/ExportConfig.cs

@@ -6,7 +6,6 @@ namespace PixiEditor.Models.IO;
 
 public class ExportConfig
 {
-   public static ExportConfig Empty { get; } = new ExportConfig();
    public VecI ExportSize { get; set; }
    public bool ExportAsSpriteSheet { get; set; } = false;
    public int SpriteSheetColumns { get; set; }

+ 2 - 1
src/PixiEditor/Models/IO/Exporter.cs

@@ -60,7 +60,8 @@ internal class Exporter
         {
             var file = await desktop.MainWindow.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
             {
-                FileTypeChoices = SupportedFilesHelper.BuildSaveFilter(), DefaultExtension = "pixi"
+                FileTypeChoices = SupportedFilesHelper.BuildSaveFilter(
+                    FileTypeDialogDataSet.SetKind.Any & ~FileTypeDialogDataSet.SetKind.Video), DefaultExtension = "pixi"
             });
 
             if (file is null)

+ 1 - 1
src/PixiEditor/Models/Serialization/Factories/VecD3SerializationFactory.cs

@@ -4,7 +4,7 @@ namespace PixiEditor.Models.Serialization.Factories;
 
 public class VecD3SerializationFactory : SerializationFactory<byte[], Vec3D>
 {
-    public override string DeserializationId { get; } = "PixiEditor.VecD";
+    public override string DeserializationId { get; } = "PixiEditor.VecD3";
 
     public override byte[] Serialize(Vec3D original)
     {

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -41,5 +41,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.30")]
-[assembly: AssemblyFileVersion("2.0.0.30")]
+[assembly: AssemblyVersion("2.0.0.31")]
+[assembly: AssemblyFileVersion("2.0.0.31")]

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

@@ -256,8 +256,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             if (args.LayerChangeType == LayerAction.Add)
             {
                 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)
             {

+ 2 - 1
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -366,7 +366,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         string finalPath = null;
         if (asNew || string.IsNullOrEmpty(document.FullFilePath))
         {
-            var result = await Exporter.TrySaveWithDialog(document, ExportConfig.Empty, null);
+            ExportConfig config = new ExportConfig() { ExportSize = document.SizeBindable };
+            var result = await Exporter.TrySaveWithDialog(document, config, null);
             if (result.Result == DialogSaveResult.Cancelled)
                 return false;
             if (result.Result != DialogSaveResult.Success)

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

+ 3 - 0
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs

@@ -385,6 +385,9 @@ internal partial class ExportFilePopup : PixiEditorPopup
 
             int newWidth = (int)(imageSize.X * scale);
             int newHeight = (int)(imageSize.Y * scale);
+            
+            newWidth = Math.Max(newWidth, 1);
+            newHeight = Math.Max(newHeight, 1);
 
             return new VecI(newWidth, newHeight);
         }

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

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

+ 10 - 5
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -164,7 +164,6 @@ internal class LineToolOverlay : Overlay
     protected override void ZoomChanged(double newZoom)
     {
         blackPaint.StrokeWidth = 1 / (float)newZoom;
-        infoBox.ZoomScale = newZoom;
     }
 
     public override void RenderOverlay(Canvas context, RectD canvasBounds)
@@ -189,8 +188,15 @@ internal class LineToolOverlay : Overlay
 
         if (IsSizeBoxEnabled)
         {
+            int toRestore = context.Save();
+            var matrix = context.TotalMatrix;
+            VecD pos = matrix.MapPoint(lastMousePos);
+            context.SetMatrix(Matrix3X3.Identity);
+            
             string length = $"L: {(mappedEnd - mappedStart).Length:0.#} px";
-            infoBox.DrawInfo(context, length, lastMousePos);
+            infoBox.DrawInfo(context, length, pos);
+            
+            context.RestoreToCount(toRestore);
         }
     }
 
@@ -201,7 +207,7 @@ internal class LineToolOverlay : Overlay
 
         movedWhileMouseDown = false;
         mouseDownPos = args.Point;
-        
+
         lineStartOnMouseDown = LineStart;
         lineEndOnMouseDown = LineEnd;
 
@@ -281,13 +287,12 @@ internal class LineToolOverlay : Overlay
     protected override void OnOverlayPointerReleased(OverlayPointerArgs args)
     {
         IsSizeBoxEnabled = false;
-        
+
         if (args.InitialPressMouseButton != MouseButton.Left)
             return;
 
         if (movedWhileMouseDown && ActionCompleted is not null && ActionCompleted.CanExecute(null))
             ActionCompleted.Execute(null);
-        
     }
 
     private ((string, string), VecD) TrySnapLine(VecD originalStart, VecD originalEnd, VecD delta)

+ 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 Avalonia;
 using Avalonia.Input;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using PixiEditor.Extensions.UI.Overlays;
@@ -54,6 +57,8 @@ public class VectorPathOverlay : Overlay
     private VecD posOnStartDrag;
     private VectorPath pathOnStartDrag;
 
+    private EditableVectorPath editableVectorPath;
+
     static VectorPathOverlay()
     {
         AffectsOverlayRender(PathProperty);
@@ -68,7 +73,7 @@ public class VectorPathOverlay : Overlay
 
         AddHandle(transformHandle);
     }
-    
+
     protected override void ZoomChanged(double newZoom)
     {
         dashedStroke.UpdateZoom((float)newZoom);
@@ -100,74 +105,75 @@ public class VectorPathOverlay : Overlay
     private void RenderHandles(Canvas context)
     {
         bool anySelected = false;
-        int anchor = 0;
-        int controlPoint = 0;
-        int anchorCount = GetAnchorCount();
-        foreach (var verb in Path)
-        {
-            if (anchor == anchorCount - 1 && !anySelected)
-            {
-                GetHandleAt(anchor).IsSelected = true;
-            }
 
-            anySelected = anySelected || GetHandleAt(anchor).IsSelected;
+        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 totalHandles = anchorCount + controlPointHandles.Count;
         if (totalHandles != pointsCount)
@@ -177,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);
@@ -194,59 +200,43 @@ public class VectorPathOverlay : Overlay
 
             ConnectControlPointsToAnchors();
         }
+
         Refresh();
     }
 
     private void ConnectControlPointsToAnchors()
     {
+        if (controlPointHandles.Count == 0)
+        {
+            return;
+        }
+
         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()
@@ -356,7 +346,7 @@ public class VectorPathOverlay : Overlay
             return;
         }
 
-        if (IsFirstHandle(anchorHandle))
+        if (anchorHandles.IndexOf(anchorHandle) == 0)
         {
             newPath.LineTo((VecF)anchorHandle.Position);
             newPath.Close();
@@ -370,11 +360,6 @@ public class VectorPathOverlay : Overlay
         Path = newPath;
     }
 
-    private bool IsFirstHandle(AnchorHandle handle)
-    {
-        return anchorHandles.IndexOf(handle) == 0;
-    }
-
     private void SelectAnchor(AnchorHandle handle)
     {
         foreach (var anchorHandle in anchorHandles)
@@ -392,64 +377,35 @@ public class VectorPathOverlay : Overlay
 
             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
-    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);
 
-        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)
@@ -460,168 +416,87 @@ public class VectorPathOverlay : Overlay
         }
 
         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)
@@ -629,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) 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 snapped = new VecD((float)snappedPoint.X, (float)snappedPoint.Y);
@@ -770,36 +607,16 @@ public class VectorPathOverlay : Overlay
 
     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)
@@ -810,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.PointCount - (path.IsClosed ? 1 : 0));
+            EditableVectorPath editablePath = new EditableVectorPath(path);
+            overlay.editableVectorPath = editablePath;
+            overlay.AdjustHandles(editablePath);
             overlay.IsVisible = true;
         }
 
@@ -825,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;
         }
     }

+ 19 - 14
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -421,21 +421,26 @@ internal class TransformOverlay : Overlay
 
         context.RestoreToCount(saved);
 
-        infoBox.ZoomScale = ZoomScale;
-
         if (IsSizeBoxEnabled)
         {
+            int toRestore = context.Save();
+            var matrix = context.TotalMatrix;
+            VecD pos = matrix.MapPoint(lastPointerPos);
+            context.SetMatrix(Matrix3X3.Identity);
+
             if (isRotating)
             {
                 infoBox.DrawInfo(context, $"{(RadiansToDegreesNormalized(corners.RectRotation)):0.#}\u00b0",
-                    lastPointerPos);
+                    pos);
             }
             else
             {
                 VecD rectSize = Corners.RectSize;
                 string sizeText = $"W: {rectSize.X:0.#} H: {rectSize.Y:0.#} px";
-                infoBox.DrawInfo(context, sizeText, lastPointerPos);
+                infoBox.DrawInfo(context, sizeText, pos);
             }
+
+            context.RestoreToCount(toRestore);
         }
     }
 
@@ -581,7 +586,7 @@ internal class TransformOverlay : Overlay
     {
         const double offsetInPixels = 30;
         double offsetToScale = offsetInPixels / ZoomScale;
-        ShapeCorners scaled = Corners;
+        ShapeCorners scaled = Corners.AsRotated(-Corners.RectRotation, Corners.RectCenter);
         ShapeCorners scaledCorners = new ShapeCorners()
         {
             BottomLeft = scaled.BottomLeft - new VecD(offsetToScale, -offsetToScale),
@@ -590,6 +595,8 @@ internal class TransformOverlay : Overlay
             TopRight = scaled.TopRight - new VecD(-offsetToScale, offsetToScale),
         };
         
+        scaledCorners = scaledCorners.AsRotated(Corners.RectRotation, Corners.RectCenter);
+
         return base.TestHit(point) || scaledCorners.IsPointInside(point);
     }
 
@@ -730,8 +737,9 @@ internal class TransformOverlay : Overlay
 
             ShapeCorners? newCorners = TransformUpdateHelper.UpdateShapeFromCorner
             ((Anchor)capturedAnchor, CornerFreedom, InternalState.ProportionalAngle1,
-                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos, SnappingController, out string snapX, out string snapY);
-            
+                InternalState.ProportionalAngle2, cornersOnStartAnchorDrag, targetPos, SnappingController,
+                out string snapX, out string snapY);
+
             HighlightSnappedAxis(snapX, snapY);
 
             if (newCorners is not null)
@@ -739,7 +747,7 @@ internal class TransformOverlay : Overlay
                 bool shouldAlign =
                     (CornerFreedom is TransformCornerFreedom.ScaleProportionally or TransformCornerFreedom.Scale) &&
                     Corners.IsAlignedToPixels;
-                
+
                 newCorners = shouldAlign
                     ? TransformHelper.AlignToPixels((ShapeCorners)newCorners)
                     : (ShapeCorners)newCorners;
@@ -785,7 +793,7 @@ internal class TransformOverlay : Overlay
                     snapped = TrySnapAnchor(adjacentPos + rawDelta);
                 }
             }
-            else if(SideFreedom is not TransformSideFreedom.ScaleProportionally)
+            else if (SideFreedom is not TransformSideFreedom.ScaleProportionally)
             {
                 // If rotation is almost cardinal, projecting snapping points result in extreme values when perpendicular to the axis
                 if (!TransformHelper.RotationIsAlmostCardinal(cornersOnStartAnchorDrag.RectRotation))
@@ -815,7 +823,7 @@ internal class TransformOverlay : Overlay
 
             string finalSnapX = snapped.SnapAxisXName ?? snapX;
             string finalSnapY = snapped.SnapAxisYName ?? snapY;
-            
+
             HighlightSnappedAxis(finalSnapX, finalSnapY);
 
             if (newCorners is not null)
@@ -883,10 +891,7 @@ internal class TransformOverlay : Overlay
 
         return new ShapeCorners()
         {
-            TopLeft = topLeftPos,
-            TopRight = topRightPos,
-            BottomLeft = bottomLeftPos,
-            BottomRight = bottomRightPos,
+            TopLeft = topLeftPos, TopRight = topRightPos, BottomLeft = bottomLeftPos, BottomRight = bottomRightPos,
         };
     }
 

+ 23 - 0
tests/PixiEditor.Backend.Tests/NodeSystemTests.cs

@@ -170,6 +170,29 @@ public class NodeSystemTests
         }
     }
 
+    [Fact]
+    public void TestThatSerializationFactoriesIdsAreNotDuplicated()
+    {
+        var factoryTypes = typeof(SerializationFactory).Assembly.GetTypes()
+            .Where(x => x.IsAssignableTo(typeof(SerializationFactory))
+                        && x is { IsAbstract: false, IsInterface: false }).ToList();
+
+        List<SerializationFactory> factories = new();
+        
+        foreach (var factoryType in factoryTypes)
+        {
+            var factory = (SerializationFactory)Activator.CreateInstance(factoryType);
+            factories.Add(factory);
+        }
+
+        List<string> ids = new();
+        foreach (var factory in factories)
+        {
+            Assert.DoesNotContain(factory.DeserializationId, ids);
+            ids.Add(factory.DeserializationId);
+        }
+    }
+
     private static List<Type> GetNodeTypesWithoutPairs()
     {
         var allNodeTypes = typeof(Node).Assembly.GetTypes()

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