ソースを参照

Dragging controls from anchor works

flabbet 9 ヶ月 前
コミット
c611b1eb82

+ 5 - 2
src/PixiEditor/Views/Overlays/Handles/ControlPointHandle.cs

@@ -7,7 +7,7 @@ namespace PixiEditor.Views.Overlays.Handles;
 
 public class ControlPointHandle : Handle
 {
-    public VecF ConnectToPosition { get; set; }
+    public Handle ConnectedTo { get; set; }
 
     public ControlPointHandle(IOverlay owner) : base(owner)
     {
@@ -28,6 +28,9 @@ public class ControlPointHandle : Handle
             target.DrawCircle(Position, radius, StrokePaint);
         }
 
-        target.DrawLine(Position, (VecD)ConnectToPosition, FillPaint);
+        if (ConnectedTo != null)
+        {
+            target.DrawLine(Position, ConnectedTo.Position, FillPaint);
+        }
     }
 }

+ 15 - 4
src/PixiEditor/Views/Overlays/Handles/Handle.cs

@@ -48,10 +48,6 @@ public abstract class Handle : IHandle
         Size = Application.Current.TryGetResource("HandleSize", out object size)
             ? new VecD((double)size)
             : new VecD(16);
-
-        Owner.PointerPressedOverlay += OnPointerPressed;
-        Owner.PointerMovedOverlay += OnPointerMoved;
-        Owner.PointerReleasedOverlay += OnPointerReleased;
     }
 
     public abstract void Draw(Canvas target);
@@ -86,6 +82,21 @@ public abstract class Handle : IHandle
         return ResourceLoader.GetPaint(key, style);
     }
 
+    public void InvokePress(OverlayPointerArgs args)
+    {
+        OnPointerPressed(args);
+    }
+
+    public void InvokeMove(OverlayPointerArgs args)
+    {
+        OnPointerMoved(args);
+    }
+
+    public void InvokeRelease(OverlayPointerArgs args)
+    {
+        OnPointerReleased(args);
+    }
+
     private void OnPointerPressed(OverlayPointerArgs args)
     {
         if (args.PointerButton != MouseButton.Left)

+ 60 - 21
src/PixiEditor/Views/Overlays/Overlay.cs

@@ -19,6 +19,15 @@ using Canvas = Drawie.Backend.Core.Surfaces.Canvas;
 
 namespace PixiEditor.Views.Overlays;
 
+enum HandleEventType
+{
+    PointerEnteredOverlay,
+    PointerExitedOverlay,
+    PointerMovedOverlay,
+    PointerPressedOverlay,
+    PointerReleasedOverlay
+}
+
 public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not avalonia element
 {
     public List<Handle> Handles { get; } = new();
@@ -40,6 +49,8 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     public event PointerEvent? PointerMovedOverlay;
     public event PointerEvent? PointerPressedOverlay;
     public event PointerEvent? PointerReleasedOverlay;
+    
+    public Handle? CapturedHandle { get; set; } = null!;
 
     private readonly Dictionary<AvaloniaProperty, OverlayTransition> activeTransitions = new();
 
@@ -66,52 +77,46 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
         InvalidateVisual(); // For elements in visual tree
     }
 
-    // Logically, subscribers (handles) of these events are rendered in 0 to n order, so the last one is rendered on top
-    // that's why reversing invocation order makes pointer events be handled in the order of rendering,
-    // which is more intuitive for the user since the topmost element should be the one to handle the event first
+    public void CaptureHandle(Handle handle)
+    {
+        CapturedHandle = handle;
+    }
     
     public void EnterPointer(OverlayPointerArgs args)
     {
         OnOverlayPointerEntered(args);
-        ReverseInvoke(PointerEnteredOverlay, args);
+        InvokeHandleEvent(HandleEventType.PointerEnteredOverlay, args);
+        PointerEnteredOverlay?.Invoke(args);
     }
 
     public void ExitPointer(OverlayPointerArgs args)
     {
         OnOverlayPointerExited(args);
-        ReverseInvoke(PointerExitedOverlay, args);
+        InvokeHandleEvent(HandleEventType.PointerExitedOverlay, args);
+        PointerExitedOverlay?.Invoke(args);
     }
 
     public void MovePointer(OverlayPointerArgs args)
     {
         OnOverlayPointerMoved(args);
-        ReverseInvoke(PointerMovedOverlay, args);
+        InvokeHandleEvent(HandleEventType.PointerMovedOverlay, args);
+        PointerMovedOverlay?.Invoke(args);
     }
 
     public void PressPointer(OverlayPointerArgs args)
     {
         OnOverlayPointerPressed(args);
-        ReverseInvoke(PointerPressedOverlay, args);
+        InvokeHandleEvent(HandleEventType.PointerPressedOverlay, args);
+        PointerPressedOverlay?.Invoke(args);
     }
 
     public void ReleasePointer(OverlayPointerArgs args)
     {
         OnOverlayPointerReleased(args);
-        ReverseInvoke(PointerReleasedOverlay, args);
-    }
-    
-    
-    private void ReverseInvoke(PointerEvent? pointerEvent, OverlayPointerArgs args)
-    {
-        if(pointerEvent == null) return;
+        InvokeHandleEvent(HandleEventType.PointerReleasedOverlay, args);
+        PointerReleasedOverlay?.Invoke(args);
         
-        var invokeList = pointerEvent.GetInvocationList();
-        
-        for (int i = invokeList.Length - 1; i >= 0; i--)
-        {
-            var handler = (PointerEvent)invokeList[i];
-            handler.Invoke(args);
-        }
+        CaptureHandle(null);
     }
 
     public virtual bool TestHit(VecD point)
@@ -145,6 +150,40 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
             }
         }
     }
