فهرست منبع

Merge pull request #720 from PixiEditor/fixes/09.01.2025

Fixes/09.01.2025
Krzysztof Krysiński 8 ماه پیش
والد
کامیت
8139160006
29فایلهای تغییر یافته به همراه569 افزوده شده و 189 حذف شده
  1. 1 1
      src/PixiDocks
  2. 6 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  3. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  4. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  5. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkCache.cs
  6. 46 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectionsData.cs
  7. 1 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs
  8. 139 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs
  9. 65 28
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  10. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  11. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  12. 1 1
      src/PixiEditor.SVG/Elements/SvgImage.cs
  13. 1 1
      src/PixiEditor.SVG/SvgDocument.cs
  14. 11 3
      src/PixiEditor.SVG/SvgElement.cs
  15. 12 1
      src/PixiEditor.SVG/SvgProperty.cs
  16. 1 1
      src/PixiEditor/Data/ShortcutActionMaps/AsepriteShortcutMap.json
  17. 26 14
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  18. 1 1
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  19. 56 11
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  20. 1 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  21. 2 2
      src/PixiEditor/Models/Handlers/IAnimationHandler.cs
  22. 1 1
      src/PixiEditor/Models/Handlers/IDocumentOperations.cs
  23. 7 3
      src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs
  24. 2 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  25. 32 42
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  26. 72 1
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  27. 4 6
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  28. 76 63
      src/PixiEditor/Views/Layers/FolderControl.axaml
  29. 1 1
      src/PixiEditor/Views/Layers/LayerControl.axaml

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit c843c98b5101129180f859f752f490fce1afc957
+Subproject commit 5f14bdf0e46dd470e46a88ce5f58de4e02c68e94

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

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

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

@@ -230,7 +230,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         var image = new ImageLayerNode(startSize, colorSpace)
         {
             MemberName = this.MemberName, LockTransparency = this.LockTransparency,
-            ClipToPreviousMember = this.ClipToPreviousMember
+            ClipToPreviousMember = this.ClipToPreviousMember, EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted()
         };
 
         image.keyFrames.Clear();

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -252,14 +252,14 @@ internal class CombineStructureMembersOnto_Change : Change
         var toDrawOnImage = ((ImageLayerNode)targetLayer).GetLayerImageAtFrame(frame);
         toDrawOnImage.EnqueueClear();
 
-        Texture tempTexture = new Texture(target.Size);
+        Texture tempTexture = Texture.ForProcessing(target.Size, target.ProcessingColorSpace);
 
         DocumentRenderer renderer = new(target);
 
         AffectedArea affArea = new();
         DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
         {
-            renderer.RenderLayers(tempTexture.DrawingSurface, layersToCombine, frame, ChunkResolution.Full);
+            renderer.RenderLayers(tempTexture.DrawingSurface, layersToCombine, frame, ChunkResolution.Full, target.Size);
 
             toDrawOnImage.EnqueueDrawTexture(VecI.Zero, tempTexture);
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkCache.cs

@@ -62,7 +62,7 @@ internal class FloodFillChunkCache : IDisposable
             
             chunk.Surface.DrawingSurface.Canvas.Translate(-chunkPos.X, -chunkPos.Y);
             
-            document.Renderer.RenderLayers(chunk.Surface.DrawingSurface, membersToRender, frame, ChunkResolution.Full);
+            document.Renderer.RenderLayers(chunk.Surface.DrawingSurface, membersToRender, frame, ChunkResolution.Full, chunk.Surface.Size);
             
             chunk.Surface.DrawingSurface.Canvas.Restore();
             

+ 46 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectionsData.cs

@@ -10,4 +10,50 @@ public class ConnectionsData
         this.originalOutputConnections = originalOutputConnections;
         this.originalInputConnections = originalInputConnections;
     }
+
+    public ConnectionsData WithUpdatedIds(Dictionary<Guid,Guid> nodeMap)
+    {
+        Dictionary<PropertyConnection, List<PropertyConnection>> newOutputConnections = new();
+        foreach (var (key, value) in originalOutputConnections)
+        {
+            Guid? sourceNodeId = key.NodeId;
+            if (sourceNodeId.HasValue)
+            {
+                sourceNodeId = nodeMap[sourceNodeId.Value];
+            }
+            
+            var valueCopy = new List<PropertyConnection>();
+            foreach (var connection in value)
+            {
+                Guid? targetNodeId = connection.NodeId;
+                if (targetNodeId.HasValue)
+                {
+                    targetNodeId = nodeMap[targetNodeId.Value];
+                }
+                valueCopy.Add(connection with { NodeId = targetNodeId });
+            }
+            
+            newOutputConnections.Add(key with { NodeId = sourceNodeId }, valueCopy);
+        }
+        
+        List<(PropertyConnection, PropertyConnection?)> newInputConnections = new();
+        foreach (var (input, output) in originalInputConnections)
+        {
+            Guid? inputNodeId = input.NodeId;
+            if (inputNodeId.HasValue)
+            {
+                inputNodeId = nodeMap[inputNodeId.Value];
+            }
+            
+            Guid? outputNodeId = output?.NodeId;
+            if (outputNodeId.HasValue)
+            {
+                outputNodeId = nodeMap[outputNodeId.Value];
+            }
+            
+            newInputConnections.Add((input with { NodeId = inputNodeId }, new PropertyConnection(outputNodeId, output?.PropertyName)));
+        }
+        
+        return new ConnectionsData(newOutputConnections, newInputConnections);
+    }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -133,7 +133,7 @@ public static class NodeOperations
         return changes;
     }
 
