Browse Source

Node position changing

flabbet 1 year ago
parent
commit
17c3962bf7
25 changed files with 585 additions and 61 deletions
  1. 1 0
      src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs
  2. 13 1
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs
  3. 1 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/INodeGraphHandler.cs
  4. 2 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/INodeHandler.cs
  5. 1 0
      src/PixiEditor.AvaloniaUI/Styles/Templates/ConnectionView.axaml
  6. 15 3
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml
  7. 9 3
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodeView.axaml
  8. 3 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs
  9. 20 4
      src/PixiEditor.AvaloniaUI/ViewModels/Document/NodeGraphViewModel.cs
  10. 41 4
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeConnectionViewModel.cs
  11. 3 2
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodePropertyViewModel.cs
  12. 46 16
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeViewModel.cs
  13. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/GenericPropertyViewModel.cs
  14. 24 0
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs
  15. 4 0
      src/PixiEditor.AvaloniaUI/ViewModels/ViewModelMain.cs
  16. 5 1
      src/PixiEditor.AvaloniaUI/Views/Dock/NodeGraphDockView.axaml
  17. 35 2
      src/PixiEditor.AvaloniaUI/Views/Nodes/ConnectionView.cs
  18. 160 6
      src/PixiEditor.AvaloniaUI/Views/Nodes/NodeGraphView.cs
  19. 125 9
      src/PixiEditor.AvaloniaUI/Views/Nodes/NodeView.cs
  20. 5 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodePosition_ChangeInfo.cs
  21. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs
  22. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  23. 1 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs
  24. 65 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodePosition_UpdateableChange.cs
  25. 3 3
      src/PixiEditor.ChangeableDocument/Rendering/DocumentEvaluator.cs

+ 1 - 0
src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs

@@ -61,6 +61,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<ViewOptionsViewModel>()
             .AddSingleton<ColorsViewModel>()
             .AddSingleton<AnimationsViewModel>()
+            .AddSingleton<NodeGraphManagerViewModel>()
             .AddSingleton<IColorsHandler, ColorsViewModel>(x => x.GetRequiredService<ColorsViewModel>())
             .AddSingleton<RegistryViewModel>()
             .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModelMain>(), "764168193685979138"))

+ 13 - 1
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -172,6 +172,9 @@ internal class DocumentUpdater
             case ConnectProperty_ChangeInfo info:
                 ProcessConnectProperty(info);
                 break;
+            case NodePosition_ChangeInfo info:
+                ProcessNodePosition(info);
+                break;
         }
     }
 
@@ -481,8 +484,11 @@ internal class DocumentUpdater
     
     private void ProcessCreateNode<T>(CreateNode_ChangeInfo info) where T : NodeViewModel, new()
     {
-        T node = new T() { NodeName = info.NodeName, Id = info.Id, Position = info.Position };
+        T node = new T() { NodeName = info.NodeName, Id = info.Id, 
+            Document = (DocumentViewModel)doc, Internals = helper };
 
+        node.SetPosition(info.Position);
+        
         List<INodePropertyHandler> inputs = CreateProperties(info.Inputs, node, true);
         List<INodePropertyHandler> outputs = CreateProperties(info.Outputs, node, false);
         node.Inputs.AddRange(inputs);
@@ -524,4 +530,10 @@ internal class DocumentUpdater
         
         doc.NodeGraphHandler.SetConnection(connection);
     }
+    
+    private void ProcessNodePosition(NodePosition_ChangeInfo info)
+    {
+        NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
+        node.SetPosition(info.NewPosition);
+    }
 }

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/INodeGraphHandler.cs

@@ -3,7 +3,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Nodes;
 
 namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
-public interface INodeGraphHandler
+internal interface INodeGraphHandler
 {
    public ObservableCollection<INodeHandler> AllNodes { get; }
    public ObservableCollection<NodeConnectionViewModel> Connections { get; }

+ 2 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/INodeHandler.cs

@@ -12,7 +12,8 @@ public interface INodeHandler
     public ObservableRangeCollection<INodePropertyHandler> Inputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
     public Surface ResultPreview { get; set; }
-    public VecD Position { get; set; }
+    public VecD PositionBindable { get; set; }
+    public bool IsSelected { get; set; }
     void TraverseBackwards(Func<INodeHandler, bool> func);
     void TraverseForwards(Func<INodeHandler, bool> func);
 }

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

@@ -3,6 +3,7 @@
                     xmlns:nodes="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes">
 
     <ControlTheme TargetType="nodes:ConnectionView" x:Key="{x:Type nodes:ConnectionView}">
+        <Setter Property="ClipToBounds" Value="False"/>
         <Setter Property="Template">
             <ControlTemplate>
                     <Line Stroke="{DynamicResource ThemeForegroundBrush}" StrokeThickness="2"

+ 15 - 3
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml

@@ -27,20 +27,30 @@
                         <ItemsControl.ItemTemplate>
                             <DataTemplate>
                                 <nodes:NodeView
