Browse Source

Connections

flabbet 1 year ago
parent
commit
36e7027835
29 changed files with 212 additions and 90 deletions
  1. 34 6
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs
  2. 1 0
      src/PixiEditor.AvaloniaUI/Models/Handlers/INodeGraphHandler.cs
  3. 2 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/INodePropertyHandler.cs
  4. 2 2
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodePropertyViewTemplate.axaml
  5. 1 1
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml
  6. 5 0
      src/PixiEditor.AvaloniaUI/ViewModels/Document/FolderViewModel.cs
  7. 11 6
      src/PixiEditor.AvaloniaUI/ViewModels/Document/LayerViewModel.cs
  8. 11 0
      src/PixiEditor.AvaloniaUI/ViewModels/Document/NodeGraphViewModel.cs
  9. 6 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/StructureMemberViewModel.cs
  10. 11 4
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodePropertyViewModel.cs
  11. 11 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeViewModel.cs
  12. 6 1
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodePropertyView.cs
  13. 9 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodeSocket.cs
  14. 7 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/ConnectProperty_ChangeInfo.cs
  15. 14 2
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs
  16. 2 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodePropertyInfo.cs
  17. 9 2
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs
  18. 9 4
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs
  19. 5 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs
  20. 8 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  21. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/INodeProperty.cs
  22. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  23. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  24. 9 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  25. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  26. 8 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  27. 7 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/OutputProperty.cs
  28. 2 7
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs
  29. 15 22
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

+ 34 - 6
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -1,5 +1,4 @@
 using System.Collections.Immutable;
-using System.Collections.ObjectModel;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.AvaloniaUI.Helpers;
@@ -8,7 +7,7 @@ using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Layers;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
-using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
@@ -54,6 +53,15 @@ internal class DocumentUpdater
         switch (arbitraryInfo)
         {
             case CreateStructureMember_ChangeInfo info:
+                if (info is CreateLayer_ChangeInfo layerChangeInfo)
+                {
+                    ProcessCreateNode<LayerViewModel>(info);
+                }
+                else if (info is CreateFolder_ChangeInfo folderChangeInfo)
+                {
+                    ProcessCreateNode<FolderViewModel>(info);
+                }
+
                 ProcessCreateStructureMember(info);
                 break;
             case DeleteStructureMember_ChangeInfo info:
@@ -156,11 +164,14 @@ internal class DocumentUpdater
                 ClearSelectedKeyFrames(info);
                 break;
             case CreateNode_ChangeInfo info:
-                ProcessCreateNode(info);
+                ProcessCreateNode<NodeViewModel>(info);
                 break;
             case DeleteNode_ChangeInfo info:
                 ProcessDeleteNode(info);
                 break;
+            case ConnectProperty_ChangeInfo info:
+                ProcessConnectProperty(info);
+                break;
         }
     }
 
@@ -468,9 +479,10 @@ internal class DocumentUpdater
         doc.AnimationHandler.ClearSelectedKeyFrames();
     }
     
-    private void ProcessCreateNode(CreateNode_ChangeInfo info)
+    private void ProcessCreateNode<T>(CreateNode_ChangeInfo info) where T : NodeViewModel, new()
     {
-        NodeViewModel node = new NodeViewModel(info.NodeName, info.Id, info.Position);
+        T node = new T() { NodeName = info.NodeName, Id = info.Id, Position = info.Position };
+
         List<INodePropertyHandler> inputs = CreateProperties(info.Inputs, node, true);
         List<INodePropertyHandler> outputs = CreateProperties(info.Outputs, node, false);
         node.Inputs.AddRange(inputs);
@@ -484,7 +496,8 @@ internal class DocumentUpdater
         foreach (var input in source)
         {
             var prop = NodePropertyViewModel.CreateFromType(input.ValueType, node);
-            prop.Name = input.Name;
+            prop.DisplayName = input.DisplayName;
+            prop.PropertyName = input.PropertyName;
             prop.IsInput = isInput;
             inputs.Add(prop);
         }
@@ -496,4 +509,19 @@ internal class DocumentUpdater
     {
         doc.NodeGraphHandler.RemoveNode(info.Id);
     }
+    
+    private void ProcessConnectProperty(ConnectProperty_ChangeInfo info)
+    {
+        NodeViewModel outputNode = doc.StructureHelper.FindNode<NodeViewModel>(info.SourceNodeId);
+        NodeViewModel inputNode = doc.StructureHelper.FindNode<NodeViewModel>(info.TargetNodeId);
+        NodeConnectionViewModel connection = new NodeConnectionViewModel()
+        {
+            InputNode = inputNode,
+            OutputNode = outputNode,
+            InputProperty = inputNode.FindInputProperty(info.TargetProperty),
+            OutputProperty = outputNode.FindOutputProperty(info.SourceProperty)
+        };
+        
+        doc.NodeGraphHandler.SetConnection(connection);
+    }
 }

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

