Răsfoiți Sursa

Use rounded convex hull for node zone

CPKreuz 1 lună în urmă
părinte
comite
7b4fccde4d

+ 4 - 7
src/PixiEditor/Styles/Templates/NodeFrameView.axaml

@@ -7,13 +7,10 @@
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate>
-                    <Grid Width="{Binding Size.X, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}">
-                        <Rectangle Fill="{TemplateBinding Background}"
-                                   Stroke="{TemplateBinding BorderBrush}"
-                                   StrokeThickness="2" RadiusX="10" RadiusY="10"
-                                   Width="{Binding Size.X, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}"
-                                   Height="{Binding Size.Y, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}" />
-                    </Grid>
+                    <Path Data="{Binding Geometry, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}"
+                          Fill="{TemplateBinding Background}"
+                          Stroke="{TemplateBinding BorderBrush}"
+                          StrokeThickness="2" />
                 </ControlTemplate>
             </Setter.Value>
         </Setter>

+ 2 - 9
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -110,9 +110,8 @@
                         <ItemsControl.ItemTemplate>
                             <DataTemplate>
                                 <nodes:NodeFrameView
-                                    TopLeft="{Binding TopLeft}"
-                                    BottomRight="{Binding BottomRight}"
-                                    Size="{Binding Size}">
+                                    Geometry="{Binding Geometry}"
+                                    ClipToBounds="False">
                                     <nodes:NodeFrameView.Background>
                                         <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
                                             <Binding Path="InternalName"
@@ -132,12 +131,6 @@
                                 </nodes:NodeFrameView>
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>
-                        <ItemsControl.ItemContainerTheme>
-                            <ControlTheme TargetType="ContentPresenter">
-                                <Setter Property="Canvas.Left" Value="{Binding TopLeft.X}" />
-                                <Setter Property="Canvas.Top" Value="{Binding TopLeft.Y}" />
-                            </ControlTheme>
-                        </ItemsControl.ItemContainerTheme>
                     </ItemsControl>
                 </Grid>
             </ControlTemplate>

+ 2 - 28
src/PixiEditor/ViewModels/Nodes/NodeFrameViewModel.cs

@@ -1,9 +1,4 @@
-using System.Collections.ObjectModel;
-using System.Collections.Specialized;
-using System.ComponentModel;
-using CommunityToolkit.Mvvm.ComponentModel;
-using PixiEditor.Models.Handlers;
-using Drawie.Numerics;
+using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.ViewModels.Nodes;
 
@@ -16,27 +11,6 @@ internal sealed class NodeFrameViewModel : NodeFrameViewModelBase
 
     protected override void CalculateBounds()
     {
-        
-        // TODO: Use the GetBounds like in NodeZoneViewModel
-        if (Nodes.Count == 0)
-        {
-            if (TopLeft == BottomRight)
-            {
-                BottomRight = TopLeft + new VecD(100, 100);
-            }
-            
-            return;
-        }
-        
-        var minX = Nodes.Min(n => n.PositionBindable.X) - 30;
-        var minY = Nodes.Min(n => n.PositionBindable.Y) - 45;
-        
-        var maxX = Nodes.Max(n => n.PositionBindable.X) + 130;
-        var maxY = Nodes.Max(n => n.PositionBindable.Y) + 130;
-
-        TopLeft = new VecD(minX, minY);
-        BottomRight = new VecD(maxX, maxY);
-
-        Size = BottomRight - TopLeft;
+        throw new NotImplementedException();
     }
 }

+ 5 - 15
src/PixiEditor/ViewModels/Nodes/NodeFrameViewModelBase.cs

@@ -1,6 +1,7 @@
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.ComponentModel;
+using Avalonia.Media;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
@@ -10,6 +11,7 @@ namespace PixiEditor.ViewModels.Nodes;
 public abstract class NodeFrameViewModelBase : ObservableObject
 {
     private Guid id;
+    private StreamGeometry geometry;
     private VecD topLeft;
     private VecD bottomRight;
     private VecD size;
@@ -24,22 +26,10 @@ public abstract class NodeFrameViewModelBase : ObservableObject
         set => SetProperty(ref id, value);
     }
     
