Przeglądaj źródła

Fixed merge members and various crashes

flabbet 1 rok temu
rodzic
commit
0df0b241c5

+ 9 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -7,6 +7,7 @@ using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Layers;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Exceptions;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
@@ -65,8 +66,8 @@ internal class DocumentUpdater
                 ProcessCreateStructureMember(info);
                 break;
             case DeleteStructureMember_ChangeInfo info:
-                ProcessDeleteNode(info);
                 ProcessDeleteStructureMember(info);
+                ProcessDeleteNode(info);
                 break;
             case StructureMemberName_ChangeInfo info:
                 ProcessUpdateStructureMemberName(info);
@@ -562,10 +563,16 @@ internal class DocumentUpdater
             
             doc.NodeGraphHandler.SetConnection(connection);
         }
-        else
+        else if(info.OutputProperty == null)
         {
             doc.NodeGraphHandler.RemoveConnection(info.InputNodeId, info.InputProperty);
         }
+        else
+        {
+#if DEBUG
+            throw new MissingNodeException("Connection requested for a node that doesn't exist");
+#endif
+        }
     }
     
     private void ProcessNodePosition(NodePosition_ChangeInfo info)

+ 3 - 3
src/PixiEditor.AvaloniaUI/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -389,11 +389,11 @@ internal class DocumentOperationsModule : IDocumentOperations
 
         INodeHandler? parent = null;
 
-        node.TraverseForwards(node =>
+        node.TraverseForwards(traversedNode =>
         {
-            if (!members.Contains(node.Id))
+            if (!members.Contains(traversedNode.Id))
             {
-                parent = node;
+                parent = traversedNode;
                 return false;
             }
             

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/IDocument.cs

@@ -41,7 +41,7 @@ internal interface IDocument : IHandler
     public double HorizontalSymmetryAxisYBindable { get; }
     public double VerticalSymmetryAxisXBindable { get; }
     public IDocumentOperations Operations { get; }
-    public DocumentEvaluator Renderer { get; }
+    public DocumentRenderer Renderer { get; }
     public void RemoveSoftSelectedMember(IStructureMemberHandler member);
     public void ClearSoftSelectedMembers();
     public void AddSoftSelectedMember(IStructureMemberHandler member);

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -151,7 +151,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public DocumentStructureModule StructureHelper { get; }
     public DocumentToolsModule Tools { get; }
     public DocumentOperationsModule Operations { get; }
-    public DocumentEvaluator Renderer { get; }
+    public DocumentRenderer Renderer { get; }
     public DocumentEventsModule EventInlet { get; }
 
     public ActionDisplayList ActionDisplays { get; } =
@@ -233,7 +233,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
         ReferenceLayerViewModel = new(this, Internals);
 
-        Renderer = new DocumentEvaluator(Internals.Tracker.Document);
+        Renderer = new DocumentRenderer(Internals.Tracker.Document);
     }
 
     /// <summary>

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Exceptions/MissingNodeException.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Exceptions;
+
+public class MissingNodeException : Exception
+{
+    public MissingNodeException(string message) : base(message)
+    {
+    }
+}

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

@@ -11,7 +11,7 @@ public class InputProperty : IInputProperty
     public string InternalPropertyName { get; }
     public string DisplayName { get; }
 
-    public object Value
+    public object? Value
     {
         get
         {
@@ -133,12 +133,12 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
 {
     public new T Value
     {
-        get => (T)base.Value;
+        get => (T)(base.Value ?? default);
     }
 
     public T NonOverridenValue
     {
-        get => (T)base.NonOverridenValue;
+        get => (T)(base.NonOverridenValue ?? default);
         set => base.NonOverridenValue = value;
     }
     

+ 3 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs

@@ -29,4 +29,7 @@ public interface IReadOnlyNode
     /// </summary>
     /// <param name="action">The action to perform on each node.</param>
     public void TraverseForwards(Func<IReadOnlyNode, bool> action);
+    
+    public IInputProperty? GetInputProperty(string internalName);
+    public IOutputProperty? GetOutputProperty(string internalName);
 }

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

@@ -1,9 +1,7 @@
-using ChunkyImageLib.Operations;
-using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
-using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 using PixiEditor.Numerics;
 
@@ -149,6 +147,11 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return new ImageLayerNode(size)
         {
             MemberName = MemberName,
+            keyFrames = new List<KeyFrameData>()
+            {
+                // we are only copying the layer image, keyframes probably shouldn't be copied since they are controlled by AnimationData
+                new ImageFrame(Guid.NewGuid(), 0, 0, ((ImageFrame)keyFrames[0]).Data.CloneFromCommitted())
+            }
         };
     }
 
@@ -206,16 +209,6 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             imgFrame.Data = newLayerImage;
         }
     }
-
-    public override void Dispose()
-    {
-        base.Dispose();
-        clearPaint.Dispose();
-        foreach (var surface in workingSurfaces.Values)
-        {
-            surface.Dispose();
-        }
-    }
 }
 
 class ImageFrame : KeyFrameData<ChunkyImage>