@@ -11,4 +11,5 @@ public interface INodeGraphHandler
    public bool TryTraverse(Func<INodeHandler, bool> func);
    public void AddNode(INodeHandler node);
    public void RemoveNode(Guid nodeId);
+   public void SetConnection(NodeConnectionViewModel connection);
 }

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

@@ -4,7 +4,8 @@ namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
 public interface INodePropertyHandler
 {
-    public string Name { get; set; }
+    public string PropertyName { get; set; }
+    public string DisplayName { get; set; }
     public object Value { get; set; }
     public bool IsInput { get; }
     public INodePropertyHandler? ConnectedOutput { get; set; }

+ 2 - 2
src/PixiEditor.AvaloniaUI/Styles/Templates/NodePropertyViewTemplate.axaml

@@ -9,7 +9,7 @@
                 <Grid Margin="-10, 0">
                     <Grid.ColumnDefinitions>10*, *, 10*</Grid.ColumnDefinitions>
                     <properties:NodeSocket Name="PART_InputSocket"
-                                           Label="{Binding DataContext.Name, RelativeSource={RelativeSource TemplatedParent}}"
+                                           Label="{Binding DataContext.DisplayName, RelativeSource={RelativeSource TemplatedParent}}"
                                            IsVisible="{Binding DataContext.IsInput, 
                     RelativeSource={RelativeSource TemplatedParent}}">
                         <properties:NodeSocket.IsInput>
@@ -18,7 +18,7 @@
                     </properties:NodeSocket>
                     <ContentPresenter Grid.Column="1" Content="{TemplateBinding Content}" />
                     <properties:NodeSocket Grid.Column="2" Name="PART_OutputSocket"
-                                           Label="{Binding DataContext.Name, RelativeSource={RelativeSource TemplatedParent}}"
+                                           Label="{Binding DataContext.DisplayName, RelativeSource={RelativeSource TemplatedParent}}"
                                            IsVisible="{Binding !DataContext.IsInput,
                     RelativeSource={RelativeSource TemplatedParent}}">
                         <properties:NodeSocket.IsInput>

+ 1 - 1
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml

@@ -7,7 +7,7 @@
             <ControlTemplate>
                 <StackPanel Orientation="Horizontal">
                     <TextBlock Text="{TemplateBinding Label}" IsVisible="{Binding !IsInput, RelativeSource={RelativeSource TemplatedParent}}"/>
-                    <Ellipse Width="10" Height="10" Fill="Red" />
+                    <Ellipse Width="10" Height="10" Fill="Red" Name="PART_ConnectPort"/>
                     <TextBlock Text="{TemplateBinding Label}" IsVisible="{Binding IsInput, RelativeSource={RelativeSource TemplatedParent}}"/>
                 </StackPanel>
             </ControlTemplate>

+ 5 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/FolderViewModel.cs

@@ -6,6 +6,11 @@ namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 #nullable enable
 internal class FolderViewModel : StructureMemberViewModel, IFolderHandler
 {
+    public FolderViewModel()
+    {
+        
+    }
+    
     public ObservableCollection<IStructureMemberHandler> Children { get; } = new();
     public FolderViewModel(DocumentViewModel doc, DocumentInternalParts internals, Guid id) : base(doc, internals, id) { }
 }

+ 11 - 6
src/PixiEditor.AvaloniaUI/ViewModels/Document/LayerViewModel.cs