-    public static ConnectionsData CreateConnectionsData(Node node)
+    public static ConnectionsData CreateConnectionsData(IReadOnlyNode node)
     {
         var originalOutputConnections = new Dictionary<PropertyConnection, List<PropertyConnection>>();
 

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

@@ -0,0 +1,139 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal class DuplicateFolder_Change : Change
+{
+    private readonly Guid folderGuid;
+    private Guid duplicateGuid;
+    private Guid[] contentGuids;
+    private Guid[] contentDuplicateGuids;
+
+    private ConnectionsData? connectionsData;
+    private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
+
+    [GenerateMakeChangeAction]
+    public DuplicateFolder_Change(Guid folderGuid, Guid newGuid)
+    {
+        this.folderGuid = folderGuid;
+        duplicateGuid = newGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (!target.TryFindMember<FolderNode>(folderGuid, out FolderNode? folder))
+            return false;
+
+        connectionsData = NodeOperations.CreateConnectionsData(folder);
+
+        List<Guid> contentGuidList = new();
+
+        folder.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            contentGuidList.Add(x.Id);
+            contentConnectionsData[x.Id] = NodeOperations.CreateConnectionsData(x);
+            return true;
+        });
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        (FolderNode existingLayer, Node parent) = ((FolderNode, Node))target.FindChildAndParentOrThrow(folderGuid);
+
+        FolderNode clone = (FolderNode)existingLayer.Clone();
+        clone.Id = duplicateGuid;
+
+        InputProperty<Painter?> targetInput = parent.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter) &&
+            x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
+
+        List<IChangeInfo> operations = new();
+
+        target.NodeGraph.AddNode(clone);
+        
+        operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
+        operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+
+        DuplicateContent(target, clone, existingLayer, operations);
+        
+        ignoreInUndo = false;
+
+        return operations;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var (member, parent) = target.FindChildAndParentOrThrow(duplicateGuid);
+
+        target.NodeGraph.RemoveNode(member);
+        member.Dispose();
+
+        List<IChangeInfo> changes = new();
+
+        changes.AddRange(NodeOperations.DetachStructureNode(member));
+        changes.Add(new DeleteStructureMember_ChangeInfo(member.Id));
+
+        if (contentDuplicateGuids is not null)
+        {
+            foreach (Guid contentGuid in contentDuplicateGuids)
+            {
+                Node contentNode = target.FindNodeOrThrow<Node>(contentGuid);
+                changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
+                changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
+                
+                target.NodeGraph.RemoveNode(contentNode);
+                contentNode.Dispose();
+            }
+        }
+
+        if (connectionsData is not null)
+        {
+            Node originalNode = target.FindNodeOrThrow<Node>(folderGuid);
+            changes.AddRange(
+                NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
+        }
+
+        return changes;
+    }
+
+    private void DuplicateContent(Document target, FolderNode clone, FolderNode existingLayer,
+        List<IChangeInfo> operations)
+    {
+        Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
+
+        nodeMap[existingLayer.Id] = clone.Id;
+        List<Guid> contentGuidList = new();
+
+        existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            if (x is not Node targetNode)
+                return false;
+
+            Node? node = targetNode.Clone();
+            nodeMap[x.Id] = node.Id;
+            contentGuidList.Add(node.Id);
+
+            target.NodeGraph.AddNode(node);
+
+            operations.Add(CreateNode_ChangeInfo.CreateFromNode(node));
+            return true;
+        });
+
+        foreach (var data in contentConnectionsData)
+        {
+            var updatedData = data.Value.WithUpdatedIds(nodeMap);
+            Guid targetNodeId = nodeMap[data.Key];
+            operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
+                target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
+        }
+        
+        contentDuplicateGuids = contentGuidList.ToArray();
+    }
+}

+ 65 - 28
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -21,7 +21,7 @@ public class DocumentRenderer : IPreviewRenderable
     };
 
     private Texture renderTexture;
-    
+
     public DocumentRenderer(IReadOnlyDocument document)
     {
         Document = document;
@@ -47,28 +47,47 @@ public class DocumentRenderer : IPreviewRenderable
         }
     }
 
-    public void RenderLayers(DrawingSurface toDrawOn, HashSet<Guid> layersToCombine, int frame,
-        ChunkResolution resolution)
+    public void RenderLayers(DrawingSurface toRenderOn, HashSet<Guid> layersToCombine, int frame,
+        ChunkResolution resolution, VecI renderSize)
     {
         IsBusy = true;
-        RenderContext context = new(toDrawOn, frame, resolution, Document.Size, Document.ProcessingColorSpace);
+
+        if (renderTexture == null || renderTexture.Size != renderSize)
+        {
+            renderTexture?.Dispose();
+            renderTexture = Texture.ForProcessing(renderSize, Document.ProcessingColorSpace);
+        }
+
+        renderTexture.DrawingSurface.Canvas.Save();
+        renderTexture.DrawingSurface.Canvas.Clear();
+
+        renderTexture.DrawingSurface.Canvas.SetMatrix(toRenderOn.Canvas.TotalMatrix);
+        toRenderOn.Canvas.Save();
+        toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
+
+        RenderContext context = new(renderTexture.DrawingSurface, frame, resolution, Document.Size,
+            Document.ProcessingColorSpace);
         context.FullRerender = true;
         IReadOnlyNodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
         try
         {
             membersOnlyGraph.Execute(context);
+            toRenderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
         }
         catch (ObjectDisposedException)
         {
         }
         finally
         {
+            renderTexture.DrawingSurface.Canvas.Restore();
+            toRenderOn.Canvas.Restore();
             IsBusy = false;
         }
     }
 
 
-    public void RenderLayer(DrawingSurface renderOn, Guid layerId, ChunkResolution resolution, KeyFrameTime frameTime)
+    public void RenderLayer(DrawingSurface toRenderOn, Guid layerId, ChunkResolution resolution, KeyFrameTime frameTime,
+        VecI renderSize)
     {
         var node = Document.FindMember(layerId);
 
@@ -79,26 +98,44 @@ public class DocumentRenderer : IPreviewRenderable
 
         IsBusy = true;
 
-        RenderContext context = new(renderOn, frameTime, resolution, Document.Size, Document.ProcessingColorSpace);
+        if (renderTexture == null || renderTexture.Size != renderSize)
+        {
+            renderTexture?.Dispose();
+            renderTexture = Texture.ForProcessing(renderSize, Document.ProcessingColorSpace);
+        }
+
+        renderTexture.DrawingSurface.Canvas.Save();
+        renderTexture.DrawingSurface.Canvas.Clear();
+
+        renderTexture.DrawingSurface.Canvas.SetMatrix(toRenderOn.Canvas.TotalMatrix);
+        toRenderOn.Canvas.Save();
+        toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
+
+        RenderContext context = new(renderTexture.DrawingSurface, frameTime, resolution, Document.Size, Document.ProcessingColorSpace);
         context.FullRerender = true;
 
-        node.RenderForOutput(context, renderOn, null);
+        node.RenderForOutput(context, toRenderOn, null);
+        
+        renderTexture.DrawingSurface.Canvas.Restore();
+        toRenderOn.Canvas.Restore();
+        
         IsBusy = false;
     }
-    
-    public void RenderNodePreview(IPreviewRenderable previewRenderable, DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+
+    public void RenderNodePreview(IPreviewRenderable previewRenderable, DrawingSurface renderOn, RenderContext context,
+        string elementToRenderName)
     {
         if (IsBusy)
         {
             return;
         }
-        
+
         IsBusy = true;
-        
-        if(previewRenderable is Node { IsDisposed: true }) return;
-        
+
+        if (previewRenderable is Node { IsDisposed: true }) return;
+
         previewRenderable.RenderPreview(renderOn, context, elementToRenderName);
-        
+
         IsBusy = false;
     }
 
@@ -132,9 +169,9 @@ public class DocumentRenderer : IPreviewRenderable
                 LayerNode clone = (LayerNode)layer.Clone();
                 membersOnlyGraph.AddNode(clone);
 
-                
+
                 IInputProperty targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping);
-                
+
                 clone.Output.ConnectTo(targetInput);
                 nodeMapping[layer.Id] = clone.Id;
             }
