Browse Source

Merge pull request #743 from PixiEditor/structure-node-autopositions

Structure node positions on operations
Krzysztof Krysiński 6 months ago
parent
commit
29bda6f59a

+ 32 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -44,7 +44,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected internal bool IsDisposed => _isDisposed;
     private bool _isDisposed;
-    
+
     public void Execute(RenderContext context)
     {
         ExecuteInternal(context);
@@ -112,6 +112,36 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
     }
 
+    public void TraverseBackwards(Func<IReadOnlyNode, IReadOnlyNode?, IInputProperty, bool> action)
+    {
+        var visited = new HashSet<IReadOnlyNode>();
+        var queueNodes = new Queue<(IReadOnlyNode, IReadOnlyNode, IInputProperty)>();
+        queueNodes.Enqueue((this, null, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add((node.Item1)))
+            {
+                continue;
+            }
+
+            if (!action(node.Item1, node.Item2, node.Item3))
+            {
+                return;
+            }
+
+            foreach (var inputProperty in node.Item1.InputProperties)
+            {
+                if (inputProperty.Connection != null)
+                {
+                    queueNodes.Enqueue((inputProperty.Connection.Node, node.Item1, inputProperty));
+                }
+            }
+        }
+    }
+
     public void TraverseBackwards(Func<IReadOnlyNode, bool> action)
     {
         var visited = new HashSet<IReadOnlyNode>();
@@ -461,7 +491,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
         return new None();
     }
-    
+
     private void InvokeConnectionsChanged()
     {
         ConnectionsChanged?.Invoke();

+ 87 - 7
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -9,6 +9,7 @@ using PixiEditor.ChangeableDocument.Changes.Structure;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
@@ -29,7 +30,7 @@ public static class NodeOperations
             INodeFactory factory = (INodeFactory)Activator.CreateInstance(factoryType);
             allFactories.Add(factory.NodeType, factory);
         }
-        
+
         nodeMap = new Dictionary<string, Type>();
         var nodeTypes = typeof(Node).Assembly.GetTypes().Where(x =>
                 x.IsSubclassOf(typeof(Node)) && x is { IsAbstract: false, IsInterface: false })
@@ -55,7 +56,7 @@ public static class NodeOperations
     {
         return nodeMap.TryGetValue(nodeUniqueName, out nodeType);
     }
-    
+
     public static Node CreateNode(Type nodeType, IReadOnlyDocument target, params object[] optionalParameters)
     {
         Node node = null;
@@ -71,12 +72,12 @@ public static class NodeOperations
         return node;
     }
 