+ 54 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -19,7 +19,19 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     public IReadOnlyCollection<InputProperty> InputProperties => inputs;
     public IReadOnlyCollection<OutputProperty> OutputProperties => outputs;
-    public Surface? CachedResult { get; private set; }
+
+    public Surface? CachedResult
+    {
+        get
+        {
+            if(_lastCachedResult == null || _lastCachedResult.IsDisposed) return null;
+            return _lastCachedResult;
+        }
+        private set
+        {
+            _lastCachedResult = value;
+        }
+    }
 
     public virtual string InternalName => $"PixiEditor.{NodeUniqueName}";
     
@@ -43,6 +55,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     private ChunkResolution? _lastResolution;
     private VecI? _lastChunkPos;
     private bool _keyFramesDirty;
+    private Surface? _lastCachedResult;
 
     public Surface? Execute(RenderingContext context)
     {
@@ -223,19 +236,22 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     public virtual void Dispose()
     {
+        DisconnectAll();
         foreach (var input in inputs)
         {
-            if (input is { Connection: null, Value: IDisposable disposable })
+            if (input is { Connection: null, NonOverridenValue: IDisposable disposable })
             {
                 disposable.Dispose();
+                input.NonOverridenValue = default;
             }
         }
 
         foreach (var output in outputs)
         {
-            if (output.Value is IDisposable disposable)
+            if (output.Connections.Count == 0 && output.Value is IDisposable disposable)
             {
                 disposable.Dispose();
+                output.Value = default;
             }
         }
         
@@ -247,6 +263,24 @@ public abstract class Node : IReadOnlyNode, IDisposable
             }
         }
     }
+    
+    public void DisconnectAll()
+    {
+        foreach (var input in inputs)
+        {
+            input.Connection?.DisconnectFrom(input);
+        }
+
+        foreach (var output in outputs)
+        {
+            var connections = output.Connections.ToArray();
+            for (var i = 0; i < connections.Length; i++)
+            {
+                var conn = connections[i];
+                output.DisconnectFrom(conn);
+            }
+        }
+    }
 
     public abstract Node CreateCopy();
 
@@ -254,19 +288,19 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
         var clone = CreateCopy();
         clone.Id = Guid.NewGuid();
-        clone.inputs = new List<InputProperty>();
-        clone.outputs = new List<OutputProperty>();
-        clone.keyFrames = new List<KeyFrameData>();
-        foreach (var input in inputs)
+
+        for (var i = 0; i < clone.inputs.Count; i++)
         {
+            var input = inputs[i];
             var newInput = input.Clone(clone);
-            clone.inputs.Add(newInput);
+            input.NonOverridenValue = newInput.NonOverridenValue;
         }
 
-        foreach (var output in outputs)
+        for (var i = 0; i < clone.outputs.Count; i++)
         {
+            var output = outputs[i];
             var newOutput = output.Clone(clone);
-            clone.outputs.Add(newOutput);
+            output.Value = newOutput.Value;
         }
 
         return clone;
@@ -281,4 +315,14 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
         return outputs.FirstOrDefault(x => x.InternalPropertyName == outputProperty);
     }
+    
+    IInputProperty? IReadOnlyNode.GetInputProperty(string inputProperty)
+    {
+        return GetInputProperty(inputProperty);
+    }
+    
+    IOutputProperty? IReadOnlyNode.GetOutputProperty(string outputProperty)
+    {
+        return GetOutputProperty(outputProperty);
+    }
 }

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