@@ -144,7 +181,7 @@ public class DocumentRenderer : IPreviewRenderable
                 membersOnlyGraph.AddNode(clone);
 
                 var targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping);
-                
+
                 clone.Output.ConnectTo(targetInput);
                 nodeMapping[folder.Id] = clone.Id;
             }
@@ -196,38 +233,38 @@ public class DocumentRenderer : IPreviewRenderable
         renderTexture.DrawingSurface.Canvas.SetMatrix(toRenderOn.Canvas.TotalMatrix);
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
-        
+
         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);
-        
+
         renderTexture.DrawingSurface.Canvas.Restore();
         toRenderOn.Canvas.Restore();
-        
+
         IsBusy = false;
     }
-    
-    private static IInputProperty GetTargetInput(IInputProperty? input, 
+
+    private static IInputProperty GetTargetInput(IInputProperty? input,
         IReadOnlyNodeGraph sourceGraph,
         NodeGraph membersOnlyGraph,
         Dictionary<Guid, Guid> nodeMapping)
     {
         if (input == null)
         {
-            if(membersOnlyGraph.OutputNode is IRenderInput inputNode) return inputNode.Background;
+            if (membersOnlyGraph.OutputNode is IRenderInput inputNode) return inputNode.Background;
 
             return null;
         }
-        
+
         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;
@@ -235,17 +272,17 @@ public class DocumentRenderer : IPreviewRenderable
         {
             if (n is StructureNode structureNode)
             {
-                if(nodeMapping.TryGetValue(structureNode.Id, out var value))
+                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 as IRenderInput)?.Background;
     }
 }

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll


+ 1 - 1
src/PixiEditor.SVG/Elements/SvgImage.cs

@@ -12,7 +12,7 @@ public class SvgImage : SvgElement
     public SvgProperty<SvgNumericUnit> Width { get; } = new("width");
     public SvgProperty<SvgNumericUnit> Height { get; } = new("height");
         
-    public SvgProperty<SvgStringUnit> Href { get; } = new("xlink:href");
+    public SvgProperty<SvgStringUnit> Href { get; } = new("href", "xlink");
     public SvgProperty<SvgLinkUnit> Mask { get; } = new("mask");
     public SvgProperty<SvgEnumUnit<SvgImageRenderingType>> ImageRendering { get; } = new("image-rendering");
 

+ 1 - 1
src/PixiEditor.SVG/SvgDocument.cs

@@ -65,7 +65,7 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
 
         foreach (var usedNamespace in usedNamespaces)
         {
-            document.Root.Add(new XAttribute($"xmlns:{usedNamespace.Key}", usedNamespace.Value));
+            document.Root.Add(new XAttribute(XNamespace.Xmlns + usedNamespace.Key, usedNamespace.Value));
         }
 
         AppendProperties(document.Root);

+ 11 - 3
src/PixiEditor.SVG/SvgElement.cs

@@ -25,7 +25,15 @@ public class SvgElement(string tagName)
                 SvgProperty prop = (SvgProperty)property.GetValue(this);
                 if (prop?.Unit != null)
                 {
-                    element.Add(new XAttribute(prop.SvgName, prop.Unit.ToXml()));
+                    if (!string.IsNullOrEmpty(prop.NamespaceName))
+                    {
+                        XName name = XNamespace.Get(RequiredNamespaces[prop.NamespaceName]) + prop.SvgName;
+                        element.Add(new XAttribute(name, prop.Unit.ToXml()));
+                    }
+                    else
+                    {
+                        element.Add(new XAttribute(prop.SvgName, prop.Unit.ToXml()));
+                    }
                 }
             }
         }
@@ -37,7 +45,7 @@ public class SvgElement(string tagName)
                 element.Add(child.ToXml(nameSpace));
             }
         }
-        
+
         return element;
     }
 
@@ -72,7 +80,7 @@ public class SvgElement(string tagName)
             property.Unit.ValuesFromXml(reader.Value);
         }
     }
-    
+
     private void ParseListProperty(SvgList list, XmlReader reader)
     {
         list.Unit ??= CreateDefaultUnit(list);

+ 12 - 1
src/PixiEditor.SVG/SvgProperty.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Units;
+using System.Xml.Linq;
+using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG;
 
@@ -8,7 +9,13 @@ public abstract class SvgProperty
     {
         SvgName = svgName;
     }
+    
+    protected SvgProperty(string svgName, string? namespaceName) : this(svgName)
+    {
+        NamespaceName = namespaceName;
+    }
 
+    public string? NamespaceName { get; set; }
     public string SvgName { get; set; }
     public ISvgUnit? Unit { get; set; }
 }
@@ -24,4 +31,8 @@ public class SvgProperty<T> : SvgProperty where T : struct, ISvgUnit
     public SvgProperty(string svgName) : base(svgName)
     {
     }
+    
+    public SvgProperty(string svgName, string? namespaceName) : base(svgName, namespaceName)
+    {
+    }
 }

+ 1 - 1
src/PixiEditor/Data/ShortcutActionMaps/AsepriteShortcutMap.json

