Browse Source

Delete Node

flabbet 1 year ago
parent
commit
11510d5cfb
22 changed files with 275 additions and 70 deletions
  1. 2 0
      src/PixiEditor.AvaloniaUI/Models/Commands/Attributes/Commands/CommandAttribute.cs
  2. 1 0
      src/PixiEditor.AvaloniaUI/Models/Commands/CommandController.cs
  3. 2 0
      src/PixiEditor.AvaloniaUI/Models/Commands/Commands/Command.cs
  4. 17 2
      src/PixiEditor.AvaloniaUI/Models/Controllers/ShortcutController.cs
  5. 15 2
      src/PixiEditor.AvaloniaUI/ViewModels/Dock/NodeGraphDockViewModel.cs
  6. 3 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentManagerViewModel.cs
  7. 28 13
      src/PixiEditor.AvaloniaUI/ViewModels/Document/NodeGraphViewModel.cs
  8. 20 3
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs
  9. 2 1
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ViewportWindowViewModel.cs
  10. 14 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs
  11. 2 4
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs
  12. 2 4
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs
  13. 0 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs
  14. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  15. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  16. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  17. 13 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectionsData.cs
  18. 71 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeleteNode_Change.cs
  19. 70 12
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs
  20. 2 4
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  21. 5 11
      src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs
  22. 3 10
      src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

+ 2 - 0
src/PixiEditor.AvaloniaUI/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -16,6 +16,8 @@ internal partial class Command
         public LocalizedString Description { get; }
 
         public string CanExecute { get; set; }
+        
+        public Type? ShortcutContext { get; set; }
 
         /// <summary>
         /// Gets or sets the default shortcut key for this command

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/Commands/CommandController.cs

@@ -257,6 +257,7 @@ internal class CommandController
                                 Parameter = basic.Parameter,
                                 MenuItemPath = basic.MenuItemPath,
                                 MenuItemOrder = basic.MenuItemOrder,
+                                ShortcutContext = basic.ShortcutContext
                             });
                     }
                     else if (attribute is CommandAttribute.FilterAttribute menu)

+ 2 - 0
src/PixiEditor.AvaloniaUI/Models/Commands/Commands/Command.cs

@@ -40,6 +40,8 @@ internal abstract partial class Command : PixiObservableObject
             }
         }
     }
+    
+    public Type? ShortcutContext { get; init; }
 
     public string? MenuItemPath { get; init; }
 

+ 17 - 2
src/PixiEditor.AvaloniaUI/Models/Controllers/ShortcutController.cs

@@ -17,6 +17,8 @@ internal class ShortcutController
     public IEnumerable<Command> LastCommands { get; private set; }
 
     public Dictionary<KeyCombination, ToolViewModel> TransientShortcuts { get; set; } = new();
+    
+    public Type? ActiveContext { get; private set; }
 
     public static void BlockShortcutExecution(string blocker)
     {
@@ -51,7 +53,7 @@ internal class ShortcutController
 
         if (!ShortcutExecutionBlocked)
         {
-            var commands = CommandController.Current.Commands[shortcut];
+            var commands = CommandController.Current.Commands[shortcut].Where(x => x.ShortcutContext is null || x.ShortcutContext == ActiveContext).ToList();
 
             if (!commands.Any())
             {
@@ -60,10 +62,23 @@ internal class ShortcutController
 
             LastCommands = commands;
 
-            foreach (var command in CommandController.Current.Commands[shortcut])
+            foreach (var command in commands)
             {
                 command.Execute();
             }
         }
     }
+
+    public void OverwriteContext(Type getType)
+    {
+        ActiveContext = getType;
+    }
+    
+    public void ClearContext(Type clearFrom)
+    {
+        if (ActiveContext == clearFrom)
+        {
+            ActiveContext = null;
+        }
+    }
 }

+ 15 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Dock/NodeGraphDockViewModel.cs

@@ -1,9 +1,12 @@
-using PixiEditor.AvaloniaUI.ViewModels.Document;
+using Avalonia.Input;
+using PixiDocks.Core.Docking.Events;
+using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
+using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.Extensions.Common.Localization;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Dock;
 
-internal class NodeGraphDockViewModel(DocumentManagerViewModel document) : DockableViewModel
+internal class NodeGraphDockViewModel(DocumentManagerViewModel document) : DockableViewModel, IDockableSelectionEvents
 {
     public const string TabId = "NodeGraph";
 
@@ -17,4 +20,14 @@ internal class NodeGraphDockViewModel(DocumentManagerViewModel document) : Docka
         get => document;
         set => SetProperty(ref document, value);
     }
