Browse Source

Added possibility to drag control points with different lengths, add wip

flabbet 8 months ago
parent
commit
6f902d46e7

+ 2 - 2
src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs

@@ -277,7 +277,7 @@ public class EditableVectorPath
         return closest;
     }
 
-    public void AddPointAt(VecF point)
+    public void AddPointAt(VecD point)
     {
         SubShape targetSubShape = null;
         Verb verb = null;
@@ -291,6 +291,6 @@ public class EditableVectorPath
             }
         }
 
-        targetSubShape?.AddPointAt(point, verb);
+        targetSubShape?.AddPointAt((VecF)point, verb);
     }
 }

+ 34 - 8
src/PixiEditor/Views/Overlays/PathOverlay/SubShape.cs

@@ -72,17 +72,20 @@ 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))));
-        
+
+        var oldTo = onVerb.To;
+        onVerb.To = point;
+        AdjustControlPoints(point, onVerb);
+
+        VecF[] data = GetDataForNewPoint(onVerb, oldTo);
+        this.points.Insert(indexOfVerb + 1,
+            new ShapePoint(point, indexOfVerb + 1, new Verb((onVerb.VerbType.Value, data, 0))));
+
         for (int i = indexOfVerb + 2; i < this.points.Count; i++)
         {
             this.points[i].Index++;
@@ -104,8 +107,8 @@ public class SubShape
 
         return null;
     }
-    
-    public Verb? FindVerbContainingPoint(VecF point)
+
+    public Verb? FindVerbContainingPoint(VecD point)
     {
         foreach (var shapePoint in points)
         {
@@ -117,4 +120,27 @@ public class SubShape
 
         return null;
     }
+
+    private static VecF[] GetDataForNewPoint(Verb onVerb, VecF to)
+    {
+        if (onVerb.VerbType.Value == PathVerb.Line)
+        {
+            return [onVerb.To, to, VecF.Zero, VecF.Zero];
+        }
+
+        if (onVerb.VerbType.Value == PathVerb.Cubic)
+        {
+            return [onVerb.To, onVerb.To, to, to];
+        }
+        
+        return [onVerb.To, to, VecF.Zero, VecF.Zero];
+    }
+
+    private static void AdjustControlPoints(VecF point, Verb onVerb)
+    {
+        if (onVerb.ControlPoint2 != null)
+        {
+            onVerb.ControlPoint2 = point;
+        }
+    }
 }

+ 115 - 23
src/PixiEditor/Views/Overlays/PathOverlay/VectorMath.cs

@@ -16,13 +16,17 @@ internal static class VectorMath
             case PathVerb.Line:
                 return ClosestPointOnLine((VecD)verb.From, (VecD)verb.To, point);
             case PathVerb.Quad:
-                break;
+                return GetClosestPointOnQuad(point, (VecD)verb.From, (VecD)(verb.ControlPoint1 ?? verb.From),
+                    (VecD)verb.To);
             case PathVerb.Conic:
-                break;
+                return GetClosestPointOnConic(point, (VecD)verb.From, (VecD)(verb.ControlPoint1 ?? verb.From),
+                    (VecD)verb.To,
+                    verb.ConicWeight);
             case PathVerb.Cubic:
-                break;
+                return GetClosestPointOnCubic(point, (VecD)verb.From, (VecD)(verb.ControlPoint1 ?? verb.From),
+                    (VecD)(verb.ControlPoint2 ?? verb.To), (VecD)verb.To);
             case PathVerb.Close:
-                break;
+                return (VecD)verb.From;
             case PathVerb.Done:
                 break;
             case null:
@@ -30,26 +34,29 @@ internal static class VectorMath
             default:
                 throw new ArgumentOutOfRangeException();
         }
-        
+
         return null;
     }
