Browse Source

Delete pairs and zones

flabbet 1 year ago
parent
commit
3f8f46f99b

+ 19 - 5
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs

@@ -16,15 +16,18 @@ public record CreateNode_ChangeInfo(
     VecD Position,
     Guid Id,
     ImmutableArray<NodePropertyInfo> Inputs,
-    ImmutableArray<NodePropertyInfo> Outputs) : IChangeInfo
+    ImmutableArray<NodePropertyInfo> Outputs,
+    NodeMetadata? Metadata) : 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, GetNonOverridenValue(p), guid))
+        return properties.Select(p => new NodePropertyInfo(p.InternalPropertyName, p.DisplayName, p.ValueType, isInput,
+                GetNonOverridenValue(p), guid))
             .ToImmutableArray();
     }
-    
+
     public static CreateNode_ChangeInfo CreateFromNode(IReadOnlyNode node)
     {
         if (node is IReadOnlyStructureNode structureNode)
@@ -42,12 +45,23 @@ public record CreateNode_ChangeInfo(
 
         if (string.IsNullOrEmpty(internalName))
         {
-            throw new ArgumentException("Node does not have a unique name attribute. Please add [NodeInfo(\"UNIQUE_NAME\")] to the node class.");
+            throw new ArgumentException(
+                "Node does not have a unique name attribute. Please add [NodeInfo(\"UNIQUE_NAME\")] to the node class.");
+        }
+
+        Guid? pairNodeGuid = null;
+        if (node is IPairNode pairNode)
+        {
+            pairNodeGuid = pairNode.OtherNode;
         }
         
+        NodeMetadata metadata = new NodeMetadata() { PairNodeGuid = pairNodeGuid };
+        metadata.AddAttributes(node);
+
         return new CreateNode_ChangeInfo(internalName, node.DisplayName, node.Position,
             node.Id,
-            CreatePropertyInfos(node.InputProperties, true, node.Id), CreatePropertyInfos(node.OutputProperties, false, node.Id));
+            CreatePropertyInfos(node.InputProperties, true, node.Id),
+            CreatePropertyInfos(node.OutputProperties, false, node.Id), metadata);
     }
 
     private static object? GetNonOverridenValue(INodeProperty property) => property switch

+ 28 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodeMetadata.cs

@@ -0,0 +1,28 @@
+using System.Reflection;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public class NodeMetadata
+{
+    public bool IsPairNode { get; private set; }
+    public bool IsPairNodeStart { get; private set; }
+    public bool IsPairNodeEnd => IsPairNode && !IsPairNodeStart;
+
+    public Guid? PairNodeGuid { get; set; }
+    public string? ZoneUniqueName { get; private set; }
+
+    public void AddAttributes(IReadOnlyNode node)
+    {
+        Type type = node.GetType();
+        PairNodeAttribute? attribute = type.GetCustomAttribute<PairNodeAttribute>();
+
+        if (attribute == null)
+            return;
+
+        ZoneUniqueName = attribute.ZoneUniqueName;
+        IsPairNode = true;
+        IsPairNodeStart = attribute.IsStartingType;
+    }
+}

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

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

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPairNodeEnd.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPairNode.cs

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
-public interface IPairNodeEnd
+public interface IPairNode
 {
-    public Node StartNode { get; set; }
+    public Guid OtherNode { get; set; }
 }