-    public static List<ConnectProperty_ChangeInfo> AppendMember(
+    public static List<IChangeInfo> AppendMember(
         InputProperty<Painter?> parentInput,
         OutputProperty<Painter> toAddOutput,
         InputProperty<Painter> toAddInput, Guid memberId)
     {
-        List<ConnectProperty_ChangeInfo> changes = new();
+        List<IChangeInfo> changes = new();
         IOutputProperty? previouslyConnected = null;
         if (parentInput.Connection != null)
         {
@@ -94,7 +95,7 @@ public static class NodeOperations
 
         changes.Add(new ConnectProperty_ChangeInfo(memberId, parentInput.Node.Id,
             toAddOutput.InternalPropertyName, parentInput.InternalPropertyName));
-        
+
         return changes;
     }
 
@@ -113,6 +114,7 @@ public static class NodeOperations
             foreach (var input in connections)
             {
                 output.ConnectTo(input);
+
                 changes.Add(new ConnectProperty_ChangeInfo(output.Node.Id, input.Node.Id,
                     output.InternalPropertyName, input.InternalPropertyName));
             }
@@ -133,6 +135,84 @@ public static class NodeOperations
         return changes;
     }
 
+    public static List<IChangeInfo> AdjustPositionsAfterAppend(Node member, Node appendedTo, Node? previouslyConnected,
+        out Dictionary<Guid, VecD> originalPositions)
+    {
+        List<IChangeInfo> changes = new();
+        Dictionary<Guid, VecD> originalPositionDict = new();
+
+        member.Position = new VecD(appendedTo.Position.X - 250, appendedTo.Position.Y);
+
+        changes.Add(new NodePosition_ChangeInfo(member.Id, member.Position));
+
+        previouslyConnected?.TraverseBackwards((aNode, previousNode, _) =>
+        {
+            if (aNode is Node toMove)
+            {
+                originalPositionDict.Add(toMove.Id, toMove.Position);
+                var y = toMove.Position.Y;
+                toMove.Position = (previousNode?.Position ?? member.Position) - new VecD(250, 0);
+                toMove.Position = new VecD(toMove.Position.X, y);
+                changes.Add(new NodePosition_ChangeInfo(toMove.Id, toMove.Position));
+            }
+
+            return true;
+        });
+
+        originalPositions = originalPositionDict;
+        return changes;
+    }
+
+    public static List<IChangeInfo> AdjustPositionsBeforeAppend(Node member, Node appendedTo,
+        out Dictionary<Guid, VecD> originalPositions)
+    {
+        List<IChangeInfo> changes = new();
+        Dictionary<Guid, VecD> originalPositionDict = new();
+
+        member.TraverseBackwards((aNode, previousNode, _) =>
+        {
+            if (aNode is Node toMove)
+            {
+                originalPositionDict.Add(toMove.Id, toMove.Position);
+                var y = toMove.Position.Y;
+                VecD pos = member.Position + new VecD(250, 0);
+                if (previousNode != null)
+                {
+                    pos = previousNode.Position - new VecD(250, 0);
+                }
+
+                toMove.Position = pos;
+                toMove.Position = new VecD(toMove.Position.X, y);
+                changes.Add(new NodePosition_ChangeInfo(toMove.Id, toMove.Position));
+                
+                if(aNode == appendedTo) return false;
+            }
+
+            return true;
+        });
+
+        member.Position = new VecD(appendedTo.Position.X - 250, appendedTo.Position.Y);
+        changes.Add(new NodePosition_ChangeInfo(member.Id, member.Position));
+
+        originalPositions = originalPositionDict;
+        return changes;
+    }
+
+    public static List<IChangeInfo> RevertPositions(Dictionary<Guid, VecD> positions, IReadOnlyDocument target)
+    {
+        List<IChangeInfo> changes = new();
+        foreach (var (guid, position) in positions)
+        {
+            var node = target.FindNode(guid) as Node;
+            if (node == null) continue;
+
+            node.Position = position;
+            changes.Add(new NodePosition_ChangeInfo(guid, position));
+        }
+
+        return changes;
+    }
+
     public static ConnectionsData CreateConnectionsData(IReadOnlyNode node)
     {
         var originalOutputConnections = new Dictionary<PropertyConnection, List<PropertyConnection>>();
@@ -152,7 +232,7 @@ public static class NodeOperations
                 (new PropertyConnection(x.Node.Id, x.InternalPropertyName),
                     new PropertyConnection(x.Connection?.Node.Id, x.Connection?.InternalPropertyName)))
             .ToList();
-        
+
         return new ConnectionsData(originalOutputConnections, originalInputConnections);
     }
 
@@ -246,7 +326,7 @@ public static class NodeOperations
                     value = expressionVariable.GetConstant();
                 }
             }
-            
+
             changes.Add(new PropertyValueUpdated_ChangeInfo(copy.Id, input.InternalPropertyName, value));
         }
 

+ 21 - 14
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
@@ -13,8 +14,9 @@ internal class CreateStructureMember_Change : Change
 
     private Guid parentGuid;
     private Type structureMemberOfType;
-    
+
     private ConnectionsData? connectionsData;
