Browse Source

Layer Structure diff algorithm is working

flabbet 1 year ago
parent
commit
be6bd6c416
30 changed files with 208 additions and 551 deletions
  1. 0 9
      src/Nodes/INodeGraph.cs
  2. 0 32
      src/Nodes/INodeProperty.cs
  3. 0 12
      src/Nodes/IReadOnlyNode.cs
  4. 0 35
      src/Nodes/InputProperty.cs
  5. 0 87
      src/Nodes/NodeGraph.cs
  6. 0 23
      src/Nodes/Nodes.csproj
  7. 0 25
      src/Nodes/Nodes/FolderNode.cs
  8. 0 35
      src/Nodes/Nodes/LayerNode.cs
  9. 0 44
      src/Nodes/Nodes/MergeNode.cs
  10. 0 50
      src/Nodes/Nodes/Node.cs
  11. 0 22
      src/Nodes/Nodes/OutputNode.cs
  12. 0 75
      src/Nodes/OutputProperty.cs
  13. 0 37
      src/Nodes/Program.cs
  14. BIN
      src/Nodes/test.png
  15. BIN
      src/Nodes/test2.png
  16. 4 3
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs
  17. 1 0
      src/PixiEditor.AvaloniaUI/Models/Handlers/IFolderHandler.cs
  18. 2 0
      src/PixiEditor.AvaloniaUI/Models/Handlers/INodeHandler.cs
  19. 15 2
      src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs
  20. 0 6
      src/PixiEditor.AvaloniaUI/ViewModels/Document/LayerViewModel.cs
  21. 10 11
      src/PixiEditor.AvaloniaUI/ViewModels/Document/NodeGraphViewModel.cs
  22. 1 0
      src/PixiEditor.AvaloniaUI/ViewModels/Document/StructureMemberViewModel.cs
  23. 93 0
      src/PixiEditor.AvaloniaUI/ViewModels/Document/StructureTree.cs
  24. 60 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeViewModel.cs
  25. 1 1
      src/PixiEditor.AvaloniaUI/Views/Layers/LayersManager.axaml
  26. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  27. 12 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  28. 5 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  29. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  30. 0 31
      src/PixiEditor.sln

+ 0 - 9
src/Nodes/INodeGraph.cs

@@ -1,9 +0,0 @@
-namespace Nodes;
-
-public interface INodeGraph
-{
-    public IReadOnlyCollection<IReadOnlyNode> AllNodes { get; }
-    public IReadOnlyNode OutputNode { get; }
-    public void AddNode(IReadOnlyNode node);
-    public void RemoveNode(IReadOnlyNode node);
-}

+ 0 - 32
src/Nodes/INodeProperty.cs

@@ -1,32 +0,0 @@
-namespace Nodes;
-
-public interface INodeProperty
-{
-    public string Name { get; }
-    public object Value { get; set; }
-    public IReadOnlyNode Node { get; }
-}
-
-public interface INodeProperty<T> : INodeProperty
-{
-    public new T Value { get; set; }
-}
-
-public interface IInputProperty : INodeProperty
-{
-    public IOutputProperty? Connection { get; set; }
-}
-
-public interface IOutputProperty : INodeProperty
-{
-    public void ConnectTo(IInputProperty property);
-    public void DisconnectFrom(IInputProperty property);
-}
-
-public interface IInputProperty<T> : IInputProperty, INodeProperty<T>
-{
-}
-
-public interface IOutputProperty<T> : IOutputProperty, INodeProperty<T>
-{
-}

+ 0 - 12
src/Nodes/IReadOnlyNode.cs

@@ -1,12 +0,0 @@
-namespace Nodes;
-
-public interface IReadOnlyNode
-{
-    public string Name { get; }
-    public IReadOnlyCollection<IInputProperty> InputProperties { get; }
-    public IReadOnlyCollection<IOutputProperty> OutputProperties { get; }
-    public IReadOnlyCollection<IReadOnlyNode> ConnectedNodes { get; }
-
-    public void Execute(int frame);
-    public bool Validate();
-}

+ 0 - 35
src/Nodes/InputProperty.cs