@@ -6,6 +6,11 @@ namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 #nullable enable
 internal class LayerViewModel : StructureMemberViewModel, ILayerHandler
 {
+    public LayerViewModel()
+    {
+        
+    }
+    
     bool lockTransparency;
     public void SetLockTransparency(bool lockTransparency)
     {
@@ -35,13 +40,13 @@ internal class LayerViewModel : StructureMemberViewModel, ILayerHandler
         }
     }
 
-    public LayerViewModel(DocumentViewModel doc, DocumentInternalParts internals, Guid id) : base(doc, internals, id)
+    public override void SetName(string name)
     {
+        base.SetName(name);
         NodeName = NameBindable;
-        PropertyChanged += (sender, args) =>
-        {
-            if (args.PropertyName == nameof(NameBindable))
-                NodeName = NameBindable;
-        };
+    }
+
+    public LayerViewModel(DocumentViewModel doc, DocumentInternalParts internals, Guid id) : base(doc, internals, id)
+    {
     }
 }

+ 11 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/NodeGraphViewModel.cs

@@ -35,6 +35,17 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         }
     }
 
+    public void SetConnection(NodeConnectionViewModel connection)
+    {
+        var existingInputConnection = Connections.FirstOrDefault(x => x.InputProperty == connection.InputProperty);
+        if (existingInputConnection != null)
+        {
+            Connections.Remove(existingInputConnection);
+        }
+        
+        Connections.Add(connection);
+    }
+
     public bool TryTraverse(Func<INodeHandler, bool> func)
     {
         if (OutputNode == null) return false;

+ 6 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/StructureMemberViewModel.cs

@@ -23,7 +23,12 @@ internal abstract class StructureMemberViewModel : NodeViewModel, IStructureMemb
 
     private string name = "";
 
-    public void SetName(string name)
+    public StructureMemberViewModel()
+    {
+        
+    }
+
+    public virtual void SetName(string name)
     {
         this.name = name;
         OnPropertyChanged(nameof(NameBindable));

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

@@ -7,7 +7,8 @@ namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
 
 public abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHandler
 {
-    private string name;
+    private string propertyName;
+    private string displayName;
     private object value;
     private INodeHandler node;
     private bool isInput;
@@ -15,10 +16,10 @@ public abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHandle
     private ObservableCollection<INodePropertyHandler> connectedInputs = new();
     private INodePropertyHandler? connectedOutput;
     
-    public string Name
+    public string DisplayName
     {
-        get => name;
-        set => SetProperty(ref name, value);
+        get => displayName;
+        set => SetProperty(ref displayName, value);
     }
     
     public object Value
@@ -51,6 +52,12 @@ public abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHandle
         set => SetProperty(ref node, value);
     }
 
+    public string PropertyName
+    {
+        get => propertyName;
+        set => SetProperty(ref propertyName, value);
+    }
+
     public NodePropertyViewModel(INodeHandler node)
     {
         Node = node;

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

@@ -33,6 +33,7 @@ public class NodeViewModel : ObservableObject, INodeHandler
     public Guid Id
     {
         get => id;
+        init => id = value;
     }
 
     public string NodeName
@@ -124,4 +125,14 @@ public class NodeViewModel : ObservableObject, INodeHandler
             }
         }
     }
+
+    public NodePropertyViewModel FindInputProperty(string propName)
+    {
+        return Inputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel;
+    }
+    
+    public NodePropertyViewModel FindOutputProperty(string propName)
+    {
+        return Outputs.FirstOrDefault(x => x.PropertyName == propName) as NodePropertyViewModel;
+    }
 }

+ 6 - 1
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodePropertyView.cs

@@ -35,7 +35,12 @@ public abstract class NodePropertyView : UserControl
             return default;
         }
         
-        Point? point = socket.TranslatePoint(new Point(socket.Bounds.Width / 2, socket.Bounds.Height / 2), canvas);
+        if(socket.ConnectPort is null)
+        {
+            return default;
+        }
+        
+        Point? point = socket.ConnectPort.TranslatePoint(new Point(socket.ConnectPort.Bounds.Width / 2, socket.ConnectPort.Bounds.Height / 2), canvas);
         
         return point ?? default;
     }

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

