Browse Source

Added Separate Shapes command

Krzysztof Krysiński 2 months ago
parent
commit
15b8f543ce

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 6a22e1b5affd0906a949f97b755ae588eda7980a
+Subproject commit 45f50107675b9212a28d0e5a8d28bf15765f3eeb

+ 31 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -72,6 +72,37 @@ public static class NodeOperations
         return node;
     }
 
+    public static List<IChangeInfo> AppendMember(Node parent, Node toAppend, out Dictionary<Guid, VecD> originalPositions)
+    {
+        InputProperty<Painter?>? parentInput = parent.GetInputProperty(OutputNode.InputPropertyName) as InputProperty<Painter?>;
+        if (parentInput == null)
+        {
+            throw new InvalidOperationException("Parent node does not have an input property for appending members.");
+        }
+
+        OutputProperty<Painter>? toAddOutput = toAppend.GetOutputProperty("Output") as OutputProperty<Painter>;
+        if (toAddOutput == null)
+        {
+            throw new InvalidOperationException("Node to append does not have an output property named 'Output'.");
+        }
+
+        InputProperty<Painter>? toAddInput = toAppend.GetInputProperty(OutputNode.InputPropertyName) as InputProperty<Painter>;
+
+        if (toAddInput == null)
+        {
+            throw new InvalidOperationException("Node to append does not have an input property for appending members.");
+        }
+
+        Guid memberId = toAppend.Id;
+
+        List<IChangeInfo> changes = AppendMember(parentInput, toAddOutput, toAddInput, memberId);
+
+        var adjustedPositions = AdjustPositionsAfterAppend(toAppend, parent, parentInput.Connection?.Node as Node ?? null, out originalPositions);
+
+        changes.AddRange(adjustedPositions);
+        return changes;
+    }
+
     public static List<IChangeInfo> AppendMember(
         InputProperty<Painter?> parentInput,
         OutputProperty<Painter> toAddOutput,

+ 156 - 0
src/PixiEditor.ChangeableDocument/Changes/Vectors/SeparateShapes_Change.cs

@@ -0,0 +1,156 @@
+using ChunkyImageLib.Operations;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.Vectors;
+
+internal class SeparateShapes_Change : Change
+{
+    private readonly Guid memberId;
+    private PathVectorData originalData;
+    private List<Guid> newMemberIds = new List<Guid>();
+    private Dictionary<Guid, VecD> originalPositions = new Dictionary<Guid, VecD>();
+
+    [GenerateMakeChangeAction]
+    public SeparateShapes_Change(Guid memberId)
+    {
+        this.memberId = memberId;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (target.TryFindMember<VectorLayerNode>(memberId, out VectorLayerNode? node))
+        {
+            // Check if the node has embedded shape data and is not already a PathVectorData
+            return node.EmbeddedShapeData is PathVectorData p && GetShapeCount(p) > 1;
+        }
+
+        return false;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
+        PathVectorData data = node.EmbeddedShapeData as PathVectorData ??
+                              throw new InvalidOperationException("Node does not contain PathVectorData.");
+        originalData = data.Clone() as PathVectorData;
+
+        // Separate the shapes into individual PathVectorData instances
+        List<PathVectorData> separatedShapes = new List<PathVectorData>();
+        var editablePath = new EditableVectorPath(data.Path);
+        foreach (var subShape in editablePath.SubShapes)
+        {
+            PathVectorData newShape = new PathVectorData(subShape.ToPath())
+            {
+                Fill = data.Fill,
+                FillPaintable = data.FillPaintable,
+                Stroke = data.Stroke,
+                StrokeWidth = data.StrokeWidth,
+                TransformationMatrix = data.TransformationMatrix,
+                FillType = data.FillType,
+                StrokeLineCap = data.StrokeLineCap,
+                StrokeLineJoin = data.StrokeLineJoin,
+            };
+
+            separatedShapes.Add(newShape);
+        }
+
+        // Replace the original data with the first separated shape
+        node.EmbeddedShapeData = separatedShapes[0];
+        ignoreInUndo = false;
+
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+        changes.Add(new VectorShape_ChangeInfo(
+            memberId,
+            new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+                (RectI)node.EmbeddedShapeData.TransformedVisualAABB, ChunkyImage.FullChunkSize))));
+
+        var previousNode = node;
+
+        for (int i = 1; i < separatedShapes.Count; i++)
+        {
+            // Create a new node for each separated shape
+            VectorLayerNode newNode = node.Clone(false) as VectorLayerNode;
+
+            if (firstApply)
+            {
+                newMemberIds.Add(newNode.Id);
+            }
+            else
+            {
+                newNode.Id = newMemberIds[i - 1];
+            }
+
+            newNode.EmbeddedShapeData = separatedShapes[i];
+
+            newNode.MemberName = $"{node.MemberName} (Shape {i + 1})"; // Rename to indicate it's a separate shape
+
+            target.NodeGraph.AddNode(newNode);
+            changes.Add(CreateLayer_ChangeInfo.FromLayer(newNode));
+            var appended = NodeOperations.AppendMember(previousNode, newNode, out var positions);
+            AppendPositions(positions);
+            changes.AddRange(appended);
+
+            previousNode = newNode;
+        }
+
+        return changes;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
+        node.EmbeddedShapeData = originalData.Clone() as PathVectorData;
+
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+
+        var aabb = node.EmbeddedShapeData.TransformedVisualAABB;
+        var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+            (RectI)aabb, ChunkyImage.FullChunkSize));
+
+        changes.Add(new VectorShape_ChangeInfo(memberId, affected));
+
+        // Remove the newly created nodes
+        foreach (var newMemberId in newMemberIds)
+        {
+            var createdNode = target.FindNode<VectorLayerNode>(newMemberId);
+            if (createdNode != null)
+            {
+                target.NodeGraph.RemoveNode(createdNode);
+                createdNode?.Dispose();
+                changes.Add(new DeleteNode_ChangeInfo(newMemberId));
+            }
+        }
+
+        originalPositions.Clear();
+
+        return changes;
+    }
+
+    private int GetShapeCount(PathVectorData data)
+    {
+        return new EditableVectorPath(data.Path).SubShapes.Count;
+    }
+
+    private void AppendPositions(Dictionary<Guid, VecD> positions)
+    {
+        foreach (var position in positions)
+        {
+            originalPositions[position.Key] = position.Value;
+        }
+    }
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        originalData?.Path?.Dispose();
+    }
+}

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