@@ -1,35 +0,0 @@
-using Nodes.Nodes;
-
-namespace Nodes;
-
-public class InputProperty : IInputProperty
-{
-    public string Name { get; }
-    public object Value { get; set; }
-    public Node Node { get; }
-    IReadOnlyNode INodeProperty.Node => Node;
-    
-    public IOutputProperty Connection { get; set; }
-    
-    internal InputProperty(Node node, string name, object defaultValue)
-    {
-        Name = name;
-        Value = defaultValue;
-        Node = node;
-    }
-
-}
-
-
-public class InputProperty<T> : InputProperty, IInputProperty<T>
-{
-    public new T Value
-    {
-        get => (T)base.Value;
-        set => base.Value = value;
-    }
-    
-    internal InputProperty(Node node, string name, T defaultValue) : base(node, name, defaultValue)
-    {
-    }
-}

+ 0 - 87
src/Nodes/NodeGraph.cs

@@ -1,87 +0,0 @@
-using Nodes.Nodes;
-using SkiaSharp;
-
-namespace Nodes;
-
-public class NodeGraph : INodeGraph
-{
-    private readonly List<Node> _nodes = new();
-    public IReadOnlyCollection<Node> Nodes => _nodes;
-    public OutputNode? OutputNode => Nodes.OfType<OutputNode>().FirstOrDefault();
-    
-    IReadOnlyCollection<IReadOnlyNode> INodeGraph.AllNodes => Nodes;
-    IReadOnlyNode INodeGraph.OutputNode => OutputNode;
-
-    public void AddNode(Node node)
-    {
-        if (Nodes.Contains(node))
-        {
-            return;
-        }
-        
-        _nodes.Add(node);
-    }
-    
-    public void RemoveNode(Node node)
-    {
-        if (!Nodes.Contains(node))
-        {
-            return;
-        }
-        
-        _nodes.Remove(node);
-    }
-    
-    public SKSurface? Execute()
-    {
-        if(OutputNode == null) return null;
-        
-        var queue = CalculateExecutionQueue(OutputNode);
-        
-        while (queue.Count > 0)
-        {
-            var node = queue.Dequeue();
-            
-            node.Execute(0);
-        }
-        
-        return OutputNode.Input.Value;
-    }
-
-    private Queue<IReadOnlyNode> CalculateExecutionQueue(OutputNode outputNode)
-    {
-        // backwards breadth-first search
-        var visited = new HashSet<IReadOnlyNode>();
-        var queueNodes = new Queue<IReadOnlyNode>();
-        List<IReadOnlyNode> finalQueue = new();
-        queueNodes.Enqueue(outputNode);
-        
-        while (queueNodes.Count > 0)
-        {
-            var node = queueNodes.Dequeue();
-            if (!visited.Add(node))
-            {
-                continue;
-            }
-            
-            finalQueue.Add(node);
-            
-            foreach (var input in node.InputProperties)
-            {
-                if (input.Connection == null)
-                {
-                    continue;
-                }
-                
-                queueNodes.Enqueue(input.Connection.Node);
-            }
-        }
-        
-        finalQueue.Reverse();
-        return new Queue<IReadOnlyNode>(finalQueue);
-    }
-
-    void INodeGraph.AddNode(IReadOnlyNode node) => AddNode((Node)node);
-
-    void INodeGraph.RemoveNode(IReadOnlyNode node) => RemoveNode((Node)node);
-}

+ 0 - 23
src/Nodes/Nodes.csproj

@@ -1,23 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-    <PropertyGroup>
-        <OutputType>Exe</OutputType>
-        <TargetFramework>net8.0</TargetFramework>
-        <ImplicitUsings>enable</ImplicitUsings>
-        <Nullable>enable</Nullable>
-    </PropertyGroup>
-
-    <ItemGroup>
-      <PackageReference Include="SkiaSharp" Version="2.88.8" />
-    </ItemGroup>
-
-    <ItemGroup>
-      <None Update="test.png">
-        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-      </None>
-      <None Update="test2.png">
-        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-      </None>
-    </ItemGroup>
-
-</Project>

+ 0 - 25
src/Nodes/Nodes/FolderNode.cs

