Browse Source

Connection

flabbet 1 year ago
parent
commit
68bd70e160

+ 7 - 0
src/PixiEditor.AvaloniaUI/PixiEditor.AvaloniaUI.csproj

@@ -146,6 +146,13 @@
       <DependentUpon>TimelineDockView.axaml</DependentUpon>
       <SubType>Code</SubType>
     </Compile>
+    <Compile Update="Views\Nodes\Properties\NodeSocket.cs">
+      <DependentUpon>NodeSocket.axaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+    <Compile Update="Views\Nodes\ConnectionView.cs">
+      <SubType>Code</SubType>
+    </Compile>
   </ItemGroup>
 
 </Project>

+ 3 - 0
src/PixiEditor.AvaloniaUI/Styles/PixiEditor.Controls.axaml

@@ -15,10 +15,13 @@
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/TimelineGroupHeader.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeView.axaml"/>
+                <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml"/>
+                <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/ConnectionView.axaml"/>
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Styles.Resources>
 
     <StyleInclude Source="avares://PixiEditor.AvaloniaUI/Styles/PortingWipStyles.axaml"/>
     <StyleInclude Source="avares://PixiEditor.AvaloniaUI/Styles/ToolPickerButton.Styles.axaml"/>
+    <StyleInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodePropertyViewTemplate.axaml"/>
 </Styles>

+ 15 - 0
src/PixiEditor.AvaloniaUI/Styles/Templates/ConnectionView.axaml

@@ -0,0 +1,15 @@
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:nodes="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes">
+
+    <ControlTheme TargetType="nodes:ConnectionView" x:Key="{x:Type nodes:ConnectionView}">
+        <Setter Property="Template">
+            <ControlTemplate>
+                    <Line Stroke="{DynamicResource ThemeForegroundBrush}" StrokeThickness="2"
+                          StartPoint="{Binding StartPoint, RelativeSource={RelativeSource TemplatedParent}}"
+                          EndPoint="{Binding EndPoint, RelativeSource={RelativeSource TemplatedParent}}" />
+            </ControlTemplate>
+        </Setter>
+    </ControlTheme>
+
+</ResourceDictionary>

+ 29 - 4
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml

@@ -7,7 +7,7 @@
             <ControlTemplate>
                 <Grid Background="Transparent">
                     <ItemsControl ClipToBounds="False"
-                        ItemsSource="{TemplateBinding Nodes}">
+                                  ItemsSource="{TemplateBinding Nodes}">
                         <ItemsControl.ItemsPanel>
                             <ItemsPanelTemplate>
                                 <Canvas RenderTransformOrigin="0, 0">
@@ -27,11 +27,10 @@
                         <ItemsControl.ItemTemplate>
                             <DataTemplate>
                                 <nodes:NodeView
-                                    DisplayName="{Binding DisplayName}" 
+                                    DisplayName="{Binding DisplayName}"
                                     Inputs="{Binding Inputs}"
                                     Outputs="{Binding Outputs}"
-                                    ResultPreview="{Binding ResultPreview}"
-                                    />
+                                    ResultPreview="{Binding ResultPreview}" />
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>
                         <ItemsControl.ItemContainerTheme>
@@ -41,6 +40,32 @@
                             </ControlTheme>
                         </ItemsControl.ItemContainerTheme>
                     </ItemsControl>
+                    <ItemsControl
+                        ItemsSource="{TemplateBinding Connections}">
+                        <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
+                                    InputProperty="{Binding InputProperty}"
+                                    OutputProperty="{Binding OutputProperty}" />
+                            </DataTemplate>
+                        </ItemsControl.ItemTemplate>
+                    </ItemsControl>
                 </Grid>
             </ControlTemplate>
         </Setter>

+ 20 - 0
src/PixiEditor.AvaloniaUI/Styles/Templates/NodePropertyViewTemplate.axaml

