Browse Source

Adding point to line works

flabbet 8 months ago
parent
commit
b7ae43fa4d

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 2ba04a8018897b11fa764943ce3c368b7eda4bc9
+Subproject commit d3d792644bb31ed627e57175e6f06a5a15dd09f6

+ 46 - 8
src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs

@@ -13,14 +13,14 @@ public class EditableVectorPath
         set
         {
             UpdatePathFrom(value);
-            path = 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
@@ -65,12 +65,12 @@ public class EditableVectorPath
 
         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));
     }
 
@@ -89,7 +89,7 @@ public class EditableVectorPath
 
         List<ShapePoint> currentSubShapePoints = new List<ShapePoint>();
 
-        foreach(var data in from)
+        foreach (var data in from)
         {
             if (data.verb == PathVerb.Done)
             {
@@ -136,7 +136,7 @@ public class EditableVectorPath
             globalVerbIndex++;
         }
     }
-    
+
     private void AddVerbToPath(Verb verb, VectorPath newPath)
     {
         if (verb.IsEmptyVerb())
@@ -203,13 +203,14 @@ public class EditableVectorPath
 
         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.VerbType != PathVerb.Cubic)
+                continue; // temporarily only cubic is supported for control points
             if (point.Verb.ControlPoint1 != null)
             {
                 count++;
@@ -255,4 +256,41 @@ public class EditableVectorPath
 
         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 void AddPointAt(VecF point)
+    {
+        SubShape targetSubShape = null;
+        Verb verb = null;
+        foreach (var subShape in subShapes)
+        {
+            verb = subShape.FindVerbContainingPoint(point);
+            if (verb != null && !verb.IsEmptyVerb())
+            {
+                targetSubShape = subShape;
+                break;
+            }
+        }
+
+        targetSubShape?.AddPointAt(point, verb);
+    }
 }

+ 1 - 1
src/PixiEditor/Views/Overlays/PathOverlay/ShapePoint.cs

@@ -9,7 +9,7 @@ public class ShapePoint
 {
     public VecF Position { get; set; }
 
-    public int Index { get; }
+    public int Index { get; set; }
     public Verb Verb { get; set; }
 
     public ShapePoint(VecF position, int index, Verb verb)

+ 50 - 2
src/PixiEditor/Views/Overlays/PathOverlay/SubShape.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.Views.Overlays.PathOverlay;
@@ -51,7 +52,6 @@ public class SubShape
             {
                 shapePoint.Verb.ControlPoint1 = shapePoint.Verb.ControlPoint1.Value + delta;
             }
-            
         }
 
         var previousPoint = GetPreviousPoint(i);
@@ -59,7 +59,7 @@ public class SubShape
         if (previousPoint?.Verb != null && previousPoint.Verb.To == oldPos)
         {
             previousPoint.Verb.To = newPos;
-            
+
             if (updateControlPoints)
             {
                 if (previousPoint.Verb.ControlPoint2 != null)
@@ -69,4 +69,52 @@ public class SubShape
             }
         }
     }
+
+    public void AddPointAt(VecF point, Verb onVerb)
+    {
+        var oldTo = onVerb.To;
+        onVerb.To = point; 
+        int indexOfVerb = this.points.FirstOrDefault(x => x.Verb == onVerb)?.Index ?? -1;
+        if (indexOfVerb == -1)
+        {
+            throw new ArgumentException("Verb not found in points list");
+        }
+        
+        VecF[] data = [ onVerb.To, oldTo, VecF.Zero, VecF.Zero ];
+        this.points.Insert(indexOfVerb + 1, new ShapePoint(point, indexOfVerb + 1, new Verb((PathVerb.Line, data, 0))));
+        
+        for (int i = indexOfVerb + 2; i < this.points.Count; i++)
+        {
+            this.points[i].Index++;
+        }
+    }
+
+    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(VecF point)
+    {
+        foreach (var shapePoint in points)
+        {
+            if (VectorMath.IsPointOnSegment(point, shapePoint.Verb))
+            {
+                return shapePoint.Verb;
+            }
+        }
+
+        return null;
+    }
 }

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

