Browse Source

Merging layers now take clipping into consideration

flabbet 7 months ago
parent
commit
1f109a1735

+ 15 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs

@@ -14,30 +14,35 @@ public interface IReadOnlyNode
     public IReadOnlyList<IReadOnlyKeyFrameData> KeyFrames { get; }
     public VecD Position { get; }
     string DisplayName { get; }
-    
+
     public void Execute(RenderContext context);
-    
-    /// <summary>
-    ///     Checks if the inputs are legal. If they are not, the node should not be executed.
-    /// Note that all nodes connected to any output of this node won't be executed either.
-    /// </summary>
-    /// <example>Divide node has two inputs, if the second input is 0, the node should not be executed. Since division by 0 is illegal</example>
-    /// <returns>True if the inputs are legal, false otherwise.</returns>
-    
+
     /// <summary>
     ///     Traverses the graph backwards from this node. Backwards means towards the input nodes.
     /// </summary>
     /// <param name="action">The action to perform on each node.</param>
     public void TraverseBackwards(Func<IReadOnlyNode, bool> action);
 
+    /// <summary>
+    ///     Traverses the graph backwards from this node. Backwards means towards the input nodes.
+    /// </summary>
+    /// <param name="action">The action to perform on each node. Input property is the input that was used to traverse this node.</param>
+    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action);
+
     /// <summary>
     ///     Traverses the graph forwards from this node. Forwards means towards the output nodes.
     /// </summary>
     /// <param name="action">The action to perform on each node.</param>
     public void TraverseForwards(Func<IReadOnlyNode, bool> action);
     
+     /// <summary>
+    ///     Traverses the graph forwards from this node. Forwards means towards the output nodes.
+    /// </summary>
+    /// <param name="action">The action to perform on each node. Input property is the input that was used to traverse this node.</param>
+    public void TraverseForwards(Func<IReadOnlyNode, IInputProperty, bool> action);
+
     public IInputProperty? GetInputProperty(string internalName);
     public IOutputProperty? GetOutputProperty(string internalName);
-    public void SerializeAdditionalData(Dictionary<string,object> additionalData);
+    public void SerializeAdditionalData(Dictionary<string, object> additionalData);
     public string GetNodeTypeUniqueName();
 }

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

@@ -22,7 +22,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
         AllowHighDpiRendering = true;
     }
 
-    public override Node CreateCopy() => new FolderNode { MemberName = MemberName };
+    public override Node CreateCopy() => new FolderNode { MemberName = MemberName, ClipToPreviousMember = this.ClipToPreviousMember };
 
     public override VecD GetScenePosition(KeyFrameTime time) =>
         documentSize / 2f; 

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

@@ -222,7 +222,11 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     public override Node CreateCopy()
     {
-        var image = new ImageLayerNode(startSize, colorSpace) { MemberName = this.MemberName, };
+        var image = new ImageLayerNode(startSize, colorSpace)
+        {
+            MemberName = this.MemberName, LockTransparency = this.LockTransparency,
+            ClipToPreviousMember = this.ClipToPreviousMember
+        };
 
         image.keyFrames.Clear();
 

+ 73 - 7
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -107,17 +107,50 @@ public abstract class Node : IReadOnlyNode, IDisposable
         _managedTextures[id] = new Texture(CreateImageInfo(size, processingCs));
         return _managedTextures[id];
     }
-    
+
     private ImageInfo CreateImageInfo(VecI size, ColorSpace processingCs)
     {
         if (processingCs == null)
         {
-            return new ImageInfo(size.X, size.Y, ColorType.RgbaF16, AlphaType.Premul, ColorSpace.CreateSrgbLinear()) { GpuBacked = true};
+            return new ImageInfo(size.X, size.Y, ColorType.RgbaF16, AlphaType.Premul, ColorSpace.CreateSrgbLinear())
+            {
+                GpuBacked = true
+            };
         }
 
         return new ImageInfo(size.X, size.Y, ColorType.RgbaF16, AlphaType.Premul, processingCs) { GpuBacked = true };
     }
 
