Browse Source

Massive node connections rendering optimization

Krzysztof Krysiński 4 months ago
parent
commit
0109cf9e1e

+ 7 - 2
src/PixiEditor/Helpers/Converters/SocketColorConverter.cs

@@ -6,8 +6,13 @@ namespace PixiEditor.Helpers.Converters;
 
 
 internal class SocketColorConverter : SingleInstanceConverter<SocketColorConverter>
 internal class SocketColorConverter : SingleInstanceConverter<SocketColorConverter>
 {
 {
-    Color unknownColor = Color.FromRgb(255, 0, 255);
+    static Color unknownColor = Color.FromRgb(255, 0, 255);
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return SocketToColor(value);
+    }
+
+    public static Color SocketToColor(object value)
     {
     {
         if (value is IBrush brush)
         if (value is IBrush brush)
         {
         {
@@ -16,7 +21,7 @@ internal class SocketColorConverter : SingleInstanceConverter<SocketColorConvert
             if (brush is GradientBrush linearGradientBrush)
             if (brush is GradientBrush linearGradientBrush)
                 return linearGradientBrush.GradientStops.FirstOrDefault()?.Color ?? unknownColor;
                 return linearGradientBrush.GradientStops.FirstOrDefault()?.Color ?? unknownColor;
         }
         }
-        
+
         return unknownColor;
         return unknownColor;
     }
     }
 }
 }

+ 17 - 35
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -69,41 +69,23 @@
                             </ControlTheme>
                             </ControlTheme>
                         </ItemsControl.ItemContainerTheme>
                         </ItemsControl.ItemContainerTheme>
                     </ItemsControl>
                     </ItemsControl>
-                    <ItemsControl Name="PART_Connections"
-                                  ItemsSource="{Binding NodeGraph.Connections, RelativeSource={RelativeSource TemplatedParent}}">
-                        <ItemsControl.ItemsPanel>
-                            <ItemsPanelTemplate>
-                                <Canvas RenderTransformOrigin="0, 0">
-                                    <Canvas.RenderTransform>
-                                        <TransformGroup>
-                                            <ScaleTransform
-                                                ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                            <TranslateTransform
-                                                X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                        </TransformGroup>
-                                    </Canvas.RenderTransform>
-                                </Canvas>
-                            </ItemsPanelTemplate>
-                        </ItemsControl.ItemsPanel>
-                        <ItemsControl.ItemTemplate>
-                            <DataTemplate>
-                                <nodes:ConnectionView
-                                    InputNodePosition="{Binding InputNode.PositionBindable}"
-                                    OutputNodePosition="{Binding OutputNode.PositionBindable}"
-                                    InputProperty="{Binding InputProperty}"
-                                    OutputProperty="{Binding OutputProperty}">
-                                    <nodes:ConnectionView.IsVisible>
-                                        <MultiBinding Converter="{x:Static BoolConverters.And}">
-                                            <Binding Path="InputProperty.IsVisible" />
-                                            <Binding Path="OutputProperty.IsVisible" />
-                                        </MultiBinding>
-                                    </nodes:ConnectionView.IsVisible>
-                                </nodes:ConnectionView>
-                            </DataTemplate>
-                        </ItemsControl.ItemTemplate>
-                    </ItemsControl>
+                    <nodes:ConnectionRenderer
+                        ClipToBounds="False"
+                        Name="PART_ConnectionRenderer"
+                        RenderTransformOrigin="0, 0"
+                        SocketsInfo="{Binding SocketsInfo, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                        Connections="{Binding NodeGraph.Connections, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}">
+                        <nodes:ConnectionRenderer.RenderTransform>
+                            <TransformGroup>
+                                <ScaleTransform
+                                    ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                <TranslateTransform
+                                    X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                            </TransformGroup>
+                        </nodes:ConnectionRenderer.RenderTransform>
+                    </nodes:ConnectionRenderer>
                     <ItemsControl
                     <ItemsControl
                         ZIndex="-1"
                         ZIndex="-1"
                         Name="PART_Frames"
                         Name="PART_Frames"

+ 47 - 0
src/PixiEditor/Views/Nodes/Connection.cs