@@ -0,0 +1,94 @@
+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:
+                break;
+            case PathVerb.Conic:
+                break;
+            case PathVerb.Cubic:
+                break;
+            case PathVerb.Close:
+                break;
+            case PathVerb.Done:
+                break;
+            case null:
+                break;
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+        
+        return null;
+    }
+    
+    public static bool IsPointOnSegment(VecF point, Verb shapePointVerb)
+    {
+        if (shapePointVerb.IsEmptyVerb()) return false;
+
+        switch (shapePointVerb.VerbType)
+        {
+            case PathVerb.Move:
+                return point == shapePointVerb.From;
+            case PathVerb.Line:
+                return IsPointOnLine(point, shapePointVerb.From, shapePointVerb.To);
+            case PathVerb.Quad:
+                break;
+            case PathVerb.Conic:
+                break;
+            case PathVerb.Cubic:
+                break;
+            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(VecF point, VecF start, VecF end)
+    {
+        VecF startToPoint = point - start;
+        VecF 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;
+        
+        return t is >= 0 and <= 1;
+    }
+}

+ 47 - 1
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -50,6 +50,7 @@ public class VectorPathOverlay : Overlay
 
     private DashedStroke dashedStroke = new DashedStroke();
     private TransformHandle transformHandle;
+    private AnchorHandle insertPreviewHandle;
 
     private List<AnchorHandle> anchorHandles = new();
     private List<ControlPointHandle> controlPointHandles = new();
@@ -58,6 +59,7 @@ public class VectorPathOverlay : Overlay
     private VectorPath pathOnStartDrag;
 
     private EditableVectorPath editableVectorPath;
+    private bool canInsert = false;
 
     static VectorPathOverlay()
     {
@@ -72,6 +74,10 @@ public class VectorPathOverlay : Overlay
         transformHandle.OnDrag += MoveHandleDrag;
 
         AddHandle(transformHandle);
+        
+        insertPreviewHandle = new AnchorHandle(this);
+        
+        AddHandle(insertPreviewHandle);
     }
 
     protected override void ZoomChanged(double newZoom)
@@ -90,8 +96,13 @@ public class VectorPathOverlay : Overlay
         dashedStroke.Draw(context, Path);
 
         RenderHandles(context);
+        
+        if (canInsert)
+        {
+            insertPreviewHandle.Draw(context);
+        }
 
-        if (IsOverAnyHandle())
+        if (IsOverAnyHandle() || canInsert)
         {
             TryHighlightSnap(null, null);
         }
@@ -368,6 +379,41 @@ public class VectorPathOverlay : Overlay
         }
     }
 
+    protected override void OnOverlayPointerPressed(OverlayPointerArgs args)
+    {
+        if(args.Modifiers.HasFlag(KeyModifiers.Shift) && IsOverPath(args.Point, out VecD closestPoint))
+        {
+            AddPointAt(closestPoint);
+        }
+    }
+
+    protected override void OnOverlayPointerMoved(OverlayPointerArgs args)
+    {
+        if(args.Modifiers.HasFlag(KeyModifiers.Shift) && IsOverPath(args.Point, out VecD closestPoint))
+        {
+            insertPreviewHandle.Position = closestPoint;
+            canInsert = true;
+        }
+        else
+        {
+            canInsert = false;
+        }
+    }
+
+    private void AddPointAt(VecD point)
+    {
+        editableVectorPath.AddPointAt((VecF)point);
+        Path = editableVectorPath.ToVectorPath();
+    }
+    
+    private bool IsOverPath(VecD point, out VecD closestPoint)
+    {
+        VecD? closest = editableVectorPath.GetClosestPointOnPath(point, 20 / (float)ZoomScale);
+        closestPoint = closest ?? point;
+        
+        return closest != null;
+    }
+
     private void OnHandlePress(Handle source, OverlayPointerArgs args)
     {
         if (source is AnchorHandle anchorHandle)