@@ -1,25 +0,0 @@
-using SkiaSharp;
-
-namespace Nodes.Nodes;
-
-public class FolderNode : Node
-{
-    public InputProperty<SKSurface> Input { get; }
-    public OutputProperty<SKSurface> Output { get; }
-    
-    public FolderNode(string name) : base(name)
-    {
-        Input = CreateInput<SKSurface>("Input", null);
-        Output = CreateOutput<SKSurface>("Output", null);
-    }    
-    
-    public override bool Validate()
-    {
-        return true;
-    }
-    
-    public override void OnExecute(int frame)
-    {
-        Output.Value = Input.Value;
-    }
-}

+ 0 - 35
src/Nodes/Nodes/LayerNode.cs

@@ -1,35 +0,0 @@
-using SkiaSharp;
-
-namespace Nodes.Nodes;
-
-public class LayerNode : Node
-{
-    public InputProperty<SKSurface?> Background { get; }
-    public OutputProperty<SKSurface> Output { get; }
-    public SKSurface LayerImage { get; set; }
-    
-    public LayerNode(string name, SKSizeI size) : base(name)
-    {
-        Background = CreateInput<SKSurface>("Background", null);
-        Output = CreateOutput<SKSurface>("Image", SKSurface.Create(new SKImageInfo(size.Width, size.Height)));
-        LayerImage = SKSurface.Create(new SKImageInfo(size.Width, size.Height));
-    }
-
-    public override bool Validate()
-    {
-        return true;
-    }
-
-    public override void OnExecute(int frame)
-    {
-        using SKPaint paint = new SKPaint();
-        if (Background.Value != null)
-        {
-            Output.Value.Draw(Background.Value.Canvas, 0, 0, paint);       
-        }
-        
-        Output.Value.Canvas.DrawSurface(LayerImage, 0, 0, paint);
-    }
-
-   
-}

+ 0 - 44
src/Nodes/Nodes/MergeNode.cs

@@ -1,44 +0,0 @@
-using SkiaSharp;
-
-namespace Nodes.Nodes;
-
-public class MergeNode : Node
-{
-    public InputProperty<SKSurface?> Top { get; }
-    public InputProperty<SKSurface?> Bottom { get; }
-    public OutputProperty<SKSurface> Output { get; }
-    
-    public MergeNode(string name) : base(name)
-    {
-        Top = CreateInput<SKSurface>("Top", null);
-        Bottom = CreateInput<SKSurface>("Bottom", null);
-        Output = CreateOutput<SKSurface>("Output", null);
-    }
-    
-    public override bool Validate()
-    {
-        return Top.Value != null || Bottom.Value != null;
-    }
-
-    public override void OnExecute(int frame)
-    {
-        SKImage topSnapshot = Top.Value?.Snapshot();
-        SKImage bottomSnapshot = Bottom.Value?.Snapshot();
-        using SKPaint paint = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
-        
-        SKSizeI size = new SKSizeI(topSnapshot?.Width ?? bottomSnapshot.Width, topSnapshot?.Height ?? bottomSnapshot.Height);
-        
-        Output.Value = SKSurface.Create(new SKImageInfo(size.Width, size.Height));
-        using SKCanvas canvas = Output.Value.Canvas;
-        
-        if (bottomSnapshot != null)
-        {
-            canvas.DrawImage(bottomSnapshot, 0, 0, paint);
-        }
-        
-        if (topSnapshot != null)
-        {
-            canvas.DrawImage(topSnapshot, 0, 0, paint);
-        }
-    }
-}

+ 0 - 50
src/Nodes/Nodes/Node.cs

@@ -1,50 +0,0 @@
-namespace Nodes.Nodes;
-
-public abstract class Node(string name) : IReadOnlyNode
-{
-    private List<InputProperty> inputs = new();
-    private List<OutputProperty> outputs = new();
-    
-    private List<IReadOnlyNode> _connectedNodes = new();
-    
-    public string Name { get; } = name;
-    
-    public IReadOnlyCollection<InputProperty> InputProperties => inputs;
-    public IReadOnlyCollection<OutputProperty> OutputProperties => outputs;
-    public IReadOnlyCollection<IReadOnlyNode> ConnectedNodes => _connectedNodes;
-
-    IReadOnlyCollection<IInputProperty> IReadOnlyNode.InputProperties => inputs;
-    IReadOnlyCollection<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
-
-    public void Execute(int frame)
-    {
-        foreach (var output in outputs)
-        {
-            foreach (var connection in output.Connections)
-            {
-                connection.Value = output.Value;
-            }
-        }
-        
-        OnExecute(frame);
-    }
-
-    public abstract void OnExecute(int frame);
-    public abstract bool Validate();
-    
-    protected InputProperty<T> CreateInput<T>(string name, T defaultValue)
-    {
-        var property = new InputProperty<T>(this, name, defaultValue);
-        inputs.Add(property);
-        return property;
-    }
-    
-    protected OutputProperty<T> CreateOutput<T>(string name, T defaultValue)
-    {
-        var property = new OutputProperty<T>(this, name, defaultValue);
-        outputs.Add(property);
-        property.Connected += (input, _) => _connectedNodes.Add(input.Node);
-        property.Disconnected += (input, _) => _connectedNodes.Remove(input.Node);
-        return property;
-    }
-}