@@ -102,6 +102,7 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
 
     protected void DrawSurface(Surface workingSurface, Surface source, RenderingContext context)
     {
+        // Maybe clip rect will allow to avoid snapshotting? Idk if it will be faster
         RectI sourceRect = CalculateSourceRect(source, workingSurface.Size, context);
         RectI targetRect = CalculateDestinationRect(context);
         using var snapshot = source.DrawingSurface.Snapshot(sourceRect);

+ 18 - 10
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -43,14 +43,19 @@ internal class CombineStructureMembersOnto_Change : Change
 
     private void AddChildren(FolderNode folder, HashSet<Guid> collection)
     {
-        //TODO: Implement
-        /*foreach (var child in folder.Children)
+        if (folder.Content.Connection != null)
         {
-            if (child is LayerNode layer)
-                collection.Add(layer.Id);
-            else if (child is FolderNode innerFolder)
-                AddChildren(innerFolder, collection);
-        }*/
+            folder.Content.Connection.Node.TraverseBackwards(node =>
+            {
+                if (node is LayerNode layer)
+                {
+                    collection.Add(layer.Id);
+                    return true;
+                }
+
+                return true;
+            });
+        }
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -68,15 +73,18 @@ internal class CombineStructureMembersOnto_Change : Change
 
         var toDrawOnImage = toDrawOn.GetLayerImageAtFrame(frame);
         toDrawOnImage.EnqueueClear();
-        /*foreach (var chunk in chunksToCombine)
+        
+        DocumentRenderer renderer = new(target);
+        
+        foreach (var chunk in chunksToCombine)
         {
-            OneOf<Chunk, EmptyChunk> combined = ChunkRenderer.MergeChosenMembers(chunk, ChunkResolution.Full, target.NodeGraph, frame, layersToCombine);
+            OneOf<Chunk, EmptyChunk> combined = renderer.RenderLayersChunk(chunk, ChunkResolution.Full, frame, layersToCombine);
             if (combined.IsT0)
             {
                 toDrawOnImage.EnqueueDrawImage(chunk * ChunkyImage.FullChunkSize, combined.AsT0.Surface);
                 combined.AsT0.Dispose();
             }
-        }*/
+        }
         var affArea = toDrawOnImage.FindAffectedArea();
         originalChunks = new CommittedChunkStorage(toDrawOnImage, affArea.Chunks);
         toDrawOnImage.CommitChanges();

+ 22 - 11
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -2,6 +2,7 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.Changes.Structure;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 
 namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
@@ -69,31 +70,40 @@ public static class NodeOperations
         return changes;
     }
 