@@ -0,0 +1,20 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties">
+    
+    <Style Selector="properties|NodePropertyView">
+        <Setter Property="ClipToBounds" Value="False" />
+        <Setter Property="Template">
+            <ControlTemplate>
+                <Grid Margin="-10, 0">
+                    <Grid.ColumnDefinitions>10, *, 10</Grid.ColumnDefinitions>
+                    <properties:NodeSocket Name="PART_InputSocket" IsVisible="{Binding DataContext.IsInput, 
+                    RelativeSource={RelativeSource TemplatedParent}}"/>
+                    <ContentPresenter Grid.Column="1" Content="{TemplateBinding Content}"/>
+                    <properties:NodeSocket Grid.Column="2" Name="PART_OutputSocket" IsVisible="{Binding !DataContext.IsInput,
+                    RelativeSource={RelativeSource TemplatedParent}}"/>
+                </Grid>
+            </ControlTemplate>
+        </Setter>
+    </Style>
+</Styles>

+ 12 - 0
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml

@@ -0,0 +1,12 @@
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:nodes="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes"
+                    xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties">
+    <ControlTheme TargetType="properties:NodeSocket" x:Key="{x:Type properties:NodeSocket}">
+        <Setter Property="Template">
+            <ControlTemplate>
+                <Ellipse Width="10" Height="10" Fill="Red"/>
+            </ControlTemplate>
+        </Setter>
+    </ControlTheme>
+</ResourceDictionary>

+ 33 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeConnectionViewModel.cs

@@ -0,0 +1,33 @@
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
+
+public class NodeConnectionViewModel : ViewModelBase
+{
+    private NodeViewModel inputNode;
+    private NodeViewModel outputNode;
+    private NodePropertyViewModel inputProperty;
+    private NodePropertyViewModel outputProperty;
+
+    public NodeViewModel InputNode
+    {
+        get => inputNode;
+        set => SetProperty(ref inputNode, value);
+    }
+
+    public NodeViewModel OutputNode
+    {
+        get => outputNode;
+        set => SetProperty(ref outputNode, value);
+    }
+
+    public NodePropertyViewModel InputProperty
+    {
+        get => inputProperty;
+        set => SetProperty(ref inputProperty, value);
+    }
+
+    public NodePropertyViewModel OutputProperty
+    {
+        get => outputProperty;
+        set => SetProperty(ref outputProperty, value);
+    }
+}

+ 11 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodePropertyViewModel.cs