@@ -1037,7 +1037,7 @@
   "SECONDARY_BG_COLOR": "Secondary background color",
   "RESET": "Reset",
   "AUTOSAVE_OPEN_FOLDER": "Open autosave folder",
-   "AUTOSAVE_OPEN_FOLDER_DESCRIPTIVE": "Open the folder where autosaves are stored",
+  "AUTOSAVE_OPEN_FOLDER_DESCRIPTIVE": "Open the folder where autosaves are stored",
   "AUTOSAVE_TOGGLE_DESCRIPTIVE": "Enable/disable autosave",
   "ERROR_GRAPH": "Graph setup produced an error. Fix it in the node graph",
   "COLOR_MATRIX_FILTER_NODE": "Color Matrix Filter",
@@ -1048,5 +1048,7 @@
   "RENDER_OUTPUT_SIZE": "Render Output Size",
   "RENDER_OUTPUT_CENTER": "Render Output Center",
   "COLOR_PICKER": "Color Picker",
-  "UNAUTHORIZED_ACCESS": "Unauthorized access"
+  "UNAUTHORIZED_ACCESS": "Unauthorized access",
+  "SEPARATE_SHAPES": "Separate Shapes",
+  "SEPARATE_SHAPES_DESCRIPTIVE": "Separate shapes from current vector into individual layers"
 }

+ 2 - 1
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -1,4 +1,5 @@
-using Avalonia.Threading;
+using System.Diagnostics;
+using Avalonia.Threading;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Generated;

+ 10 - 0
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -956,4 +956,14 @@ internal class DocumentOperationsModule : IDocumentOperations
 
         Internals.ActionAccumulator.AddFinishedActions(new ConvertToCurve_Action(memberId));
     }
+
+    public void SeparateShapes(Guid memberId)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+
+        Internals.ActionAccumulator.AddFinishedActions(new SeparateShapes_Action(memberId));
+    }
 }

+ 4 - 0
src/PixiEditor/Styles/Templates/Timeline.axaml

@@ -77,9 +77,13 @@
                     </Border>
                     <Border Grid.Row="0" Grid.Column="1">
                         <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5">