-    
-    public static bool IsPointOnSegment(VecF point, Verb shapePointVerb)
+
+    public static bool IsPointOnSegment(VecD point, Verb shapePointVerb)
     {
         if (shapePointVerb.IsEmptyVerb()) return false;
 
         switch (shapePointVerb.VerbType)
         {
             case PathVerb.Move:
-                return point == shapePointVerb.From;
+                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, shapePointVerb.From, shapePointVerb.To);
+                return IsPointOnLine(point, (VecD)shapePointVerb.From, (VecD)shapePointVerb.To);
             case PathVerb.Quad:
-                break;
+                return IsPointOnQuad(point, (VecD)shapePointVerb.From, (VecD)(shapePointVerb.ControlPoint1 ?? shapePointVerb.From),
+                    (VecD)shapePointVerb.To);
             case PathVerb.Conic:
-                break;
+                return IsPointOnConic(point, (VecD)shapePointVerb.From, (VecD)(shapePointVerb.ControlPoint1 ?? shapePointVerb.From),
+                    (VecD)shapePointVerb.To, shapePointVerb.ConicWeight);
             case PathVerb.Cubic:
-                break;
+                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:
@@ -67,28 +74,113 @@ internal static class VectorMath
     {
         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)
+
+    public static bool IsPointOnLine(VecD point, VecD start, VecD end)
     {
-        VecF startToPoint = point - start;
-        VecF startToEnd = end - start;
-        
+        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;
-        
+
         return t is >= 0 and <= 1;
     }
+
+    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));
+    }
+
+    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 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 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);
+    }
 }

+ 53 - 18
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -74,9 +74,9 @@ public class VectorPathOverlay : Overlay
         transformHandle.OnDrag += MoveHandleDrag;
 
         AddHandle(transformHandle);
-        
+
         insertPreviewHandle = new AnchorHandle(this);
-        
+
         AddHandle(insertPreviewHandle);
     }
 
@@ -96,7 +96,7 @@ public class VectorPathOverlay : Overlay
         dashedStroke.Draw(context, Path);
 
         RenderHandles(context);