@@ -1,4 +1,5 @@
 using Avalonia;
+using Avalonia.Controls;
 using Avalonia.Controls.Primitives;
 
 namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
@@ -19,5 +20,13 @@ public class NodeSocket : TemplatedControl
         get { return (string)GetValue(LabelProperty); }
         set { SetValue(LabelProperty, value); }
     }
+    
+    public Control ConnectPort { get; set; }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        ConnectPort = e.NameScope.Find<Control>("PART_ConnectPort");
+    }
 }
 

+ 7 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/ConnectProperty_ChangeInfo.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record ConnectProperty_ChangeInfo(
+    Guid SourceNodeId,
+    Guid TargetNodeId,
+    string SourceProperty,
+    string TargetProperty) : IChangeInfo;

+ 14 - 2
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs

@@ -1,9 +1,21 @@
 using System.Collections;
 using System.Collections.Immutable;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 
-public record CreateNode_ChangeInfo(string NodeName, VecD Position, Guid Id, 
+public record CreateNode_ChangeInfo(
+    string NodeName,
+    VecD Position,
+    Guid Id,
     ImmutableArray<NodePropertyInfo> Inputs,
-    ImmutableArray<NodePropertyInfo> Outputs) : IChangeInfo;
+    ImmutableArray<NodePropertyInfo> Outputs) : IChangeInfo
+{
+    public static ImmutableArray<NodePropertyInfo> CreatePropertyInfos(IEnumerable<INodeProperty> properties,
+        bool isInput, Guid guid)
+    {
+        return properties.Select(p => new NodePropertyInfo(p.InternalPropertyName, p.DisplayName, p.ValueType, isInput, guid))
+            .ToImmutableArray();
+    }
+}

+ 2 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodePropertyInfo.cs

@@ -1,7 +1,8 @@
 namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 
 public record NodePropertyInfo(
-    string Name,
+    string PropertyName,
+    string DisplayName,
     Type ValueType,
     bool IsInput,
     Guid NodeId);

+ 9 - 2
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs

@@ -1,8 +1,10 @@
 using System.Collections.Immutable;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+
 public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
 {
     public CreateFolder_ChangeInfo(
@@ -15,7 +17,11 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
         BlendMode blendMode,
         Guid guidValue,
         bool hasMask,
-        bool maskIsVisible) : base(parentGuid, index, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask, maskIsVisible)
+        bool maskIsVisible,
+        ImmutableArray<NodePropertyInfo> Inputs,
+        ImmutableArray<NodePropertyInfo> Outputs
+    ) : base(parentGuid, index, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
+        maskIsVisible, Inputs, Outputs)
     {
     }
 
@@ -31,6 +37,7 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
             folder.BlendMode.Value,
             folder.Id,
             folder.Mask.Value is not null,
-            folder.MaskIsVisible.Value);
+            folder.MaskIsVisible.Value, CreatePropertyInfos(folder.InputProperties, true, folder.Id),
+            CreatePropertyInfos(folder.OutputProperties, false, folder.Id));
     }
 }

+ 9 - 4
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs

@@ -1,10 +1,12 @@
 using System.Collections.Immutable;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+
 public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
 {
     public CreateLayer_ChangeInfo(
@@ -20,8 +22,9 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
         bool maskIsVisible,
         bool lockTransparency,
         ImmutableArray<NodePropertyInfo> inputs,
-        ImmutableArray<NodePropertyInfo> outputs) : 
-        base(parentGuid, index, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask, maskIsVisible, inputs, outputs)
+        ImmutableArray<NodePropertyInfo> outputs) :
+        base(parentGuid, index, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
+            maskIsVisible, inputs, outputs)
     {
         LockTransparency = lockTransparency;
     }
@@ -41,7 +44,9 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
             layer.Id,
             layer.Mask.Value is not null,
             layer.MaskIsVisible.Value,
-            layer is ITransparencyLockable { LockTransparency: true }
-            );
+            layer is ITransparencyLockable { LockTransparency: true },
+            CreatePropertyInfos(layer.InputProperties, true, layer.Id),
+            CreatePropertyInfos(layer.OutputProperties, false, layer.Id)
+        );
     }
 }

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

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