+    
+    void IDockableSelectionEvents.OnSelected()
+    {
+        DocumentManagerSubViewModel.Owner.ShortcutController.OverwriteContext(this.GetType());
+    }
+
+    void IDockableSelectionEvents.OnDeselected()
+    {
+        DocumentManagerSubViewModel.Owner.ShortcutController.ClearContext(this.GetType());
+    }
 }

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

@@ -155,7 +155,9 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ActiveDocument.EventInlet.OnSymmetryDragEnded(dir);
     }
 
-    [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE", CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete, 
+    [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE", 
+        CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete,
+        ShortcutContext = typeof(ViewportWindowViewModel),
         Icon = PixiPerfectIcons.Eraser,
         MenuItemPath = "EDIT/DELETE_SELECTED_PIXELS", MenuItemOrder = 6)]
     public void DeletePixels()

+ 28 - 13
src/PixiEditor.AvaloniaUI/ViewModels/Document/NodeGraphViewModel.cs

@@ -1,5 +1,7 @@
 using System.Collections.ObjectModel;
 using System.Reflection;
+using Avalonia.Input;
+using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
 using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
@@ -56,7 +58,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
     public void AddFrame(Guid frameId, IEnumerable<Guid> nodes)
     {
         var frame = new NodeFrameViewModel(frameId, AllNodes.Where(x => nodes.Contains(x.Id)));
-        
+
         Frames.Add(frame);
     }
 
@@ -64,9 +66,9 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
     {
         var start = AllNodes.First(x => x.Id == startId);
         var end = AllNodes.First(x => x.Id == endId);
-        
+
         var zone = new NodeZoneViewModel(frameId, internalName, start, end);
-        
+
         Frames.Add(zone);
     }
 
@@ -182,7 +184,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
     {
         Internals.ActionAccumulator.AddFinishedActions(new UpdatePropertyValue_Action(node.Id, property, value));
     }