+ 6 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -15,14 +15,16 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("ModifyImageLeft", "MODIFY_IMAGE_LEFT_NODE", PickerName = "MODIFY_IMAGE_PAIR_NODE")]
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
-public class ModifyImageLeftNode : Node
+public class ModifyImageLeftNode : Node, IPairNode
 {
     public InputProperty<Texture?> Image { get; }
-    
+
     public FuncOutputProperty<Float2> Coordinate { get; }
-    
+
     public FuncOutputProperty<Half4> Color { get; }
 
+    public Guid OtherNode { get; set; }
+
     private ConcurrentDictionary<RenderingContext, Pixmap> pixmapCache = new();
 
     public ModifyImageLeftNode()
@@ -53,7 +55,7 @@ public class ModifyImageLeftNode : Node
     {
         pixmapCache[forContext] = Image.Value?.PeekReadOnlyPixels();
     }
-    
+
     internal void DisposePixmap(RenderingContext forContext)
     {
         if (pixmapCache.TryRemove(forContext, out var targetPixmap))

+ 16 - 7
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -16,9 +16,9 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("ModifyImageRight", "MODIFY_IMAGE_RIGHT_NODE", PickerName = "")]
 [PairNode(typeof(ModifyImageLeftNode), "ModifyImageZone")]
-public class ModifyImageRightNode : Node, IPairNodeEnd, ICustomShaderNode
+public class ModifyImageRightNode : Node, IPairNode, ICustomShaderNode
 {
-    public Node StartNode { get; set; }
+    public Guid OtherNode { get; set; }
 
     private Paint drawingPaint = new Paint() { BlendMode = BlendMode.Src };
 
@@ -40,16 +40,21 @@ public class ModifyImageRightNode : Node, IPairNodeEnd, ICustomShaderNode
 
     protected override Texture? OnExecute(RenderingContext renderingContext)
     {
-        if (StartNode == null)
+        if (OtherNode == null)
         {
             FindStartNode();
-            if (StartNode == null)
+            if (OtherNode == null)
             {
                 return null;
             }
         }
 
-        var startNode = StartNode as ModifyImageLeftNode;
+        var startNode = FindStartNode(); 
+        if (startNode == null)
+        {
+            return null;
+        }
+        
         if (startNode.Image.Value is not { Size: var size })
         {
             return null;
@@ -171,18 +176,22 @@ public class ModifyImageRightNode : Node, IPairNodeEnd, ICustomShaderNode
         drawingPaint?.Dispose();
     }
 
-    private void FindStartNode()
+    private ModifyImageLeftNode FindStartNode()
     {
+        ModifyImageLeftNode startNode = null;
         TraverseBackwards(node =>
         {
             if (node is ModifyImageLeftNode leftNode)
             {
-                StartNode = leftNode;
+                startNode = leftNode;
+                OtherNode = leftNode.Id;
                 return false;
             }
 
             return true;
         });
+        
+        return startNode;
     }
 
     public override Node CreateCopy() => new ModifyImageRightNode();

+ 18 - 16
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodePair_Change.cs

@@ -11,54 +11,57 @@ internal class CreateNodePair_Change : Change
 {
     private Guid startId;
     private Guid endId;
-    private Guid zoneId;
     private Type nodeType;
-    
+
     [GenerateMakeChangeAction]
-    public CreateNodePair_Change(Guid startId, Guid endId, Guid zoneId, Type nodeType)
+    public CreateNodePair_Change(Guid startId, Guid endId, Type nodeType)
     {
         this.startId = startId;
         this.endId = endId;
-        this.zoneId = zoneId;
         this.nodeType = nodeType;
     }
 
     public override bool InitializeAndValidate(Document target)
     {
-        return nodeType.GetCustomAttribute<PairNodeAttribute>() != null && nodeType is { IsAbstract: false, IsInterface: false };
+        return nodeType.GetCustomAttribute<PairNodeAttribute>() != null &&
+               nodeType is { IsAbstract: false, IsInterface: false };
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         if (startId == Guid.Empty)
             startId = Guid.NewGuid();
         if (endId == Guid.Empty)
             endId = Guid.NewGuid();
-        
+
         PairNodeAttribute attribute = nodeType.GetCustomAttribute<PairNodeAttribute>();
         Type startingType = attribute.IsStartingType ? nodeType : attribute.OtherType;
         Type endingType = attribute.IsStartingType ? attribute.OtherType : nodeType;
-        
+
         var start = NodeOperations.CreateNode(startingType, target);
         var end = NodeOperations.CreateNode(endingType, target);
-        
-        if(end is IPairNodeEnd pairEnd)
-            pairEnd.StartNode = start;
 
         start.Id = startId;
         end.Id = endId;
+
+        if (start is IPairNode pairStart)
+            pairStart.OtherNode = end.Id;
+
+        if (end is IPairNode pairEnd)
+            pairEnd.OtherNode = start.Id;
+
         end.Position = new VecD(100, 0);
-        
+
         target.NodeGraph.AddNode(start);
         target.NodeGraph.AddNode(end);
-        
+
         ignoreInUndo = false;
 
         return new List<IChangeInfo>
         {
             CreateNode_ChangeInfo.CreateFromNode(start),
             CreateNode_ChangeInfo.CreateFromNode(end),
-            new CreateNodeZone_ChangeInfo(zoneId, $"PixiEditor.{attribute.ZoneUniqueName}", startId, endId)
         };
     }
 
@@ -66,9 +69,8 @@ internal class CreateNodePair_Change : Change
     {
         var startChange = RemoveNode(target, startId);
         var endChange = RemoveNode(target, endId);
-        var zoneChange = new DeleteNodeFrame_ChangeInfo(zoneId);
 
-        return new List<IChangeInfo> { startChange, endChange, zoneChange };
+        return new List<IChangeInfo> { startChange, endChange };
     }
 
     private static DeleteNode_ChangeInfo RemoveNode(Document target, Guid id)

+ 10 - 11
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs

@@ -15,43 +15,42 @@ internal class CreateNode_Change : Change
 {
     private Type nodeType;
     private Guid id;
-    
+
     [GenerateMakeChangeAction]
     public CreateNode_Change(Type nodeType, Guid id)
     {
         this.id = id;
         this.nodeType = nodeType;
     }
-    
+
     public override bool InitializeAndValidate(Document target)
     {
         bool canCreate = nodeType.IsSubclassOf(typeof(Node)) && nodeType is { IsAbstract: false, IsInterface: false };
         return canCreate && (!nodeType.IsAssignableTo(typeof(OutputNode)) || target.NodeGraph.OutputNode is null);
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
-        if(id == Guid.Empty)
+        if (id == Guid.Empty)
             id = Guid.NewGuid();
 
         Node node = NodeOperations.CreateNode(nodeType, target);
-        
+
         node.Position = new VecD(0, 0);
         node.Id = id;
-        
+
         target.NodeGraph.AddNode(node);
         ignoreInUndo = false;
-       
-        using RenderingContext context = new RenderingContext(new KeyFrameTime(0, 0), VecI.Zero, ChunkResolution.Full, target.Size);
-        
-        return CreateNode_ChangeInfo.CreateFromNode(node); 
+
+        return CreateNode_ChangeInfo.CreateFromNode(node);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         Node node = target.FindNodeOrThrow<Node>(id);
         target.NodeGraph.RemoveNode(node);
-        
+
         return new DeleteNode_ChangeInfo(id);
     }
 }

+ 16 - 5
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeleteNode_Change.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
@@ -13,7 +14,7 @@ internal class DeleteNode_Change : Change
     private ConnectionsData originalConnections;
 
     private Node savedCopy;
-    
+
     private GroupKeyFrame? savedKeyFrameGroup;
 
     [GenerateMakeChangeAction]
@@ -32,6 +33,10 @@ internal class DeleteNode_Change : Change
         originalConnections = NodeOperations.CreateConnectionsData(node);
 
         savedCopy = node.Clone();
+        if (savedCopy is IPairNode pairNode)
+        {
+            pairNode.OtherNode = (node as IPairNode).OtherNode;
+        }
 
         savedKeyFrameGroup = CloneGroupKeyFrame(target, NodeId);
 
@@ -71,6 +76,11 @@ internal class DeleteNode_Change : Change
     {
         var copy = savedCopy!.Clone();
         copy.Id = NodeId;
+        
+        if (copy is IPairNode pairNode)
+        {
+            pairNode.OtherNode = (savedCopy as IPairNode).OtherNode;
+        }
 
         doc.NodeGraph.AddNode(copy);
 
@@ -81,7 +91,7 @@ internal class DeleteNode_Change : Change
         changes.Add(createChange);
 
         changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, copy, doc.NodeGraph));
-        
+
         RevertKeyFrames(doc, savedKeyFrameGroup, changes);
 
         return changes;
@@ -95,10 +105,11 @@ internal class DeleteNode_Change : Change
             doc.AnimationData.AddKeyFrame(cloned);
             foreach (var keyFrame in savedKeyFrameGroup.Children)
             {
-                changes.Add(new CreateRasterKeyFrame_ChangeInfo(keyFrame.NodeId, keyFrame.StartFrame, keyFrame.Id, false));
+                changes.Add(new CreateRasterKeyFrame_ChangeInfo(keyFrame.NodeId, keyFrame.StartFrame, keyFrame.Id,
+                    false));
                 changes.Add(new KeyFrameLength_ChangeInfo(keyFrame.Id, keyFrame.StartFrame, keyFrame.Duration));
-            } 
-            
+            }
+
             changes.Add(new KeyFrameVisibility_ChangeInfo(savedKeyFrameGroup.Id, savedKeyFrameGroup.IsVisible));
         }
     }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Rendering/RenderingContext.cs