+                                    Node="{Binding}"
                                     DisplayName="{Binding NodeName}"
                                     Inputs="{Binding Inputs}"
                                     Outputs="{Binding Outputs}"
+                                    IsSelected="{Binding IsSelected}"
+                                    SelectNodeCommand="{Binding SelectNodeCommand, 
+                                    RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    StartDragCommand="{Binding StartDraggingCommand,
+                                        RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    DragCommand="{Binding DraggedCommand,
+                                        RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    EndDragCommand="{Binding EndDragCommand,
+                                        RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
                                     ResultPreview="{Binding ResultPreview}" />
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>
                         <ItemsControl.ItemContainerTheme>
                             <ControlTheme TargetType="ContentPresenter">
-                                <Setter Property="Canvas.Left" Value="{Binding Position.X}" />
-                                <Setter Property="Canvas.Top" Value="{Binding Position.Y}" />
+                                <Setter Property="Canvas.Left" Value="{Binding PositionBindable.X}" />
+                                <Setter Property="Canvas.Top" Value="{Binding PositionBindable.Y}" />
                             </ControlTheme>
                         </ItemsControl.ItemContainerTheme>
                     </ItemsControl>
-                    <ItemsControl
+                    <ItemsControl Name="PART_Connections"
                         ItemsSource="{Binding NodeGraph.Connections, RelativeSource={RelativeSource TemplatedParent}}">
                         <ItemsControl.ItemsPanel>
                             <ItemsPanelTemplate>
@@ -61,6 +71,8 @@
                         <ItemsControl.ItemTemplate>
                             <DataTemplate>
                                 <nodes:ConnectionView
+                                    InputNodePosition="{Binding InputNode.PositionBindable}"
+                                    OutputNodePosition="{Binding OutputNode.PositionBindable}"
                                     InputProperty="{Binding InputProperty}"
                                     OutputProperty="{Binding OutputProperty}" />
                             </DataTemplate>

+ 9 - 3
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeView.axaml

@@ -16,6 +16,7 @@
                         BorderThickness="{TemplateBinding BorderThickness}"
                         CornerRadius="{TemplateBinding CornerRadius}"
                         Margin="{TemplateBinding Margin}"
+                        Name="RootBorder"
                         Padding="{TemplateBinding Padding}">
                     <Grid>
                         <Grid.RowDefinitions>
@@ -30,18 +31,18 @@
                                 <ColumnDefinition Width="0.5*" />
                                 <ColumnDefinition Width="0.5*" />
                             </Grid.ColumnDefinitions>
-                            
+
                             <ItemsControl ItemsSource="{TemplateBinding Inputs}" ClipToBounds="False">
                                 <ItemsControl.ItemContainerTheme>
                                     <ControlTheme TargetType="ContentPresenter">
-                                        <Setter Property="DataContext" Value="."/>
+                                        <Setter Property="DataContext" Value="." />
                                     </ControlTheme>
                                 </ItemsControl.ItemContainerTheme>
                             </ItemsControl>
                             <ItemsControl Grid.Column="1" ItemsSource="{TemplateBinding Outputs}" ClipToBounds="False">
                                 <ItemsControl.ItemContainerTheme>
                                     <ControlTheme TargetType="ContentPresenter">
-                                        <Setter Property="DataContext" Value="."/>
+                                        <Setter Property="DataContext" Value="." />
                                     </ControlTheme>
                                 </ItemsControl.ItemContainerTheme>
                             </ItemsControl>
@@ -60,5 +61,10 @@
                 </Border>
             </ControlTemplate>
         </Setter>
+
+        <Style Selector="^:selected /template/ Border#RootBorder">
+            <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Brush}" />
+            <Setter Property="BorderThickness" Value="1" />
+        </Style>
     </ControlTheme>
 </ResourceDictionary>

+ 3 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -21,6 +21,7 @@ using PixiEditor.AvaloniaUI.Models.Position;
 using PixiEditor.AvaloniaUI.Models.Structures;
 using PixiEditor.AvaloniaUI.Models.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Document.TransformOverlays;
+using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 using PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
@@ -191,6 +192,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public ReferenceLayerViewModel ReferenceLayerViewModel { get; }
     public LineToolOverlayViewModel LineToolOverlayViewModel { get; }
     public AnimationDataViewModel AnimationDataViewModel { get; }
+    public NodeGraphManagerViewModel NodeGraphManagerViewModel { get; }
 
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers => softSelectedStructureMembers;
     private DocumentInternalParts Internals { get; }
@@ -217,7 +219,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         FolderHandlerFactory = new FolderHandlerFactory(this);
         AnimationDataViewModel = new(this, Internals);
 
-        NodeGraph = new NodeGraphViewModel(this);
+        NodeGraph = new NodeGraphViewModel(this, Internals);
 
         TransformViewModel = new(this);
         TransformViewModel.TransformMoved += (_, args) => Internals.ChangeController.TransformMovedInlet(args);

+ 20 - 4
src/PixiEditor.AvaloniaUI/ViewModels/Document/NodeGraphViewModel.cs