+ 0 - 22
src/Nodes/Nodes/OutputNode.cs

@@ -1,22 +0,0 @@
-using SkiaSharp;
-
-namespace Nodes.Nodes;
-
-public class OutputNode : Node
-{
-    public InputProperty<SKSurface?> Input { get; } 
-    public OutputNode(string name) : base(name)
-    {
-        Input = CreateInput<SKSurface>("Input", null);
-    }
-    
-    public override bool Validate()
-    {
-        return Input.Value != null;
-    }
-    
-    public override void OnExecute(int frame)
-    {
-        
-    }
-}

+ 0 - 75
src/Nodes/OutputProperty.cs

@@ -1,75 +0,0 @@
-using Nodes.Nodes;
-
-namespace Nodes;
-
-public delegate void InputConnectedEvent(IInputProperty input, IOutputProperty output);
-public class OutputProperty : IOutputProperty
-{
-    private List<IInputProperty> _connections = new();
-    private object _value;
-    public string Name { get; }
-    
-    public Node Node { get; }
-    IReadOnlyNode INodeProperty.Node => Node;
-
-    public object Value
-    {
-        get => _value;
-        set
-        {
-            _value = value;
-            foreach (var connection in Connections)
-            {
-                connection.Value = value;
-            }
-        }    
-    }
-
-    public IReadOnlyCollection<IInputProperty> Connections => _connections;
-    
-    public event InputConnectedEvent Connected;
-    public event InputConnectedEvent Disconnected;
-    
-    internal OutputProperty(Node node, string name, object defaultValue)
-    {
-        Name = name;
-        Value = defaultValue;
-        _connections = new List<IInputProperty>();
-        Node = node;
-    }
-    
-    public void ConnectTo(IInputProperty property)
-    {
-        if(Connections.Contains(property)) return;
-        
-        _connections.Add(property);
-        property.Connection = this;
-        Connected?.Invoke(property, this);
-    }
-    
-    public void DisconnectFrom(IInputProperty property)
-    {
-        if(!Connections.Contains(property)) return;
-        
-        _connections.Remove(property);
-        if(property.Connection == this)
-        {
-            property.Connection = null;
-        }
-        
-        Disconnected?.Invoke(property, this);
-    }
-}
-
-public class OutputProperty<T> : OutputProperty, INodeProperty<T>
-{
-    public new T Value
-    {
-        get => (T)base.Value;
-        set => base.Value = value;
-    }
-    
-    internal OutputProperty(Node node ,string name, T defaultValue) : base(node, name, defaultValue)
-    {
-    }
-}

+ 0 - 37
src/Nodes/Program.cs

@@ -1,37 +0,0 @@
-using Nodes;
-using Nodes.Nodes;
-using SkiaSharp;
-
-LayerNode layerNode = new LayerNode("Layer", new SKSizeI(100, 100));
-
-SKBitmap testBitmap = SKBitmap.Decode("test.png");
-using SKSurface surface = SKSurface.Create(new SKImageInfo(100, 100));
-using SKCanvas canvas = surface.Canvas;
-canvas.DrawBitmap(testBitmap, 0, 0);
-
-layerNode.LayerImage.Canvas.DrawSurface(surface, 0, 0);
-
-LayerNode layerNode2 = new LayerNode("Layer2", new SKSizeI(100, 100));
-
-SKBitmap testBitmap2 = SKBitmap.Decode("test2.png");
-using SKSurface surface2 = SKSurface.Create(new SKImageInfo(100, 100));
-using SKCanvas canvas2 = surface2.Canvas;
-canvas2.DrawBitmap(testBitmap2, 0, 0);
-
-layerNode2.LayerImage.Canvas.DrawSurface(surface2, 0, 0);
-
-MergeNode mergeNode = new MergeNode("Merge");
-OutputNode outputNode = new OutputNode("Output");
-
-layerNode.Output.ConnectTo(mergeNode.Top);
-layerNode2.Output.ConnectTo(mergeNode.Bottom);
-mergeNode.Output.ConnectTo(outputNode.Input);
-
-NodeGraph graph = new();
-graph.AddNode(layerNode);
-graph.AddNode(layerNode2);
-graph.AddNode(mergeNode);
-graph.AddNode(outputNode);
-
-using FileStream fileStream = new("output.png", FileMode.Create);
-graph.Execute().Snapshot().Encode().SaveTo(fileStream);