+    private Dictionary<Guid, VecD> originalPositions;
 
     [GenerateMakeChangeAction]
     public CreateStructureMember_Change(Guid parent, Guid newGuid,
@@ -27,25 +29,28 @@ internal class CreateStructureMember_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        if(structureMemberOfType == null || structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface || !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
+        if (structureMemberOfType == null || structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface ||
+            !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
             return false;
-        
+
         return target.TryFindNode<Node>(parentGuid, out _);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, bool firstApply,
         out bool ignoreInUndo)
     {
-        StructureNode member = (StructureNode)NodeOperations.CreateNode(structureMemberOfType, document); 
+        StructureNode member = (StructureNode)NodeOperations.CreateNode(structureMemberOfType, document);
         member.Id = newMemberGuid;
 
         document.TryFindNode<Node>(parentGuid, out var parentNode);
 
         List<IChangeInfo> changes = new() { CreateChangeInfo(member) };
-        
-        InputProperty<Painter> targetInput = parentNode.InputProperties.FirstOrDefault(x => 
+
+        InputProperty<Painter> targetInput = parentNode.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
-        
+
+        var previouslyConnected = targetInput.Connection;
+
         if (member is FolderNode folder)
         {
             document.NodeGraph.AddNode(member);
@@ -54,11 +59,12 @@ internal class CreateStructureMember_Change : Change
         else
         {
             document.NodeGraph.AddNode(member);
-            List<ConnectProperty_ChangeInfo> connectPropertyChangeInfo =
+            var connectPropertyChangeInfo =
                 NodeOperations.AppendMember(targetInput, member.Output, member.Background, member.Id);
             changes.AddRange(connectPropertyChangeInfo);
         }
-
+        
+        changes.AddRange(NodeOperations.AdjustPositionsAfterAppend(member, targetInput.Node, previouslyConnected?.Node as Node, out originalPositions));
 
         ignoreInUndo = false;
 
@@ -79,7 +85,7 @@ internal class CreateStructureMember_Change : Change
     {
         var container = document.FindNodeOrThrow<Node>(parentGuid);
 
-       InputProperty<Painter> backgroundInput = container.InputProperties.FirstOrDefault(x => 
+        InputProperty<Painter> backgroundInput = container.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
 
         StructureNode child = document.FindMemberOrThrow(newMemberGuid);
@@ -98,15 +104,16 @@ internal class CreateStructureMember_Change : Change
                 backgroundInput.InternalPropertyName);
             changes.Add(change);
         }
+        
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, document));
 
         return changes;
     }
 
-    private static void AppendFolder(InputProperty<Painter> backgroundInput, FolderNode folder, List<IChangeInfo> changes)
+    private static void AppendFolder(InputProperty<Painter> backgroundInput, FolderNode folder,
+        List<IChangeInfo> changes)
     {
         var appened = NodeOperations.AppendMember(backgroundInput, folder.Output, folder.Background, folder.Id);
         changes.AddRange(appened);
     }
-
-    
 }

+ 7 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs

@@ -1,5 +1,6 @@
 using System.Collections.Immutable;
 using System.Collections.ObjectModel;
+using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
@@ -19,6 +20,7 @@ internal class DuplicateFolder_Change : Change
 
     private ConnectionsData? connectionsData;
     private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
+    private Dictionary<Guid, VecD> originalPositions;
 
     [GenerateMakeChangeAction]
     public DuplicateFolder_Change(Guid folderGuid, Guid newGuid, ImmutableList<Guid>? childGuids)
@@ -62,9 +64,12 @@ internal class DuplicateFolder_Change : Change
         List<IChangeInfo> operations = new();
 
         target.NodeGraph.AddNode(clone);
+        
+        var previousConnection = targetInput.Connection;
 
         operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node, previousConnection?.Node as Node, out originalPositions));
 
         DuplicateContent(target, clone, existingLayer, operations);
 
@@ -105,6 +110,8 @@ internal class DuplicateFolder_Change : Change
                 NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
         }
 
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
         return changes;
     }
 

+ 10 - 1
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
@@ -12,6 +13,7 @@ internal class DuplicateLayer_Change : Change
     private Guid duplicateGuid;
     
     private ConnectionsData? connectionsData;
+    private Dictionary<Guid, VecD> originalPositions;
 
     [GenerateMakeChangeAction]
     public DuplicateLayer_Change(Guid layerGuid, Guid newGuid)
@@ -42,6 +44,8 @@ internal class DuplicateLayer_Change : Change
         InputProperty<Painter?> targetInput = parent.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter) &&
             x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
+        
+        var previousConnection = targetInput.Connection;
 
         List<IChangeInfo> operations = new();
 