+                            <Button />
+                            <Button />
                             <ToggleButton Margin="0, 5" Width="24" HorizontalAlignment="Center" Classes="PlayButton"
                                           Name="PART_PlayToggle"
                                           IsChecked="{Binding IsPlaying, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
+                            <Button/>
+                            <Button/>
                             <TextBlock VerticalAlignment="Center" FontSize="14">
                                 <Run>
                                     <Run.Text>

+ 13 - 1
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -116,7 +116,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
 
         using var block = doc.Operations.StartChangeBlock();
         Guid? guid = doc.Operations.CreateStructureMember(StructureMemberType.Folder);
-        if(doc.SoftSelectedStructureMembers.Count == 0)
+        if (doc.SoftSelectedStructureMembers.Count == 0)
             return;
         var selectedInOrder = doc.GetSelectedMembersInOrder();
         selectedInOrder.Reverse();
@@ -571,6 +571,18 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc!.Operations.ConvertToCurve(member.Id);
     }
 
+    [Command.Basic("PixiEditor.Layer.SeparateShapes", "SEPARATE_SHAPES", "SEPARATE_SHAPES_DESCRIPTIVE",
+        CanExecute = "PixiEditor.Layer.SelectedMemberIsVectorLayer")]
+    public void SeparateShapes()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        var member = doc?.SelectedStructureMember;
+        if (member is null)
+            return;
+
+        doc!.Operations.SeparateShapes(member.Id);
+    }
+
     [Evaluator.Icon("PixiEditor.Layer.ToggleReferenceLayerTopMostIcon")]
     public IImage GetAboveEverythingReferenceLayerIcon()
     {

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

@@ -1,350 +0,0 @@
-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 PathFillType FillType { get; set; }
-
-    public int ControlPointsCount
-    {
-        get
-        {
-            // count verbs with control points
-            return subShapes.Sum(x => CountControlPoints(x.Points));
-        }
-    }
-
-    public EditableVectorPath(IEnumerable<SubShape> subShapes, PathFillType fillType)
-    {
-        this.subShapes = new List<SubShape>(subShapes);
-        FillType = fillType;
-    }
-
-    public EditableVectorPath(VectorPath path)
-    {
-        if (path != null)
-        {
-            Path = new VectorPath(path);
-            UpdatePathFrom(Path);
-        }
-        else
-        {
-            this.path = null;
-        }
-    }
-
-    public VectorPath ToVectorPath()
-    {
-        VectorPath newPath;
-        if (Path != null)
-        {
-            newPath = new VectorPath(Path);
-            newPath.Reset(); // preserve fill type and other properties
-        }
-        else
-        {
-            newPath = new VectorPath();
-        }
-
-        newPath.FillType = FillType;
-
-        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();
-            }
-            else
-            {
-                isSubShapeClosed = false;
-                if (data.verb == PathVerb.Move)
-                {
-                    if (currentSubShapePoints.Count > 0)
-                    {
-                        subShapes.Add(new SubShape(currentSubShapePoints, isSubShapeClosed));
-                        currentSubShapePoints.Clear();
-
-                        currentSubShapePoints.Add(new ShapePoint(data.points[0], 0, new Verb()));
-                    }
-                    else
-                    {
-                        currentSubShapePoints.Add(new ShapePoint(data.points[0], 0, new Verb()));
-                    }
-                }
-                else
-                {
-                    AddVerb(data, currentSubShapePoints);
-                }
-            }
-
-            globalVerbIndex++;
-        }
-
-        FillType = from.FillType;
-    }
-
-    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;
-    }
-
-    public VecD? GetClosestPointOnPath(VecD point, float maxDistanceInPixels)
-    {
-        VecD? closest = null;
-
-        foreach (var subShape in subShapes)
-        {
-            VecD? closestInSubShape = subShape.GetClosestPointOnPath(point, maxDistanceInPixels);
-
-            if (closestInSubShape != null)
-            {
-                if (closest == null ||
-                    VecD.Distance(closestInSubShape.Value, point) < VecD.Distance(closest.Value, point))
-                {
-                    closest = closestInSubShape;
-                }
-            }
-        }
-
-        return closest;
-    }
-
-    public int? AddPointAt(VecD point)
-    {
-        SubShape targetSubShape = null;
-        Verb verb = null;
-        foreach (var subShape in subShapes)
-        {
-            verb = subShape.FindVerbContainingPoint(point);
-            if (verb != null && !verb.IsEmptyVerb())
-            {
-                targetSubShape = subShape;
-                break;
-            }
-        }
-
-        if (targetSubShape != null)
-        {
-            int localIndex = targetSubShape.InsertPointAt((VecF)point, verb);
-            int globalIndex = GetGlobalIndex(targetSubShape, localIndex);
-            return globalIndex;
-        }
-
-        return null;
-    }
-
-    /*
-    public void NewSubShape(VecD point)
-    {
-        VecF pointF = (VecF)point;
-        ShapePoint newPoint = new ShapePoint(pointF, 0, new Verb(PathVerb.Move, pointF, pointF, null, null, 0));
-        var newSubShape = new SubShape(new List<ShapePoint>() { newPoint }, false);
-        subShapes.Add(newSubShape);
-    }*/
-
-    public void RemoveSubShape(SubShape subShapeContainingIndex)
-    {
-        if (subShapes.Contains(subShapeContainingIndex))
-        {
-            subShapes.Remove(subShapeContainingIndex);
-        }
-    }
-}

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