BIN
src/Nodes/test.png


BIN
src/Nodes/test2.png


+ 4 - 3
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -352,12 +352,12 @@ internal class DocumentUpdater
         IStructureMemberHandler memberVM;
         if (info is CreateLayer_ChangeInfo layerInfo)
         {
-            memberVM = doc.LayerHandlerFactory.CreateLayerHandler(helper, info.Id);
+            memberVM = doc.NodeGraphHandler.AllNodes.FirstOrDefault(x => x.Id == info.Id) as ILayerHandler;
             ((ILayerHandler)memberVM).SetLockTransparency(layerInfo.LockTransparency);
         }
         else if (info is CreateFolder_ChangeInfo)
         {
-            memberVM = doc.FolderHandlerFactory.CreateFolderHandler(helper, info.Id);
+            memberVM = doc.NodeGraphHandler.AllNodes.FirstOrDefault(x => x.Id == info.Id) as IFolderHandler;
         }
         else
         {
@@ -485,7 +485,8 @@ internal class DocumentUpdater
     
     private void ProcessCreateNode<T>(CreateNode_ChangeInfo info) where T : NodeViewModel, new()
     {
-        T node = new T() { NodeName = info.NodeName, Id = info.Id, 
+        T node = new T() { 
+            NodeName = info.NodeName, Id = info.Id, 
             Document = (DocumentViewModel)doc, Internals = helper };
 
         node.SetPosition(info.Position);

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

@@ -4,4 +4,5 @@ namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
 internal interface IFolderHandler : IStructureMemberHandler
 {
+    internal ObservableCollection<IStructureMemberHandler> Children { get; }
 }

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

@@ -15,5 +15,7 @@ public interface INodeHandler
     public VecD PositionBindable { get; set; }
     public bool IsSelected { get; set; }
     void TraverseBackwards(Func<INodeHandler, bool> func);
+    void TraverseBackwards(Func<INodeHandler, INodeHandler, bool> func);
     void TraverseForwards(Func<INodeHandler, bool> func);
+    void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func);
 }

+ 15 - 2
src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs

@@ -140,7 +140,13 @@ internal class AffectedAreasGatherer
         var member = tracker.Document.FindMember(memberGuid);
         if (member is IReadOnlyLayerNode layer)
         {
-            var chunks = layer.Execute(frame).FindAllChunks();
+            var result = layer.Execute(frame);
+            if (result == null)
+            {
+                AddWholeCanvasToImagePreviews(memberGuid, ignoreSelf);
+                return;
+            }
+            var chunks = result.FindAllChunks();
             AddToImagePreviews(memberGuid, new AffectedArea(chunks), ignoreSelf);
         }
         else if (member is IReadOnlyFolderNode folder)
@@ -156,7 +162,14 @@ internal class AffectedAreasGatherer
         var member = tracker.Document.FindMember(memberGuid);
         if (member is IReadOnlyLayerNode layer)
         {
-            var chunks = layer.Execute(frame).FindAllChunks();
+            var result = layer.Execute(frame);
+            if (result == null)
+            {
+                AddWholeCanvasToMainImage();
+                return;
+            }
+            
+            var chunks = result.FindAllChunks();
             if (layer.Mask.Value is not null && layer.MaskIsVisible.Value && useMask)
                 chunks.IntersectWith(layer.Mask.Value.FindAllChunks());
             AddToMainImage(new AffectedArea(chunks));

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

@@ -40,12 +40,6 @@ internal class LayerViewModel : StructureMemberViewModel, ILayerHandler
         }
     }
 