+ 8 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -5,7 +5,8 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 public class InputProperty : IInputProperty
 {
-    public string Name { get; }
+    public string InternalPropertyName { get; }
+    public string DisplayName { get; }
     public object Value { get; set; }
     public Node Node { get; }
     public Type ValueType { get; } 
@@ -13,9 +14,10 @@ public class InputProperty : IInputProperty
     
     public IOutputProperty? Connection { get; set; }
     
-    internal InputProperty(Node node, string name, object defaultValue, Type valueType)
+    internal InputProperty(Node node, string internalName, string displayName, object defaultValue, Type valueType)
     {
-        Name = name;
+        InternalPropertyName = internalName;
+        DisplayName = displayName;
         Value = defaultValue;
         Node = node;
         ValueType = valueType;
@@ -24,12 +26,12 @@ public class InputProperty : IInputProperty
     public InputProperty Clone(Node forNode)
     {
         if(Value is ICloneable cloneable)
-            return new InputProperty(forNode, Name, cloneable.Clone(), ValueType);
+            return new InputProperty(forNode, InternalPropertyName, DisplayName, cloneable.Clone(), ValueType);
         
         if(!Value.GetType().IsPrimitive && Value.GetType() != typeof(string))
             throw new InvalidOperationException("Value is not cloneable and not a primitive type");
         
-        return new InputProperty(forNode, Name, Value, ValueType);
+        return new InputProperty(forNode, InternalPropertyName, DisplayName, Value, ValueType);
     }
 }
 
@@ -42,7 +44,7 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
         set => base.Value = value;
     }
     
-    internal InputProperty(Node node, string name, T defaultValue) : base(node, name, defaultValue, typeof(T))
+    internal InputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node, internalName, displayName, defaultValue, typeof(T))
     {
     }
 }

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/INodeProperty.cs

@@ -2,7 +2,8 @@
 
 public interface INodeProperty
 {
-    public string Name { get; }
+    public string InternalPropertyName { get; }
+    public string DisplayName { get; }
     public object Value { get; set; }
     public IReadOnlyNode Node { get; }
     public Type ValueType { get; }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -12,7 +12,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     public ImageLayerNode(VecI size)
     {
-        LockTransparency = CreateInput<bool>("LockTransparency", false);
+        LockTransparency = CreateInput<bool>("LockTransparency", "LOCK_TRANSPARENCY", false);
         frames.Add(new ImageFrame(Guid.NewGuid(), 0, 0, new(size)));
     }
 

+ 3 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -11,9 +11,9 @@ public class MergeNode : Node
     
     public MergeNode() 
     {
-        Top = CreateInput<ChunkyImage>("Top", null);
-        Bottom = CreateInput<ChunkyImage>("Bottom", null);
-        Output = CreateOutput<ChunkyImage>("Output", null);
+        Top = CreateInput<ChunkyImage>("Top", "TOP", null);
+        Bottom = CreateInput<ChunkyImage>("Bottom", "BOTTOM", null);
+        Output = CreateOutput<ChunkyImage>("Output", "OUTPUT", null);
     }
     
     public override bool Validate()

+ 9 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -112,16 +112,21 @@ public abstract class Node(Guid? id = null) : IReadOnlyNode, IDisposable
         }
     }
 
-    protected InputProperty<T> CreateInput<T>(string name, T defaultValue)
+    protected InputProperty<T> CreateInput<T>(string propName, string displayName, T defaultValue)
     {
-        var property = new InputProperty<T>(this, name, defaultValue);
+        var property = new InputProperty<T>(this, propName, displayName, defaultValue);
+        if (InputProperties.Any(x => x.InternalPropertyName == propName))
+        {
+            throw new InvalidOperationException($"Input with name {propName} already exists.");
+        }
+
         inputs.Add(property);
         return property;
     }
 