-    public VecD TopLeft
+    public StreamGeometry Geometry
     {
-        get => topLeft;
-        set => SetProperty(ref topLeft, value);
-    }
-
-    public VecD BottomRight
-    {
-        get => bottomRight;
-        set => SetProperty(ref bottomRight, value);
-    }
-
-    public VecD Size
-    {
-        get => size;
-        set => SetProperty(ref size, value);
+        get => geometry;
+        set => SetProperty(ref geometry, value);
     }
 
     public NodeFrameViewModelBase(Guid id, IEnumerable<INodeHandler> nodes)

+ 215 - 36
src/PixiEditor/ViewModels/Nodes/NodeZoneViewModel.cs

@@ -1,4 +1,8 @@
-using PixiEditor.Models.Handlers;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using Avalonia;
+using Avalonia.Media;
+using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
 
 namespace PixiEditor.ViewModels.Nodes;
@@ -7,14 +11,15 @@ public sealed class NodeZoneViewModel : NodeFrameViewModelBase
 {
     private INodeHandler start;
     private INodeHandler end;
-    
-    public NodeZoneViewModel(Guid id, string internalName, INodeHandler start, INodeHandler end) : base(id, [start, end])
+
+    public NodeZoneViewModel(Guid id, string internalName, INodeHandler start, INodeHandler end) : base(id,
+        [start, end])
     {
         InternalName = internalName;
-        
+
         this.start = start.Metadata.IsPairNodeStart ? start : end;
         this.end = start.Metadata.IsPairNodeStart ? end : start;
-        
+
         CalculateBounds();
     }
 
@@ -22,61 +27,235 @@ public sealed class NodeZoneViewModel : NodeFrameViewModelBase
     {
         if (Nodes.Count == 0)
         {
-            if (TopLeft == BottomRight)
+            return;
+        }
+
+        var points = GetBoundPoints();
+
+        Geometry = BuildRoundedHullGeometry(points, 25);
+    }
+
+    private static StreamGeometry BuildRoundedHullGeometry(List<VecD> points, double cornerRadius)
+    {
+        const double startBoostDeg = 100;
+        const double maxBoost = 2.5;
+
+        var span = CollectionsMarshal.AsSpan(points);
+
+        var pool = ArrayPool<VecD>.Shared;
+        var hullBuf = pool.Rent(Math.Max(3, span.Length));
+
+        try
+        {
+            var hullCount = ConvexHull(span, hullBuf.AsSpan());
+            var hull = hullBuf.AsSpan(0, hullCount);
+
+            var geometry = new StreamGeometry();
+            if (hull.IsEmpty) return geometry;
+
+            using var ctx = geometry.Open();
+            
+            if (hull.Length <= 2 || cornerRadius <= 0)
             {
-                BottomRight = TopLeft + new VecD(100, 100);
+                ctx.BeginFigure(new Point(hull[0].X, hull[0].Y), isFilled: true);
+                for (var i = 1; i < hull.Length; i++)
+                    ctx.LineTo(new Point(hull[i].X, hull[i].Y));
+                ctx.EndFigure(isClosed: true);
+                return geometry;
             }
-            
-            return;
+
+            var n = hull.Length;
+
+            var enter = n <= 256 ? stackalloc VecD[n] : pool.Rent(n).AsSpan(0, n);
+            var exit = n <= 256 ? stackalloc VecD[n] : pool.Rent(n).AsSpan(0, n);
+            var rented = n > 256;
+
+            try
+            {
+                for (var i = 0; i < n; i++)
+                {
+                    var prev = hull[(i - 1 + n) % n];
+                    var current = hull[i];
+                    var next = hull[(i + 1) % n];
+
+                    var directionIn = (current - prev).Normalize();
+                    var directionOut = (next - current).Normalize();
+                    var lenIn = (prev - current).Length;
+                    var lenOut = (current - next).Length;
+
+                    var a = (prev - current).Normalize();
+                    var b = (next - current).Normalize();
+                    var dot = Math.Clamp(a.X * b.X + a.Y * b.Y, -1, 1);
+                    var theta = Math.Acos(dot); // radians
+
+                    // Boost wide angles a bit (same curve, fewer ops)
+                    var thetaDeg = theta * (180.0 / Math.PI);
+                    var tNorm = Math.Clamp((thetaDeg - startBoostDeg) / (180.0 - startBoostDeg), 0, 1);
+                    var s = tNorm * tNorm * (3 - 2 * tNorm); // smoothstep
+                    var radiusHere = cornerRadius * (1 + (maxBoost - 1) * s);
+
+                    var t = (theta > 1e-6) ? radiusHere / Math.Tan(theta / 2.0) : 0;
+                    var tMax = Math.Min(lenIn, lenOut) * 0.5;
+                    t = Math.Min(t, tMax);
+
+                    if (t <= 1e-6)
+                    {
+                        enter[i] = current;
+                        exit[i] = current;
+                    }
+                    else
+                    {
+                        enter[i] = current + directionIn * -t;
+                        exit[i] = current + directionOut * t;
+                    }
+                }
+
+                ctx.BeginFigure(new Point(enter[0].X, enter[0].Y), isFilled: true);
+
+                for (var i = 0; i < n; i++)
+                {
+                    ctx.QuadraticBezierTo(
+                        new Point(hull[i].X, hull[i].Y),
+                        new Point(exit[i].X, exit[i].Y));
+
+                    var nextEnter = enter[(i + 1) % n];
+                    ctx.LineTo(new Point(nextEnter.X, nextEnter.Y));
+                }
+
+                ctx.EndFigure(isClosed: true);
+            }
+            finally
+            {
+                if (rented)
+                {
+                    pool.Return(
+                        MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(enter), n).ToArray());
+
+                    pool.Return(MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(exit), n)
+                        .ToArray());
+                }
+            }
+
+            return geometry;
+        }
+        finally
+        {
+            pool.Return(hullBuf);
+        }
+    }
+
+    private static int ConvexHull(ReadOnlySpan<VecD> input, Span<VecD> hull)
+    {
+        var n = input.Length;
+        if (n <= 1)
+        {
+            if (n == 1) hull[0] = input[0];
+            return n;
         }
 
-        var bounds = GetBounds();
-        
-        var minX = bounds.Min(n => n.X);
-        var minY = bounds.Min(n => n.Y);
-        
-        var maxX = bounds.Max(n => n.Right);
-        var maxY = bounds.Max(n => n.Bottom);
+        var pool = ArrayPool<VecD>.Shared;
+        var pts = pool.Rent(n);
+
+        try
+        {
+            input.CopyTo(pts);
+            Array.Sort(pts, 0, n, VecDComparer.Instance);
+
+            var m = 0;
+            for (var i = 0; i < n; i++)
+            {
+                if (m == 0 || !pts[i].Equals(pts[m - 1]))
+                    pts[m++] = pts[i];
+            }
+
+            if (m <= 1)
+            {
+                if (m == 1) hull[0] = pts[0];
+                return m;
+            }
+
+            var k = 0;
+
+            for (var i = 0; i < m; i++)
+            {
+                while (k >= 2 && (hull[k - 1] - hull[k - 2]).Cross(pts[i] - hull[k - 2]) <= 0)
+                    k--;
+                hull[k++] = pts[i];
+            }
+
+            var t = k + 1;
+            for (var i = m - 2; i >= 0; i--)
+            {
+                while (k >= t && (hull[k - 1] - hull[k - 2]).Cross(pts[i] - hull[k - 2]) <= 0)
+                    k--;
+                hull[k++] = pts[i];
+            }
+
+            return k - 1;
+        }
+        finally
+        {
+            pool.Return(pts);
+        }
+    }
 