@@ -51,6 +55,9 @@ internal class DuplicateLayer_Change : Change
         
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
 
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node,
+            previousConnection?.Node as Node, out originalPositions));
+
         ignoreInUndo = false;
 
         return operations;
@@ -74,6 +81,8 @@ internal class DuplicateLayer_Change : Change
             changes.AddRange(NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
         }
         
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+        
         return changes;
     }
 }

+ 99 - 13
src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

@@ -1,6 +1,8 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 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.Changes.NodeGraph;
 
@@ -14,8 +16,9 @@ internal class MoveStructureMember_Change : Change
 
     private Guid originalFolderGuid;
 
-    private ConnectionsData originalConnections; 
-    
+    private ConnectionsData originalConnections;
+    private Dictionary<Guid, VecD> originalPositions;
+
     private bool putInsideFolder;
 
 
@@ -34,20 +37,22 @@ internal class MoveStructureMember_Change : Change
         if (member is null || targetFolder is null)
             return false;
 
-        originalConnections = NodeOperations.CreateConnectionsData(member); 
-          
+        originalConnections = NodeOperations.CreateConnectionsData(member);
+
         return true;
     }
 
-    private static List<IChangeInfo> Move(Document document, Guid sourceNodeGuid, Guid targetNodeGuid, bool putInsideFolder)
+    private static List<IChangeInfo> Move(Document document, Guid sourceNodeGuid, Guid targetNodeGuid,
+        bool putInsideFolder, out Dictionary<Guid, VecD> originalPositions)
     {
         var sourceNode = document.FindMember(sourceNodeGuid);
         var targetNode = document.FindNode(targetNodeGuid);
+        originalPositions = null;
         if (sourceNode is null || targetNode is not IRenderInput backgroundInput)
             return [];
 
         List<IChangeInfo> changes = new();
-        
+
         Guid oldBackgroundId = sourceNode.Background.Node.Id;
 
         InputProperty<Painter?> inputProperty = backgroundInput.Background;
@@ -58,12 +63,43 @@ internal class MoveStructureMember_Change : Change
         }
 
         MoveStructureMember_ChangeInfo changeInfo = new(sourceNodeGuid, oldBackgroundId, targetNodeGuid);
+
+        var previouslyConnected = inputProperty.Connection;
+
+        bool isMovingBelow = false;
         
+        inputProperty.Node.TraverseForwards(x =>
+        {
+            if (x.Id == sourceNodeGuid)
+            {
+                isMovingBelow = true;
+                return false;
+            }
+            
+            return true;
+        });
+
+        if (isMovingBelow)
+        {
+            changes.AddRange(NodeOperations.AdjustPositionsBeforeAppend(sourceNode, inputProperty.Node, out originalPositions));
+        }
+
         changes.AddRange(NodeOperations.DetachStructureNode(sourceNode));
         changes.AddRange(NodeOperations.AppendMember(inputProperty, sourceNode.Output,
             sourceNode.Background,
             sourceNode.Id));
-        
+
+        if (!isMovingBelow)
+        {
+            changes.AddRange(NodeOperations.AdjustPositionsAfterAppend(sourceNode, inputProperty.Node,
+                previouslyConnected?.Node as Node, out originalPositions));
+        }
+
+        if (targetNode is FolderNode)
+        {
+            changes.AddRange(AdjustPutIntoFolderPositions(targetNode, originalPositions));
+        }
+
         changes.Add(changeInfo);
 
         return changes;
@@ -72,7 +108,7 @@ internal class MoveStructureMember_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
     {
-        var changes = Move(target, memberGuid, targetNodeGuid, putInsideFolder);
+        var changes = Move(target, memberGuid, targetNodeGuid, putInsideFolder, out originalPositions);
         ignoreInUndo = false;
         return changes;
     }
@@ -82,14 +118,64 @@ internal class MoveStructureMember_Change : Change
         StructureNode member = target.FindMember(memberGuid);
 
         List<IChangeInfo> changes = new List<IChangeInfo>();
-        
+
         MoveStructureMember_ChangeInfo changeInfo = new(memberGuid, targetNodeGuid, originalFolderGuid);