-    protected OutputProperty<T> CreateOutput<T>(string name, T defaultValue)
+    protected OutputProperty<T> CreateOutput<T>(string propName, string displayName, T defaultValue)
     {
-        var property = new OutputProperty<T>(this, name, defaultValue);
+        var property = new OutputProperty<T>(this, propName, displayName, defaultValue);
         outputs.Add(property);
         property.Connected += (input, _) => _connectedNodes.Add(input.Node);
         property.Disconnected += (input, _) => _connectedNodes.Remove(input.Node);

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs

@@ -8,7 +8,7 @@ public class OutputNode : Node, IBackgroundInput
     public InputProperty<ChunkyImage?> Input { get; } 
     public OutputNode()
     {
-        Input = CreateInput<ChunkyImage>("Input", null);
+        Input = CreateInput<ChunkyImage>("Background", "INPUT", null);
     }
     
     public override bool Validate()

+ 8 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -21,15 +21,15 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
 
     protected StructureNode(Guid? id = null) : base(id)
     {
-        Background = CreateInput<ChunkyImage?>("Background", null);
-        Opacity = CreateInput<float>("Opacity", 1);
-        IsVisible = CreateInput<bool>("IsVisible", true);
-        ClipToMemberBelow = CreateInput<bool>("ClipToMemberBelow", false);
-        BlendMode = CreateInput<BlendMode>("BlendMode", Enums.BlendMode.Normal);
-        Mask = CreateInput<ChunkyImage?>("Mask", null);
-        MaskIsVisible = CreateInput<bool>("MaskIsVisible", true);
+        Background = CreateInput<ChunkyImage?>("Background", "BACKGROUND", null);
+        Opacity = CreateInput<float>("Opacity", "OPACITY", 1);
+        IsVisible = CreateInput<bool>("IsVisible", "IS_VISIBLE", true);
+        ClipToMemberBelow = CreateInput<bool>("ClipToMemberBelow", "CLIP_TO_MEMBER_BELOW", false);
+        BlendMode = CreateInput<BlendMode>("BlendMode", "BLEND_MODE", Enums.BlendMode.Normal);
+        Mask = CreateInput<ChunkyImage?>("Mask", "MASK", null);
+        MaskIsVisible = CreateInput<bool>("MaskIsVisible", "MASK_IS_VISIBLE", true);
         
-        Output = CreateOutput<ChunkyImage?>("Output", null);
+        Output = CreateOutput<ChunkyImage?>("Output", "OUTPUT", null);
     }
     
     public abstract override ChunkyImage? OnExecute(KeyFrameTime frameTime);

+ 7 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/OutputProperty.cs

@@ -9,7 +9,8 @@ public class OutputProperty : IOutputProperty
 {
     private List<IInputProperty> _connections = new();
     private object _value;
-    public string Name { get; }
+    public string InternalPropertyName { get; }
+    public string DisplayName { get; }
     public Node Node { get; }
 
     public Type ValueType { get; }
@@ -34,9 +35,10 @@ public class OutputProperty : IOutputProperty
     public event InputConnectedEvent Connected;
     public event InputConnectedEvent Disconnected;
 
-    internal OutputProperty(Node node, string name, object defaultValue, Type valueType)
+    internal OutputProperty(Node node, string internalName, string displayName, object defaultValue, Type valueType)
     {
-        Name = name;
+        InternalPropertyName = internalName;
+        DisplayName = displayName;
         Value = defaultValue;
         Node = node;
         ValueType = valueType;
@@ -71,7 +73,7 @@ public class OutputProperty : IOutputProperty
      
         object value = Value is ICloneable cloneableValue ? cloneableValue.Clone() : Value;
         
-        var newOutput = new OutputProperty(clone, Name, value, ValueType);
+        var newOutput = new OutputProperty(clone, InternalPropertyName, DisplayName, value, ValueType);
         foreach (var connection in Connections)
         {
             newOutput.ConnectTo(connection);
@@ -89,7 +91,7 @@ public class OutputProperty<T> : OutputProperty, INodeProperty<T>
         set => base.Value = value;
     }
 
-    internal OutputProperty(Node node, string name, T defaultValue) : base(node, name, defaultValue, typeof(T))
+    internal OutputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node, internalName, displayName, defaultValue, typeof(T))
     {
     }
 }

+ 2 - 7
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs

@@ -34,8 +34,8 @@ internal class CreateNode_Change : Change
         target.NodeGraph.AddNode(node);
         ignoreInUndo = false;
         
-        var inputInfos = CreatePropertyInfos(node.InputProperties, true);
-        var outputInfos = CreatePropertyInfos(node.OutputProperties, false);
+        var inputInfos = CreateNode_ChangeInfo.CreatePropertyInfos(node.InputProperties, true, id);
+        var outputInfos = CreateNode_ChangeInfo.CreatePropertyInfos(node.OutputProperties, false, id);
         
         return new CreateNode_ChangeInfo(nodeType.Name, node.Position, id, inputInfos, outputInfos);
     }