+    
+    private void InvokeHandleEvent(HandleEventType eventName, OverlayPointerArgs args)
+    {
+        if (CapturedHandle != null)
+        {
+            InvokeHandleEvent(CapturedHandle, args, eventName);
+        }
+        else
+        {
+            var reversedHandles = Handles.Reverse<Handle>();
+            foreach (var handle in reversedHandles)
+            {
+                InvokeHandleEvent(handle, args, eventName);
+            }
+        }
+    }
+    
+    private void InvokeHandleEvent(Handle handle, OverlayPointerArgs args, HandleEventType pointerEvent)
+    {
+        if(pointerEvent == null) return;
+        
+        if (pointerEvent == HandleEventType.PointerMovedOverlay)
+        {
+            handle.InvokeMove(args);
+        }
+        else if (pointerEvent == HandleEventType.PointerPressedOverlay)
+        {
+            handle.InvokePress(args);
+        }
+        else if (pointerEvent == HandleEventType.PointerReleasedOverlay)
+        {
+            handle.InvokeRelease(args);
+        }
+    }
 
     protected void TransitionTo(AvaloniaProperty property, double durationSeconds, double to, Easing? easing = null)
     {

+ 202 - 77
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -119,11 +119,9 @@ public class VectorPathOverlay : Overlay
                 VecD controlPoint2 = (VecD)verb.points[2];
 
                 controlPointHandles[controlPoint].Position = controlPoint1;
-                controlPointHandles[controlPoint].ConnectToPosition = verb.points[0];
                 controlPointHandles[controlPoint].Draw(context);
 
                 controlPointHandles[controlPoint + 1].Position = controlPoint2;
-                controlPointHandles[controlPoint + 1].ConnectToPosition = verb.points[3];
                 controlPointHandles[controlPoint + 1].Draw(context);
 
                 controlPoint += 2;
@@ -146,24 +144,77 @@ public class VectorPathOverlay : Overlay
     private void AdjustHandles(int pointsCount)
     {
         int anchorCount = anchorHandles.Count;
-        if (anchorCount + controlPointHandles.Count != pointsCount)
+        int totalHandles = anchorCount + controlPointHandles.Count;
+        if (totalHandles != pointsCount)
         {
-            //if (anchorCount > pointsCount)
-            // {
-            RecreateHandles();
-            //}
-            /*else
+            if (anchorCount > pointsCount)
             {
-                for (int i = anchorCount; i < pointsCount; i++)
+                RecreateHandles();
+            }
+            else
+            {
+                int missingControlPoints = CalculateMissingHandles(controlPointHandles.Count, true);
+                int missingAnchors = CalculateMissingHandles(anchorHandles.Count, false);
+                for (int i = 0; i < missingAnchors; i++)
+                {
+                    CreateHandle(anchorHandles.Count);
+                }
+
+                for (int i = 0; i < missingControlPoints; i++)
                 {
-                    CreateHandle(i);
+                    CreateHandle(controlPointHandles.Count, true);
                 }
 
+
                 SelectAnchor(GetHandleAt(pointsCount - 1));
-            }*/
+            }
+
+            ConnectControlPointsToAnchors();
         }
     }
 