-        
+
         if (canInsert)
         {
             insertPreviewHandle.Draw(context);
@@ -157,7 +157,7 @@ public class VectorPathOverlay : Overlay
         if (point.Verb.ControlPoint1 != null)
         {
             var controlPoint1 = controlPointHandles[controlPointIndex];
-            controlPoint1.HitTestVisible = controlPoint1.Position != controlPoint1.ConnectedTo.Position;
+            controlPoint1.HitTestVisible = CapturedHandle == controlPoint1 || controlPoint1.Position != controlPoint1.ConnectedTo.Position;
             controlPoint1.Position = (VecD)point.Verb.ControlPoint1;
             if (controlPoint1.HitTestVisible)
             {
@@ -171,7 +171,7 @@ public class VectorPathOverlay : Overlay
         {
             var controlPoint2 = controlPointHandles[controlPointIndex];
             controlPoint2.Position = (VecD)point.Verb.ControlPoint2;
-            controlPoint2.HitTestVisible = controlPoint2.Position != controlPoint2.ConnectedTo.Position;
+            controlPoint2.HitTestVisible = CapturedHandle == controlPoint2 || controlPoint2.Position != controlPoint2.ConnectedTo.Position;
 
             if (controlPoint2.HitTestVisible)
             {
@@ -272,6 +272,7 @@ public class VectorPathOverlay : Overlay
         for (int i = controlPointHandles.Count - 1; i >= 0; i--)
         {
             var handle = controlPointHandles[i];
+            handle.OnPress -= OnControlPointPress;
             handle.OnDrag -= OnControlPointDrag;
             handle.OnRelease -= OnHandleRelease;
 
@@ -331,6 +332,7 @@ public class VectorPathOverlay : Overlay
         else
         {
             var controlPoint = new ControlPointHandle(this);
+            controlPoint.OnPress += OnControlPointPress;
             controlPoint.OnDrag += OnControlPointDrag;
             controlPoint.OnRelease += OnHandleRelease;
             controlPointHandles.Add(controlPoint);
@@ -381,15 +383,16 @@ public class VectorPathOverlay : Overlay
 
     protected override void OnOverlayPointerPressed(OverlayPointerArgs args)
     {
-        if(args.Modifiers.HasFlag(KeyModifiers.Shift) && IsOverPath(args.Point, out VecD closestPoint))
+        if (args.Modifiers.HasFlag(KeyModifiers.Shift) && IsOverPath(args.Point, out VecD closestPoint))
         {
             AddPointAt(closestPoint);
+            AddToUndoCommand.Execute(Path);
         }
     }
 
     protected override void OnOverlayPointerMoved(OverlayPointerArgs args)
     {
-        if(args.Modifiers.HasFlag(KeyModifiers.Shift) && IsOverPath(args.Point, out VecD closestPoint))
+        if (args.Modifiers.HasFlag(KeyModifiers.Shift) && IsOverPath(args.Point, out VecD closestPoint))
         {
             insertPreviewHandle.Position = closestPoint;
             canInsert = true;
@@ -402,15 +405,15 @@ public class VectorPathOverlay : Overlay
 
     private void AddPointAt(VecD point)
     {
-        editableVectorPath.AddPointAt((VecF)point);
+        editableVectorPath.AddPointAt(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;
     }
 
@@ -429,7 +432,7 @@ public class VectorPathOverlay : Overlay
             SubShape subShapeContainingIndex = editableVectorPath.GetSubShapeContainingIndex(index);
             int localIndex = editableVectorPath.GetSubShapePointIndex(index, subShapeContainingIndex);
 
-            HandleContinousCubicDrag(anchorHandle.Position, anchorHandle, subShapeContainingIndex, localIndex);
+            HandleContinousCubicDrag(anchorHandle.Position, anchorHandle, subShapeContainingIndex, localIndex, true);
 
             Path = newPath.ToVectorPath();
         }
@@ -473,7 +476,7 @@ public class VectorPathOverlay : Overlay
 
         if (isDraggingControlPoints)
         {
-            HandleContinousCubicDrag(targetPos, handle, subShapeContainingIndex, localIndex);
+            HandleContinousCubicDrag(targetPos, handle, subShapeContainingIndex, localIndex, true);
         }
         else
         {
@@ -484,9 +487,37 @@ public class VectorPathOverlay : Overlay
     }
 
     private void HandleContinousCubicDrag(VecD targetPos, AnchorHandle handle, SubShape subShapeContainingIndex,
-        int localIndex, bool swapOrder = false)
+        int localIndex, bool constrainRatio, bool swapOrder = false)
     {
-        var symmetricPos = GetMirroredControlPoint((VecF)targetPos, (VecF)handle.Position);
+        var previousPoint = subShapeContainingIndex.GetPreviousPoint(localIndex);
+
+        VecD symmetricPos = targetPos;
+        bool canMirror = true;
+
+        if (constrainRatio)
+        {
+            symmetricPos = GetMirroredControlPoint((VecF)targetPos, (VecF)handle.Position);
+        }
+        else
+        {
+            VecD direction = targetPos - handle.Position;
+            direction = direction.Normalize();
+            var controlPos = ((VecD?)previousPoint?.Verb.ControlPoint2 ?? targetPos);
+            if (swapOrder)
+            {
+                controlPos = ((VecD?)subShapeContainingIndex.Points[localIndex]?.Verb.ControlPoint1 ?? targetPos);
+            }
+
+            double length = VecD.Distance(handle.Position, controlPos);
+            if (!direction.IsNaNOrInfinity())
+            { 
+                symmetricPos = handle.Position - direction * length;
+            }
+            else
+            {
+                canMirror = false;
+            }
+        }
 
         if (swapOrder)
         {
@@ -495,9 +526,7 @@ public class VectorPathOverlay : Overlay
 
         subShapeContainingIndex.Points[localIndex].Verb.ControlPoint1 = (VecF)targetPos;
 
-        var previousPoint = subShapeContainingIndex.GetPreviousPoint(localIndex);
-
-        if (previousPoint != null)
+        if (previousPoint != null && canMirror)
         {
             previousPoint.Verb.ControlPoint2 = (VecF)symmetricPos;
         }
@@ -523,7 +552,8 @@ public class VectorPathOverlay : Overlay
         if (!dragOnlyOne)
         {
             bool isDraggingFirst = controlPointHandles.IndexOf(controlPointHandle) % 2 == 0;
-            HandleContinousCubicDrag(targetPos, to, subShapeContainingIndex, localIndex, !isDraggingFirst);
+            bool constrainRatio = args.Modifiers.HasFlag(KeyModifiers.Control);
+            HandleContinousCubicDrag(targetPos, to, subShapeContainingIndex, localIndex, constrainRatio, !isDraggingFirst);
         }
         else
         {
@@ -557,6 +587,11 @@ public class VectorPathOverlay : Overlay
         TryHighlightSnap(axisX, axisY);
         return snapped;
     }
+    
+    private void OnControlPointPress(Handle source, OverlayPointerArgs args)
+    {
+        CaptureHandle(source);
+    }
 
     private void OnHandleRelease(Handle source, OverlayPointerArgs args)
     {