@@ -1,152 +0,0 @@
-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; set; }
-    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 fixedConic = 1 - Verb.ConicWeight;
-
-            // TODO: Make sure it is adjusted/works for other cases
-            // 0.77 works for oval case, it probably will need to be adjusted for other cases
-            // we don't have a case for any other shape than oval so right now it's hardcoded like so.
-            float factor = 2 * fixedConic / (0.77f + fixedConic);
-
-            VecF from1 = (mid1 - Verb.From);
-            from1 = new VecF(from1.X * factor, from1.Y * factor);
-            
-            VecF from2 = (mid1 - Verb.To);
-            from2 = new VecF(from2.X * factor, from2.Y * factor);
-
-            VecF control1 = Verb.From + from1;
-            VecF control2 = Verb.To + from2;
-
-            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 from, VecF to, VecF? controlPoint1, VecF? controlPoint2, float conicWeight)
-    {
-        VerbType = verb;
-        From = from;
-        To = to;
-        ControlPoint1 = controlPoint1;
-        ControlPoint2 = controlPoint2;
-        ConicWeight = conicWeight;
-    }
-    
-    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();
-        }
-    }
-}

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

@@ -1,240 +0,0 @@
-using System.Diagnostics;
-using Drawie.Backend.Core.Vector;
-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; private set; }
-
-    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 RemovePoint(int i)
-    {
-        bool isFirst = i == 0;
-        bool isLast = i == points.Count - 1;
-
-        if (!isFirst)
-        {
-            var previousPoint = GetPreviousPoint(i);
-            var nextPoint = GetNextPoint(i);
-
-            if (previousPoint?.Verb != null && nextPoint?.Verb != null)
-            {
-                previousPoint.Verb.To = nextPoint.Position;
-            }
-
-            for (int j = i + 1; j < points.Count; j++)
-            {
-                points[j].Index--;
-            }
-        }
-
-        if (isLast)
-        {
-            points[^2].Verb = new Verb();
-        }
-
-        if (isFirst && points.Count > 2)
-        {
-            points[^1].Verb.To = points[1].Position;
-        }
-
-        points.RemoveAt(i);
-
-        if (points.Count < 3)
-        {
-            IsClosed = false;
-        }
-    }
-
-    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;
-                }
-            }
-        }
-    }
-
-    public void AppendPoint(VecF point)
-    {
-        if (points.Count == 0)
-        {
-            VecF[] data = new VecF[4];
-            data[0] = VecF.Zero;
-            data[1] = point;
-            points.Add(new ShapePoint(point, 0, new Verb((PathVerb.Move, data, 0))));
-        }
-        else
-        {
-            var lastPoint = points[^1];
-            VecF[] data = new VecF[4];
-            data[0] = lastPoint.Position;
-            data[1] = point;
-            points.Add(new ShapePoint(point, lastPoint.Index + 1, new Verb((PathVerb.Line, data, 0))));
-        }
-    }
-
-    public int InsertPointAt(VecF point, Verb pointVerb)
-    {
-        int indexOfVerb = this.points.FirstOrDefault(x => x.Verb == pointVerb)?.Index ?? -1;
-        if (indexOfVerb == -1)
-        {
-            throw new ArgumentException("Verb not found in points list");
-        }
-
-        Verb onVerb = pointVerb;
-
-        if (onVerb.VerbType is PathVerb.Quad or PathVerb.Conic)
-        {
-            this.points[indexOfVerb].ConvertVerbToCubic();
-            onVerb = this.points[indexOfVerb].Verb;
-        }
-
-        var oldTo = onVerb.To;
-        VecF[] data = new VecF[4];
-        VecF insertPoint = point;
-
-        if (onVerb.VerbType == PathVerb.Line)
-        {
-            onVerb.To = point;
-            data = [onVerb.To, oldTo, VecF.Zero, VecF.Zero];
-        }
-        else
-        {
-            float t = VectorMath.GetNormalizedSegmentPosition(onVerb, point);
-            VecD oldControlPoint1 = (VecD)onVerb.ControlPoint1.Value;
-            VecD oldControlPoint2 = (VecD)onVerb.ControlPoint2.Value;
-
-            // de Casteljau's algorithm
-
-            var q0 = ((VecD)onVerb.From).Lerp(oldControlPoint1, t);
-            var q1 = oldControlPoint1.Lerp(oldControlPoint2, t);
-            var q2 = oldControlPoint2.Lerp((VecD)oldTo, t);
-
-            var r0 = q0.Lerp(q1, t);
-            var r1 = q1.Lerp(q2, t);
-
-            var s0 = r0.Lerp(r1, t);
-
-            onVerb.ControlPoint1 = (VecF)q0;
-            onVerb.ControlPoint2 = (VecF)r0;
-
-            onVerb.To = (VecF)s0;
-
-            data = [(VecF)s0, (VecF)r1, (VecF)q2, oldTo];
-
-            insertPoint = (VecF)s0;
-        }
-
-        this.points.Insert(indexOfVerb + 1,
-            new ShapePoint(insertPoint, indexOfVerb + 1, new Verb((onVerb.VerbType.Value, data, 0))));
-
-        for (int i = indexOfVerb + 2; i < this.points.Count; i++)
-        {
-            this.points[i].Index++;
-        }
-
-        return indexOfVerb + 1;
-    }
-
-    public VecD? GetClosestPointOnPath(VecD point, float maxDistanceInPixels)
-    {
-        for (int i = 0; i < points.Count; i++)
-        {
-            var currentPoint = points[i];
-
-            VecD? closest = VectorMath.GetClosestPointOnSegment(point, currentPoint.Verb);
-            if (closest != null && VecD.Distance(closest.Value, point) < maxDistanceInPixels)
-            {
-                return closest;
-            }
-        }
-
-        return null;
-    }
-
-    public Verb? FindVerbContainingPoint(VecD point)
-    {
-        foreach (var shapePoint in points)
-        {
-            if (VectorMath.IsPointOnSegment(point, shapePoint.Verb))
-            {
-                return shapePoint.Verb;
-            }
-        }
-
-        return null;
-    }
-
-    public void Close()
-    {
-        if (IsClosed)
-        {
-            return;
-        }
-
-        IsClosed = true;
-
-        if (points.Count > 1)
-        {
-            VecF[] data = new VecF[4];
-            data[0] = points[^1].Position;
-            data[1] = points[0].Position;
-            points.Add(new ShapePoint(points[0].Position, points[^1].Index + 1, new Verb((PathVerb.Line, data, 0))));
-        }
-    }
-}