-        
+
         changes.AddRange(NodeOperations.DetachStructureNode(member));
         changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, member, target.NodeGraph));
-        
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
         changes.Add(changeInfo);
-        
+
+        return changes;
+    }
+    
+    private static List<IChangeInfo> AdjustPutIntoFolderPositions(Node targetNode, Dictionary<Guid, VecD> originalPositions)
+    {
+        List<IChangeInfo> changes = new();
+
+        if (targetNode is FolderNode folder)
+        {
+            folder.Content.Connection.Node.TraverseBackwards(contentNode =>
+            {
+                if (contentNode is Node node)
+                {
+                    if (!originalPositions.ContainsKey(node.Id))
+                    {
+                        originalPositions[node.Id] = node.Position;
+                    }
+                    
+                    node.Position = new VecD(node.Position.X, folder.Position.Y + 250);
+                    changes.Add(new NodePosition_ChangeInfo(node.Id, node.Position));
+                }
+                
+                return true;
+            });
+            
+            folder.Background.Connection?.Node.TraverseBackwards(bgNode =>
+            {
+                if (bgNode is Node node)
+                {
+                    if (!originalPositions.ContainsKey(node.Id))
+                    {
+                        originalPositions[node.Id] = node.Position;
+                    }
+
+                    double pos = folder.Position.Y;
+
+                    if (folder.Content.Connection != null)
+                    {
+                        pos -= 250;
+                    }
+                    
+                    node.Position = new VecD(node.Position.X, pos);
+                    changes.Add(new NodePosition_ChangeInfo(node.Id, node.Position));
+                }
+                
+                return true;
+            });
+        }
+
         return changes;
     }
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs

@@ -46,6 +46,7 @@ internal class RasterizeMember_Change : Change
         
         ImageLayerNode imageLayer = new ImageLayerNode(target.Size, target.ProcessingColorSpace);
         imageLayer.MemberName = node.DisplayName;
+        imageLayer.Position = node.Position;
 
         target.NodeGraph.AddNode(imageLayer);
         

+ 9 - 6
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -148,11 +148,12 @@ internal class DocumentViewModelBuilder
 
             data?.Add(builder);
         }
-        
+
         TryAddMissingKeyFrames(root, data, documentGraph);
     }
 
-    private static void TryAddMissingKeyFrames(List<KeyFrameGroup> groups, List<KeyFrameBuilder>? data, NodeGraph documentGraph)
+    private static void TryAddMissingKeyFrames(List<KeyFrameGroup> groups, List<KeyFrameBuilder>? data,
+        NodeGraph documentGraph)
     {
         if (data == null)
         {
@@ -164,15 +165,15 @@ internal class DocumentViewModelBuilder
             if (node.KeyFrames.Length > 1 && data.All(x => x.NodeId != node.Id))
             {
                 GroupKeyFrameBuilder builder = new GroupKeyFrameBuilder()
-                .WithNodeId(node.Id);
-                
+                    .WithNodeId(node.Id);
+
                 foreach (var keyFrame in node.KeyFrames)
                 {
                     builder.WithChild<KeyFrameBuilder>(x => x
                         .WithKeyFrameId(keyFrame.Id)
                         .WithNodeId(node.Id));
-                }   
-                
+                }
+
                 data.Add(builder);
             }
         }