@@ -1,6 +1,9 @@
 using System.Collections.ObjectModel;
+using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.Numerics;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 
@@ -10,12 +13,15 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
     public ObservableCollection<INodeHandler> AllNodes { get; } = new();
     public ObservableCollection<NodeConnectionViewModel> Connections { get; } = new();
     public INodeHandler? OutputNode { get; private set; }
+    
+    private DocumentInternalParts Internals { get; }
 
-    public NodeGraphViewModel(DocumentViewModel documentViewModel)
+    public NodeGraphViewModel(DocumentViewModel documentViewModel, DocumentInternalParts internals)
     {
         DocumentViewModel = documentViewModel;
+        Internals = internals;
     }
-    
+
     public void AddNode(INodeHandler node)
     {
         if(OutputNode == null)
@@ -45,7 +51,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         
         Connections.Add(connection);
     }
-
+    
     public bool TryTraverse(Func<INodeHandler, bool> func)
     {
         if (OutputNode == null) return false;
@@ -92,5 +98,15 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
    
            finalQueue.Reverse();
            return new Queue<INodeHandler>(finalQueue);
-       } 
+       }
+
+    public void SetNodePosition(INodeHandler node, VecD newPos)
+    {
+        Internals.ActionAccumulator.AddActions(new NodePosition_Action(node.Id, newPos));
+    }
+    
+    public void EndChangeNodePosition()
+    {
+        Internals.ActionAccumulator.AddFinishedActions(new EndNodePosition_Action());
+    }
 }

+ 41 - 4
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeConnectionViewModel.cs

@@ -1,6 +1,8 @@
-namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
+using System.ComponentModel;
+using PixiEditor.AvaloniaUI.Models.Handlers;
 
-public class NodeConnectionViewModel : ViewModelBase
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
+internal class NodeConnectionViewModel : ViewModelBase
 {
     private NodeViewModel inputNode;
     private NodeViewModel outputNode;
@@ -10,13 +12,27 @@ public class NodeConnectionViewModel : ViewModelBase
     public NodeViewModel InputNode
     {
         get => inputNode;
-        set => SetProperty(ref inputNode, value);
+        set
+        {
+            if(InputNode != null)
+                InputNode.PropertyChanged -= OnInputNodePropertyChanged;
+            SetProperty(ref inputNode, value);
+            if(InputNode != null)
+                InputNode.PropertyChanged += OnInputNodePropertyChanged;
+        }
     }
 
     public NodeViewModel OutputNode
     {
         get => outputNode;
-        set => SetProperty(ref outputNode, value);
+        set
+        {
+            if(OutputNode != null)
+                OutputNode.PropertyChanged -= OnOutputNodePropertyChanged;
+            SetProperty(ref outputNode, value);
+            if(OutputNode != null)
+                OutputNode.PropertyChanged += OnOutputNodePropertyChanged;
+        }
     }
 
     public NodePropertyViewModel InputProperty
@@ -30,4 +46,25 @@ public class NodeConnectionViewModel : ViewModelBase
         get => outputProperty;
         set => SetProperty(ref outputProperty, value);
     }
+
+    public NodeConnectionViewModel()
+    {
+        
+    }
+    
+    private void OnInputNodePropertyChanged(object sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName == nameof(INodeHandler.PositionBindable))
+        {
+            OnPropertyChanged(nameof(InputProperty));
+        }
+    }
+    
+    private void OnOutputNodePropertyChanged(object sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName == nameof(INodeHandler.PositionBindable))
+        {
+            OnPropertyChanged(nameof(OutputProperty));
+        }
+    }
 }

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

@@ -1,11 +1,12 @@
 using System.Collections.ObjectModel;
 using Avalonia;
+using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
 
-public abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHandler
+internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHandler
 {
     private string propertyName;
     private string displayName;
@@ -78,7 +79,7 @@ public abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHandle
     }
 }
 