@@ -0,0 +1,47 @@
+using Avalonia;
+using Avalonia.Media;
+using Drawie.Numerics;
+
+namespace PixiEditor.Views.Nodes;
+
+public class Connection
+{
+    public VecD StartPoint { get; set; }
+    public VecD EndPoint { get; set; }
+    public LinearGradientBrush LineBrush { get; set; }
+    public double Thickness { get; set; }
+
+    private Pen pen = new() { LineCap = PenLineCap.Round };
+
+    public void Render(DrawingContext context)
+    {
+        var p1 = new Point(StartPoint.X, StartPoint.Y);
+        var p2 = new Point(EndPoint.X, EndPoint.Y);
+
+        // curved line
+        var controlPoint = new Point((p1.X + p2.X) / 2, p1.Y);
+        var controlPoint2 = new Point((p1.X + p2.X) / 2, p2.Y);
+
+        if (p1.X < p2.X)
+        {
+            p1 = new Point(p1.X - 5, p1.Y);
+            p2 = new Point(p2.X + 5, p2.Y);
+
+            controlPoint2 = new Point(p2.X, (p1.Y + p2.Y) / 2);
+            controlPoint = new Point(p1.X, (p1.Y + p2.Y) / 2);
+        }
+
+        var geometry = new StreamGeometry();
+        using var ctx = geometry.Open();
+        ctx.BeginFigure(p1, false);
+        ctx.CubicBezierTo(controlPoint, controlPoint2, p2);
+
+        LineBrush.StartPoint = new RelativePoint(p1.X, p1.Y, RelativeUnit.Absolute);
+        LineBrush.EndPoint = new RelativePoint(p2.X, p2.Y, RelativeUnit.Absolute);
+
+        pen.Brush = LineBrush;
+        pen.Thickness = Thickness;
+
+        context.DrawGeometry(LineBrush, pen, geometry);
+    }
+}

+ 110 - 0
src/PixiEditor/Views/Nodes/ConnectionRenderer.cs

@@ -0,0 +1,110 @@
+using System.Collections.ObjectModel;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Drawie.Numerics;
+using PixiEditor.Helpers.Converters;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.Views.Nodes;
+
+public class ConnectionRenderer : Control
+{
+    public static readonly StyledProperty<SocketsInfo> SocketsInfoProperty =
+        AvaloniaProperty.Register<ConnectionRenderer, SocketsInfo>(
+            nameof(SocketsInfo));
+
+    public SocketsInfo SocketsInfo
+    {
+        get => GetValue(SocketsInfoProperty);
+        set => SetValue(SocketsInfoProperty, value);
+    }
+
+    internal static readonly StyledProperty<ObservableCollection<NodeConnectionViewModel>> ConnectionsProperty =
+        AvaloniaProperty.Register<ConnectionRenderer, ObservableCollection<NodeConnectionViewModel>>(
+            nameof(Connections));
+
+    internal ObservableCollection<NodeConnectionViewModel> Connections
+    {
+        get => GetValue(ConnectionsProperty);
+        set => SetValue(ConnectionsProperty, value);
+    }
+
+    private double thickness = 2;
+    public static readonly StyledProperty<TransformGroup> ContentTransformProperty = AvaloniaProperty.Register<ConnectionRenderer, TransformGroup>("ContentTransform");
+
+    public override void Render(DrawingContext context)
+    {
+        if (SocketsInfo == null || Connections == null)
+        {
+            return;
+        }
+
+        foreach (var connection in Connections)
+        {
+            var inputSocket =
+                SocketsInfo.Sockets.TryGetValue($"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}", out var socket)
+                    ? socket
+                    : null;
+            var outputSocket =
+                SocketsInfo.Sockets.TryGetValue($"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}", out socket)
+                    ? socket
+                    : null;
+
+            if (inputSocket == null || outputSocket == null)
+            {
+                continue;
+            }
+
+            Point startPoint = SocketsInfo.GetSocketPosition(inputSocket);
+            Point endPoint = SocketsInfo.GetSocketPosition(outputSocket);
+
+            LinearGradientBrush brush = new LinearGradientBrush
+            {
+                GradientStops = new GradientStops
+                {
+                    new GradientStop { Offset = 0, Color = Color.FromRgb(85, 85, 85) },
+                    new GradientStop { Offset = 0.05, Color = SocketColorConverter.SocketToColor(connection.InputProperty.SocketBrush) },
+                    new GradientStop { Offset = 0.95, Color = SocketColorConverter.SocketToColor(connection.OutputProperty.SocketBrush) },
+                    new GradientStop { Offset = 1, Color = Color.FromRgb(85, 85, 85) }
+                }
+            };
+
+            Pen pen = new() { LineCap = PenLineCap.Round };
+
+            var p1 = new Point(startPoint.X, startPoint.Y);
+            var p2 = new Point(endPoint.X, endPoint.Y);
+
+            // curved line
+            var controlPoint = new Point((p1.X + p2.X) / 2, p1.Y);
+            var controlPoint2 = new Point((p1.X + p2.X) / 2, p2.Y);
+
+            if (p1.X < p2.X)
+            {
+                p1 = new Point(p1.X - 5, p1.Y);
+                p2 = new Point(p2.X + 5, p2.Y);
+
+                controlPoint2 = new Point(p2.X, (p1.Y + p2.Y) / 2);
+                controlPoint = new Point(p1.X, (p1.Y + p2.Y) / 2);
+            }
+
+            var geometry = new StreamGeometry();
+            using var ctx = geometry.Open();
+            ctx.BeginFigure(p1, false);
+            ctx.CubicBezierTo(controlPoint, controlPoint2, p2);
+
+            brush.StartPoint = new RelativePoint(new Point(0, 0), RelativeUnit.Relative);
+            brush.EndPoint = new RelativePoint(new Point(1, 0), RelativeUnit.Relative);
+
+            /*if (startPoint.X < endPoint.X)
+            {
+                (brush.StartPoint, brush.EndPoint) = (brush.EndPoint, brush.StartPoint);
+            }*/
+
+            pen.Brush = brush;
+            pen.Thickness = thickness;
+
+            context.DrawGeometry(brush, pen, geometry);
+        }
+    }
+}