-    public static List<IChangeInfo> ConnectStructureNodeProperties(List<IInputProperty> originalOutputConnections,
-        List<(IInputProperty, IOutputProperty?)> originalInputConnections, StructureNode node)
+    public static List<IChangeInfo> ConnectStructureNodeProperties(
+        List<PropertyConnection> originalOutputConnections,
+        List<(PropertyConnection, PropertyConnection?)> originalInputConnections, StructureNode node, IReadOnlyNodeGraph graph)
     {
         List<IChangeInfo> changes = new();
         foreach (var connection in originalOutputConnections)
         {
-            node.Output.ConnectTo(connection);
-            changes.Add(new ConnectProperty_ChangeInfo(node.Id, connection.Node.Id, node.Output.InternalPropertyName,
-                connection.InternalPropertyName));
+            var inputNode = graph.AllNodes.FirstOrDefault(x => x.Id == connection.NodeId);
+            IInputProperty property = inputNode.GetInputProperty(connection.PropertyName);
+            node.Output.ConnectTo(property);
+            changes.Add(new ConnectProperty_ChangeInfo(node.Id, property.Node.Id, node.Output.InternalPropertyName,
+                property.InternalPropertyName));
         }
 
         foreach (var connection in originalInputConnections)
         {
-            if (connection.Item2 is null)
+            var outputNode = graph.AllNodes.FirstOrDefault(x => x.Id == connection.Item2?.NodeId);
+            
+            if (outputNode is null)
                 continue;
 
+            IOutputProperty output = outputNode.GetOutputProperty(connection.Item2.PropertyName);
+            
+            if (output is null)
+                continue;
+            
             IInputProperty? input =
-                node.InputProperties.FirstOrDefault(
-                    x => x.InternalPropertyName == connection.Item1.InternalPropertyName);
+                node.GetInputProperty(connection.Item1.PropertyName);
 
             if (input != null)
             {
-                connection.Item2.ConnectTo(input);
-                changes.Add(new ConnectProperty_ChangeInfo(connection.Item2.Node.Id, node.Id,
-                    connection.Item2.InternalPropertyName,
+                output.ConnectTo(input);
+                changes.Add(new ConnectProperty_ChangeInfo(output.Node.Id, node.Id,
+                    output.InternalPropertyName,
                     input.InternalPropertyName));
             }
         }
@@ -101,3 +111,4 @@ public static class NodeOperations
         return changes;
     }
 }
+public record PropertyConnection(Guid? NodeId, string? PropertyName);

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

@@ -10,8 +10,8 @@ internal class DeleteStructureMember_Change : Change
 {
     private Guid memberGuid;
     private int originalIndex;
-    private List<IInputProperty> originalOutputConnections = new();
-    private List<(IInputProperty, IOutputProperty?)> originalInputConnections = new();
+    private List<PropertyConnection> originalOutputConnections = new();
+    private List<(PropertyConnection input, PropertyConnection? output)> originalInputConnections = new();
     private StructureNode? savedCopy;
 
     [GenerateMakeChangeAction]
@@ -26,8 +26,13 @@ internal class DeleteStructureMember_Change : Change
         if (member is null)
             return false;
 
-        originalOutputConnections = member.Output.Connections.ToList();
-        originalInputConnections = member.InputProperties.Select(x => ((IInputProperty)x, x.Connection)).ToList();
+        originalOutputConnections = member.Output.Connections.Select(x => new PropertyConnection(x.Node.Id, x.InternalPropertyName))
+            .ToList();
+        
+        originalInputConnections = member.InputProperties.Select(x => 
+            (new PropertyConnection(x.Node.Id, x.InternalPropertyName), new PropertyConnection(x.Connection?.Node.Id, x.Connection?.InternalPropertyName)))
+            .ToList();
+        
         savedCopy = (StructureNode)member.Clone();
         savedCopy.Id = memberGuid;
         return true;
@@ -52,6 +57,8 @@ internal class DeleteStructureMember_Change : Change
                 bgConnection.ConnectTo(connection);
                 changes.Add(new ConnectProperty_ChangeInfo(bgConnection.Node.Id, connection.Node.Id,
                     bgConnection.InternalPropertyName, connection.InternalPropertyName));
+                
+                node.Output.DisconnectFrom(connection);
             }
         }
 
@@ -80,7 +87,7 @@ internal class DeleteStructureMember_Change : Change
         
         changes.Add(createChange);
 
-        changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalOutputConnections, originalInputConnections, copy)); 
+        changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalOutputConnections, originalInputConnections, copy, doc.NodeGraph)); 
         
         return changes;
     }
@@ -90,3 +97,4 @@ internal class DeleteStructureMember_Change : Change
         savedCopy?.Dispose();
     }
 }
+

+ 11 - 6
src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

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

+ 95 - 28
src/PixiEditor.ChangeableDocument/Rendering/DocumentEvaluator.cs → src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -1,20 +1,23 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Rendering;
 
-public class DocumentEvaluator
+public class DocumentRenderer
 {
-    public DocumentEvaluator(IReadOnlyDocument document)
+    public DocumentRenderer(IReadOnlyDocument document)
     {
         Document = document;
     }
 
     private IReadOnlyDocument Document { get; }
-    
-    public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution, int frame, RectI? globalClippingRect = null)
+
+    public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution, int frame,
+        RectI? globalClippingRect = null)
     {
         using RenderingContext context = new(frame, chunkPos, resolution, Document.Size);
         try
@@ -36,17 +39,17 @@ public class DocumentEvaluator
             {
                 chunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
             }
-            
+
             VecD pos = chunkPos;
             int x = (int)(pos.X * ChunkyImage.FullChunkSize * resolution.Multiplier());
             int y = (int)(pos.Y * ChunkyImage.FullChunkSize * resolution.Multiplier());
             int width = (int)(ChunkyImage.FullChunkSize * resolution.Multiplier());
             int height = (int)(ChunkyImage.FullChunkSize * resolution.Multiplier());
-            
+
             RectD sourceRect = new(x, y, width, height);
 
             using var chunkSnapshot = evaluated.DrawingSurface.Snapshot((RectI)sourceRect);
-            
+
             chunk.Surface.DrawingSurface.Canvas.DrawImage(chunkSnapshot, 0, 0, context.ReplacingPaintWithOpacity);
 
             chunk.Surface.DrawingSurface.Canvas.Restore();
@@ -73,26 +76,7 @@ public class DocumentEvaluator
                 return new EmptyChunk();
             }
 