-public abstract class NodePropertyViewModel<T> : NodePropertyViewModel
+internal abstract class NodePropertyViewModel<T> : NodePropertyViewModel
 {
     private T nodeValue;
     

+ 46 - 16
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeViewModel.cs

@@ -2,34 +2,26 @@
 using Avalonia;
 using ChunkyImageLib;
 using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Structures;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
-
-public class NodeViewModel : ObservableObject, INodeHandler
+internal class NodeViewModel : ObservableObject, INodeHandler
 {
     private string nodeName;
     private VecD position;
     private ObservableRangeCollection<INodePropertyHandler> inputs = new();
     private ObservableRangeCollection<INodePropertyHandler> outputs = new();
     private Surface resultPreview;
+    private bool isSelected;
 
     protected Guid id;
 
-    public NodeViewModel()
-    {
-        
-    }
-
-    public NodeViewModel(string nodeName, Guid id, VecD position)
-    {
-        this.nodeName = nodeName;
-        this.id = id;
-        this.position = position;
-    }
-
     public Guid Id
     {
         get => id;
@@ -42,10 +34,18 @@ public class NodeViewModel : ObservableObject, INodeHandler
         set => SetProperty(ref nodeName, value);
     }
 
-    public VecD Position
+    public VecD PositionBindable
     {
         get => position;
-        set => SetProperty(ref position, value);
+        set
+        {
+            if (!Document.UpdateableChangeActive)
+            {
+                Internals.ActionAccumulator.AddFinishedActions(
+                    new NodePosition_Action(Id, value),
+                    new EndNodePosition_Action());
+            }
+        }
     }
 
     public ObservableRangeCollection<INodePropertyHandler> Inputs
@@ -65,6 +65,36 @@ public class NodeViewModel : ObservableObject, INodeHandler
         get => resultPreview;
         set => SetProperty(ref resultPreview, value);
     }
+    
+    public bool IsSelected
+    {
+        get => isSelected;
+        set => SetProperty(ref isSelected, value);
+    }
+
+    internal DocumentViewModel Document { get; init; }
+    internal DocumentInternalParts Internals { get; init; }
+    
+    
+    public NodeViewModel()
+    {
+        
+    }
+
+    public NodeViewModel(string nodeName, Guid id, VecD position, DocumentViewModel document, DocumentInternalParts internals)
+    {
+        this.nodeName = nodeName;
+        this.id = id;
+        this.position = position;
+        Document = document;
+        Internals = internals;
+    }
+    
+    public void SetPosition(VecD newPosition)
+    {
+        position = newPosition;
+        OnPropertyChanged(nameof(PositionBindable));
+    }
 
     public void TraverseBackwards(Func<INodeHandler, bool> func)
     {

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

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
 
-public class GenericPropertyViewModel : NodePropertyViewModel
+internal class GenericPropertyViewModel : NodePropertyViewModel
 {
     public GenericPropertyViewModel(INodeHandler node) : base(node)
     {

+ 24 - 0
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs

@@ -0,0 +1,24 @@
+using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
+using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
+
+internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
+{
+    public NodeGraphManagerViewModel(ViewModelMain owner) : base(owner)
+    {
+    }
+
+    [Command.Internal("PixiEditor.NodeGraph.ChangeNodePos")]
+    public void ChangeNodePos((INodeHandler node, VecD newPos) args)
+    {
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.SetNodePosition(args.node, args.newPos);
+    }
+    
+    [Command.Internal("PixiEditor.NodeGraph.EndChangeNodePos")]
+    public void EndChangeNodePos()
+    {
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.EndChangeNodePosition();
+    }
+}

+ 4 - 0
src/PixiEditor.AvaloniaUI/ViewModels/ViewModelMain.cs

@@ -78,6 +78,8 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
 
     public MenuBarViewModel MenuBarViewModel { get; set; }
     public AnimationsViewModel AnimationsSubViewModel { get; set; }
+    
+    public NodeGraphManagerViewModel NodeGraphManager { get; set; }
 
     public IPreferences Preferences { get; set; }
     public ILocalizationProvider LocalizationProvider { get; set; }
@@ -168,6 +170,8 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         
         AnimationsSubViewModel = services.GetService<AnimationsViewModel>();
         
+        NodeGraphManager = services.GetService<NodeGraphManagerViewModel>();
+        
         ExtensionsSubViewModel = services.GetService<ExtensionsViewModel>(); // Must be last
 
         DocumentManagerSubViewModel.ActiveDocumentChanged += OnActiveDocumentChanged;

+ 5 - 1
src/PixiEditor.AvaloniaUI/Views/Dock/NodeGraphDockView.axaml

@@ -4,11 +4,15 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:nodes="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes"
              xmlns:dock="clr-namespace:PixiEditor.AvaloniaUI.ViewModels.Dock"
+             xmlns:xaml="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.XAML"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:DataType="dock:NodeGraphDockViewModel"
              x:Class="PixiEditor.AvaloniaUI.Views.Dock.NodeGraphDockView">
     <Design.DataContext>
         <dock:NodeGraphDockViewModel/>
     </Design.DataContext>
-    <nodes:NodeGraphView NodeGraph="{Binding DocumentManagerSubViewModel.ActiveDocument.NodeGraph}"/>
+    <nodes:NodeGraphView 
+        ChangeNodePosCommand="{xaml:Command PixiEditor.NodeGraph.ChangeNodePos, UseProvided=True}"
+        EndChangeNodePosCommand="{xaml:Command PixiEditor.NodeGraph.EndChangeNodePos, UseProvided=True}"
+        NodeGraph="{Binding DocumentManagerSubViewModel.ActiveDocument.NodeGraph}"/>
 </UserControl>

+ 35 - 2
src/PixiEditor.AvaloniaUI/Views/Nodes/ConnectionView.cs

@@ -1,14 +1,18 @@
-using Avalonia;
+using System.ComponentModel;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Primitives;
 using Avalonia.Data;
 using Avalonia.Threading;
 using Avalonia.VisualTree;
+using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
+using PixiEditor.Numerics;
+using Point = Avalonia.Point;
 
 namespace PixiEditor.AvaloniaUI.Views.Nodes;
 
-public class ConnectionView : TemplatedControl
+internal class ConnectionView : TemplatedControl
 {
     public static readonly StyledProperty<NodePropertyViewModel> InputPropertyProperty =
         AvaloniaProperty.Register<ConnectionView, NodePropertyViewModel>(
@@ -24,6 +28,9 @@ public class ConnectionView : TemplatedControl
     public static readonly StyledProperty<Point> EndPointProperty = AvaloniaProperty.Register<ConnectionView, Point>(
         nameof(EndPoint));
 
+    public static readonly StyledProperty<VecD> InputNodePositionProperty = AvaloniaProperty.Register<ConnectionView, VecD>("InputNodePosition");
+    public static readonly StyledProperty<VecD> OutputNodePositionProperty = AvaloniaProperty.Register<ConnectionView, VecD>("OutputNodePosition");
+
     public Point StartPoint
     {
         get => GetValue(StartPointProperty);
@@ -48,12 +55,26 @@ public class ConnectionView : TemplatedControl
         set => SetValue(OutputPropertyProperty, value);
     }
 
+    public VecD InputNodePosition
+    {
+        get { return (VecD)GetValue(InputNodePositionProperty); }
+        set { SetValue(InputNodePositionProperty, value); }
+    }
+
+    public VecD OutputNodePosition
+    {
+        get { return (VecD)GetValue(OutputNodePositionProperty); }
+        set { SetValue(OutputNodePositionProperty, value); }
+    }
+
 
     static ConnectionView()
     {
         AffectsRender<ConnectionView>(InputPropertyProperty, OutputPropertyProperty);
         InputPropertyProperty.Changed.Subscribe(OnInputPropertyChanged);
         OutputPropertyProperty.Changed.Subscribe(OnOutputPropertyChanged);
+        InputNodePositionProperty.Changed.Subscribe(OnInputNodePositionChanged);
+        OutputNodePositionProperty.Changed.Subscribe(OnOutputNodePositionChanged);
     }
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -104,4 +125,16 @@ public class ConnectionView : TemplatedControl
         ConnectionView connectionView = args.Sender as ConnectionView;
         connectionView.EndPoint = connectionView.CalculateSocketPoint(args.NewValue);
     }
+    
+    private static void OnInputNodePositionChanged(AvaloniaPropertyChangedEventArgs<VecD> args)
+    {
+        ConnectionView connectionView = args.Sender as ConnectionView;
+        connectionView.StartPoint = connectionView.CalculateSocketPoint(connectionView.InputProperty);
+    }
+    
+    private static void OnOutputNodePositionChanged(AvaloniaPropertyChangedEventArgs<VecD> args)
+    {
+        ConnectionView connectionView = args.Sender as ConnectionView;
+        connectionView.EndPoint = connectionView.CalculateSocketPoint(connectionView.OutputProperty);
+    }
 }

+ 160 - 6
src/PixiEditor.AvaloniaUI/Views/Nodes/NodeGraphView.cs

@@ -1,12 +1,86 @@
-using Avalonia;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using CommunityToolkit.Mvvm.Input;
+using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.AvaloniaUI.ViewModels.Nodes;
+using PixiEditor.Numerics;
+using Point = Avalonia.Point;
 
 namespace PixiEditor.AvaloniaUI.Views.Nodes;
 
-public class NodeGraphView : Zoombox.Zoombox
+internal class NodeGraphView : Zoombox.Zoombox
 {
-    public static readonly StyledProperty<INodeGraphHandler> NodeGraphProperty = AvaloniaProperty.Register<NodeGraphView, INodeGraphHandler>(
-        nameof(NodeGraph));
+    public static readonly StyledProperty<INodeGraphHandler> NodeGraphProperty =
+        AvaloniaProperty.Register<NodeGraphView, INodeGraphHandler>(
+            nameof(NodeGraph));
+
+    public static readonly StyledProperty<ICommand> SelectNodeCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>(
+            nameof(SelectNodeCommand));
+
+    public static readonly StyledProperty<ICommand> StartDraggingCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>(
+            nameof(StartDraggingCommand));
+
+    public static readonly StyledProperty<ICommand> DraggedCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>(
+            nameof(DraggedCommand));
+
+    public static readonly StyledProperty<ICommand> EndDragCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>(
+            nameof(EndDragCommand));
+
+    public static readonly StyledProperty<ICommand> ChangeNodePosCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>(
+            nameof(ChangeNodePosCommand));
+
+    public static readonly StyledProperty<ICommand> EndChangeNodePosCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>(
+            nameof(EndChangeNodePosCommand));
+
+    public ICommand EndChangeNodePosCommand
+    {
+        get => GetValue(EndChangeNodePosCommandProperty);
+        set => SetValue(EndChangeNodePosCommandProperty, value);
+    }
+
+    public ICommand ChangeNodePosCommand
+    {
+        get => GetValue(ChangeNodePosCommandProperty);
+        set => SetValue(ChangeNodePosCommandProperty, value);
+    }
+
+    public ICommand EndDragCommand
+    {
+        get => GetValue(EndDragCommandProperty);
+        set => SetValue(EndDragCommandProperty, value);
+    }
+
+    public ICommand DraggedCommand
+    {
+        get => GetValue(DraggedCommandProperty);
+        set => SetValue(DraggedCommandProperty, value);
+    }
+
+    public ICommand StartDraggingCommand
+    {
+        get => GetValue(StartDraggingCommandProperty);
+        set => SetValue(StartDraggingCommandProperty, value);
+    }
+
+    public ICommand SelectNodeCommand
+    {
+        get => GetValue(SelectNodeCommandProperty);
+        set => SetValue(SelectNodeCommandProperty, value);
+    }
 
     public INodeGraphHandler NodeGraph
     {
@@ -14,11 +88,91 @@ public class NodeGraphView : Zoombox.Zoombox
         set => SetValue(NodeGraphProperty, value);
     }
 
+    public List<INodeHandler> SelectedNodes => NodeGraph != null
+        ? NodeGraph.AllNodes.Where(x => x.IsSelected).ToList()
+        : new List<INodeHandler>();
+
     protected override Type StyleKeyOverride => typeof(NodeGraphView);
 
+    private bool isDraggingNodes;
+    private VecD clickPointOffset;
+
+    private List<VecD> initialNodePositions;
+
     public NodeGraphView()
     {
-        
+        SelectNodeCommand = new RelayCommand<PointerPressedEventArgs>(SelectNode);
+        StartDraggingCommand = new RelayCommand<PointerPressedEventArgs>(StartDragging);
+        DraggedCommand = new RelayCommand<PointerEventArgs>(Dragged);
+        EndDragCommand = new RelayCommand<PointerCaptureLostEventArgs>(EndDrag);
     }
-}
 
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+    }
+
+    protected override void OnPointerPressed(PointerPressedEventArgs e)
+    {
+        base.OnPointerPressed(e);
+
+        if (e.GetMouseButton(this) == MouseButton.Left)
+            ClearSelection();
+    }
+
+    private void StartDragging(PointerPressedEventArgs e)
+    {
+        if (e.GetMouseButton(this) == MouseButton.Left)
+        {
+            isDraggingNodes = true;
+            Point pt = e.GetPosition(this);
+            clickPointOffset = ToZoomboxSpace(new VecD(pt.X, pt.Y));
+            initialNodePositions = SelectedNodes.Select(x => x.PositionBindable).ToList();
+        }
+    }
+
+    private void Dragged(PointerEventArgs e)
+    {
+        if (isDraggingNodes)
+        {
+            Point pos = e.GetPosition(this);
+            VecD currentPoint = ToZoomboxSpace(new VecD(pos.X, pos.Y));
+            VecD delta = currentPoint - clickPointOffset;
+            foreach (var node in SelectedNodes)
+            {
+                ChangeNodePosCommand?.Execute((node, initialNodePositions[SelectedNodes.IndexOf(node)] + delta));
+            }
+        }
+    }
+
+    private void EndDrag(PointerCaptureLostEventArgs e)
+    {
+        isDraggingNodes = false;
+        EndChangeNodePosCommand?.Execute(null);
+    }
+
+    private void SelectNode(PointerPressedEventArgs e)
+    {
+        NodeViewModel viewModel = (NodeViewModel)e.Source;
+
+        if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
+        {
+            if (SelectedNodes.Contains(viewModel)) return;
+        }
+        // TODO: Add shift
+        else if (!SelectedNodes.Contains(viewModel))
+        {
+            ClearSelection();
+        }
+
+        viewModel.IsSelected = true;
+    }
+
+    private void ClearSelection()
+    {
+        foreach (var node in SelectedNodes)
+        {
+            node.IsSelected = false;
+        }
+    }
+}

+ 125 - 9
src/PixiEditor.AvaloniaUI/Views/Nodes/NodeView.cs

@@ -1,9 +1,13 @@
 using System.Collections.ObjectModel;
+using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
+using Avalonia.Input;
 using Avalonia.VisualTree;
 using ChunkyImageLib;
+using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Structures;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
@@ -11,24 +15,50 @@ using PixiEditor.AvaloniaUI.Views.Nodes.Properties;
 
 namespace PixiEditor.AvaloniaUI.Views.Nodes;
 
+[PseudoClasses(":selected")]
 public class NodeView : TemplatedControl
 {
+    public static readonly StyledProperty<INodeHandler> NodeProperty = AvaloniaProperty.Register<NodeView, INodeHandler>(
+        nameof(Node));
+    
     public static readonly StyledProperty<string> DisplayNameProperty = AvaloniaProperty.Register<NodeView, string>(
         nameof(DisplayName), "Node");
 
-    public static readonly StyledProperty<ObservableRangeCollection<INodePropertyHandler>> InputsProperty = AvaloniaProperty.Register<NodeView, ObservableRangeCollection<INodePropertyHandler>>(
-        nameof(Inputs));
-    
-    public static readonly StyledProperty<ObservableRangeCollection<INodePropertyHandler>> OutputsProperty = AvaloniaProperty.Register<NodeView, ObservableRangeCollection<INodePropertyHandler>>(
-        nameof(Outputs));
+    public static readonly StyledProperty<ObservableRangeCollection<INodePropertyHandler>> InputsProperty =
+        AvaloniaProperty.Register<NodeView, ObservableRangeCollection<INodePropertyHandler>>(
+            nameof(Inputs));
+
+    public static readonly StyledProperty<ObservableRangeCollection<INodePropertyHandler>> OutputsProperty =
+        AvaloniaProperty.Register<NodeView, ObservableRangeCollection<INodePropertyHandler>>(
+            nameof(Outputs));
 
     public static readonly StyledProperty<Surface> ResultPreviewProperty = AvaloniaProperty.Register<NodeView, Surface>(
         nameof(ResultPreview));
 
+    public static readonly StyledProperty<bool> IsSelectedProperty = AvaloniaProperty.Register<NodeView, bool>(
+        nameof(IsSelected));
+
+    public static readonly StyledProperty<ICommand> SelectNodeCommandProperty = AvaloniaProperty.Register<NodeView, ICommand>("SelectNodeCommand");
+    public static readonly StyledProperty<ICommand> StartDragCommandProperty = AvaloniaProperty.Register<NodeView, ICommand>("StartDragCommand");
+    public static readonly StyledProperty<ICommand> DragCommandProperty = AvaloniaProperty.Register<NodeView, ICommand>("DragCommand");
+    public static readonly StyledProperty<ICommand> EndDragCommandProperty = AvaloniaProperty.Register<NodeView, ICommand>("EndDragCommand");
+
+    public INodeHandler Node
+    {
+        get => GetValue(NodeProperty);
+        set => SetValue(NodeProperty, value);
+    }
+    
+    public bool IsSelected
+    {
+        get => GetValue(IsSelectedProperty);
+        set => SetValue(IsSelectedProperty, value);
+    }
+
     public Surface ResultPreview
     {
-        get => GetValue( ResultPreviewProperty);
-        set => SetValue( ResultPreviewProperty, value);
+        get => GetValue(ResultPreviewProperty);
+        set => SetValue(ResultPreviewProperty, value);
     }
 
     public ObservableRangeCollection<INodePropertyHandler> Outputs
@@ -49,10 +79,88 @@ public class NodeView : TemplatedControl
         set => SetValue(DisplayNameProperty, value);
     }
 
-    public Point GetSocketPoint(INodePropertyHandler property, Canvas canvas)
+    public ICommand SelectNodeCommand
+    {
+        get { return (ICommand)GetValue(SelectNodeCommandProperty); }
+        set { SetValue(SelectNodeCommandProperty, value); }
+    }
+
+    public ICommand StartDragCommand
+    {
+        get { return (ICommand)GetValue(StartDragCommandProperty); }
+        set { SetValue(StartDragCommandProperty, value); }
+    }
+
+    public ICommand DragCommand
+    {
+        get { return (ICommand)GetValue(DragCommandProperty); }
+        set { SetValue(DragCommandProperty, value); }
+    }
+
+    public ICommand EndDragCommand
     {
-        NodePropertyView propertyView = this.GetVisualDescendants().OfType<NodePropertyView>().FirstOrDefault(x => x.DataContext == property);
+        get { return (ICommand)GetValue(EndDragCommandProperty); }
+        set { SetValue(EndDragCommandProperty, value); }
+    }
+
+    static NodeView()
+    {
+        IsSelectedProperty.Changed.Subscribe(NodeSelectionChanged);
+    }
+
+    protected override void OnPointerPressed(PointerPressedEventArgs e)
+    {
+        base.OnPointerPressed(e);
+        if(e.GetMouseButton(this) != MouseButton.Left)
+            return;
         
+        var originalSource = e.Source;
+        e.Source = Node; 
+        if (SelectNodeCommand != null && SelectNodeCommand.CanExecute(e))
+        {
+            SelectNodeCommand.Execute(e);
+        }
+        
+        if(StartDragCommand != null && StartDragCommand.CanExecute(e))
+        {
+            e.Pointer.Capture(this);
+            StartDragCommand.Execute(e);
+        }
+        
+        e.Source = originalSource;
+        e.Handled = true;
+    }
+    
+    protected override void OnPointerMoved(PointerEventArgs e)
+    {
+        base.OnPointerMoved(e);
+        if(e.Pointer.Captured != this)
+            return;
+        
+        if (DragCommand != null && DragCommand.CanExecute(e))
+        {
+            DragCommand.Execute(e);
+        }
+    }
+
+    protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+    {
+        var originalSource = e.Source;
+        e.Source = Node; 
+        if (EndDragCommand != null && EndDragCommand.CanExecute(e))
+        {
+            EndDragCommand.Execute(e);
+        }
+        
+        e.Source = originalSource;
+        e.Handled = true;
+    }
+
+    public Point GetSocketPoint(INodePropertyHandler property, Canvas canvas)
+    {
+        NodePropertyView propertyView = this.GetVisualDescendants().OfType<NodePropertyView>()
+            .FirstOrDefault(x => x.DataContext == property);
+
         if (propertyView is null)
         {
             return default;
@@ -60,4 +168,12 @@ public class NodeView : TemplatedControl
 
         return propertyView.GetSocketPoint(property.IsInput, canvas);
     }
+
+    private static void NodeSelectionChanged(AvaloniaPropertyChangedEventArgs<bool> e)
+    {
+        if (e.Sender is NodeView nodeView)
+        {
+            nodeView.PseudoClasses.Set(":selected", e.NewValue.Value);
+        }
+    }
 }

+ 5 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodePosition_ChangeInfo.cs

@@ -0,0 +1,5 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record NodePosition_ChangeInfo(Guid NodeId, VecD NewPosition) : IChangeInfo;

+ 1 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs

@@ -19,7 +19,7 @@ public abstract record class CreateStructureMember_ChangeInfo(
     bool MaskIsVisible,
     ImmutableArray<NodePropertyInfo> InputProperties,
     ImmutableArray<NodePropertyInfo> OutputProperties
-) : CreateNode_ChangeInfo(Name, new VecD(-100, 0), Id, InputProperties, OutputProperties)
+) : CreateNode_ChangeInfo(Name, new VecD(0, 0), Id, InputProperties, OutputProperties)
 {
     public ImmutableArray<NodePropertyInfo> InputProperties { get; init; } = InputProperties;
     public ImmutableArray<NodePropertyInfo> OutputProperties { get; init; } = OutputProperties;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -216,7 +216,7 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         return NodeGraph.Nodes.FirstOrDefault(x => x.Id == guid);
     }
 
-    public T FindNode<T>(Guid guid) where T : Node
+    public T? FindNode<T>(Guid guid) where T : Node
     {
         return NodeGraph.Nodes.FirstOrDefault(x => x.Id == guid && x is T) as T;
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs

@@ -29,7 +29,7 @@ internal class CreateNode_Change : Change
             id = Guid.NewGuid();
         
         Node node = (Node)Activator.CreateInstance(nodeType);
-        node.Position = new VecD(100, 100);
+        node.Position = new VecD(0, 0);
         node.Id = id;
         target.NodeGraph.AddNode(node);
         ignoreInUndo = false;

+ 65 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodePosition_UpdateableChange.cs

@@ -0,0 +1,65 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class NodePosition_UpdateableChange : UpdateableChange
+{
+    public Guid NodeId { get; }
+    public VecD NewPosition { get; private set; } 
+    
+    private VecD originalPosition;
+    
+    [GenerateUpdateableChangeActions]
+    public NodePosition_UpdateableChange(Guid nodeId, VecD newPosition)
+    {
+        NodeId = nodeId;
+        NewPosition = newPosition;
+    }
+
+    [UpdateChangeMethod]
+    public void Update(VecD newPosition)
+    {
+        NewPosition = newPosition;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        var node = target.FindNode<Node>(NodeId);
+        if (node == null)
+        {
+            return false;
+        }
+
+        originalPosition = node.Position;
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        var node = target.FindNode<Node>(NodeId);
+        node.Position = NewPosition;
+        return new NodePosition_ChangeInfo(NodeId, NewPosition);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+        var node = target.FindNode<Node>(NodeId);
+        node.Position = NewPosition;
+        return new NodePosition_ChangeInfo(NodeId, NewPosition);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var node = target.FindNode<Node>(NodeId);
+        node.Position = originalPosition;
+        return new NodePosition_ChangeInfo(NodeId, originalPosition);
+    }
+
+    public override bool IsMergeableWith(Change other)
+    {
+        return other is NodePosition_UpdateableChange change && change.NodeId == NodeId;
+    }
+}

+ 3 - 3
src/PixiEditor.ChangeableDocument/Rendering/DocumentEvaluator.cs

@@ -13,8 +13,8 @@ public static class DocumentEvaluator
         {
             RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
 
-            ChunkyImage? evaulated = graph.Execute(frame);
-            if (evaulated is null)
+            ChunkyImage? evaluated = graph.Execute(frame);
+            if (evaluated is null)
             {
                 return new EmptyChunk();
             }
@@ -29,7 +29,7 @@ public static class DocumentEvaluator
                 chunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
             }
 
-            evaulated.DrawMostUpToDateChunkOn(chunkPos, resolution, chunk.Surface.DrawingSurface, VecI.Zero,
+            evaluated.DrawMostUpToDateChunkOn(chunkPos, resolution, chunk.Surface.DrawingSurface, VecI.Zero,
                 context.ReplacingPaintWithOpacity);
             
             chunk.Surface.DrawingSurface.Canvas.Restore();