@@ -224,7 +224,7 @@
       "Parameters": []
     },
     "DuplicateLayer": {
-      "Command": "PixiEditor.Layer.DuplicateSelectedLayer",
+      "Command": "PixiEditor.Layer.DuplicateSelectedMember",
       "DefaultShortcut": {
         "key": "None",
         "modifiers": null

+ 26 - 14
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -200,7 +200,7 @@ internal static class ClipboardController
         {
             foreach (var layerId in layerIds)
             {
-                document.Operations.DuplicateLayer(layerId);
+                document.Operations.DuplicateMember(layerId);
             }
 
             return true;
@@ -577,27 +577,29 @@ internal static class ClipboardController
         await CopyIds(nodeIds, ClipboardDataFormats.NodeIdList);
     }
 
-    public static async Task<List<Guid>> GetNodeIds()
+    public static async Task<Guid[]> GetNodeIds()
     {
-        var data = await TryGetDataObject();
-        var nodeIds = GetNodeIds(data);
-        
-        return nodeIds.ToList();
+        return await GetIds(ClipboardDataFormats.NodeIdList);
     }
 
-    public static async Task<Guid[]> GetNodesFromClipboard()
+    public static async Task<Guid[]> GetCelIds()
+    {
+        return await GetIds(ClipboardDataFormats.CelIdList);
+    }
+    
+    public static async Task<Guid[]> GetIds(string format)
     {
         var data = await TryGetDataObject();
-        return GetNodeIds(data);
+        return GetIds(data, format);
     }
 
-    private static Guid[] GetNodeIds(IEnumerable<IDataObject?> data)
+    private static Guid[] GetIds(IEnumerable<IDataObject?> data, string format)
     {
         foreach (var dataObject in data)
         {
-            if (dataObject.Contains(ClipboardDataFormats.NodeIdList))
+            if (dataObject.Contains(format))
             {
-                byte[] nodeIds = (byte[])dataObject.Get(ClipboardDataFormats.NodeIdList);
+                byte[] nodeIds = (byte[])dataObject.Get(format);
                 string nodeIdsString = System.Text.Encoding.UTF8.GetString(nodeIds);
                 return nodeIdsString.Split(';').Select(Guid.Parse).ToArray();
             }
@@ -607,19 +609,29 @@ internal static class ClipboardController
     }
 
     public static async Task<bool> AreNodesInClipboard()
+    {
+        return await AreIdsInClipboard(ClipboardDataFormats.NodeIdList);
+    }
+
+    public static async Task<bool> AreCelsInClipboard()
+    {
+        return await AreIdsInClipboard(ClipboardDataFormats.CelIdList);
+    }
+
+    public static async Task<bool> AreIdsInClipboard(string format)
     {
         var formats = await Clipboard.GetFormatsAsync();
         if (formats == null || formats.Length == 0)
             return false;
-        
-        return formats.Contains(ClipboardDataFormats.NodeIdList);
+
+        return formats.Contains(format);
     }
 
     public static async Task CopyCels(Guid[] celIds)
     {
         await CopyIds(celIds, ClipboardDataFormats.CelIdList);
     }
-    
+
     public static async Task CopyIds(Guid[] ids, string format)
     {
         await Clipboard.ClearAsync();

+ 1 - 1
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -487,7 +487,7 @@ internal class DocumentUpdater
 
     private void ProcessKeyFrameLength(KeyFrameLength_ChangeInfo info)
     {
-        doc.AnimationHandler.SetFrameLength(info.KeyFrameGuid, info.StartFrame, info.Duration);
+        doc.AnimationHandler.SetCelLength(info.KeyFrameGuid, info.StartFrame, info.Duration);
     }
 
     private void ProcessKeyFrameVisibility(KeyFrameVisibility_ChangeInfo info)

+ 56 - 11
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -1,4 +1,5 @@
-using System.Collections.Immutable;
+using System.Collections;
+using System.Collections.Immutable;
 using System.Reactive.Disposables;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument;
@@ -34,7 +35,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         Document = document;
         Internals = internals;
     }
-    
+
     public ChangeBlock StartChangeBlock()
     {
         return new ChangeBlock(Internals.ActionAccumulator);
@@ -196,17 +197,28 @@ internal class DocumentOperationsModule : IDocumentOperations
     }
 
     /// <summary>
-    /// Duplicates the layer with the <paramref name="guidValue"/>
+    /// Duplicates the member with the <paramref name="guidValue"/>
     /// </summary>
-    /// <param name="guidValue">The Guid of the layer</param>
-    public void DuplicateLayer(Guid guidValue)
+    /// <param name="guidValue">The Guid of the member</param>
+    public void DuplicateMember(Guid guidValue)
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
 
         Internals.ChangeController.TryStopActiveExecutor();
 
-        Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
+        bool isFolder = Document.StructureHelper.Find(guidValue) is IFolderHandler;
+        if (!isFolder)
+        {
+            Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
+        }
+        else
+        {
+            Guid newGuid = Guid.NewGuid();
+            Internals.ActionAccumulator.AddFinishedActions(
+                new DuplicateFolder_Action(guidValue, newGuid),
+                new SetSelectedMember_PassthroughAction(newGuid));
+        }
     }
 
     /// <summary>
@@ -452,7 +464,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         IMidChangeUndoableExecutor executor =
             Internals.ChangeController.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
-        if (executor is { CanRedo: true }) 
+        if (executor is { CanRedo: true })
         {
             executor.OnMidChangeRedo();
             return;
@@ -850,14 +862,47 @@ internal class DocumentOperationsModule : IDocumentOperations
             return null;
 
         Internals.ChangeController.TryStopActiveExecutor();
-        
-        if(!Document.StructureHelper.TryFindNode(nodeId, out INodeHandler node) || node.InternalName == OutputNode.UniqueName)
+
+        if (!Document.StructureHelper.TryFindNode(nodeId, out INodeHandler node) ||
+            node.InternalName == OutputNode.UniqueName)
             return null;
 
         Guid newGuid = Guid.NewGuid();
-        
+
         Internals.ActionAccumulator.AddFinishedActions(new DuplicateNode_Action(nodeId, newGuid));
-        
+
         return newGuid;
     }
+
+    public void ChangeCelLength(Guid celId, int startFrame, int duration)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+
+        Internals.ActionAccumulator.AddFinishedActions(new KeyFrameLength_Action(celId, startFrame, duration), new EndKeyFrameLength_Action());
+    }
+
+    public void DeleteNodes(Guid[] nodes)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+        
+        List<IAction> actions = new();
+
+        for (var i = 0; i < nodes.Length; i++)
+        {
+            var node = nodes[i];
+            if (Document.StructureHelper.TryFindNode(node, out INodeHandler nodeHandler) &&
+                nodeHandler.InternalName == OutputNode.UniqueName)
+                return;
+            
+            actions.Add(new DeleteNode_Action(node));
+        }
+
+        Internals.ActionAccumulator.AddFinishedActions(actions.ToArray());
+    }
 }

+ 1 - 1
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -132,7 +132,7 @@ internal class DocumentStructureModule
     ///     Returns all layers in the document.
     /// </summary>
     /// <returns>List of ILayerHandlers. Empty if no layers found.</returns>