-        TopLeft = new VecD(minX, minY);
-        BottomRight = new VecD(maxX, maxY);
+    private sealed class VecDComparer : IComparer<VecD>
+    {
+        public static readonly VecDComparer Instance = new();
 
-        Size = BottomRight - TopLeft;
+        public int Compare(VecD a, VecD b)
+        {
+            var cx = a.X.CompareTo(b.X);
+            return cx != 0 ? cx : a.Y.CompareTo(b.Y);
+        }
     }
 
-    private List<RectD> GetBounds()
+    private List<VecD> GetBoundPoints()
     {
-        var list = new List<RectD>();
+        var list = new List<VecD>(Nodes.Count * 4);
 
-        const int defaultXOffset = -30;
-        const int defaultYOffset = -45;
-        
-        foreach (var node in Nodes)
+        const int defaultXOffset = 30;
+        const int defaultYOffset = 45;
+
+        for (var i = 0; i < Nodes.Count; i++)
         {
+            var node = Nodes[i];
+            var pos = node.PositionBindable;
             var size = new VecD(node.UiSize.Size.Width, node.UiSize.Size.Height);
 
             if (node == start)
             {
-                // The + 40 ensure that the rectangle has a minimum size
-                var safeSizeOffset = size + new VecD(-size.X + 40, defaultYOffset * -2);
-                
-                list.Add(new RectD(node.PositionBindable + new VecD(size.X / 3 * 2, defaultYOffset), safeSizeOffset));
+                var twoThirdsX = size.X * (2.0 / 3.0);
+
+                list.Add(pos + new VecD(twoThirdsX, -defaultYOffset));
+                list.Add(pos + new VecD(twoThirdsX, defaultYOffset + size.Y));
+
+                list.Add(pos + new VecD(size.X + defaultXOffset, -defaultYOffset));
+                list.Add(pos + new VecD(size.X + defaultXOffset, defaultYOffset + size.Y));
                 continue;
             }
-            
+
             if (node == end)
             {
-                var sizeOffset = size + new VecD(-size.X, defaultYOffset * -2);
+                var oneThirdX = size.X / 3.0;
 
-                list.Add(new RectD(node.PositionBindable + new VecD(size.X / 3, defaultYOffset), sizeOffset));
+                list.Add(pos + new VecD(oneThirdX, -defaultYOffset));
+                list.Add(pos + new VecD(oneThirdX, defaultYOffset + size.Y));
+
+                list.Add(pos + new VecD(-defaultXOffset, -defaultYOffset));
+                list.Add(pos + new VecD(-defaultXOffset, defaultYOffset + size.Y));
                 continue;
             }
-            
-            var sizeNormalOffset = size + new VecD(defaultXOffset * -2, defaultYOffset * -2);
 
-            list.Add(new RectD(node.PositionBindable + new VecD(defaultXOffset, defaultYOffset), sizeNormalOffset));
+            var right = defaultXOffset + size.X;
+            var bottom = defaultYOffset + size.Y;
+
+            list.Add(pos + new VecD(-defaultXOffset, -defaultYOffset));
+            list.Add(pos + new VecD(right, -defaultYOffset));
+            list.Add(pos + new VecD(-defaultXOffset, -bottom));
+            list.Add(pos + new VecD(right, bottom));
         }
-        
+
         return list;
     }
 }