@@ -19,6 +19,7 @@ public class RenderingContext : IDisposable
     public VecI ChunkToUpdate { get; }
     public ChunkResolution ChunkResolution { get; }
     public VecI DocumentSize { get; set; }
+    
 
     public bool IsDisposed { get; private set; }
     

+ 75 - 35
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -227,7 +227,7 @@ internal class DocumentUpdater
     {
         doc.ReferenceLayerHandler.SetReferenceLayer(info.ImagePbgra8888Bytes, info.ImageSize, info.Shape);
     }
-    
+
     private void ProcessReferenceLayerTopMost(ReferenceLayerTopMost_ChangeInfo info)
     {
         doc.ReferenceLayerHandler.SetReferenceLayerTopMost(info.IsTopMost);
@@ -255,6 +255,7 @@ internal class DocumentUpdater
             oldMember.Selection = StructureMemberSelectionType.None;
             //oldMember.OnPropertyChanged(nameof(oldMember.Selection));
         }
+
         doc.ClearSoftSelectedMembers();
     }
 
@@ -273,12 +274,13 @@ internal class DocumentUpdater
         IStructureMemberHandler? member = doc.StructureHelper.Find(info.Id);
         if (member is null || member.Selection == StructureMemberSelectionType.Hard)
             return;
-        
+
         if (doc.SelectedStructureMember is { } oldMember)
         {
             oldMember.Selection = StructureMemberSelectionType.None;
             //oldMember.OnPropertyChanged(nameof(oldMember.Selection));
         }