-    public List<ILayerHandler> GetAllLayers()
+    public List<ILayerHandler> GetAllLayers(bool includeFoldersWithMask = false)
     {
         List<ILayerHandler> layers = new List<ILayerHandler>();
 

+ 2 - 2
src/PixiEditor/Models/Handlers/IAnimationHandler.cs

@@ -11,10 +11,10 @@ internal interface IAnimationHandler
     public int OnionFramesBindable { get; set; }
     public double OnionOpacityBindable { get; set; }
     public bool IsPlayingBindable { get; set; }
-    public void CreateCel(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null);
+    public Guid? CreateCel(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null);
     public void SetFrameRate(int newFrameRate);
     public void SetActiveFrame(int newFrame);
-    public void SetFrameLength(Guid keyFrameId, int newStartFrame, int newDuration);
+    public void SetCelLength(Guid keyFrameId, int newStartFrame, int newDuration);
     public void SetKeyFrameVisibility(Guid infoKeyFrameId, bool infoIsVisible);
     public bool FindKeyFrame<T>(Guid guid, out T keyFrameHandler) where T : ICelHandler;
     internal void AddKeyFrame(ICelHandler iCel);

+ 1 - 1
src/PixiEditor/Models/Handlers/IDocumentOperations.cs

@@ -6,7 +6,7 @@ namespace PixiEditor.Models.Handlers;
 internal interface IDocumentOperations
 {
     public void DeleteStructureMember(Guid memberGuidValue);
-    public void DuplicateLayer(Guid memberGuidValue);
+    public void DuplicateMember(Guid memberGuidValue);
     public void AddSoftSelectedMember(Guid memberGuidValue);
     public void MoveStructureMember(Guid memberGuidValue, Guid target, StructureMemberPlacement placement);
     public void SetSelectedMember(Guid memberId);

+ 7 - 3
src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs

@@ -129,15 +129,19 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 
     public KeyFrameTime ActiveFrameTime => new KeyFrameTime(ActiveFrameBindable, ActiveNormalizedTime);
 
-    public void CreateCel(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null,
+    public Guid? CreateCel(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null,
         int? frameToCopyFrom = null)
     {
         if (!Document.BlockingUpdateableChangeActive)
         {
+            Guid newCelGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(new CreateCel_Action(targetLayerGuid,
-                Guid.NewGuid(), Math.Max(1, frame),
+                newCelGuid, Math.Max(1, frame),
                 frameToCopyFrom ?? -1, toCloneFrom ?? Guid.Empty));
+            return newCelGuid;
         }
+        
+        return null;
     }
 
     public void DeleteCels(List<Guid> keyFrameIds)
@@ -220,7 +224,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         OnPropertyChanged(nameof(OnionOpacityBindable));
     }
 
-    public void SetFrameLength(Guid keyFrameId, int newStartFrame, int newDuration)
+    public void SetCelLength(Guid keyFrameId, int newStartFrame, int newDuration)
     {
         if (TryFindCels(keyFrameId, out CelViewModel keyFrame))
         {

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

@@ -253,7 +253,8 @@ internal partial class DocumentViewModel
         DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
         {
             using Surface surface = new Surface(SizeBindable);
-            Renderer.RenderLayer(surface.DrawingSurface, imageNode.Id, ChunkResolution.Full, atTime.Frame);
+            Renderer.RenderLayer(surface.DrawingSurface, imageNode.Id, ChunkResolution.Full, atTime.Frame,
+                SizeBindable);
 
             toSave = surface.DrawingSurface.Snapshot((RectI)tightBounds.Value);
         });

+ 32 - 42
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -160,7 +160,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
     public bool IsChangeFeatureActive<T>() where T : IExecutorFeature =>
         Internals.ChangeController.IsChangeOfTypeActive<T>();
-    
+
     public T? TryGetExecutorFeature<T>() where T : IExecutorFeature =>
         Internals.ChangeController.TryGetExecutorFeature<T>();
 
@@ -537,7 +537,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public OneOf<Error, None, (Surface, RectI)> TryExtractAreaFromSelected(
         RectI bounds)
     {
-        var selectedLayers = ExtractSelectedLayers();
+        var selectedLayers = ExtractSelectedLayers(true);
         if (selectedLayers.Count == 0)
             return new Error();
         if (bounds.IsZeroOrNegativeArea)
@@ -547,23 +547,15 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
         for (int i = 0; i < selectedLayers.Count; i++)
         {
-            var layerVm = StructureHelper.Find(selectedLayers[i]);
-            IReadOnlyStructureNode? layer = Internals.Tracker.Document.FindMember(layerVm.Id);
+            var memberVm = StructureHelper.Find(selectedLayers[i]);
+            IReadOnlyStructureNode? layer = Internals.Tracker.Document.FindMember(memberVm.Id);
             if (layer is null)
                 return new Error();
 
             RectI? memberImageBounds;
             try
             {
-                if (layer is IReadOnlyImageNode imgNode)
-                {
-                    memberImageBounds = imgNode.GetLayerImageAtFrame(AnimationDataViewModel.ActiveFrameBindable)
-                        .FindChunkAlignedMostUpToDateBounds();
-                }
-                else
-                {
-                    memberImageBounds = (RectI?)layer.GetTightBounds(AnimationDataViewModel.ActiveFrameTime);
-                }
+                memberImageBounds = (RectI?)layer.GetTightBounds(AnimationDataViewModel.ActiveFrameTime);
             }
             catch (ObjectDisposedException)
             {
@@ -595,8 +587,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         Surface output = new(finalBounds.Size);
 
         VectorPath clipPath = new VectorPath(SelectionPathBindable) { FillType = PathFillType.EvenOdd };
-        clipPath.Transform(Matrix3X3.CreateTranslation(-bounds.X, -bounds.Y));
+        //clipPath.Transform(Matrix3X3.CreateTranslation(-bounds.X, -bounds.Y));
         output.DrawingSurface.Canvas.Save();
+        output.DrawingSurface.Canvas.Translate(-finalBounds.X, -finalBounds.Y);
         if (!clipPath.IsEmpty)
         {
             output.DrawingSurface.Canvas.ClipPath(clipPath);
@@ -604,28 +597,18 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
         using Paint paint = new Paint() { BlendMode = BlendMode.SrcOver };
 
-        foreach (var layer in selectedLayers)
+        DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
         {
             try
             {
-                var layerVm = Internals.Tracker.Document.FindMember(layer);
-
-                DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
-                {
-                    using Surface toPaintOn = new Surface(SizeBindable);
-
-                    Renderer.RenderLayer(toPaintOn.DrawingSurface, layerVm.Id, ChunkResolution.Full,
-                        AnimationDataViewModel.ActiveFrameTime);
-                    using Image snapshot = toPaintOn.DrawingSurface.Snapshot(finalBounds);
-                    output.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, paint);
-                });
+                Renderer.RenderLayers(output.DrawingSurface, selectedLayers.ToHashSet(),
+                    AnimationDataViewModel.ActiveFrameBindable, ChunkResolution.Full, SizeBindable);
             }
             catch (ObjectDisposedException)
             {
-                output.Dispose();
-                return new Error();
+                output?.Dispose();
             }
-        }
+        });
 
         output.DrawingSurface.Canvas.Restore();
         return (output, finalBounds);
@@ -695,14 +678,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             // via a passthrough action to avoid all the try catches
             if (scope == DocumentScope.AllLayers)
             {
-                VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
-                using Texture tmpTexture = Texture.ForProcessing(SizeBindable);
+                using Surface tmpSurface = new Surface(SizeBindable);
                 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);
-                tmpSurface.DrawingSurface.Canvas.DrawImage(tmpTexture.DrawingSurface.Snapshot(), 0, 0);
-                
+                Renderer.RenderLayers(tmpSurface.DrawingSurface, layers, frameTime.Frame, ChunkResolution.Full,
+                    SizeBindable);
+
                 return tmpSurface.GetSrgbPixel(pos);
             }
 
@@ -723,7 +703,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             {
                 return layer.GetLayerImageAtFrame(frameTime.Frame).GetMostUpToDatePixel(pos);
             }
-            
+
             return Colors.Transparent;
         }
         catch (ObjectDisposedException)
@@ -848,13 +828,23 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     {
         var result = new List<Guid>();
         List<Guid> selectedMembers = GetSelectedMembers();
-        var allLayers = StructureHelper.GetAllLayers();
+        var allLayers = StructureHelper.GetAllMembers();
         foreach (var member in allLayers)
         {
-            if (selectedMembers.Contains(member.Id))
+            if(!selectedMembers.Contains(member.Id))
+                continue;
+            
+            if (member is ILayerHandler)
             {
                 result.Add(member.Id);
             }
+            else if (member is IFolderHandler folder)
+            {
+                if (includeFoldersWithMask && folder.HasMaskBindable)
+                    result.Add(folder.Id);
+
+                ExtractSelectedLayers(folder, result, includeFoldersWithMask);
+            }
         }
 
         return result;
@@ -870,11 +860,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     {
         foreach (var member in folder.Children)
         {
-            if (member is ImageLayerNodeViewModel layer && !list.Contains(layer.Id))
+            if (member is ILayerHandler layer && !list.Contains(layer.Id))
             {
                 list.Add(layer.Id);
             }
-            else if (member is FolderNodeViewModel childFolder)
+            else if (member is IFolderHandler childFolder)
             {
                 if (includeFoldersWithMask && childFolder.HasMaskBindable && !list.Contains(childFolder.Id))
                     list.Add(childFolder.Id);
@@ -894,7 +884,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
         int firstFrame = AnimationDataViewModel.FirstFrame;
         int lastFrame = AnimationDataViewModel.LastFrame;
-        
+
         int framesCount = lastFrame - firstFrame;
 
         Image[] images = new Image[framesCount];

+ 72 - 1
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -157,7 +157,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         if (doc is null)
             return;
 
-        List<Guid> toDuplicate = await ClipboardController.GetNodeIds();
+        Guid[] toDuplicate = await ClipboardController.GetNodeIds();
 
         List<Guid> newIds = new();
 
@@ -200,6 +200,71 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         });
     }
 
+    [Command.Basic("PixiEditor.Clipboard.PasteCels", "PASTE_CELS", "PASTE_CELS_DESCRIPTIVE",
+        CanExecute = "PixiEditor.Clipboard.CanPasteCels", Key = Key.V, Modifiers = KeyModifiers.Control,
+        ShortcutContexts = [typeof(TimelineDockViewModel)], Icon = PixiPerfectIcons.Paste, AnalyticsTrack = true)]
+    public async Task PasteCels()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+
+        var cels = await ClipboardController.GetCelIds();
+
+        if (cels.Length == 0)
+            return;
+
+        using var block = doc.Operations.StartChangeBlock();
+
+        List<Guid> newCels = new();
+        List<ICelHandler> celsToSelect = new();
+
+        int minStartFrame = int.MaxValue;
+        
+        foreach (var cel in cels)
+        {
+            var foundCel = doc.AnimationDataViewModel.AllCels.FirstOrDefault(x => x.Id == cel);
+            if (foundCel == null)
+                continue;
+            
+            celsToSelect.Add(foundCel);
+            minStartFrame = Math.Min(minStartFrame, foundCel.StartFrameBindable);
+        }
+        
+        int delta = doc.AnimationDataViewModel.ActiveFrameBindable - minStartFrame;
+
+        foreach (var cel in celsToSelect)
+        {
+            int celFrame = cel.StartFrameBindable + delta;
+            Guid? newCel = doc.AnimationDataViewModel.CreateCel(cel.LayerGuid,
+                celFrame, cel.LayerGuid,
+                cel.StartFrameBindable);
+            if (newCel != null)
+            {
+                int duration = cel.DurationBindable;
+                doc.Operations.ChangeCelLength(newCel.Value, celFrame, duration);
+                newCels.Add(newCel.Value);
+            }
+        }
+
+        doc.Operations.InvokeCustomAction(() =>
+        {
+            foreach (var cel in doc.AnimationDataViewModel.AllCels)
+            {
+                cel.IsSelected = false;
+            }
+
+            foreach (var cel in newCels)
+            {
+                var celInstance = doc.AnimationDataViewModel.AllCels.FirstOrDefault(x => x.Id == cel);
+                if (celInstance != null)
+                {
+                    celInstance.IsSelected = true;
+                }
+            }
+        });
+    }
+
 
     [Command.Basic("PixiEditor.Clipboard.Copy", "COPY", "COPY_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanCopy",
         Key = Key.C, Modifiers = KeyModifiers.Control,
@@ -324,6 +389,12 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         return Owner.DocumentIsNotNull(null) && ClipboardController.AreNodesInClipboard().Result;
     }
 
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteCels")]
+    public bool CanPasteCels()
+    {
+        return Owner.DocumentIsNotNull(null) && ClipboardController.AreCelsInClipboard().Result;
+    }
+
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteColor")]
     public static async Task<bool> CanPasteColor()
     {

+ 4 - 6
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -181,16 +181,14 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    [Command.Basic("PixiEditor.Layer.DuplicateSelectedLayer", "DUPLICATE_SELECTED_LAYER", "DUPLICATE_SELECTED_LAYER",
-        CanExecute = "PixiEditor.Layer.SelectedMemberIsLayer",
+    [Command.Basic("PixiEditor.Layer.DuplicateSelectedMember", "DUPLICATE_SELECTED_LAYER", "DUPLICATE_SELECTED_LAYER",
         Icon = PixiPerfectIcons.DuplicateFile, MenuItemPath = "EDIT/DUPLICATE", MenuItemOrder = 5,
         AnalyticsTrack = true)]
-    public void DuplicateLayer()
+    public void DuplicateMember()
     {
         var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
-        if (member is not ILayerHandler)
-            return;
-        member.Document.Operations.DuplicateLayer(member.Id);
+
+        member.Document.Operations.DuplicateMember(member.Id);
     }
 
     [Evaluator.CanExecute("PixiEditor.Layer.SelectedMemberIsLayer")]

+ 76 - 63
src/PixiEditor/Views/Layers/FolderControl.axaml

@@ -14,65 +14,71 @@
              xmlns:input1="clr-namespace:PixiEditor.Views.Input;assembly=PixiEditor.UI.Common"
              xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
              xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
-             mc:Ignorable="d" 
+             mc:Ignorable="d"
              Focusable="True"
-             d:DesignHeight="35" 
-             d:DesignWidth="250" 
+             d:DesignHeight="35"
+             d:DesignWidth="250"
              x:Name="folderControl">
-    <Border BorderThickness="0 0 0 0.5" BorderBrush="Gray" MinWidth="60" Focusable="True" Tag="{Binding ElementName=folderControl}">
+    <Border BorderThickness="0 0 0 0.5" BorderBrush="Gray" MinWidth="60" Focusable="True"
+            Tag="{Binding ElementName=folderControl}">
         <Border.Background>
             <Binding ElementName="folderControl" Path="Folder.Selection">
                 <Binding.Converter>
                     <converters:StructureMemberSelectionTypeToColorConverter
                         SoftColor="{StaticResource SoftSelectedLayerBrush}"
                         HardColor="{StaticResource SelectedLayerBrush}"
-                        NoneColor="Transparent"
-                        />
+                        NoneColor="Transparent" />
                 </Binding.Converter>
             </Binding>
         </Border.Background>
         <Interaction.Behaviors>
-            <behaviours:ClearFocusOnClickBehavior/>
+            <behaviours:ClearFocusOnClickBehavior />
         </Interaction.Behaviors>
         <Grid>
             <Grid.RowDefinitions>
-                <RowDefinition Height="10"/>
-                <RowDefinition Height="16"/>
-                <RowDefinition Height="10"/>
+                <RowDefinition Height="10" />
+                <RowDefinition Height="16" />
+                <RowDefinition Height="10" />
             </Grid.RowDefinitions>
-            <Grid DragDrop.AllowDrop="True" Name="TopDropGrid" Grid.Row="0" Grid.ColumnSpan="3" Background="Transparent" Panel.ZIndex="3"/>
-            <Grid IsVisible="True" Margin="20, 0, 0,0" x:Name="middleDropGrid" Grid.Row="1" DragDrop.AllowDrop="True" Panel.ZIndex="2" Background="Transparent"  />
+            <Grid DragDrop.AllowDrop="True" Name="TopDropGrid" Grid.Row="0" Grid.ColumnSpan="3"
+                  Background="Transparent" Panel.ZIndex="3" />
+            <Grid IsVisible="True" Margin="20, 0, 0,0" x:Name="middleDropGrid" Grid.Row="1" DragDrop.AllowDrop="True"
+                  Panel.ZIndex="2" Background="Transparent" />
             <Grid x:Name="centerGrid" Grid.Row="0" Grid.RowSpan="3" Background="Transparent">
                 <Grid.ColumnDefinitions>
-                    <ColumnDefinition Width="24"/>
-                    <ColumnDefinition Width="*"/>
+                    <ColumnDefinition Width="24" />
+                    <ColumnDefinition Width="*" />
                 </Grid.ColumnDefinitions>
                 <CheckBox Classes="ImageCheckBox" VerticalAlignment="Center"
-                      IsThreeState="False" HorizontalAlignment="Center"
-                      IsChecked="{Binding Path=Folder.IsVisibleBindable, ElementName=folderControl, Mode=TwoWay}" Grid.Column="0" Height="16"/>
+                          IsThreeState="False" HorizontalAlignment="Center"
+                          IsChecked="{Binding Path=Folder.IsVisibleBindable, ElementName=folderControl, Mode=TwoWay}"
+                          Grid.Column="0" Height="16" />
 
                 <StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Left">
-                    <Rectangle Width="{Binding Path=(helpers:TreeViewItemHelper.Indent).Value, Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}}" Fill="Transparent" StrokeThickness="0"/>
-                    <Border 
+                    <Rectangle
+                        Width="{Binding Path=(helpers:TreeViewItemHelper.Indent).Value, Mode=OneWay, RelativeSource={RelativeSource AncestorType=ItemsPresenter}}"
+                        Fill="Transparent" StrokeThickness="0" />
+                    <Border
                         IsVisible="{Binding Folder.ClipToMemberBelowEnabledBindable, ElementName=folderControl}"
-                        Background="{DynamicResource ThemeAccentBrush}" Width="3" Margin="1,1,2,1" CornerRadius="1"/>
+                        Background="{DynamicResource ThemeAccentBrush}" Width="3" Margin="1,1,2,1" CornerRadius="1" />
                     <StackPanel Grid.Row="1" Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left">
-                        <Border Width="32" Height="32" BorderThickness="1" BorderBrush="Black" RenderOptions.BitmapInterpolationMode="None">
+                        <Border Width="32" Height="32" BorderThickness="1" BorderBrush="Black"
+                                RenderOptions.BitmapInterpolationMode="None">
                             <Border.Background>
                                 <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile">
                                     <ImageBrush.Transform>
-                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4"/>
+                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4" />
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                             </Border.Background>
-                            <visuals:PreviewPainterControl 
+                            <visuals:PreviewPainterControl
                                 PreviewPainter="{Binding Folder.PreviewPainter, ElementName=folderControl}"
                                 ClipToBounds="True"
                                 FrameToRender="{Binding Manager.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, ElementName=folderControl}"
-                                Width="30" Height="30" RenderOptions.BitmapInterpolationMode="None"/>
+                                Width="30" Height="30" RenderOptions.BitmapInterpolationMode="None" />
                         </Border>
-                        <Border 
-                            Width="32" Height="32" 
+                        <Border
+                            Width="32" Height="32"
                             BorderThickness="1"
                             Margin="3,0,0,0"
                             RenderOptions.BitmapInterpolationMode="None"
@@ -81,24 +87,25 @@
                             <Border.Background>
                                 <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile">
                                     <ImageBrush.Transform>
-                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4"/>
+                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4" />
                                     </ImageBrush.Transform>
                                 </ImageBrush>
                             </Border.Background>
                             <Grid IsHitTestVisible="False">
-                                <visuals:PreviewPainterControl 
-                                    PreviewPainter="{Binding Folder.MaskPreviewPainter, ElementName=folderControl}" 
+                                <visuals:PreviewPainterControl
+                                    PreviewPainter="{Binding Folder.MaskPreviewPainter, ElementName=folderControl}"
                                     Width="30" Height="30"
                                     ClipToBounds="True"
                                     FrameToRender="{Binding Manager.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, ElementName=folderControl}"
-                                    RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
-                                <Path 
-                                Data="M 2 0 L 10 8 L 18 0 L 20 2 L 12 10 L 20 18 L 18 20 L 10 12 L 2 20 L 0 18 L 8 10 L 0 2 Z" 
-                                Fill="{DynamicResource ThemeAccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"
-                                IsVisible="{Binding !Folder.MaskIsVisibleBindable, ElementName=folderControl}"/>
+                                    RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False" />
+                                <Path
+                                    Data="M 2 0 L 10 8 L 18 0 L 20 2 L 12 10 L 20 18 L 18 20 L 10 12 L 2 20 L 0 18 L 8 10 L 0 2 Z"
+                                    Fill="{DynamicResource ThemeAccentBrush}" HorizontalAlignment="Center"
+                                    VerticalAlignment="Center"
+                                    IsVisible="{Binding !Folder.MaskIsVisibleBindable, ElementName=folderControl}" />
                             </Grid>
                         </Border>
-                        <StackPanel Orientation="Vertical"  Margin="3,0,5,0">
+                        <StackPanel Orientation="Vertical" Margin="3,0,5,0">
                             <input:EditableTextBlock
                                 x:Name="editableTextBlock"
                                 d:Text="New Folder" FontSize="14"
@@ -108,7 +115,8 @@
                             <StackPanel Orientation="Horizontal">
                                 <TextBlock d:Text="100" Foreground="White" FontSize="11">
                                     <TextBlock.Text>
-                                        <Binding ElementName="folderControl" Path="Folder.OpacityBindable" Converter="{converters:MultiplyConverter}" StringFormat="N0">
+                                        <Binding ElementName="folderControl" Path="Folder.OpacityBindable"
+                                                 Converter="{converters:MultiplyConverter}" StringFormat="N0">
                                             <Binding.ConverterParameter>
                                                 <sys:Double>100.0</sys:Double>
                                             </Binding.ConverterParameter>
@@ -116,46 +124,51 @@
                                     </TextBlock.Text>
                                 </TextBlock>
                                 <TextBlock Foreground="White" FontSize="11">%</TextBlock>
-                                <TextBlock 
-                                Margin="5,0,0,0" 
-                                d:Text="Normal" 
-                                Foreground="White"
-                                FontSize="11"
-                                Text="{Binding Folder.BlendModeBindable, ElementName=folderControl, Converter={converters:BlendModeToStringConverter}}"/>
+                                <TextBlock
+                                    Margin="5,0,0,0"
+                                    d:Text="Normal"
+                                    Foreground="White"
+                                    FontSize="11"
+                                    Text="{Binding Folder.BlendModeBindable, ElementName=folderControl, Converter={converters:BlendModeToStringConverter}}" />
                             </StackPanel>
                         </StackPanel>
                     </StackPanel>
-                    <TextBlock Text="{DynamicResource icon-folder}" Classes="pixi-icon" FontSize="20" Margin="0,0,10,0" HorizontalAlignment="Right"/>
+                    <TextBlock Text="{DynamicResource icon-folder}" Classes="pixi-icon" FontSize="20" Margin="0,0,10,0"
+                               HorizontalAlignment="Right" />
                 </StackPanel>
             </Grid>
             <Grid
-                  Grid.Row="2"
-                  Name="BottomDropGrid"
-                  DragDrop.AllowDrop="True"
-                  Grid.ColumnSpan="2" Background="Transparent"/>
+                Grid.Row="2"
+                Name="BottomDropGrid"
+                DragDrop.AllowDrop="True"
+                Grid.ColumnSpan="2" Background="Transparent" />
         </Grid>
         <Border.ContextMenu>
             <ContextMenu>
-                <MenuItem ui:Translator.Key="DELETE" Command="{xaml:Command PixiEditor.Layer.DeleteAllSelected}"/>
-                <MenuItem ui:Translator.Key="RENAME" Click="RenameMenuItem_Click"/>
+                <MenuItem ui:Translator.Key="DUPLICATE"
+                          Command="{xaml:Command PixiEditor.Layer.DuplicateSelectedMember}" />
+                <MenuItem ui:Translator.Key="DELETE" Command="{xaml:Command PixiEditor.Layer.DeleteAllSelected}" />
+                <MenuItem ui:Translator.Key="RENAME" Click="RenameMenuItem_Click" />
                 <controls:ToggleableMenuItem
-                    IsChecked="{Binding $parent[UserControl].Folder.ClipToMemberBelowEnabledBindable, Mode=TwoWay}" 
-                    ui:Translator.Key="CLIP_TO_BELOW"/>
-                <Separator/>
-                <MenuItem ui:Translator.Key="MOVE_UPWARDS" Command="{xaml:Command PixiEditor.Layer.MoveSelectedMemberUpwards}"/>
-                <MenuItem ui:Translator.Key="MOVE_DOWNWARDS" Command="{xaml:Command PixiEditor.Layer.MoveSelectedMemberDownwards}"/>
-                <Separator/>
-                <MenuItem ui:Translator.Key="CREATE_MASK" Command="{xaml:Command PixiEditor.Layer.CreateMask}"/>
-                <MenuItem ui:Translator.Key="DELETE_MASK" Command="{xaml:Command PixiEditor.Layer.DeleteMask}"/>
+                    IsChecked="{Binding $parent[UserControl].Folder.ClipToMemberBelowEnabledBindable, Mode=TwoWay}"
+                    ui:Translator.Key="CLIP_TO_BELOW" />
+                <Separator />
+                <MenuItem ui:Translator.Key="MOVE_UPWARDS"
+                          Command="{xaml:Command PixiEditor.Layer.MoveSelectedMemberUpwards}" />
+                <MenuItem ui:Translator.Key="MOVE_DOWNWARDS"
+                          Command="{xaml:Command PixiEditor.Layer.MoveSelectedMemberDownwards}" />
+                <Separator />
+                <MenuItem ui:Translator.Key="CREATE_MASK" Command="{xaml:Command PixiEditor.Layer.CreateMask}" />
+                <MenuItem ui:Translator.Key="DELETE_MASK" Command="{xaml:Command PixiEditor.Layer.DeleteMask}" />
                 <controls:ToggleableMenuItem
-                    IsChecked="{Binding $parent[UserControl].Folder.MaskIsVisibleBindable, Mode=TwoWay}" 
+                    IsChecked="{Binding $parent[UserControl].Folder.MaskIsVisibleBindable, Mode=TwoWay}"
                     IsEnabled="{Binding $parent[UserControl].Folder.HasMaskBindable}"
-                    ui:Translator.Key="ENABLE_MASK"/>
-                <Separator/>
-                <MenuItem ui:Translator.Key="MERGE_SELECTED" Command="{xaml:Command PixiEditor.Layer.MergeSelected}"/>
-                <MenuItem ui:Translator.Key="MERGE_WITH_ABOVE" Command="{xaml:Command PixiEditor.Layer.MergeWithAbove}"/>
-                <MenuItem ui:Translator.Key="MERGE_WITH_BELOW" Command="{xaml:Command PixiEditor.Layer.MergeWithBelow}"/>
+                    ui:Translator.Key="ENABLE_MASK" />
+                <Separator />
+                <MenuItem ui:Translator.Key="MERGE_SELECTED" Command="{xaml:Command PixiEditor.Layer.MergeSelected}" />
+                <MenuItem ui:Translator.Key="MERGE_WITH_ABOVE" Command="{xaml:Command PixiEditor.Layer.MergeWithAbove}" />
+                <MenuItem ui:Translator.Key="MERGE_WITH_BELOW" Command="{xaml:Command PixiEditor.Layer.MergeWithBelow}" />
             </ContextMenu>
         </Border.ContextMenu>
     </Border>
-</UserControl>
+</UserControl>

+ 1 - 1
src/PixiEditor/Views/Layers/LayerControl.axaml

@@ -172,7 +172,7 @@
         <Border.ContextMenu>
             <ContextMenu>
                 <MenuItem ui:Translator.Key="DUPLICATE"
-                          Command="{xaml:Command PixiEditor.Layer.DuplicateSelectedLayer}" />
+                          Command="{xaml:Command PixiEditor.Layer.DuplicateSelectedMember}" />
                 <MenuItem ui:Translator.Key="DELETE" Command="{xaml:Command PixiEditor.Layer.DeleteAllSelected}" />
                 <MenuItem ui:Translator.Key="RENAME" Click="RenameMenuItem_Click" />
                 <controls:ToggleableMenuItem