@@ -1,10 +1,13 @@
-namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
+using Avalonia;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
 
 public abstract class NodePropertyViewModel : ViewModelBase
 {
     private string name;
     private object value;
     private NodeViewModel node;
+    private bool isInput;
     
     public string Name
     {
@@ -18,12 +21,18 @@ public abstract class NodePropertyViewModel : ViewModelBase
         set => SetProperty(ref value, value);
     }
     
+    public bool IsInput
+    {
+        get => isInput;
+        set => SetProperty(ref isInput, value);
+    }
+    
     public NodeViewModel Node
     {
         get => node;
         set => SetProperty(ref node, value);
     }
-    
+
     public NodePropertyViewModel(NodeViewModel node)
     {
         Node = node;

+ 1 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeViewModel.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using Avalonia;
 using ChunkyImageLib;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/ImageNodePropertyViewModel.cs

@@ -6,7 +6,7 @@ public class ImageNodePropertyViewModel : NodePropertyViewModel<Surface>
 {
     public ImageNodePropertyViewModel(NodeViewModel node) : base(node)
     {
-        base.PropertyChanged += (sender, args) =>
+        this.PropertyChanged += (sender, args) =>
         {
             if (args.PropertyName == nameof(Value))
             {

+ 107 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/ConnectionView.cs

@@ -0,0 +1,107 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+using PixiEditor.AvaloniaUI.ViewModels.Nodes;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes;
+
+public class ConnectionView : TemplatedControl
+{
+    public static readonly StyledProperty<NodePropertyViewModel> InputPropertyProperty =
+        AvaloniaProperty.Register<ConnectionView, NodePropertyViewModel>(
+            nameof(InputProperty));
+
+    public static readonly StyledProperty<NodePropertyViewModel> OutputPropertyProperty =
+        AvaloniaProperty.Register<ConnectionView, NodePropertyViewModel>(
+            nameof(OutputProperty));
+
+    public static readonly StyledProperty<Point> StartPointProperty = AvaloniaProperty.Register<ConnectionView, Point>(
+        nameof(StartPoint));
+    
+    public static readonly StyledProperty<Point> EndPointProperty = AvaloniaProperty.Register<ConnectionView, Point>(
+        nameof(EndPoint));
+
+    public Point StartPoint
+    {
+        get => GetValue(StartPointProperty);
+        set => SetValue(StartPointProperty, value);
+    }
+    
+    public Point EndPoint
+    {
+        get => GetValue(EndPointProperty);
+        set => SetValue(EndPointProperty, value);
+    }
+
+    public NodePropertyViewModel InputProperty
+    {
+        get => GetValue(InputPropertyProperty);
+        set => SetValue(InputPropertyProperty, value);
+    }
+
+    public NodePropertyViewModel OutputProperty
+    {
+        get => GetValue(OutputPropertyProperty);
+        set => SetValue(OutputPropertyProperty, value);
+    }
+
+
+    static ConnectionView()
+    {
+        AffectsRender<ConnectionView>(InputPropertyProperty, OutputPropertyProperty);
+        InputPropertyProperty.Changed.Subscribe(OnInputPropertyChanged);
+        OutputPropertyProperty.Changed.Subscribe(OnOutputPropertyChanged);
+    }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        Dispatcher.UIThread.Post(
+            () =>
+        {
+            StartPoint = CalculateSocketPoint(InputProperty);
+            EndPoint = CalculateSocketPoint(OutputProperty);
+        }, DispatcherPriority.Render);
+    }
+
+    private Point CalculateSocketPoint(BindingValue<NodePropertyViewModel> argsNewValue)
+    {
+        NodePropertyViewModel property = argsNewValue.Value;
+        if (this.VisualRoot is null)
+        {
+            return default;
+        }
+
+        Canvas canvas = this.FindAncestorOfType<NodeGraphView>().FindDescendantOfType<Canvas>();
+
+        if (property.Node is null || canvas is null)
+        {
+            return default;
+        }
+
+        NodeView nodeView = canvas.GetVisualDescendants().OfType<NodeView>()
+            .FirstOrDefault(x => x.DataContext == property.Node);
+
+        if (nodeView is null)
+        {
+            return default;
+        }
+
+        return nodeView.GetSocketPoint(property, canvas);
+    }
+
+    private static void OnInputPropertyChanged(AvaloniaPropertyChangedEventArgs<NodePropertyViewModel> args)
+    {
+        ConnectionView connectionView = args.Sender as ConnectionView;
+        connectionView.StartPoint = connectionView.CalculateSocketPoint(args.NewValue);
+    }
+
+    private static void OnOutputPropertyChanged(AvaloniaPropertyChangedEventArgs<NodePropertyViewModel> args)
+    {
+        ConnectionView connectionView = args.Sender as ConnectionView;
+        connectionView.EndPoint = connectionView.CalculateSocketPoint(args.NewValue);
+    }
+}

+ 45 - 1
src/PixiEditor.AvaloniaUI/Views/Nodes/NodeGraphView.cs

@@ -1,5 +1,8 @@
 using System.Collections.ObjectModel;
 using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.VisualTree;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
 
@@ -10,6 +13,15 @@ public class NodeGraphView : Zoombox.Zoombox
     public static readonly StyledProperty<ObservableCollection<NodeViewModel>> NodesProperty = AvaloniaProperty.Register<NodeGraphView, ObservableCollection<NodeViewModel>>(
         nameof(Nodes));
 
+    public static readonly StyledProperty<ObservableCollection<NodeConnectionViewModel>> ConnectionsProperty = AvaloniaProperty.Register<NodeGraphView, ObservableCollection<NodeConnectionViewModel>>(
+        nameof(Connections));
+
+    public ObservableCollection<NodeConnectionViewModel> Connections
+    {
+        get => GetValue(ConnectionsProperty);
+        set => SetValue(ConnectionsProperty, value);
+    }
+    
     public ObservableCollection<NodeViewModel> Nodes
     {
         get => GetValue(NodesProperty);
@@ -25,14 +37,46 @@ public class NodeGraphView : Zoombox.Zoombox
             Name = "Node 1",
             X = 100,
             Y = 100,
+            Outputs = new ObservableCollection<NodePropertyViewModel>()
+        };
+        
+        NodeViewModel node2 = new NodeViewModel()
+        {
+            Name = "Node 2",
+            X = 500,
+            Y = 100,
             Inputs = new ObservableCollection<NodePropertyViewModel>()
         };
         
-        node.Inputs.Add(new ImageNodePropertyViewModel(node) { Name = "Input 1" });
+        node.Outputs.Add(new ImageNodePropertyViewModel(node)
+        {
+            Name = "Output 1",
+            Node = node,
+            IsInput = false,
+        });
+        
+        node2.Inputs.Add(new ImageNodePropertyViewModel(node2)
+        {
+            Name = "Input 1",
+            Node = node2,
+            IsInput = true,
+        });
 
         Nodes =
         [
             node,
+            node2,
+        ];
+        
+        Connections =
+        [
+            new NodeConnectionViewModel()
+            {
+                InputNode = node2,
+                InputProperty = node2.Inputs[0],
+                OutputNode = node,
+                OutputProperty = node.Outputs[0],
+            },
         ];
     }
 }

+ 15 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/NodeView.cs

@@ -1,8 +1,11 @@
 using System.Collections.ObjectModel;
 using Avalonia;
+using Avalonia.Controls;
 using Avalonia.Controls.Primitives;
+using Avalonia.VisualTree;
 using ChunkyImageLib;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
+using PixiEditor.AvaloniaUI.Views.Nodes.Properties;
 
 namespace PixiEditor.AvaloniaUI.Views.Nodes;
 
@@ -43,4 +46,16 @@ public class NodeView : TemplatedControl
         get => GetValue(DisplayNameProperty);
         set => SetValue(DisplayNameProperty, value);
     }
+
+    public Point GetSocketPoint(NodePropertyViewModel property, Canvas canvas)
+    {
+        NodePropertyView propertyView = this.GetVisualDescendants().OfType<NodePropertyView>().FirstOrDefault(x => x.DataContext == property);
+        
+        if (propertyView is null)
+        {
+            return default;
+        }
+
+        return propertyView.GetSocketPoint(property.IsInput, canvas);
+    }
 }

+ 0 - 3
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ImageNodePropertyView.axaml

@@ -6,8 +6,5 @@
                              xmlns:chunkyImageLib="clr-namespace:ChunkyImageLib;assembly=ChunkyImageLib"
                              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" ClipToBounds="False"
                              x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.ImageNodePropertyView">
-    <StackPanel Orientation="Horizontal">
-        <Ellipse Margin="-10, 0, 0, 0" Width="10" Height="10" Fill="Red"/>
         <Button Content="Open Image (temp)" Click="Button_OnClick"/>
-    </StackPanel>
 </properties:NodePropertyView>

+ 40 - 2
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodePropertyView.cs

@@ -1,9 +1,47 @@
-using Avalonia.Controls;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
 
 namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
 
-public abstract class NodePropertyView<T> : UserControl
+public abstract class NodePropertyView : UserControl
+{
+    public NodeSocket InputSocket { get; private set; }
+    public NodeSocket OutputSocket { get; private set; }
+    protected override Type StyleKeyOverride => typeof(NodePropertyView);
+
+    protected void SetValue(object value)
+    {
+        if (DataContext is NodePropertyViewModel viewModel)
+        {
+            viewModel.Value = value;
+        }
+    }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        InputSocket = e.NameScope.Find<NodeSocket>("PART_InputSocket");
+        OutputSocket = e.NameScope.Find<NodeSocket>("PART_OutputSocket");
+    }
+
+    public Point GetSocketPoint(bool getInputSocket, Canvas canvas)
+    {
+        NodeSocket socket = getInputSocket ? InputSocket : OutputSocket;
+        
+        if (socket is null)
+        {
+            return default;
+        }
+        
+        Point? point = socket.TranslatePoint(new Point(socket.Bounds.Width / 2, socket.Bounds.Height / 2), canvas);
+        
+        return point ?? default;
+    }
+}
+
+public abstract class NodePropertyView<T> : NodePropertyView
 {
     protected void SetValue(T value)
     {

+ 8 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodeSocket.cs

@@ -0,0 +1,8 @@
+using Avalonia.Controls.Primitives;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public class NodeSocket : TemplatedControl
+{
+}
+