-            Chunk chunk = Chunk.Create(resolution);
-
-            chunk.Surface.DrawingSurface.Canvas.Save();
-            chunk.Surface.DrawingSurface.Canvas.Clear();
-
-            int x = 0;
-            int y = 0;
-            
-            if (transformedClippingRect is not null)
-            {
-                chunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
-                x = transformedClippingRect.Value.X;
-                y = transformedClippingRect.Value.Y;
-            }
-            
-            chunk.Surface.DrawingSurface.Canvas.DrawSurface(evaluated.DrawingSurface, x, y, context.ReplacingPaintWithOpacity);
-
-            chunk.Surface.DrawingSurface.Canvas.Restore();
-
-            return chunk;
+            return ChunkFromResult(resolution, transformedClippingRect, evaluated, context);
         }
         catch (ObjectDisposedException)
         {
@@ -109,4 +93,87 @@ public class DocumentEvaluator
         VecI pixelChunkPos = chunkPos * (int)(ChunkyImage.FullChunkSize * multiplier);
         return (RectI?)rect.Scale(multiplier).Translate(-pixelChunkPos).RoundOutwards();
     }
+
+    public OneOf<Chunk, EmptyChunk> RenderLayersChunk(VecI chunkPos, ChunkResolution resolution, int frame,
+        HashSet<Guid> layersToCombine)
+    {
+        using RenderingContext context = new(frame, chunkPos, resolution, Document.Size);
+        NodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
+        try
+        {
+            Surface? evaluated = membersOnlyGraph.Execute(context);
+            if (evaluated is null)
+            {
+                return new EmptyChunk();
+            }
+
+            var result = ChunkFromResult(resolution, null, evaluated, context);
+            
+            membersOnlyGraph.Dispose();
+            return result;
+        }
+        catch (ObjectDisposedException)
+        {
+            return new EmptyChunk();
+        }
+    }
+    
+    private NodeGraph ConstructMembersOnlyGraph(HashSet<Guid> layersToCombine, IReadOnlyNodeGraph fullGraph)
+    {
+        NodeGraph membersOnlyGraph = new();
+
+        OutputNode outputNode = new();
+        
+        membersOnlyGraph.AddNode(outputNode);
+
+        List<LayerNode> layersInOrder = new();
+
+        fullGraph.TryTraverse(node =>
+        {
+            if (node is LayerNode layer && layersToCombine.Contains(layer.Id))
+            {
+                layersInOrder.Insert(0, layer);
+            }
+        });
+
+        IInputProperty<Surface> lastInput = outputNode.Input;
+
+        foreach (var layer in layersInOrder)
+        {
+            var clone = (LayerNode)layer.Clone();
+            membersOnlyGraph.AddNode(clone);
+            
+            clone.Output.ConnectTo(lastInput);
+            lastInput = clone.Background;
+        }
+        
+        return membersOnlyGraph;
+    }
+
+    private static OneOf<Chunk, EmptyChunk> ChunkFromResult(ChunkResolution resolution,
+        RectI? transformedClippingRect, Surface evaluated,
+        RenderingContext context)
+    {
+        Chunk chunk = Chunk.Create(resolution);
+
+        chunk.Surface.DrawingSurface.Canvas.Save();
+        chunk.Surface.DrawingSurface.Canvas.Clear();
+
+        int x = 0;
+        int y = 0;
+
+        if (transformedClippingRect is not null)
+        {
+            chunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
+            x = transformedClippingRect.Value.X;
+            y = transformedClippingRect.Value.Y;
+        }
+
+        chunk.Surface.DrawingSurface.Canvas.DrawSurface(evaluated.DrawingSurface, x, y,
+            context.ReplacingPaintWithOpacity);
+
+        chunk.Surface.DrawingSurface.Canvas.Restore();
+
+        return chunk;
+    }
 }