+
         member.Selection = StructureMemberSelectionType.Hard;
         //member.OnPropertyChanged(nameof(member.Selection));
         doc.SetSelectedMember(member);
@@ -394,7 +396,7 @@ internal class DocumentUpdater
         memberVM.SetHasMask(info.HasMask);
         memberVM.SetMaskIsVisible(info.MaskIsVisible);
         memberVM.SetBlendMode(info.BlendMode);
-        
+
         //parentFolderVM.Children.Insert(info.Index, memberVM);
 
         /*if (info is CreateFolder_ChangeInfo folderInfo)
@@ -454,81 +456,105 @@ internal class DocumentUpdater
 
     private void ProcessMoveStructureMember(MoveStructureMember_ChangeInfo info)
     {
-         // TODO: uh why is this empty, find out why
+        // TODO: uh why is this empty, find out why
     }
-    
+
     private void ProcessToggleOnionSkinning(ToggleOnionSkinning_PassthroughAction info)
     {
         doc.AnimationHandler.SetOnionSkinning(info.IsOnionSkinningEnabled);
     }
-    
+
     private void ProcessPlayAnimation(SetPlayingState_PassthroughAction info)
     {
         doc.AnimationHandler.SetPlayingState(info.Play);
     }
-    
+
     private void ProcessCreateRasterKeyFrame(CreateRasterKeyFrame_ChangeInfo info)
     {
-        doc.AnimationHandler.AddKeyFrame(new RasterKeyFrameViewModel(info.TargetLayerGuid, info.Frame, 1, info.KeyFrameId, 
+        doc.AnimationHandler.AddKeyFrame(new RasterKeyFrameViewModel(info.TargetLayerGuid, info.Frame, 1,
+            info.KeyFrameId,
             (DocumentViewModel)doc, helper));
     }
-    
+
     private void ProcessDeleteKeyFrame(DeleteKeyFrame_ChangeInfo info)
     {
         doc.AnimationHandler.RemoveKeyFrame(info.DeletedKeyFrameId);
     }
-    
+
     private void ProcessActiveFrame(SetActiveFrame_PassthroughAction info)
     {
         doc.AnimationHandler.SetActiveFrame(info.Frame);
     }
-    
+
     private void ProcessKeyFrameLength(KeyFrameLength_ChangeInfo info)
     {
         doc.AnimationHandler.SetFrameLength(info.KeyFrameGuid, info.StartFrame, info.Duration);
     }
-    
+
     private void ProcessKeyFrameVisibility(KeyFrameVisibility_ChangeInfo info)
     {
         doc.AnimationHandler.SetKeyFrameVisibility(info.KeyFrameId, info.IsVisible);
     }
-    
+
     private void ProcessAddSelectedKeyFrame(AddSelectedKeyFrame_PassthroughAction info)
     {
         doc.AnimationHandler.AddSelectedKeyFrame(info.KeyFrameGuid);
     }
-    
+
     private void ProcessRemoveSelectedKeyFrame(RemoveSelectedKeyFrame_PassthroughAction info)
     {
         doc.AnimationHandler.RemoveSelectedKeyFrame(info.KeyFrameGuid);
     }
-    
+
     private void ClearSelectedKeyFrames(ClearSelectedKeyFrames_PassthroughAction info)
     {
         doc.AnimationHandler.ClearSelectedKeyFrames();
     }
-    
+
     private void ProcessCreateNode<T>(CreateNode_ChangeInfo info) where T : NodeViewModel, new()
     {
         T node = new T()
         {
-            InternalName = info.InternalName,
-            Id = info.Id,
-            Document = (DocumentViewModel)doc,
-            Internals = helper
+            InternalName = info.InternalName, Id = info.Id, Document = (DocumentViewModel)doc, Internals = helper
         };
 
         node.SetName(info.NodeName);
         node.SetPosition(info.Position);
-        
+
         List<INodePropertyHandler> inputs = CreateProperties(info.Inputs, node, true);
         List<INodePropertyHandler> outputs = CreateProperties(info.Outputs, node, false);
         node.Inputs.AddRange(inputs);
         node.Outputs.AddRange(outputs);
         doc.NodeGraphHandler.AddNode(node);
+
+        node.Metadata = info.Metadata;
+
+        AddZoneIfNeeded(info, node);
     }
-    
-    private List<INodePropertyHandler> CreateProperties(ImmutableArray<NodePropertyInfo> source, NodeViewModel node, bool isInput)
+
+    private void AddZoneIfNeeded<T>(CreateNode_ChangeInfo info, T node) where T : NodeViewModel, new()
+    {
+        if (node.Metadata?.PairNodeGuid != null)
+        {
+            if (node.Metadata.PairNodeGuid == Guid.Empty) return;
+            
+            INodeHandler otherNode = doc.NodeGraphHandler.AllNodes.FirstOrDefault(x => x.Id == node.Metadata.PairNodeGuid);
+            if (otherNode != null)
+            {
+                bool zoneExists =
+                    doc.NodeGraphHandler.Frames.Any(x => x is NodeZoneViewModel zone && zone.Nodes.Contains(node));
+
+                if (!zoneExists)
+                {
+                    doc.NodeGraphHandler.AddZone(Guid.NewGuid(), $"PixiEditor.{info.Metadata.ZoneUniqueName}", node.Id,
+                        node.Metadata.PairNodeGuid.Value);
+                }
+            }
+        }
+    }
+
+    private List<INodePropertyHandler> CreateProperties(ImmutableArray<NodePropertyInfo> source, NodeViewModel node,
+        bool isInput)
     {
         List<INodePropertyHandler> inputs = new();
         foreach (var input in source)
@@ -541,16 +567,28 @@ internal class DocumentUpdater
             prop.InternalSetValue(input.InputValue);
             inputs.Add(prop);
         }
-        
+
         return inputs;
     }
-    
+
     private void ProcessDeleteNode(DeleteNode_ChangeInfo info)
     {
+        foreach (var frame in doc.NodeGraphHandler.Frames)
+        {
+            if (frame is NodeZoneViewModel zone)
+            {
+                if (zone.Nodes.Any(x => x.Id == info.Id))
+                {
+                    doc.NodeGraphHandler.RemoveFrame(zone.Id);
+                    break;
+                }
+            }
+        }
+
         doc.NodeGraphHandler.RemoveConnections(info.Id);
         doc.NodeGraphHandler.RemoveNode(info.Id);
     }
-    
+
     private void ProcessCreateNodeFrame(CreateNodeFrame_ChangeInfo info)
     {
         doc.NodeGraphHandler.AddFrame(info.Id, info.NodeIds);
@@ -568,7 +606,9 @@ internal class DocumentUpdater
 
     private void ProcessConnectProperty(ConnectProperty_ChangeInfo info)
     {
-        NodeViewModel outputNode = info.OutputNodeId.HasValue ? doc.StructureHelper.FindNode<NodeViewModel>(info.OutputNodeId.Value) : null;
+        NodeViewModel outputNode = info.OutputNodeId.HasValue
+            ? doc.StructureHelper.FindNode<NodeViewModel>(info.OutputNodeId.Value)
+            : null;
         NodeViewModel inputNode = doc.StructureHelper.FindNode<NodeViewModel>(info.InputNodeId);
 
         if (inputNode != null && outputNode != null)
@@ -580,10 +620,10 @@ internal class DocumentUpdater
                 InputProperty = inputNode.FindInputProperty(info.InputProperty),
                 OutputProperty = outputNode.FindOutputProperty(info.OutputProperty)
             };
-            
+
             doc.NodeGraphHandler.SetConnection(connection);
         }
-        else if(info.OutputProperty == null)
+        else if (info.OutputProperty == null)
         {
             doc.NodeGraphHandler.RemoveConnection(info.InputNodeId, info.InputProperty);
         }
@@ -594,32 +634,32 @@ internal class DocumentUpdater
 #endif
         }
     }
-    
+
     private void ProcessNodePosition(NodePosition_ChangeInfo info)
     {
         NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
         node.SetPosition(info.NewPosition);
     }
-    
+
     private void ProcessNodePropertyValueUpdated(PropertyValueUpdated_ChangeInfo info)
     {
         NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
         var property = node.FindInputProperty(info.Property);
-        
+
         property.InternalSetValue(info.Value);
     }
-    
+
     private void ProcessNodeName(NodeName_ChangeInfo info)
     {
         NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
         node.SetName(info.NewName);
     }
-    
+
     private void ProcessFrameRate(FrameRate_ChangeInfo info)
     {
         doc.AnimationHandler.SetFrameRate(info.NewFrameRate);
     }
-    
+
     private void ProcessSetOnionFrames(OnionFrames_ChangeInfo info)
     {
         doc.AnimationHandler.SetOnionFrames(info.OnionFrames, info.Opacity);

+ 2 - 0
src/PixiEditor/Models/Handlers/INodeHandler.cs

@@ -2,6 +2,7 @@
 using System.ComponentModel;
 using ChunkyImageLib;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Models.Structures;
 using PixiEditor.Numerics;
@@ -13,6 +14,7 @@ public interface INodeHandler : INotifyPropertyChanged
     public Guid Id { get; }
     public string NodeNameBindable { get; set; }
     public string InternalName { get; }
+    public NodeMetadata Metadata { get; set; }
     public ObservableRangeCollection<INodePropertyHandler> Inputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
     public Texture? ResultPreview { get; set; }

+ 1 - 1
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -731,7 +731,7 @@ internal class MemberPreviewUpdater
 
     private void RenderNodePreviews(List<IRenderInfo> infos)
     {
-        using RenderingContext previewContext = new(doc.AnimationHandler.ActiveFrameTime, VecI.Zero, ChunkResolution.Full, doc.SizeBindable);
+        using RenderingContext previewContext = new(doc.AnimationHandler.ActiveFrameTime,  VecI.Zero, ChunkResolution.Full, doc.SizeBindable);
 
         var outputNode = internals.Tracker.Document.NodeGraph.OutputNode;
         

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

@@ -236,7 +236,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         {
             Guid startId = Guid.NewGuid();
             Guid endId = Guid.NewGuid();
-            changes.Add(new CreateNodePair_Action(startId, endId, Guid.NewGuid(), nodeType));
+            changes.Add(new CreateNodePair_Action(startId, endId, nodeType));
 
             if (pos != default)
             {
@@ -263,14 +263,14 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
 
     public void RemoveNodes(Guid[] selectedNodes)
     {
-        IAction[] actions = new IAction[selectedNodes.Length];
+        List<IAction> actions = new(); 
 
         for (int i = 0; i < selectedNodes.Length; i++)
         {
-            actions[i] = new DeleteNode_Action(selectedNodes[i]);
+            actions.Add(new DeleteNode_Action(selectedNodes[i]));
         }
 
-        Internals.ActionAccumulator.AddFinishedActions(actions);
+        Internals.ActionAccumulator.AddFinishedActions(actions.ToArray());
     }
 
     // TODO: Remove this

+ 3 - 0
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.DocumentModels;
@@ -45,6 +46,8 @@ internal class NodeViewModel : ObservableObject, INodeHandler
     }
 
     public string InternalName { get; init; }
+    
+    public NodeMetadata? Metadata { get; set; }
 
     public VecD PositionBindable
     {

+ 2 - 2
src/PixiEditor/ViewModels/Nodes/NodeZoneViewModel.cs

@@ -12,8 +12,8 @@ public sealed class NodeZoneViewModel : NodeFrameViewModelBase
     {
         InternalName = internalName;
         
-        this.start = start;
-        this.end = end;
+        this.start = start.Metadata.IsPairNodeStart ? start : end;
+        this.end = start.Metadata.IsPairNodeStart ? end : start;
         
         CalculateBounds();
     }

+ 21 - 2
src/PixiEditor/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs

@@ -18,9 +18,28 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
         Key = Key.Delete, ShortcutContext = typeof(NodeGraphDockViewModel), AnalyticsTrack = true)]
     public void DeleteSelectedNodes()
     {
-        Guid[] selectedNodes = Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.AllNodes
-            .Where(x => x.IsSelected).Select(x => x.Id).ToArray();
+        var nodes = Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.AllNodes
+            .Where(x => x.IsSelected).ToList();
         
+        if (nodes == null || nodes.Count == 0)
+            return;
+        
+        for (var i = 0; i < nodes.Count; i++)
+        {
+            var node = nodes[i];
+            if(node.Metadata?.PairNodeGuid == null) continue;
+            
+            INodeHandler otherNode = Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.AllNodes
+                .FirstOrDefault(x => x.Id == node.Metadata.PairNodeGuid);
+            
+            if (otherNode != null && !nodes.Contains(otherNode))
+            {
+                nodes.Add(otherNode);
+            }
+        }
+        
+        Guid[] selectedNodes = nodes.Select(x => x.Id).ToArray();
+
         if (selectedNodes == null || selectedNodes.Length == 0)
             return;