@@ -351,6 +352,7 @@ internal class NodeGraphBuilder
     {
         this.WithNodeOfType(typeof(ImageLayerNode))
             .WithName(name)
+            .WithPosition(new Vector2 { X = -250, Y = 0 })
             .WithId(AllNodes.Count)
             .WithKeyFrames(
             [
@@ -372,6 +374,7 @@ internal class NodeGraphBuilder
     {
         this.WithNodeOfType(typeof(ImageLayerNode))
             .WithName(name)
+            .WithPosition(new Vector2 { X = -250, Y = 0 })
             .WithId(AllNodes.Count)
             .WithKeyFrames(
             [

+ 62 - 22
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -1,4 +1,5 @@
-using System.Collections.ObjectModel;
+using System.Collections;
+using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.ComponentModel;
 using System.Windows.Input;
@@ -234,31 +235,30 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
             nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged += NodeItems_CollectionChanged;
             nodeViewsCache = nodeItemsControl.ItemsPanelRoot.Children.ToList();
+            HandleNodesAdded(nodeViewsCache);
         });
     }
 
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged -= NodeItems_CollectionChanged;
+    }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        if (nodeItemsControl is { ItemsPanelRoot: not null })
+        {
+            nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged += NodeItems_CollectionChanged;
+            nodeViewsCache = nodeItemsControl.ItemsPanelRoot.Children.ToList();
+            HandleNodesAdded(nodeViewsCache);
+        }
+    }
+
     private void NodeItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
     {
         if (e.Action == NotifyCollectionChangedAction.Add)
         {
-            foreach (Control control in e.NewItems)
-            {
-                if (control is not ContentPresenter presenter)
-                {
-                    continue;
-                }
-
-                nodeViewsCache.Add(presenter);
-                presenter.PropertyChanged += OnPresenterPropertyChanged;
-                if (presenter.Child == null)
-                {
-                    continue;
-                }
-
-                NodeView nodeView = (NodeView)presenter.Child;
-                nodeView.PropertyChanged += NodeView_PropertyChanged;
-                nodeView.Node.PropertyChanged += Node_PropertyChanged;
-            }
+            HandleNodesAdded(e.NewItems);
         }
         else if (e.Action == NotifyCollectionChangedAction.Remove)
         {
@@ -272,15 +272,18 @@ internal class NodeGraphView : Zoombox.Zoombox
                 nodeViewsCache.Remove(presenter);
 
                 presenter.PropertyChanged -= OnPresenterPropertyChanged;
+                if (presenter.Content is NodeViewModel nvm)
+                {
+                    nvm.PropertyChanged -= Node_PropertyChanged;
+                }
+
                 if (presenter.Child == null)
                 {
                     continue;
                 }
 
-
                 NodeView nodeView = (NodeView)presenter.Child;
                 nodeView.PropertyChanged -= NodeView_PropertyChanged;
-                nodeView.Node.PropertyChanged -= Node_PropertyChanged;
             }
         }
         else if (e.Action == NotifyCollectionChangedAction.Reset)
@@ -289,6 +292,37 @@ internal class NodeGraphView : Zoombox.Zoombox
         }
     }
 
+    private void HandleNodesAdded(IList? items)
+    {
+        foreach (Control control in items)
+        {
+            if (control is not ContentPresenter presenter)
+            {
+                continue;
+            }
+
+            if (!nodeViewsCache.Contains(presenter))
+            {
+                nodeViewsCache.Add(presenter);
+            }
+
+            presenter.PropertyChanged += OnPresenterPropertyChanged;
+
+            if (presenter.Content is NodeViewModel nvm)
+            {
+                nvm.PropertyChanged += Node_PropertyChanged;
+            }
+
+            if (presenter.Child == null)
+            {
+                continue;
+            }
+
+            NodeView nodeView = (NodeView)presenter.Child;
+            nodeView.PropertyChanged += NodeView_PropertyChanged;
+        }
+    }
+
     private void OnPresenterPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
     {
         if (e.Property == ContentPresenter.ChildProperty)
@@ -602,6 +636,11 @@ internal class NodeGraphView : Zoombox.Zoombox
             {
                 ConnectionView connectionView = (ConnectionView)contentPresenter.FindDescendantOfType<ConnectionView>();
 
+                if (connectionView == null)
+                {
+                    continue;
+                }
+
                 if (connectionView.InputProperty == propertyView || connectionView.OutputProperty == propertyView)
                 {
                     connectionView.UpdateSocketPoints();
@@ -639,7 +678,8 @@ internal class NodeGraphView : Zoombox.Zoombox
             return;
         }
 
-        (INodePropertyHandler, INodePropertyHandler, INodePropertyHandler?) connection = (startConnectionProperty, null, null);
+        (INodePropertyHandler, INodePropertyHandler, INodePropertyHandler?) connection = (startConnectionProperty, null,
+            null);
         if (socket != null)
         {
             endConnectionNode = socket.Node;