-    public override void SetName(string name)
-    {
-        base.SetName(name);
-        NodeName = NameBindable;
-    }
-
     public LayerViewModel(DocumentViewModel doc, DocumentInternalParts internals, Guid id) : base(doc, internals, id)
     {
     }

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

@@ -13,7 +13,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
     public DocumentViewModel DocumentViewModel { get; }
     public ObservableCollection<INodeHandler> AllNodes { get; } = new();
     public ObservableCollection<NodeConnectionViewModel> Connections { get; } = new();
-    public ObservableCollection<IStructureMemberHandler> StructureTree { get; } = new();
+    public StructureTree StructureTree { get; } = new();
     public INodeHandler? OutputNode { get; private set; }
     
     private DocumentInternalParts Internals { get; }
@@ -30,28 +30,21 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         {
             OutputNode = node; // TODO: this is not really correct yet, a way to check what node type is added is needed
         }
-
-        if (node is IStructureMemberHandler handler)
-        {
-            StructureTree.Add(handler);
-        }
         
         AllNodes.Add(node);
+        StructureTree.Update(this);
     }
     
     public void RemoveNode(Guid nodeId)
     {
         var node = AllNodes.FirstOrDefault(x => x.Id == nodeId);
         
-        if (node is IStructureMemberHandler handler)
-        {
-            StructureTree.Remove(handler);
-        }
-        
         if (node != null)
         {
             AllNodes.Remove(node);
         }
+        
+        StructureTree.Update(this);
     }
 
     public void SetConnection(NodeConnectionViewModel connection)
@@ -68,6 +61,8 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         connection.OutputProperty.ConnectedInputs.Add(connection.InputProperty);
         
         Connections.Add(connection);
+        
+        StructureTree.Update(this);
     }
 
     public void RemoveConnection(Guid nodeId, string property)
@@ -79,6 +74,8 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
             connection.OutputProperty.ConnectedInputs.Remove(connection.InputProperty);
             Connections.Remove(connection);
         }
+        
+        StructureTree.Update(this);
     }
     
     public void RemoveConnections(Guid nodeId)
@@ -90,6 +87,8 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
             connection.OutputProperty.ConnectedInputs.Remove(connection.InputProperty);
             Connections.Remove(connection);
         }
+        
+        StructureTree.Update(this);
     }
 
     public bool TryTraverse(Func<INodeHandler, bool> func)

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

@@ -29,6 +29,7 @@ internal abstract class StructureMemberViewModel : NodeViewModel, IStructureMemb
     {
         this.name = name;
         OnPropertyChanged(nameof(NameBindable));
+        NodeName = NameBindable;
     }
 
     public string NameBindable

+ 93 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/StructureTree.cs

@@ -0,0 +1,93 @@
+using System.Collections.ObjectModel;
+using PixiEditor.AvaloniaUI.Models.Handlers;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Document;
+
+internal class StructureTree
+{
+    public ObservableCollection<IStructureMemberHandler> Members { get; } = new();
+   
+    private Dictionary<IStructureMemberHandler, ObservableCollection<IStructureMemberHandler>> _memberMap = new();
+
+    public void Update(NodeGraphViewModel nodeGraphViewModel)
+    {
+        if (nodeGraphViewModel.OutputNode == null)
+        {
+            Members.Clear();
+            _memberMap.Clear();
+            return;
+        }
+        
+        int relativeFolderIndex = 0;
+        List<IStructureMemberHandler> membersMet = new();
+        ObservableCollection<IStructureMemberHandler> lastRoot = Members;
+        nodeGraphViewModel.OutputNode.TraverseBackwards((node, previous) =>
+        {
+            if (node is IStructureMemberHandler structureMemberHandler)
+            {
+                membersMet.Add(structureMemberHandler);
+            }
+            
+            if (previous is IFolderHandler folder)
+            {
+                lastRoot = folder.Children;
+                relativeFolderIndex = 0;
+            }
+            
+            if (node is IFolderHandler handler)
+            {
+                UpdateMember(handler, relativeFolderIndex, lastRoot);
+            }
+            if (node is ILayerHandler layerHandler)
+            {
+                UpdateMember(layerHandler, relativeFolderIndex, lastRoot);
+                relativeFolderIndex++;
+            }
+            
+            return true;
+        });
+        
+        List<IStructureMemberHandler> toRemove = new();
+        
+        foreach (var member in _memberMap)
+        {
+            if (!membersMet.Contains(member.Key))
+            {
+                toRemove.Add(member.Key);
+                member.Value.Remove(member.Key);
+            }
+        }
+        
+        foreach (var member in toRemove)
+        {
+            _memberMap.Remove(member);
+        }
+    }
+    
+    private void UpdateMember(IStructureMemberHandler member, int relativeIndex, ObservableCollection<IStructureMemberHandler> root)
+    {
+        bool existsInMembers = _memberMap.ContainsKey(member);
+        if(!existsInMembers)
+        {
+            root.Insert(relativeIndex, member);
+            _memberMap.Add(member, root);
+            return;
+        }
+        else
+        {
+            ObservableCollection<IStructureMemberHandler> oldRoot = _memberMap[member];
+            if (oldRoot != root)
+            {
+                oldRoot.Remove(member);
+                root.Insert(relativeIndex, member);
+                _memberMap[member] = root;
+            }
+        }
+            
+        bool existsAtIndex = root.Count > relativeIndex && root[relativeIndex] == member;
+        if (!existsAtIndex)
+        {
+            root.Move(root.IndexOf(member), relativeIndex);
+        }
+    }
+}

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