+    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action)
+    {
+        var visited = new HashSet<IReadOnlyNode>();
+        var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
+        queueNodes.Enqueue((this, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add((node.Item1)))
+            {
+                continue;
+            }
+
+            if (!action(node.Item1, node.Item2))
+            {
+                return;
+            }
+
+            foreach (var inputProperty in node.Item1.InputProperties)
+            {
+                if (inputProperty.Connection != null)
+                {
+                    queueNodes.Enqueue((inputProperty.Connection.Node, inputProperty));
+                }
+            }
+        }
+    }
+
     public void TraverseBackwards(Func<IReadOnlyNode, bool> action)
     {
         var visited = new HashSet<IReadOnlyNode>();
@@ -181,6 +214,39 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
     }
 
+    public void TraverseForwards(Func<IReadOnlyNode, IInputProperty, bool> action)
+    {
+        var visited = new HashSet<IReadOnlyNode>();
+        var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
+        queueNodes.Enqueue((this, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add((node.Item1)))
+            {
+                continue;
+            }
+
+            if (!action(node.Item1, node.Item2))
+            {
+                return;
+            }
+
+            foreach (var outputProperty in node.Item1.OutputProperties)
+            {
+                foreach (var connection in outputProperty.Connections)
+                {
+                    if (connection.Connection != null)
+                    {
+                        queueNodes.Enqueue((connection.Node, connection));
+                    }
+                }
+            }
+        }
+    }
+
     public void RemoveKeyFrame(Guid keyFrameId)
     {
         keyFrames.RemoveAll(x => x.KeyFrameGuid == keyFrameId);
@@ -370,10 +436,10 @@ public abstract class Node : IReadOnlyNode, IDisposable
             object value = CloneValue(toClone.NonOverridenValue, clone.inputs[i]);
             clone.inputs[i].NonOverridenValue = value;
         }
-        
+
         // This makes shader outputs copy old delegate, also I don't think it's required because output is calculated based on inputs,
         // leaving commented in case I'm wrong