+ 70 - 9
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -87,6 +87,15 @@ internal class NodeGraphView : Zoombox.Zoombox
         AvaloniaProperty.Register<NodeGraphView, ICommand>(
         AvaloniaProperty.Register<NodeGraphView, ICommand>(
             "CreateNodeFromContextCommand");
             "CreateNodeFromContextCommand");
 
 
+    public static readonly StyledProperty<SocketsInfo> SocketsInfoProperty = AvaloniaProperty.Register<NodeGraphView, SocketsInfo>(
+        nameof(SocketsInfo));
+
+    public SocketsInfo SocketsInfo
+    {
+        get => GetValue(SocketsInfoProperty);
+        set => SetValue(SocketsInfoProperty, value);
+    }
+
     public ICommand CreateNodeFromContextCommand
     public ICommand CreateNodeFromContextCommand
     {
     {
         get => GetValue(CreateNodeFromContextCommandProperty);
         get => GetValue(CreateNodeFromContextCommandProperty);
@@ -214,6 +223,12 @@ internal class NodeGraphView : Zoombox.Zoombox
         AvaloniaProperty.Register<NodeGraphView, int>("ActiveFrame");
         AvaloniaProperty.Register<NodeGraphView, int>("ActiveFrame");
 
 
     private Panel rootPanel;
     private Panel rootPanel;
+    private ConnectionRenderer connectionRenderer;
+
+    static NodeGraphView()
+    {
+        NodeGraphProperty.Changed.Subscribe(OnNodeGraphChanged);
+    }
 
 
     public NodeGraphView()
     public NodeGraphView()
     {
     {
@@ -226,6 +241,16 @@ internal class NodeGraphView : Zoombox.Zoombox
 
 
         AllNodeTypes = new ObservableCollection<Type>(GatherAssemblyTypes<NodeViewModel>());
         AllNodeTypes = new ObservableCollection<Type>(GatherAssemblyTypes<NodeViewModel>());
         AllNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(AllNodeTypes.Select(x => new NodeTypeInfo(x)));
         AllNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(AllNodeTypes.Select(x => new NodeTypeInfo(x)));
+        SocketsInfo = new SocketsInfo((socket =>
+        {
+            var canvas = nodeItemsControl.FindDescendantOfType<Canvas>();
+            var view = nodeViewsCache.FirstOrDefault(x =>
+                x is ContentPresenter { Child: NodeView nodeView } && nodeView.Node == socket.Node) as ContentPresenter;
+            if (view == null)
+                return new Point(0, 0);
+
+            return (view.Child as NodeView).GetSocketPoint(socket, canvas);
+        }));
     }
     }
 
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -234,6 +259,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         nodeItemsControl = e.NameScope.Find<ItemsControl>("PART_Nodes");
         nodeItemsControl = e.NameScope.Find<ItemsControl>("PART_Nodes");
         connectionItemsControl = e.NameScope.Find<ItemsControl>("PART_Connections");
         connectionItemsControl = e.NameScope.Find<ItemsControl>("PART_Connections");
         selectionRectangle = e.NameScope.Find<Rectangle>("PART_SelectionRectangle");
         selectionRectangle = e.NameScope.Find<Rectangle>("PART_SelectionRectangle");
+        connectionRenderer = e.NameScope.Find<ConnectionRenderer>("PART_ConnectionRenderer");
 
 
         rootPanel = e.NameScope.Find<Panel>("PART_RootPanel");
         rootPanel = e.NameScope.Find<Panel>("PART_RootPanel");
 
 
@@ -369,10 +395,7 @@ internal class NodeGraphView : Zoombox.Zoombox
             if (sender is NodeViewModel node)
             if (sender is NodeViewModel node)
             {
             {
                 Dispatcher.UIThread.Post(
                 Dispatcher.UIThread.Post(
-                    () =>
-                    {
-                        UpdateConnections(FindNodeView(node));
-                    }, DispatcherPriority.Render);
+                    UpdateConnections, DispatcherPriority.Render);
             }
             }
         }
         }
     }
     }