+    private void ConnectControlPointsToAnchors()
+    {
+        int controlPointIndex = 0;
+        int anchorIndex = 0;
+        foreach (var data in Path)
+        {
+            if (data.verb == PathVerb.Cubic)
+            {
+                AnchorHandle previousAnchor = anchorHandles.ElementAtOrDefault(anchorIndex - 1);
+                AnchorHandle nextAnchor = anchorHandles.ElementAtOrDefault(anchorIndex);
+
+                if (previousAnchor != null)
+                {
+                    controlPointHandles[controlPointIndex].ConnectedTo = previousAnchor; 
+                }
+
+                controlPointHandles[controlPointIndex + 1].ConnectedTo = nextAnchor;
+                controlPointIndex += 2;
+            }
+
+            anchorIndex++;
+        }
+    }
+
+    private int CalculateMissingHandles(int handleCount, bool isControlPoint)
+    {
+        int totalHandles = 0;
+        int totalControlPoints = 0;
+
+        foreach (var point in Path)
+        {
+            if (point.verb == PathVerb.Cubic)
+            {
+                totalControlPoints += 2;
+            }
+
+            totalHandles++;
+        }
+
+        return isControlPoint ? totalControlPoints - handleCount : totalHandles - handleCount;
+    }
+
     private void RecreateHandles()
     {
         int previouslySelectedIndex = -1;
@@ -194,13 +245,12 @@ public class VectorPathOverlay : Overlay
 
         foreach (var path in Path)
         {
+            CreateHandle(anchorHandles.Count);
             if (path.verb == PathVerb.Cubic)
             {
                 CreateHandle(controlPointHandles.Count, true);
                 CreateHandle(controlPointHandles.Count, true);
             }
-
-            CreateHandle(anchorHandles.Count);
         }
     }
 
@@ -303,20 +353,80 @@ public class VectorPathOverlay : Overlay
         }
     }
 