@@ -126,6 +126,36 @@ internal class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
+    public void TraverseBackwards(Func<INodeHandler, INodeHandler, bool> func)
+    {
+        var visited = new HashSet<INodeHandler>();
+        var queueNodes = new Queue<(INodeHandler, INodeHandler)>();
+        queueNodes.Enqueue((this, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add(node.Item1))
+            {
+                continue;
+            }
+            
+            if (!func(node.Item1, node.Item2))
+            {
+                return;
+            }
+
+            foreach (var inputProperty in node.Item1.Inputs)
+            {
+                if (inputProperty.ConnectedOutput != null)
+                {
+                    queueNodes.Enqueue((inputProperty.ConnectedOutput.Node, node.Item1));
+                } 
+            }
+        }
+    }
+
     public void TraverseForwards(Func<INodeHandler, bool> func)
     {
         var visited = new HashSet<INodeHandler>();
@@ -155,6 +185,36 @@ internal class NodeViewModel : ObservableObject, INodeHandler
             }
         }
     }
+    
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func)
+    {
+        var visited = new HashSet<INodeHandler>();
+        var queueNodes = new Queue<(INodeHandler, INodeHandler)>();
+        queueNodes.Enqueue((this, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add(node.Item1))
+            {
+                continue;
+            }
+            
+            if (!func(node.Item1, node.Item2))
+            {
+                return;
+            }
+
+            foreach (var outputProperty in node.Item1.Outputs)
+            {
+                foreach (var connection in outputProperty.ConnectedInputs)
+                {
+                    queueNodes.Enqueue((connection.Node, node.Item1));
+                }
+            }
+        }
+    }
 
     public NodePropertyViewModel FindInputProperty(string propName)
     {

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Layers/LayersManager.axaml

@@ -125,7 +125,7 @@
                 Background="{DynamicResource ThemeBackgroundBrush}"
                 Grid.Row="3" VerticalAlignment="Bottom"/>
             <TreeView DockPanel.Dock="Top" Name="treeView" BorderThickness="0"
-                      ItemsSource="{Binding DataContext.ActiveDocument.NodeGraph.StructureTree, ElementName=layersManager}">
+                      ItemsSource="{Binding DataContext.ActiveDocument.NodeGraph.StructureTree.Members, ElementName=layersManager}">
                 <TreeView.ItemsPanel>
                     <ItemsPanelTemplate>
                         <panels:ReversedOrderStackPanel />

+ 3 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -33,7 +33,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         _nodes.Remove(node);
     }
 