+ 6 - 21
src/PixiEditor/Views/Nodes/NodeFrameView.cs

@@ -1,32 +1,17 @@
 using Avalonia;
 using Avalonia.Controls.Primitives;
+using Avalonia.Media;
 using Drawie.Numerics;
 
 namespace PixiEditor.Views.Nodes;
 
 public class NodeFrameView : TemplatedControl
 {
-    public static readonly StyledProperty<VecD> TopLeftProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(TopLeft));
-    
-    public VecD TopLeft
-    {
-        get => GetValue(TopLeftProperty);
-        set => SetValue(TopLeftProperty, value);
-    }
-    
-    public static readonly StyledProperty<VecD> BottomRightProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(BottomRight));
-    
-    public VecD BottomRight
-    {
-        get => GetValue(BottomRightProperty);
-        set => SetValue(BottomRightProperty, value);
-    }
-    
-    public static readonly StyledProperty<VecD> SizeProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(Size));
-    
-    public VecD Size
+    public static readonly StyledProperty<StreamGeometry> GeometryProperty = AvaloniaProperty.Register<NodeFrameView, StreamGeometry>(nameof(Geometry));
+
+    public StreamGeometry Geometry
     {
-        get => GetValue(SizeProperty);
-        set => SetValue(SizeProperty, value);
+        get => GetValue(GeometryProperty);
+        set => SetValue(GeometryProperty, value);
     }
 }