+    private void OnHandlePress(Handle source, OverlayPointerArgs args)
+    {
+        if (source is AnchorHandle anchorHandle)
+        {
+            SnappingController.RemoveAll($"editingPath[{anchorHandles.IndexOf(anchorHandle)}]");
+            CaptureHandle(source);
+
+            if (!args.Modifiers.HasFlag(KeyModifiers.Control)) return;
+
+            var newPath = ConvertTouchingLineVerbsToCubic(anchorHandle);
+
+            Path = newPath;
+        }
+    }
+
+    private VectorPath ConvertTouchingLineVerbsToCubic(AnchorHandle anchorHandle)
+    {
+        bool convertNextToCubic = false;
+        int i = 0;
+        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 (convertNextToCubic)
+                    {
+                        newPath.CubicTo(data.points[0], data.points[1], data.points[1]);
+                        convertNextToCubic = false;
+                    }
+                    else
+                    {
+                        newPath.LineTo(data.points[1]);
+                    }
+                }
+            }
+            else
+            {
+                DefaultPathVerb(data, newPath);
+            }
+
+            i++;
+        }
+
+        return newPath;
+    }
+
     private void OnHandleDrag(Handle source, OverlayPointerArgs args)
     {
         if (source is not AnchorHandle handle)
         {
             return;
         }
-        
+
         var index = anchorHandles.IndexOf(handle);
         VectorPath newPath = new VectorPath();
 
         bool pointHandled = false;
         int i = 0;
 
-        bool convertNextToCubic = false;
+        bool wasPreviousControlPoint = false;
+        VecF previousControlPoint = new VecF();
+        int controlPointIndex = 0;
+        var targetControlPoint = controlPointHandles.FirstOrDefault(x => x.ConnectedTo == handle);
+        int targetControlPointIndex = controlPointHandles.IndexOf(targetControlPoint);
+
         foreach (var data in Path)
         {
             VecF point;
@@ -333,36 +443,21 @@ public class VectorPathOverlay : Overlay
                     point = data.points[1];
                     point = TryApplyNewPos(args, i, index, point);
 
-                    if (i == index && args.Modifiers.HasFlag(KeyModifiers.Control))
-                    {
-                        newPath.CubicTo(data.points[0], point, point);
-                        convertNextToCubic = true;
-                    }
-                    else
-                    {
-                        if (convertNextToCubic)
-                        {
-                            newPath.CubicTo(data.points[0], point, point);
-                            convertNextToCubic = false;
-                        }
-                        else
-                        {
-                            newPath.LineTo(point);
-                        }
-                    }
+                    newPath.LineTo(point);
 
                     i++;
                     break;
                 case PathVerb.Cubic:
-                    point = data.points[3];
-                    point = TryApplyNewPos(args, i, index, point);
-
-                    if (i == index && args.Modifiers.HasFlag(KeyModifiers.Control))
+                    if (args.Modifiers.HasFlag(KeyModifiers.Control))
                     {
-                        newPath.CubicTo(data.points[1], point, data.points[3]);
+                        HandleCubicControlContinousDrag(args, controlPointIndex, 1, data, ref wasPreviousControlPoint,
+                            ref previousControlPoint, newPath);
+                        controlPointIndex += 2;
                     }
                     else
                     {
+                        point = data.points[3];
+                        point = TryApplyNewPos(args, i, index, point);
                         newPath.CubicTo(data.points[1], data.points[2], point);
                     }
 
@@ -415,38 +510,9 @@ public class VectorPathOverlay : Overlay
                     newPath.LineTo(point);
                     break;
                 case PathVerb.Cubic:
-                    bool isFirstControlPoint = i == index;
-                    bool isSecondControlPoint = i + 1 == index;
-                    bool isNextFirstControlPoint = i + 2 == index;
-
-                    VecF controlPoint1 = data.points[1];
-                    VecF controlPoint2 = data.points[2];
-                    VecF endPoint = data.points[3];
-
-                    if (isFirstControlPoint)
-                    {
-                        controlPoint1 = TryApplyNewPos(args, i, index, controlPoint1);
-                    }
-                    else if (isSecondControlPoint)
-                    {
-                        controlPoint2 = TryApplyNewPos(args, i + 1, index, controlPoint2);
-                        wasPreviousControlPoint = true;
-                        previousControlPoint = controlPoint2;
-                    }
-                    else if (isNextFirstControlPoint)
-                    {
-                        VecD mirroredControlPoint = GetMirroredControlPoint(
-                            TryApplyNewPos(args, i + 2, index, controlPoint1), endPoint);
-                        controlPoint2 = (VecF)mirroredControlPoint;
-                    }
-                    else if (wasPreviousControlPoint)
-                    {
-                        VecD mirroredControlPoint = GetMirroredControlPoint(
-                            previousControlPoint, data.points[0]);
-                        controlPoint1 = (VecF)mirroredControlPoint;
-                    }
-
-                    newPath.CubicTo(controlPoint1, controlPoint2, endPoint);
+                    HandleCubicControlContinousDrag(args, i, index, data, ref wasPreviousControlPoint,
+                        ref previousControlPoint,
+                        newPath);
                     i += 2;
                     break;
                 case PathVerb.Quad:
@@ -469,6 +535,44 @@ public class VectorPathOverlay : Overlay
         Path = newPath;
     }
 
+    private void HandleCubicControlContinousDrag(OverlayPointerArgs args, int i, int index,
+        (PathVerb verb, VecF[] points) data, ref bool wasPreviousControlPoint, ref VecF previousControlPoint,
+        VectorPath newPath)
+    {
+        bool isFirstControlPoint = i == index;
+        bool isSecondControlPoint = i + 1 == index;
+        bool isNextFirstControlPoint = i + 2 == index;
+
+        VecF controlPoint1 = data.points[1];
+        VecF controlPoint2 = data.points[2];
+        VecF endPoint = data.points[3];
+
+        if (isFirstControlPoint)
+        {
+            controlPoint1 = TryApplyNewPos(args, i, index, controlPoint1);
+        }
+        else if (isSecondControlPoint)
+        {
+            controlPoint2 = TryApplyNewPos(args, i + 1, index, controlPoint2);
+            wasPreviousControlPoint = true;
+            previousControlPoint = controlPoint2;
+        }
+        else if (isNextFirstControlPoint)
+        {
+            VecD mirroredControlPoint = GetMirroredControlPoint(
+                TryApplyNewPos(args, i + 2, index, controlPoint1), endPoint);
+            controlPoint2 = (VecF)mirroredControlPoint;
+        }
+        else if (wasPreviousControlPoint)
+        {
+            VecD mirroredControlPoint = GetMirroredControlPoint(
+                previousControlPoint, data.points[0]);
+            controlPoint1 = (VecF)mirroredControlPoint;
+        }
+
+        newPath.CubicTo(controlPoint1, controlPoint2, endPoint);
+    }
+
     private VecD GetMirroredControlPoint(VecF controlPoint, VecF endPoint)
     {
         return new VecD(2 * endPoint.X - controlPoint.X, 2 * endPoint.Y - controlPoint.Y);
@@ -509,14 +613,6 @@ public class VectorPathOverlay : Overlay
         return point;
     }
 
-    private void OnHandlePress(Handle source, OverlayPointerArgs args)
-    {
-        if (source is AnchorHandle anchorHandle)
-        {
-            SnappingController.RemoveAll($"editingPath[{anchorHandles.IndexOf(anchorHandle)}]");
-        }
-    }
-
     private void OnHandleRelease(Handle source, OverlayPointerArgs args)
     {
         if (source is not AnchorHandle anchorHandle)
@@ -613,6 +709,35 @@ public class VectorPathOverlay : Overlay
         }
     }
 
+    private static void DefaultPathVerb((PathVerb verb, VecF[] points) data, VectorPath newPath)
+    {
+        switch (data.verb)
+        {
+            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();
+        }
+    }
+
     private static void OnPathChanged(AvaloniaPropertyChangedEventArgs<VectorPath> args)
     {
         if (args.NewValue.Value == null)