-    private Queue<IReadOnlyNode> CalculateExecutionQueue(OutputNode outputNode)
+    private Queue<IReadOnlyNode> CalculateExecutionQueue(OutputNode outputNode, bool validate = true)
     {
         // backwards breadth-first search
         var visited = new HashSet<IReadOnlyNode>();
@@ -44,7 +44,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         while (queueNodes.Count > 0)
         {
             var node = queueNodes.Dequeue();
-            if (!visited.Add(node))
+            if (!visited.Add(node) || (validate && !node.Validate()))
             {
                 continue;
             }
@@ -82,7 +82,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     {
         if(OutputNode == null) return false;
         
-        var queue = CalculateExecutionQueue(OutputNode);
+        var queue = CalculateExecutionQueue(OutputNode, false);
         
         while (queue.Count > 0)
         {

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

@@ -1,6 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
-using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
@@ -19,25 +18,31 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         this.size = size;
     }
 
-    public override bool Validate()
+    public override RectI? GetTightBounds(KeyFrameTime frameTime)
     {
-        return true;
+        return Execute(frameTime).FindTightCommittedBounds();
     }
 
-    public override RectI? GetTightBounds(KeyFrameTime frameTime)
+    public override bool Validate()
     {
-        return Execute(frameTime).FindTightCommittedBounds();
+        return true; 
     }
 
     public override ChunkyImage OnExecute(KeyFrameTime frame)
     {
+        if (!IsVisible.Value)
+        {
+            Output.Value = Background.Value;
+            return Output.Value;
+        }
+        
         var imageFrame = frames.FirstOrDefault(x => x.IsInFrame(frame.Frame));
         var frameImage = imageFrame?.Image ?? frames[0].Image;
 
         if (Background.Value != null)
         {
-            VecI size = GetBiggerSize(frameImage.LatestSize, Background.Value.LatestSize);
-            ChunkyImage combined = new(size);
+            VecI targetSize = GetBiggerSize(frameImage.LatestSize, Background.Value.LatestSize);
+            ChunkyImage combined = new(targetSize);
             combined.EnqueueDrawUpToDateChunkyImage(VecI.Zero, Background.Value);
             combined.EnqueueDrawUpToDateChunkyImage(VecI.Zero, frameImage);
             combined.CommitChanges();

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

@@ -147,6 +147,11 @@ public abstract class Node : IReadOnlyNode, IDisposable
         {
             if (output.Value is IDisposable disposable)
             {
+                foreach (var connection in output.Connections)
+                { 
+                    connection.Value = default!;
+                }
+                
                 disposable.Dispose();
             }
         }

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

@@ -31,7 +31,7 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
         
         Output = CreateOutput<ChunkyImage?>("Output", "OUTPUT", null);
     }
-    
+
     public abstract override ChunkyImage? OnExecute(KeyFrameTime frameTime);
     public abstract override bool Validate();
 

+ 0 - 31
src/PixiEditor.sln

@@ -102,8 +102,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.AnimationRendere
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AnimationRendering", "AnimationRendering", "{2BA72059-FFD7-4887-AE88-269017198933}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nodes", "Nodes\Nodes.csproj", "{9D50FB5B-F835-411C-B735-45124CB65865}"
-EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x64 = Debug|x64
@@ -1502,34 +1500,6 @@ Global
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|x64.Build.0 = Debug|Any CPU
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|ARM64.ActiveCfg = Debug|Any CPU
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|ARM64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Debug|x64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Debug|x64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Debug|ARM64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Debug|ARM64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.DevRelease|x64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.DevRelease|x64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.DevRelease|ARM64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.DevSteam|x64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.DevSteam|x64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.DevSteam|ARM64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.MSIX Debug|x64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.MSIX|x64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.MSIX|x64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.MSIX|ARM64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Release|x64.ActiveCfg = Release|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Release|x64.Build.0 = Release|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Release|ARM64.ActiveCfg = Release|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Release|ARM64.Build.0 = Release|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Steam|x64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Steam|x64.Build.0 = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Steam|ARM64.ActiveCfg = Debug|Any CPU
-		{9D50FB5B-F835-411C-B735-45124CB65865}.Steam|ARM64.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1576,7 +1546,6 @@ Global
 		{2BA72059-FFD7-4887-AE88-269017198933} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{9B552A44-9587-4410-8673-254B31E2E4F7} = {2BA72059-FFD7-4887-AE88-269017198933}
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C} = {2BA72059-FFD7-4887-AE88-269017198933}
-		{9D50FB5B-F835-411C-B735-45124CB65865} = {5AFBF881-C054-4CE4-8159-8D4017FFD27A}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}