-        
+
         /*for (var i = 0; i < clone.outputs.Count; i++)
         {
             var cloneOutput = outputs[i];
@@ -436,7 +502,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
         return new None();
     }
-    
+
     private static object CloneValue(object? value, InputProperty? input)
     {
         if (value is null)
@@ -452,7 +518,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 return input.FuncFactory(expr.GetConstant());
             }
         }
-        
+
         if (value is ICloneable cloneable)
         {
             return cloneable.Clone();
@@ -463,7 +529,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         {
             return value;
         }
-        
+
         return default;
     }
 }

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

@@ -167,6 +167,6 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
 
     public override Node CreateCopy()
     {
-        return new VectorLayerNode() { ShapeData = (ShapeVectorData?)ShapeData?.Clone(), };
+        return new VectorLayerNode() { ShapeData = (ShapeVectorData?)ShapeData?.Clone(), ClipToPreviousMember = this.ClipToPreviousMember };
     }
 }

+ 47 - 13
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -42,15 +42,46 @@ internal class CombineStructureMembersOnto_Change : Change
             if (!target.TryFindMember(guid, out var member))
                 return false;
 
-            if (member is LayerNode layer)
-                layersToCombine.Add(layer.Id);
-            else if (member is FolderNode innerFolder)
-                AddChildren(innerFolder, layersToCombine);
+            AddMember(member);
         }
 
         return true;
     }
 
+    private void AddMember(StructureNode member)
+    {
+        if (member is LayerNode layer)
+        {
+            layersToCombine.Add(layer.Id);
+        }
+        else if (member is FolderNode innerFolder)
+        {
+            layersToCombine.Add(innerFolder.Id);
+            AddChildren(innerFolder, layersToCombine);
+        }
+
+        if (member is { ClipToPreviousMember: true, Background.Connection: not null })
+        {
+            if (member.Background.Connection.Node is StructureNode structureNode)
+            {
+                AddMember(structureNode);
+            }
+            else
+            {
+                member.Background.Connection.Node.TraverseBackwards(node =>
+                {
+                    if (node is StructureNode strNode)
+                    {
+                        layersToCombine.Add(strNode.Id);
+                        return false;
+                    }
+
+                    return true;
+                });
+            }
+        }
+    }
+
     private void AddChildren(FolderNode folder, HashSet<Guid> collection)
     {
         if (folder.Content.Connection != null)
@@ -72,7 +103,7 @@ internal class CombineStructureMembersOnto_Change : Change
         out bool ignoreInUndo)
     {
         List<IChangeInfo> changes = new();
-        var targetLayer = target.FindMemberOrThrow<LayerNode>(targetLayerGuid);
+        var targetLayer = target.FindMemberOrThrow<StructureNode>(targetLayerGuid);
 
         int maxFrame = GetMaxFrame(target, targetLayer);
 
@@ -106,7 +137,7 @@ internal class CombineStructureMembersOnto_Change : Change
         return changes;
     }
 
-    private List<IChangeInfo> ApplyToFrame(Document target, LayerNode targetLayer, int frame)
+    private List<IChangeInfo> ApplyToFrame(Document target, StructureNode targetLayer, int frame)
     {
         var chunksToCombine = new HashSet<VecI>();
         List<IChangeInfo> changes = new();
@@ -115,7 +146,7 @@ internal class CombineStructureMembersOnto_Change : Change
 
         foreach (var guid in ordererd)
         {
-            var layer = target.FindMemberOrThrow<LayerNode>(guid);
+            var layer = target.FindMemberOrThrow<StructureNode>(guid);
 
             AddMissingKeyFrame(targetLayer, frame, layer, changes, target);
 
@@ -151,7 +182,7 @@ internal class CombineStructureMembersOnto_Change : Change
         return changes;
     }
 
-    private AffectedArea VectorMerge(Document target, LayerNode targetLayer, int frame, HashSet<Guid> toCombine)
+    private AffectedArea VectorMerge(Document target, StructureNode targetLayer, int frame, HashSet<Guid> toCombine)
     {
         if (targetLayer is not VectorLayerNode vectorLayer)
             throw new InvalidOperationException("Target layer is not a vector layer");
@@ -213,8 +244,11 @@ internal class CombineStructureMembersOnto_Change : Change
         return new AffectedArea(new HashSet<VecI>());
     }
 
-    private AffectedArea RasterMerge(Document target, LayerNode targetLayer, int frame)
+    private AffectedArea RasterMerge(Document target, StructureNode targetLayer, int frame)
     {
+        if(targetLayer is not ImageLayerNode)
+            throw new InvalidOperationException("Target layer is not a raster layer");
+        
         var toDrawOnImage = ((ImageLayerNode)targetLayer).GetLayerImageAtFrame(frame);
         toDrawOnImage.EnqueueClear();
 
@@ -252,7 +286,7 @@ internal class CombineStructureMembersOnto_Change : Change
         return ordered.Reverse().ToHashSet();
     }
 
-    private void AddMissingKeyFrame(LayerNode targetLayer, int frame, LayerNode layer, List<IChangeInfo> changes,
+    private void AddMissingKeyFrame(StructureNode targetLayer, int frame, StructureNode layer, List<IChangeInfo> changes,
         Document target)
     {
         bool hasKeyframe = targetLayer.KeyFrames.Any(x => x.IsInFrame(frame));
@@ -276,7 +310,7 @@ internal class CombineStructureMembersOnto_Change : Change
         target.AnimationData.AddKeyFrame(new RasterKeyFrame(clonedData.KeyFrameGuid, targetLayerGuid, frame, target));
     }
 
-    private int GetMaxFrame(Document target, LayerNode targetLayer)
+    private int GetMaxFrame(Document target, StructureNode targetLayer)
     {
         if (targetLayer.KeyFrames.Count == 0)
             return 0;
@@ -284,7 +318,7 @@ internal class CombineStructureMembersOnto_Change : Change
         int maxFrame = targetLayer.KeyFrames.Max(x => x.StartFrame + x.Duration);
         foreach (var toMerge in membersToMerge)
         {
-            var member = target.FindMemberOrThrow<LayerNode>(toMerge);
+            var member = target.FindMemberOrThrow<StructureNode>(toMerge);
             if (member.KeyFrames.Count > 0)
             {
                 maxFrame = Math.Max(maxFrame, member.KeyFrames.Max(x => x.StartFrame + x.Duration));
@@ -294,7 +328,7 @@ internal class CombineStructureMembersOnto_Change : Change
         return maxFrame;
     }
 
-    private void AddChunksByTightBounds(LayerNode layer, HashSet<VecI> chunksToCombine, int frame)
+    private void AddChunksByTightBounds(StructureNode layer, HashSet<VecI> chunksToCombine, int frame)
     {
         var tightBounds = layer.GetTightBounds(frame);
         if (tightBounds.HasValue)

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

@@ -94,6 +94,7 @@ internal class DeleteNode_Change : Change
 
         changes.AddRange(NodeOperations.CreateUpdateInputs(copy));
         changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, copy, doc.NodeGraph));
+        changes.Add(new NodePosition_ChangeInfo(copy.Id, copy.Position));
 
         RevertKeyFrames(doc, savedKeyFrameGroup, changes);
 

+ 78 - 27
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -19,7 +19,7 @@ public class DocumentRenderer : IPreviewRenderable
     };
 
     private Texture renderTexture;
-    
+
     public DocumentRenderer(IReadOnlyDocument document)
     {
         Document = document;
@@ -74,12 +74,12 @@ public class DocumentRenderer : IPreviewRenderable
         {
             return;
         }
-        
+
         IsBusy = true;
 
         RenderContext context = new(renderOn, frameTime, resolution, Document.Size, Document.ProcessingColorSpace);
         context.FullRerender = true;
-        
+
         node.RenderForOutput(context, renderOn, null);
         IsBusy = false;
     }
@@ -89,7 +89,8 @@ public class DocumentRenderer : IPreviewRenderable
         return ConstructMembersOnlyGraph(null, fullGraph);
     }
 
-    public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(HashSet<Guid>? layersToCombine,
+    public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(
+        HashSet<Guid>? membersToCombine,
         IReadOnlyNodeGraph fullGraph)
     {
         NodeGraph membersOnlyGraph = new();
@@ -98,26 +99,40 @@ public class DocumentRenderer : IPreviewRenderable
 
         membersOnlyGraph.AddNode(outputNode);
 
-        List<LayerNode> layersInOrder = new();
+        Dictionary<Guid, Guid> nodeMapping = new();
 
-        fullGraph.TryTraverse(node =>
+        fullGraph.OutputNode.TraverseBackwards((node, input) =>
         {
-            if (node is LayerNode layer && (layersToCombine == null || layersToCombine.Contains(layer.Id)))
+            if (node is StructureNode structureNode && membersToCombine != null &&
+                !membersToCombine.Contains(structureNode.Id))
             {
-                layersInOrder.Insert(0, layer);
+                return true;
             }
-        });
 
-        IInputProperty<Painter> lastInput = outputNode.Input;
+            if (node is LayerNode layer)
+            {
+                LayerNode clone = (LayerNode)layer.Clone();
+                membersOnlyGraph.AddNode(clone);
+
+                
+                IInputProperty targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping);
+                
+                clone.Output.ConnectTo(targetInput);
+                nodeMapping[layer.Id] = clone.Id;
+            }
+            else if (node is FolderNode folder)
+            {
+                FolderNode clone = (FolderNode)folder.Clone();
+                membersOnlyGraph.AddNode(clone);
 
-        foreach (var layer in layersInOrder)
-        {
-            var clone = (LayerNode)layer.Clone();
-            membersOnlyGraph.AddNode(clone);
+                var targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping);
+                
+                clone.Output.ConnectTo(targetInput);
+                nodeMapping[folder.Id] = clone.Id;
+            }
 
-            clone.Output.ConnectTo(lastInput);
-            lastInput = clone.Background;
-        }
+            return true;
+        });
 
         return membersOnlyGraph;
     }
@@ -129,19 +144,19 @@ public class DocumentRenderer : IPreviewRenderable
         string elementToRenderName)
     {
         IsBusy = true;
-        
-        if(renderTexture == null || renderTexture.Size != Document.Size)
+
+        if (renderTexture == null || renderTexture.Size != Document.Size)
         {
             renderTexture?.Dispose();
             renderTexture = Texture.ForProcessing(Document.Size, Document.ProcessingColorSpace);
         }
-        
+
         renderTexture.DrawingSurface.Canvas.Clear();
         context.RenderSurface = renderTexture.DrawingSurface;
         Document.NodeGraph.Execute(context);
-        
+
         renderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
-        
+
         IsBusy = false;
 
         return true;
@@ -150,18 +165,54 @@ public class DocumentRenderer : IPreviewRenderable
     public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime)
     {
         IsBusy = true;
-        
-        if(renderTexture == null || renderTexture.Size != Document.Size)
+
+        if (renderTexture == null || renderTexture.Size != Document.Size)
         {
             renderTexture?.Dispose();
             renderTexture = Texture.ForProcessing(Document.Size, Document.ProcessingColorSpace);
         }
-        
+
         renderTexture.DrawingSurface.Canvas.Clear();
-        RenderContext context = new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, Document.Size, Document.ProcessingColorSpace) { FullRerender = true };
+        RenderContext context =
+            new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, Document.Size,
+                Document.ProcessingColorSpace) { FullRerender = true };
         Document.NodeGraph.Execute(context);
-        
+
         toRenderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
         IsBusy = false;
     }
+    
+    private static IInputProperty GetTargetInput(IInputProperty? input, 
+        IReadOnlyNodeGraph sourceGraph,
+        NodeGraph membersOnlyGraph,
+        Dictionary<Guid, Guid> nodeMapping)
+    {
+        if(input == null) return membersOnlyGraph.OutputNode.Input;
+        
+        if (nodeMapping.ContainsKey(input.Node?.Id ?? Guid.Empty))
+        {
+            return membersOnlyGraph.Nodes.First(x => x.Id == nodeMapping[input.Node.Id])
+                .GetInputProperty(input.InternalPropertyName);
+        }
+        
+        var sourceNode = sourceGraph.AllNodes.First(x => x.Id == input.Node.Id);
+
+        IInputProperty? found = null;
+        sourceNode.TraverseForwards((n, input) =>
+        {
+            if (n is StructureNode structureNode)
+            {
+                if(nodeMapping.TryGetValue(structureNode.Id, out var value))
+                {
+                    Node mappedNode = membersOnlyGraph.Nodes.First(x => x.Id == value);
+                    found = mappedNode.GetInputProperty(input.InternalPropertyName);
+                    return false;
+                }
+            }
+            
+            return true;
+        });
+        
+        return found ?? membersOnlyGraph.OutputNode.Input;
+    }
 }

+ 14 - 0
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -140,6 +140,20 @@ internal class DocumentStructureModule
 
         return layers;
     }
+    
+    public List<IStructureMemberHandler> GetAllMembers()
+    {
+        List<IStructureMemberHandler> members = new List<IStructureMemberHandler>();
+
+        doc.NodeGraphHandler.TryTraverse(node =>
+        {
+            if (node is IStructureMemberHandler member)
+                members.Add(member);
+            return true;
+        });
+
+        return members;
+    }
 
     private void FillPath(INodeHandler node, List<INodeHandler> toFill)
     {

+ 1 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -697,7 +697,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
                 using Texture tmpTexture = Texture.ForProcessing(SizeBindable);
-                HashSet<Guid> layers = StructureHelper.GetAllLayers().Select(x => x.Id).ToHashSet();
+                HashSet<Guid> layers = StructureHelper.GetAllMembers().Select(x => x.Id).ToHashSet();
                 Renderer.RenderLayers(tmpTexture.DrawingSurface, layers, frameTime.Frame, ChunkResolution.Full);
                 
                 using Surface tmpSurface = new Surface(tmpTexture.Size);

+ 2 - 2
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -351,8 +351,8 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         if (doc is null || member is null)
             return;
 
-        IStructureMemberHandler? nextMergeableMember = doc.StructureHelper.GetAboveMember(member.Id, false);
-        IStructureMemberHandler? previousMergeableMember = doc.StructureHelper.GetBelowMember(member.Id, false);
+        IStructureMemberHandler? nextMergeableMember = doc.StructureHelper.GetAboveMember(member.Id, true);
+        IStructureMemberHandler? previousMergeableMember = doc.StructureHelper.GetBelowMember(member.Id, true);
 
         if (!above && previousMergeableMember is null)
             return;