-    
+
     public void EndChangeNodePosition()
     {
         Internals.ActionAccumulator.AddFinishedActions(new EndNodePosition_Action());
@@ -191,7 +193,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
     public void CreateNode(Type nodeType)
     {
         IAction change;
-        
+
         PairNodeAttribute? pairAttribute = nodeType.GetCustomAttribute<PairNodeAttribute>(true);
         if (pairAttribute != null)
         {
@@ -201,19 +203,32 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         {
             change = new CreateNode_Action(nodeType, Guid.NewGuid());
         }
-        
+
         Internals.ActionAccumulator.AddFinishedActions(change);
     }
 
+    public void RemoveNodes(Guid[] selectedNodes)
+    {
+        IAction[] actions = new IAction[selectedNodes.Length];
+        
+        for (int i = 0; i < selectedNodes.Length; i++)
+        {
+            actions[i] = new DeleteNode_Action(selectedNodes[i]);
+        }
+        
+        Internals.ActionAccumulator.AddFinishedActions(actions);
+    }
+
     // TODO: Remove this
     public void CreateNodeFrameAroundEverything()
     {
         CreateNodeFrame(AllNodes);
     }
-    
+
     public void CreateNodeFrame(IEnumerable<INodeHandler> nodes)
     {
-        Internals.ActionAccumulator.AddFinishedActions(new CreateNodeFrame_Action(Guid.NewGuid(), nodes.Select(x => x.Id)));
+        Internals.ActionAccumulator.AddFinishedActions(new CreateNodeFrame_Action(Guid.NewGuid(),
+            nodes.Select(x => x.Id)));
     }
 
     public void ConnectProperties(INodePropertyHandler? start, INodePropertyHandler? end)
@@ -225,7 +240,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
 
         var input = start?.IsInput == true ? start : end;
         var output = start?.IsInput == false ? start : end;
-        
+
         if (input == null && output != null)
         {
             input = output.ConnectedInputs.FirstOrDefault();
@@ -246,10 +261,10 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
 
         if (input == null) return;
 
-        IAction action = input != null && output != null ?
-            new ConnectProperties_Action(inputNode.Id, outputNode.Id, inputProperty, outputProperty) :
-            new DisconnectProperty_Action(inputNode.Id, inputProperty);
-        
+        IAction action = input != null && output != null
+            ? new ConnectProperties_Action(inputNode.Id, outputNode.Id, inputProperty, outputProperty)
+            : new DisconnectProperty_Action(inputNode.Id, inputProperty);
+
         Internals.ActionAccumulator.AddFinishedActions(action);
     }
 }

+ 20 - 3
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs

@@ -1,5 +1,8 @@
-using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
+using Avalonia.Input;
+using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
 using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.AvaloniaUI.ViewModels.Dock;
+using PixiEditor.AvaloniaUI.ViewModels.Nodes;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
@@ -10,6 +13,19 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
     {
     }
 
+    [Command.Basic("PixiEditor.NodeGraph.DeleteSelectedNodes", "DELETE_NODES", "DELETE_NODES_DESCRIPTIVE", 
+        Key = Key.Delete, ShortcutContext = typeof(NodeGraphDockViewModel))]
+    public void DeleteSelectedNodes()
+    {
+        Guid[] selectedNodes = Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.AllNodes
+            .Where(x => x.IsSelected).Select(x => x.Id).ToArray();
+        
+        if (selectedNodes == null || selectedNodes.Length == 0)
+            return;
+
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.RemoveNodes(selectedNodes);
+    }
+
     [Command.Debug("PixiEditor.NodeGraph.CreateNodeFrameAroundEverything", "Create node frame", "Create node frame")]
     public void CreateNodeFrameAroundEverything()
     {
@@ -37,9 +53,10 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
     [Command.Internal("PixiEditor.NodeGraph.UpdateValue")]
     public void UpdatePropertyValue((INodeHandler node, string property, object value) args)
     {
-        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.UpdatePropertyValue(args.node, args.property, args.value);
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.UpdatePropertyValue(args.node, args.property,
+            args.value);
     }
-    
+
     [Command.Internal("PixiEditor.NodeGraph.EndChangeNodePos")]
     public void EndChangeNodePos()
     {

+ 2 - 1
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ViewportWindowViewModel.cs

@@ -131,10 +131,11 @@ internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockabl
     void IDockableSelectionEvents.OnSelected()
     {
         Owner.ActiveWindow = this;
+        Owner.Owner.ShortcutController.OverwriteContext(this.GetType());
     }
 
     void IDockableSelectionEvents.OnDeselected()
     {
-
+        Owner.Owner.ShortcutController.ClearContext(GetType());
     }
 }

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

@@ -2,6 +2,9 @@
 using System.Collections.Immutable;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Changes.Structure;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
@@ -23,6 +26,17 @@ public record CreateNode_ChangeInfo(
     
     public static CreateNode_ChangeInfo CreateFromNode(IReadOnlyNode node)
     {
+        if (node is IReadOnlyStructureNode structureNode)
+        {
+            switch (structureNode)
+            {
+                case LayerNode layerNode:
+                    return CreateLayer_ChangeInfo.FromLayer(layerNode);
+                case FolderNode folderNode:
+                    return CreateFolder_ChangeInfo.FromFolder(folderNode);
+            }
+        }
+        
         return new CreateNode_ChangeInfo(node.InternalName, node.DisplayName, node.Position,
             node.Id,
             CreatePropertyInfos(node.InputProperties, true, node.Id), CreatePropertyInfos(node.OutputProperties, false, node.Id));

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

@@ -9,7 +9,6 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
 {
     public CreateFolder_ChangeInfo(
         string internalName,
-        Guid parentGuid,
         float opacity,
         bool isVisible,
         bool clipToMemberBelow,
@@ -20,16 +19,15 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
         bool maskIsVisible,
         ImmutableArray<NodePropertyInfo> Inputs,
         ImmutableArray<NodePropertyInfo> Outputs
-    ) : base(internalName, parentGuid, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
+    ) : base(internalName, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
         maskIsVisible, Inputs, Outputs)
     {
     }
 
-    internal static CreateFolder_ChangeInfo FromFolder(Guid parentGuid, FolderNode folder)
+    internal static CreateFolder_ChangeInfo FromFolder(FolderNode folder)
     {
         return new CreateFolder_ChangeInfo(
             folder.InternalName,
-            parentGuid,
             folder.Opacity.Value,
             folder.IsVisible.Value,
             folder.ClipToPreviousMember.Value,

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

@@ -11,7 +11,6 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
 {
     public CreateLayer_ChangeInfo(
         string internalName,
-        Guid parentGuid,
         float opacity,
         bool isVisible,
         bool clipToMemberBelow,
@@ -23,7 +22,7 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
         bool lockTransparency,
         ImmutableArray<NodePropertyInfo> inputs,
         ImmutableArray<NodePropertyInfo> outputs) :
-        base(internalName, parentGuid, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
+        base(internalName, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
             maskIsVisible, inputs, outputs)
     {
         LockTransparency = lockTransparency;
@@ -31,11 +30,10 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
 
     public bool LockTransparency { get; }
 
-    internal static CreateLayer_ChangeInfo FromLayer(Guid parentGuid, LayerNode layer)
+    internal static CreateLayer_ChangeInfo FromLayer(LayerNode layer)
     {
         return new CreateLayer_ChangeInfo(
             layer.InternalName,
-            parentGuid,
             layer.Opacity.Value,
             layer.IsVisible.Value,
             layer.ClipToPreviousMember.Value,

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

@@ -8,7 +8,6 @@ namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 
 public abstract record class CreateStructureMember_ChangeInfo(
     string InternalName,
-    Guid ParentGuid,
     float Opacity,
     bool IsVisible,
     bool ClipToMemberBelow,

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -127,7 +127,7 @@ public class InputProperty : IInputProperty
             return new InputProperty(forNode, InternalPropertyName, DisplayName, nullValue, ValueType);
         }
         
-        if(!NonOverridenValue.GetType().IsPrimitive && NonOverridenValue.GetType() != typeof(string))
+        if(!NonOverridenValue.GetType().IsValueType && NonOverridenValue.GetType() != typeof(string))
             throw new InvalidOperationException("Value is not cloneable and not a primitive type");
         
         return new InputProperty(forNode, InternalPropertyName, DisplayName, NonOverridenValue, ValueType);

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

@@ -289,6 +289,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
         var clone = CreateCopy();
         clone.Id = Guid.NewGuid();
+        clone.Position = Position;
 
         for (var i = 0; i < clone.inputs.Count; i++)
         {

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

@@ -21,7 +21,7 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
 
     public OutputProperty<Surface?> Output { get; }
 
-    public string MemberName { get; set; } = string.Empty;
+    public string MemberName { get; set; } = "New Element"; // would be good to add localization here, it is set if node is created via node graph
     
     public override string DisplayName
     {

+ 13 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectionsData.cs

@@ -0,0 +1,13 @@
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+public class ConnectionsData
+{
+    public Dictionary<PropertyConnection, List<PropertyConnection>> originalOutputConnections = new();
+    public List<(PropertyConnection input, PropertyConnection? output)> originalInputConnections = new();
+    
+    public ConnectionsData(Dictionary<PropertyConnection, List<PropertyConnection>> originalOutputConnections, List<(PropertyConnection, PropertyConnection?)> originalInputConnections)
+    {
+        this.originalOutputConnections = originalOutputConnections;
+        this.originalInputConnections = originalInputConnections;
+    }
+}

+ 71 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeleteNode_Change.cs

@@ -0,0 +1,71 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class DeleteNode_Change : Change
+{
+    public Guid NodeId { get; set; }
+
+    private ConnectionsData originalConnections;
+
+    private Node savedCopy;
+
+    [GenerateMakeChangeAction]
+    public DeleteNode_Change(Guid nodeId)
+    {
+        NodeId = nodeId;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        Node node = target.FindNode<Node>(NodeId);
+
+        if (node is null)
+            return false;
+
+        originalConnections = NodeOperations.CreateConnectionsData(node);
+
+        savedCopy = node.Clone();
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+        var node = target.FindNode<Node>(NodeId);
+
+        List<IChangeInfo> changes = NodeOperations.DetachNode(target.NodeGraph, node);
+
+        target.NodeGraph.RemoveNode(node);
+
+        changes.Add(new DeleteNode_ChangeInfo(NodeId));
+
+        return changes;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document doc)
+    {
+        var copy = savedCopy!.Clone();
+        copy.Id = NodeId;
+
+        doc.NodeGraph.AddNode(copy);
+
+        List<IChangeInfo> changes = new();
+
+        IChangeInfo createChange = CreateNode_ChangeInfo.CreateFromNode(copy);
+
+        changes.Add(createChange);
+
+        changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, copy, doc.NodeGraph));
+
+        return changes;
+    }
+
+    public override void Dispose()
+    {
+        savedCopy?.Dispose();
+    }
+}

+ 70 - 12
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -18,7 +18,8 @@ public static class NodeOperations
     {
         allFactories = new Dictionary<Type, INodeFactory>();
         var factoryTypes = typeof(Node).Assembly.GetTypes().Where(x =>
-            x.IsAssignableTo(typeof(INodeFactory)) && x is { IsAbstract: false, IsInterface: false }).ToImmutableArray();
+                x.IsAssignableTo(typeof(INodeFactory)) && x is { IsAbstract: false, IsInterface: false })
+            .ToImmutableArray();
         foreach (var factoryType in factoryTypes)
         {
             INodeFactory factory = (INodeFactory)Activator.CreateInstance(factoryType);
@@ -37,7 +38,7 @@ public static class NodeOperations
         {
             node = (Node)Activator.CreateInstance(nodeType, optionalParameters);
         }
-        
+
         return node;
     }
 
@@ -102,22 +103,48 @@ public static class NodeOperations
         return changes;
     }
 
-    public static List<IChangeInfo> ConnectStructureNodeProperties(
-        List<PropertyConnection> originalOutputConnections,
-        List<(PropertyConnection, PropertyConnection?)> originalInputConnections, StructureNode node,
+    public static ConnectionsData CreateConnectionsData(Node node)
+    {
+        var originalOutputConnections = new Dictionary<PropertyConnection, List<PropertyConnection>>();
+
+        foreach (var outputProp in node.OutputProperties)
+        {
+            PropertyConnection outputConnection = new(outputProp.Node.Id, outputProp.InternalPropertyName);
+            originalOutputConnections.Add(outputConnection, new List<PropertyConnection>());
+            foreach (var conn in outputProp.Connections)
+            {
+                originalOutputConnections[outputConnection]
+                    .Add(new PropertyConnection(conn.Node.Id, conn.InternalPropertyName));
+            }
+        }
+
+        var originalInputConnections = node.InputProperties.Select(x =>
+                (new PropertyConnection(x.Node.Id, x.InternalPropertyName),
+                    new PropertyConnection(x.Connection?.Node.Id, x.Connection?.InternalPropertyName)))
+            .ToList();
+        
+        return new ConnectionsData(originalOutputConnections, originalInputConnections);
+    }
+
+    public static List<IChangeInfo> ConnectStructureNodeProperties(ConnectionsData originalConnections, Node node,
         IReadOnlyNodeGraph graph)
     {
         List<IChangeInfo> changes = new();
-        foreach (var connection in originalOutputConnections)
+        foreach (var connections in originalConnections.originalOutputConnections)
         {
-            var inputNode = graph.AllNodes.FirstOrDefault(x => x.Id == connection.NodeId);
-            IInputProperty property = inputNode.GetInputProperty(connection.PropertyName);
-            node.Output.ConnectTo(property);
-            changes.Add(new ConnectProperty_ChangeInfo(node.Id, property.Node.Id, node.Output.InternalPropertyName,
-                property.InternalPropertyName));
+            PropertyConnection outputConnection = connections.Key;
+            IOutputProperty outputProp = node.GetOutputProperty(outputConnection.PropertyName);
+            foreach (var connection in connections.Value)
+            {
+                var inputNode = graph.AllNodes.FirstOrDefault(x => x.Id == connection.NodeId);
+                IInputProperty property = inputNode.GetInputProperty(connection.PropertyName);
+                outputProp.ConnectTo(property);
+                changes.Add(new ConnectProperty_ChangeInfo(node.Id, property.Node.Id, outputProp.InternalPropertyName,
+                    property.InternalPropertyName));
+            }
         }
 
-        foreach (var connection in originalInputConnections)
+        foreach (var connection in originalConnections.originalInputConnections)
         {
             var outputNode = graph.AllNodes.FirstOrDefault(x => x.Id == connection.Item2?.NodeId);
 
@@ -143,6 +170,37 @@ public static class NodeOperations
 
         return changes;
     }
+
+    public static List<IChangeInfo> DetachNode(Changeables.Graph.NodeGraph target, Node? node)
+    {
+        List<IChangeInfo> changes = new();
+        if (node == null)
+        {
+            return changes;
+        }
+
+        foreach (var input in node.InputProperties)
+        {
+            if (input.Connection == null)
+            {
+                continue;
+            }
+
+            input.Connection.DisconnectFrom(input);
+            changes.Add(new ConnectProperty_ChangeInfo(null, node.Id, null, input.InternalPropertyName));
+        }
+
+        foreach (var output in node.OutputProperties)
+        {
+            foreach (var input in output.Connections.ToArray())
+            {
+                output.DisconnectFrom(input);
+                changes.Add(new ConnectProperty_ChangeInfo(null, input.Node.Id, null, input.InternalPropertyName));
+            }
+        }
+
+        return changes;
+    }
 }
 
 public record PropertyConnection(Guid? NodeId, string? PropertyName);

+ 2 - 4
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -70,10 +70,8 @@ internal class CreateStructureMember_Change : Change
     {
         return type switch
         {
-            StructureMemberType.Layer => CreateLayer_ChangeInfo.FromLayer(parentGuid,
-                (LayerNode)member),
-            StructureMemberType.Folder => CreateFolder_ChangeInfo.FromFolder(parentGuid,
-                (FolderNode)member),
+            StructureMemberType.Layer => CreateLayer_ChangeInfo.FromLayer((LayerNode)member),
+            StructureMemberType.Folder => CreateFolder_ChangeInfo.FromFolder((FolderNode)member),
             _ => throw new NotSupportedException(),
         };
     }

+ 5 - 11
src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs

@@ -10,8 +10,7 @@ internal class DeleteStructureMember_Change : Change
 {
     private Guid memberGuid;
     private int originalIndex;
-    private List<PropertyConnection> originalOutputConnections = new();
-    private List<(PropertyConnection input, PropertyConnection? output)> originalInputConnections = new();
+    private ConnectionsData originalConnections; 
     private StructureNode? savedCopy;
 
     [GenerateMakeChangeAction]
@@ -26,12 +25,7 @@ internal class DeleteStructureMember_Change : Change
         if (member is null)
             return false;
 
-        originalOutputConnections = member.Output.Connections.Select(x => new PropertyConnection(x.Node.Id, x.InternalPropertyName))
-            .ToList();
-        
-        originalInputConnections = member.InputProperties.Select(x => 
-            (new PropertyConnection(x.Node.Id, x.InternalPropertyName), new PropertyConnection(x.Connection?.Node.Id, x.Connection?.InternalPropertyName)))
-            .ToList();
+        originalConnections = NodeOperations.CreateConnectionsData(member); 
         
         savedCopy = (StructureNode)member.Clone();
         savedCopy.Id = memberGuid;
@@ -80,14 +74,14 @@ internal class DeleteStructureMember_Change : Change
 
         IChangeInfo createChange = copy switch
         {
-            LayerNode => CreateLayer_ChangeInfo.FromLayer(memberGuid, (LayerNode)copy),
-            FolderNode => CreateFolder_ChangeInfo.FromFolder(memberGuid, (FolderNode)copy),
+            LayerNode => CreateLayer_ChangeInfo.FromLayer((LayerNode)copy),
+            FolderNode => CreateFolder_ChangeInfo.FromFolder((FolderNode)copy),
             _ => throw new NotSupportedException(),
         };
         
         changes.Add(createChange);
 
-        changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalOutputConnections, originalInputConnections, copy, doc.NodeGraph)); 
+        changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, copy, doc.NodeGraph)); 
         
         return changes;
     }

+ 3 - 10
src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

@@ -15,8 +15,7 @@ internal class MoveStructureMember_Change : Change
 
     private Guid originalFolderGuid;
 
-    private List<PropertyConnection> originalOutputConnections = new();
-    private List<(PropertyConnection input, PropertyConnection? output)> originalInputConnections = new();
+    private ConnectionsData originalConnections; 
     
     private bool putInsideFolder;
 
@@ -36,12 +35,7 @@ internal class MoveStructureMember_Change : Change
         if (member is null || targetFolder is null)
             return false;
 
-        originalOutputConnections = member.Output.Connections.Select(x => new PropertyConnection(x.Node.Id, x.InternalPropertyName))
-            .ToList();
-        
-        originalInputConnections = member.InputProperties.Select(x => 
-            (new PropertyConnection(x.Node.Id, x.InternalPropertyName), new PropertyConnection(x.Connection?.Node.Id, x.Connection?.InternalPropertyName)))
-            .ToList();
+        originalConnections = NodeOperations.CreateConnectionsData(member); 
           
         return true;
     }
@@ -93,8 +87,7 @@ internal class MoveStructureMember_Change : Change
         MoveStructureMember_ChangeInfo changeInfo = new(memberGuid, targetNodeGuid, originalFolderGuid);
         
         changes.AddRange(NodeOperations.DetachStructureNode(member));
-        changes.AddRange(NodeOperations.ConnectStructureNodeProperties(
-            originalOutputConnections, originalInputConnections, member, target.NodeGraph));
+        changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, member, target.NodeGraph));
         
         changes.Add(changeInfo);