+ 0 - 227
src/PixiEditor/Views/Overlays/PathOverlay/VectorMath.cs

@@ -1,227 +0,0 @@
-using Drawie.Backend.Core.Vector;
-using Drawie.Numerics;
-
-namespace PixiEditor.Views.Overlays.PathOverlay;
-
-internal static class VectorMath
-{
-    public static VecD? GetClosestPointOnSegment(VecD point, Verb verb)
-    {
-        if (verb == null || verb.IsEmptyVerb()) return null;
-
-        switch (verb.VerbType)
-        {
-            case PathVerb.Move:
-                return (VecD)verb.From;
-            case PathVerb.Line:
-                return ClosestPointOnLine((VecD)verb.From, (VecD)verb.To, point);
-            case PathVerb.Quad:
-                return GetClosestPointOnQuad(point, (VecD)verb.From, (VecD)(verb.ControlPoint1 ?? verb.From),
-                    (VecD)verb.To);
-            case PathVerb.Conic:
-                return GetClosestPointOnConic(point, (VecD)verb.From, (VecD)(verb.ControlPoint1 ?? verb.From),
-                    (VecD)verb.To,
-                    verb.ConicWeight);
-            case PathVerb.Cubic:
-                return GetClosestPointOnCubic(point, (VecD)verb.From, (VecD)(verb.ControlPoint1 ?? verb.From),
-                    (VecD)(verb.ControlPoint2 ?? verb.To), (VecD)verb.To);
-            case PathVerb.Close:
-                return (VecD)verb.From;
-            case PathVerb.Done:
-                break;
-            case null:
-                break;
-            default:
-                throw new ArgumentOutOfRangeException();
-        }
-
-        return null;
-    }
-
-    public static bool IsPointOnSegment(VecD point, Verb shapePointVerb)
-    {
-        if (shapePointVerb.IsEmptyVerb()) return false;
-
-        switch (shapePointVerb.VerbType)
-        {
-            case PathVerb.Move:
-                return Math.Abs(point.X - shapePointVerb.From.X) < 0.0001 &&
-                       Math.Abs(point.Y - shapePointVerb.From.Y) < 0.0001;
-            case PathVerb.Line:
-                return IsPointOnLine(point, (VecD)shapePointVerb.From, (VecD)shapePointVerb.To);
-            case PathVerb.Quad:
-                return IsPointOnQuad(point, (VecD)shapePointVerb.From,
-                    (VecD)(shapePointVerb.ControlPoint1 ?? shapePointVerb.From),
-                    (VecD)shapePointVerb.To);
-            case PathVerb.Conic:
-                return IsPointOnConic(point, (VecD)shapePointVerb.From,
-                    (VecD)(shapePointVerb.ControlPoint1 ?? shapePointVerb.From),
-                    (VecD)shapePointVerb.To, shapePointVerb.ConicWeight);
-            case PathVerb.Cubic:
-                return IsPointOnCubic(point, (VecD)shapePointVerb.From,
-                    (VecD)(shapePointVerb.ControlPoint1 ?? shapePointVerb.From),
-                    (VecD)(shapePointVerb.ControlPoint2 ?? shapePointVerb.To), (VecD)shapePointVerb.To);
-            case PathVerb.Close:
-                break;
-            case PathVerb.Done:
-                break;
-            case null:
-                break;
-            default:
-                throw new ArgumentOutOfRangeException();
-        }
-
-        return false;
-    }
-
-    public static VecD ClosestPointOnLine(VecD start, VecD end, VecD point)
-    {
-        VecD startToPoint = point - start;
-        VecD startToEnd = end - start;
-
-        double sqrtMagnitudeToEnd = Math.Pow(startToEnd.X, 2) + Math.Pow(startToEnd.Y, 2);
-
-        double dot = startToPoint.X * startToEnd.X + startToPoint.Y * startToEnd.Y;
-        var t = dot / sqrtMagnitudeToEnd;
-
-        if (t < 0) return start;
-        if (t > 1) return end;
-
-        return start + startToEnd * t;
-    }
-
-    public static bool IsPointOnLine(VecD point, VecD start, VecD end)
-    {
-        return Math.Abs(VecD.Distance(start, point) + VecD.Distance(end, point) - VecD.Distance(start, end)) < 0.001f;
-    }
-
-    public static VecD GetClosestPointOnQuad(VecD point, VecD start, VecD controlPoint, VecD end)
-    {
-        return FindClosestPointBruteForce(point, (t) => QuadraticBezier(start, controlPoint, end, t));
-    }
-
-    public static VecD GetClosestPointOnCubic(VecD point, VecD start, VecD controlPoint1, VecD controlPoint2, VecD end)
-    {
-        return FindClosestPointBruteForce(point, (t) => CubicBezier(start, controlPoint1, controlPoint2, end, t));
-    }
-
-    public static VecD GetClosestPointOnConic(VecD point, VecD start, VecD controlPoint, VecD end, float weight)
-    {
-        return FindClosestPointBruteForce(point, (t) => ConicBezier(start, controlPoint, end, weight, t));
-    }
-
-    public static bool IsPointOnQuad(VecD point, VecD start, VecD controlPoint, VecD end)
-    {
-        return IsPointOnPath(point, (t) => QuadraticBezier(start, controlPoint, end, t));
-    }
-
-    public static bool IsPointOnCubic(VecD point, VecD start, VecD controlPoint1, VecD controlPoint2, VecD end)
-    {
-        return IsPointOnPath(point, (t) => CubicBezier(start, controlPoint1, controlPoint2, end, t));
-    }
-
-    public static bool IsPointOnConic(VecD point, VecD start, VecD controlPoint, VecD end, float weight)
-    {
-        return IsPointOnPath(point, (t) => ConicBezier(start, controlPoint, end, weight, t));
-    }
-
-    /// <summary>
-    ///     Finds value from 0 to 1 that represents the position of point on the segment.
-    /// </summary>
-    /// <param name="onVerb">Verb that represents the segment.</param>
-    /// <param name="point">Point that is on the segment.</param>
-    /// <returns>Value from 0 to 1 that represents the position of point on the segment.</returns>
-    public static float GetNormalizedSegmentPosition(Verb onVerb, VecF point)
-    {
-        if (onVerb.IsEmptyVerb()) return 0;
-
-        if (onVerb.VerbType == PathVerb.Cubic)
-        {
-            return (float)FindNormalizedSegmentPositionBruteForce(point, (t) =>
-                CubicBezier((VecD)onVerb.From, (VecD)(onVerb.ControlPoint1 ?? onVerb.From),
-                    (VecD)(onVerb.ControlPoint2 ?? onVerb.To), (VecD)onVerb.To, t));
-        }
-        
-        throw new NotImplementedException();
-    }
-
-    private static VecD FindClosestPointBruteForce(VecD point, Func<double, VecD> func, double step = 0.001)
-    {
-        double minDistance = double.MaxValue;
-        VecD closestPoint = new VecD();
-        for (double t = 0; t <= 1; t += step)
-        {
-            VecD currentPoint = func(t);
-            double distance = VecD.Distance(point, currentPoint);
-            if (distance < minDistance)
-            {
-                minDistance = distance;
-                closestPoint = currentPoint;
-            }
-        }
-
-        return closestPoint;
-    }
-
-    private static double FindNormalizedSegmentPositionBruteForce(VecF point, Func<double, VecD> func,
-        double step = 0.001)
-    {
-        double minDistance = float.MaxValue;
-        double closestT = 0;
-        for (double t = 0; t <= 1; t += step)
-        {
-            VecD currentPoint = func(t);
-            float distance = (point - currentPoint).Length;
-            if (distance < minDistance)
-            {
-                minDistance = distance;
-                closestT = t;
-            }
-        }
-
-        return closestT;
-    }
-
-    private static bool IsPointOnPath(VecD point, Func<double, VecD> func, double step = 0.001)
-    {
-        for (double t = 0; t <= 1; t += step)
-        {
-            VecD currentPoint = func(t);
-            if (VecD.Distance(point, currentPoint) < 0.1)
-            {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    private static VecD QuadraticBezier(VecD start, VecD control, VecD end, double t)
-    {
-        double x = Math.Pow(1 - t, 2) * start.X + 2 * (1 - t) * t * control.X + Math.Pow(t, 2) * end.X;
-        double y = Math.Pow(1 - t, 2) * start.Y + 2 * (1 - t) * t * control.Y + Math.Pow(t, 2) * end.Y;
-        return new VecD(x, y);
-    }
-
-    private static VecD CubicBezier(VecD start, VecD control1, VecD control2, VecD end, double t)
-    {
-        double x = Math.Pow(1 - t, 3) * start.X + 3 * Math.Pow(1 - t, 2) * t * control1.X +
-                   3 * (1 - t) * Math.Pow(t, 2) * control2.X + Math.Pow(t, 3) * end.X;
-        double y = Math.Pow(1 - t, 3) * start.Y + 3 * Math.Pow(1 - t, 2) * t * control1.Y +
-                   3 * (1 - t) * Math.Pow(t, 2) * control2.Y + Math.Pow(t, 3) * end.Y;
-        return new VecD(x, y);
-    }
-
-    private static VecD ConicBezier(VecD start, VecD control, VecD end, float weight, double t)
-    {
-        double b0 = (1 - t) * (1 - t);
-        double b1 = 2 * t * (1 - t);
-        double b2 = t * t;
-
-        VecD numerator = (start * b0) + (control * b1 * weight) + (end * b2);
-
-        double denominator = b0 + (b1 * weight) + b2;
-
-        return numerator / denominator;
-    }
-}