@@ -47,9 +47,4 @@ internal class CreateNode_Change : Change
         
         return new DeleteNode_ChangeInfo(id);
     }
-    
-    private ImmutableArray<NodePropertyInfo> CreatePropertyInfos(IEnumerable<INodeProperty> properties, bool isInput)
-    {
-        return properties.Select(p => new NodePropertyInfo(p.Name, p.ValueType, isInput, id)).ToImmutableArray();
-    }
 }

+ 15 - 22
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -1,10 +1,8 @@
-using System.Collections.Immutable;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 using PixiEditor.ChangeableDocument.Enums;
-using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Structure;
 
@@ -46,24 +44,18 @@ internal class CreateStructureMember_Change : Change
         document.NodeGraph.AddNode(member);
 
         IBackgroundInput backgroundInput = (IBackgroundInput)parentNode;
-        AppendMember(backgroundInput, member);
-
-        ignoreInUndo = false;
-
-        CreateNode_ChangeInfo changeInfo = new CreateNode_ChangeInfo(
-            member.MemberName, 
-            new VecD(0, 0),
-            member.Id, 
-            CreatePropertyInfos(member.InputProperties, true),
-            CreatePropertyInfos(member.OutputProperties, false));
+        List<ConnectProperty_ChangeInfo> connectPropertyChangeInfo = AppendMember(backgroundInput, member);
         
         List<IChangeInfo> changes = new()
         {
-            changeInfo,
-            CreateChangeInfo(member)
+            CreateChangeInfo(member),
         };
+        
+        changes.AddRange(connectPropertyChangeInfo);
 
-        return changes; 
+        ignoreInUndo = false;
+        
+        return changes;
     }
     
     private IChangeInfo CreateChangeInfo(StructureNode member)
@@ -78,11 +70,6 @@ internal class CreateStructureMember_Change : Change
         };
     }
 
-    private ImmutableArray<NodePropertyInfo> CreatePropertyInfos(IEnumerable<INodeProperty> properties, bool isInput)
-    {
-        return properties.Select(p => new NodePropertyInfo(p.Name, p.ValueType, isInput, newMemberGuid)).ToImmutableArray();
-    }
-
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document document)
     {
         var container = document.FindNodeOrThrow<Node>(parentFolderGuid);
@@ -102,8 +89,9 @@ internal class CreateStructureMember_Change : Change
         return new DeleteStructureMember_ChangeInfo(newMemberGuid, parentFolderGuid);
     }
 
-    private static void AppendMember(IBackgroundInput backgroundInput, StructureNode member)
+    private static List<ConnectProperty_ChangeInfo> AppendMember(IBackgroundInput backgroundInput, StructureNode member)
     {
+        List<ConnectProperty_ChangeInfo> changes = new();
         IOutputProperty? previouslyConnected = null;
         if (backgroundInput.Background.Connection != null)
         {
@@ -115,6 +103,11 @@ internal class CreateStructureMember_Change : Change
         if (previouslyConnected != null)
         {
             member.Background.Connection = previouslyConnected;
+            changes.Add(new ConnectProperty_ChangeInfo(previouslyConnected.Node.Id, member.Id, previouslyConnected.InternalPropertyName, member.Background.InternalPropertyName));
         }
+        
+        changes.Add(new ConnectProperty_ChangeInfo(member.Id, backgroundInput.Background.Node.Id, member.Output.InternalPropertyName, backgroundInput.Background.InternalPropertyName));
+        
+        return changes;
     }
 }