@@ -566,7 +589,7 @@ internal class NodeGraphView : Zoombox.Zoombox
 
 
         if (e.Source is NodeView nodeView)
         if (e.Source is NodeView nodeView)
         {
         {
-            UpdateConnections(nodeView);
+            UpdateConnections(/*nodeView*/);
         }
         }
     }
     }
 
 
@@ -616,7 +639,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         if (e.Property == BoundsProperty)
         if (e.Property == BoundsProperty)
         {
         {
             NodeView nodeView = (NodeView)sender!;
             NodeView nodeView = (NodeView)sender!;
-            UpdateConnections(nodeView);
+            UpdateConnections(/*nodeView*/);
         }
         }
     }
     }
 
 
@@ -650,9 +673,10 @@ internal class NodeGraphView : Zoombox.Zoombox
         startDragConnectionPoint = _previewConnectionLine.StartPoint;
         startDragConnectionPoint = _previewConnectionLine.StartPoint;
     }
     }
 
 
-    private void UpdateConnections(NodeView nodeView)
+    private void UpdateConnections(/*NodeView nodeView*/)
     {
     {
-        if (nodeView == null)
+        connectionRenderer?.InvalidateVisual();
+        /*if (nodeView == null)
         {
         {
             return;
             return;
         }
         }
@@ -661,7 +685,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
         {
             NodePropertyViewModel property = (NodePropertyViewModel)propertyView.DataContext;
             NodePropertyViewModel property = (NodePropertyViewModel)propertyView.DataContext;
             UpdateConnectionView(property);
             UpdateConnectionView(property);
-        }
+        }*/
     }
     }
 
 
     private void UpdateConnectionView(NodePropertyViewModel? propertyView)
     private void UpdateConnectionView(NodePropertyViewModel? propertyView)
@@ -791,4 +815,41 @@ internal class NodeGraphView : Zoombox.Zoombox
             node.IsNodeSelected = false;
             node.IsNodeSelected = false;
         }
         }
     }
     }
+
+    private void OnConnectionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
+    {
+        if (e.Action == NotifyCollectionChangedAction.Add)
+        {
+            foreach (NodeConnectionViewModel connection in e.NewItems)
+            {
+                SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] = connection.InputProperty;
+                SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] = connection.OutputProperty;
+            }
+        }
+        else if (e.Action == NotifyCollectionChangedAction.Reset)
+        {
+            SocketsInfo.Sockets.Clear();
+        }
+
+        UpdateConnections();
+    }
+
+    private static void OnNodeGraphChanged(AvaloniaPropertyChangedEventArgs<INodeGraphHandler> e)
+    {
+        var nodeGraph = e.Sender as NodeGraphView;
+        if (e.OldValue.Value != null)
+        {
+            e.OldValue.Value.Connections.CollectionChanged += nodeGraph.OnConnectionsChanged;
+        }
+        if (e.NewValue.Value != null)
+        {
+            e.NewValue.Value.Connections.CollectionChanged += nodeGraph.OnConnectionsChanged;
+            nodeGraph.SocketsInfo.Sockets.Clear();
+            foreach (var connection in e.NewValue.Value.Connections)
+            {
+                nodeGraph.SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] = connection.InputProperty;
+                nodeGraph.SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] = connection.OutputProperty;
+            }
+        }
+    }
 }
 }

+ 16 - 0
src/PixiEditor/Views/Nodes/SocketsInfo.cs

@@ -0,0 +1,16 @@
+using Avalonia;
+using PixiEditor.Models.Handlers;
+using PixiEditor.Views.Nodes.Properties;
+
+namespace PixiEditor.Views.Nodes;
+
+public class SocketsInfo
+{
+    public Dictionary<string, INodePropertyHandler> Sockets { get; } = new();
+    public Func<INodePropertyHandler, Point> GetSocketPosition { get; set; }
+
+    public SocketsInfo(Func<INodePropertyHandler, Point> getSocketPosition)
+    {
+        GetSocketPosition = getSocketPosition;
+    }
+}