Browse Source

Merge pull request #706 from PixiEditor/fixes/28.12.2024

Fixes/28.12.2024
Krzysztof Krysiński 7 months ago
parent
commit
d7867ac33d
35 changed files with 461 additions and 144 deletions
  1. 1 1
      src/Drawie
  2. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  3. 15 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs
  4. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  5. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  6. 5 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  7. 5 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  8. 0 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  9. 73 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  10. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  11. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  12. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateCel_Change.cs
  13. 47 13
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  14. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  15. 1 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeleteNode_Change.cs
  16. 110 18
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  17. 8 1
      src/PixiEditor/Data/Localization/Languages/en.json
  18. 42 5
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  19. 14 0
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  20. 17 11
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  21. 1 1
      src/PixiEditor/Models/Handlers/IAnimationHandler.cs
  22. 6 5
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  23. 26 5
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  24. 1 1
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  25. 3 3
      src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs
  26. 7 9
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  27. 23 17
      src/PixiEditor/ViewModels/SubViewModels/AnimationsViewModel.cs
  28. 13 1
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  29. 12 9
      src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs
  30. 2 2
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  31. 1 1
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  32. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs
  33. 2 2
      src/PixiEditor/Views/Dock/TimelineDockView.axaml
  34. 2 0
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs
  35. 7 5
      src/PixiEditor/Views/Visuals/PreviewPainterControl.cs

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit c90f46c8ac9050511e5d65999c72b27e8b32ebfb
+Subproject commit ce63fc3f4fac69fd8108f828a776dd0a36d05a47

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs

@@ -55,9 +55,9 @@ public class FuncContext
         SamplePosition = Builder.ConstructFloat2(OriginalPosition.X, OriginalPosition.Y);
     }
 
-    public Half4 SampleSurface(DrawingSurface surface, Expression pos)
+    public Half4 SampleSurface(DrawingSurface surface, Expression pos, ColorSampleMode sampleMode)
     {
-        SurfaceSampler texName = Builder.AddOrGetSurface(surface);
+        SurfaceSampler texName = Builder.AddOrGetSurface(surface, sampleMode);
         return Builder.Sample(texName, pos);
     }
 

+ 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();
 }

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -3,6 +3,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
@@ -48,6 +49,7 @@ public class CreateImageNode : Node
         Content.Value?.Paint(ctx, surface.DrawingSurface);
 
         surface.DrawingSurface.Canvas.RestoreToCount(saved);
+        
         Output.Value = surface;
 
         RenderOutput.ChainToPainterValue();

+ 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();
 

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

@@ -3,6 +3,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Numerics;
@@ -18,6 +19,8 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
     public FuncOutputProperty<Float2> Coordinate { get; }
 
     public FuncOutputProperty<Half4> Color { get; }
+    
+    public InputProperty<ColorSampleMode> SampleMode { get; }
 
     public Guid OtherNode { get; set; }
     
@@ -26,6 +29,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
         Image = CreateInput<Texture?>("Surface", "IMAGE", null);
         Coordinate = CreateFuncOutput("Coordinate", "UV", ctx => ctx.OriginalPosition);
         Color = CreateFuncOutput("Color", "COLOR", GetColor);
+        SampleMode = CreateInput("SampleMode", "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
     }
     
     private Half4 GetColor(FuncContext context)
@@ -37,7 +41,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
             return new Half4("") { ConstantValue = Colors.Transparent };
         }
 
-        return context.SampleSurface(Image.Value.DrawingSurface, context.SamplePosition);
+        return context.SampleSurface(Image.Value.DrawingSurface, context.SamplePosition, SampleMode.Value);
     }
 
     protected override void OnExecute(RenderContext context)

+ 0 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -122,12 +122,8 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         var startNode = FindStartNode();
         if (drawingPaint != null && startNode != null && startNode.Image.Value != null)
         {
-            int saved = renderOn.Canvas.SaveLayer(drawingPaint);
-            
             renderOn.Canvas.DrawRect(0, 0, startNode.Image.Value.Size.X, startNode.Image.Value.Size.Y, drawingPaint);
             
-            renderOn.Canvas.RestoreToCount(saved);
-            
             return true;
         }
 

+ 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;
     }
 }

+ 6 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs

@@ -2,6 +2,7 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Numerics;
@@ -17,11 +18,14 @@ public class SampleImageNode : Node
 
     public FuncOutputProperty<Half4> Color { get; }
 
+    public InputProperty<ColorSampleMode> SampleMode { get; }
+
     public SampleImageNode()
     {
         Image = CreateInput<Texture>(nameof(Texture), "IMAGE", null);
         Coordinate = CreateFuncInput<Float2>(nameof(Coordinate), "UV", VecD.Zero);
         Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
+        SampleMode = CreateInput(nameof(SampleMode), "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
     }
 
     private Half4 GetColor(FuncContext context)
@@ -35,12 +39,12 @@ public class SampleImageNode : Node
 
         Expression uv = context.GetValue(Coordinate);
 
-        return context.SampleSurface(Image.Value.DrawingSurface, uv);
+        return context.SampleSurface(Image.Value.DrawingSurface, uv, SampleMode.Value);
     }
 
     protected override void OnExecute(RenderContext context)
     {
-        
+
     }
 
     public override Node CreateCopy() => new SampleImageNode();

+ 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 };
     }
 }

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs → src/PixiEditor.ChangeableDocument/Changes/Animation/CreateCel_Change.cs

@@ -4,7 +4,7 @@ using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 
 namespace PixiEditor.ChangeableDocument.Changes.Animation;
 
-internal class CreateRasterKeyFrame_Change : Change
+internal class CreateCel_Change : Change
 {
     private readonly Guid _targetLayerGuid;
     private int _frame;
@@ -14,7 +14,7 @@ internal class CreateRasterKeyFrame_Change : Change
     private Guid createdKeyFrameId;
 
     [GenerateMakeChangeAction]
-    public CreateRasterKeyFrame_Change(Guid targetLayerGuid, Guid newKeyFrameGuid, int frame,
+    public CreateCel_Change(Guid targetLayerGuid, Guid newKeyFrameGuid, int frame,
         int cloneFromFrame = -1,
         Guid cloneFromExisting = default)
     {

+ 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)

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

@@ -140,14 +140,14 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
     {
         ChunkyImage image =
             DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
-        VectorPath pathToExtract = originalPath;
+        VectorPath? pathToExtract = originalPath;
         RectD targetBounds = originalTightBounds;
 
         if (pathToExtract == null)
         {
             RectD tightBounds = layer.GetTightBounds(frame).GetValueOrDefault();
             pathToExtract = new VectorPath();
-            pathToExtract.AddRect((RectD)(RectI)tightBounds);
+            pathToExtract.AddRect(tightBounds.RoundOutwards());
         }
 
         member.OriginalPath = pathToExtract;

+ 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);
 

+ 110 - 18
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
+using System.Collections.Concurrent;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
@@ -18,6 +19,8 @@ public class DocumentRenderer : IPreviewRenderable
         BlendMode = BlendMode.Src, Color = Drawie.Backend.Core.ColorsImpl.Colors.Transparent
     };
 
+    private Texture renderTexture;
+    
     public DocumentRenderer(IReadOnlyDocument document)
     {
         Document = document;
@@ -72,22 +75,37 @@ 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;
     }
+    
+    public void RenderNodePreview(IPreviewRenderable previewRenderable, DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        if (IsBusy)
+        {
+            return;
+        }
+        
+        IsBusy = true;
+        
+        previewRenderable.RenderPreview(renderOn, context, elementToRenderName);
+        
+        IsBusy = false;
+    }
 
     public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(IReadOnlyNodeGraph fullGraph)
     {
         return ConstructMembersOnlyGraph(null, fullGraph);
     }
 
-    public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(HashSet<Guid>? layersToCombine,
+    public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(
+        HashSet<Guid>? membersToCombine,
         IReadOnlyNodeGraph fullGraph)
     {
         NodeGraph membersOnlyGraph = new();
@@ -96,26 +114,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;
     }
@@ -126,16 +158,76 @@ public class DocumentRenderer : IPreviewRenderable
     public bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
+        IsBusy = true;
+
+        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;
     }
 
     public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime)
     {
         IsBusy = true;
-        RenderContext context = new(toRenderOn, frameTime, ChunkResolution.Full, Document.Size, Document.ProcessingColorSpace) { FullRerender = true };
+
+        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 };
         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;
+    }
 }

+ 8 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -789,5 +789,12 @@
   "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
   "FILL_TYPE_INVERSE_EVEN_ODD": "Inverse Even Odd",
   "STROKE_CAP": "Stroke Cap",
-  "STROKE_JOIN": "Stroke Join"
+  "STROKE_JOIN": "Stroke Join",
+  "COPY_VISIBLE": "Copy visible",
+  "COPY_VISIBLE_DESCRIPTIVE": "Copy visible pixels",
+  "COLOR_SAMPLE_MODE": "Sample mode",
+  "CREATE_CEL": "Create cel",
+  "CREATE_CEL_DESCRIPTIVE": "Create a new cel",
+  "DUPLICATE_CEL": "Duplicate cel",
+  "DUPLICATE_CEL_DESCRIPTIVE": "Duplicate cel in the current frame",
 }

+ 42 - 5
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -18,6 +18,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.Constants;
@@ -63,7 +64,7 @@ internal static class ClipboardController
         DataObject data = new DataObject();
 
         Surface surfaceToCopy = null;
-        RectI copyArea = RectI.Empty;
+        RectD copyArea = RectD.Empty;
 
         if (!document.SelectionPathBindable.IsEmpty)
         {
@@ -77,7 +78,8 @@ internal static class ClipboardController
                 return;
             }
 
-            (surfaceToCopy, copyArea) = surface.AsT2;
+            surfaceToCopy = surface.AsT2.Item1;
+            copyArea = (RectD)surface.AsT2.Item2;
         }
         else if (document.TransformViewModel.TransformActive)
         {
@@ -87,7 +89,8 @@ internal static class ClipboardController
             if (surface.IsT0 || surface.IsT1)
                 return;
 
-            (surfaceToCopy, copyArea) = surface.AsT2;
+            surfaceToCopy = surface.AsT2.Item1;
+            copyArea = document.TransformViewModel.Corners.AABBBounds;
         }
         else if (document.SelectedStructureMember != null)
         {
@@ -97,7 +100,8 @@ internal static class ClipboardController
             if (surface.IsT0 || surface.IsT1)
                 return;
 
-            (surfaceToCopy, copyArea) = surface.AsT2;
+            surfaceToCopy = surface.AsT2.Item1;
+            copyArea = (RectD)bounds;
         }
 
         if (surfaceToCopy == null)
@@ -107,7 +111,7 @@ internal static class ClipboardController
 
         await AddImageToClipboard(surfaceToCopy, data);
 
-        if (copyArea.Size != document.SizeBindable && copyArea.Pos != VecI.Zero && copyArea != RectI.Empty)
+        if (copyArea.Size != document.SizeBindable && copyArea.Pos != VecI.Zero && copyArea != RectD.Empty)
         {
             data.SetVecD(ClipboardDataFormats.PositionFormat, copyArea.Pos);
         }
@@ -122,6 +126,39 @@ internal static class ClipboardController
 
         await Clipboard.SetDataObjectAsync(data);
     }
+    
+    public static async Task CopyVisibleToClipboard(DocumentViewModel document)
+    {
+        await Clipboard.ClearAsync();
+
+        DataObject data = new DataObject();
+
+        RectD copyArea = new RectD(VecD.Zero, document.SizeBindable);
+        
+        if (!document.SelectionPathBindable.IsEmpty)
+        {
+            copyArea = document.SelectionPathBindable.TightBounds;
+        }
+        else if (document.TransformViewModel.TransformActive)
+        {
+            copyArea = document.TransformViewModel.Corners.AABBBounds;
+        }
+
+        using Surface documentSurface = new Surface(document.SizeBindable);
+        
+        document.Renderer.RenderDocument(documentSurface.DrawingSurface, document.AnimationDataViewModel.ActiveFrameTime);
+        
+        Surface surfaceToCopy = new Surface((VecI)copyArea.Size.Ceiling());
+        using Paint paint = new Paint();
+        
+        surfaceToCopy.DrawingSurface.Canvas.DrawImage(
+        documentSurface.DrawingSurface.Snapshot(),
+        copyArea, new RectD(0, 0, copyArea.Size.X, copyArea.Size.Y), paint);
+
+        await AddImageToClipboard(surfaceToCopy, data);
+
+        await Clipboard.SetDataObjectAsync(data);
+    }
 
     private static async Task AddImageToClipboard(Surface actuallySurface, DataObject data)
     {

+ 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)
     {

+ 17 - 11
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -27,6 +27,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
     private List<Guid> selectedMembers = new();
 
     private ShapeCorners lastCorners = new();
+    private bool movedOnce;
 
     public TransformSelectedExecutor(bool toolLinked)
     {
@@ -70,7 +71,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             {
                 allRaster = false;
             }
-            
+
             if (member is IRasterLayerHandler)
             {
                 anyRaster = true;
@@ -117,13 +118,10 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         lastCorners = masterCorners;
         document.TransformHandler.ShowTransform(mode, true, masterCorners,
             Type == ExecutorType.Regular || tool.KeepOriginalImage);
-        
-        document.TransformHandler.CanAlignToPixels = anyRaster;
 
-        internals!.ActionAccumulator.AddActions(
-            new TransformSelected_Action(masterCorners, tool.KeepOriginalImage, memberCorners, false,
-                document.AnimationHandler.ActiveFrameBindable));
+        document.TransformHandler.CanAlignToPixels = anyRaster;
 
+        movedOnce = false;
         isInProgress = true;
         return ExecutionState.Success;
     }
@@ -139,7 +137,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         var nonSelected = topMostWithinClick.Where(x => x != document.SelectedStructureMember
                                                         && !document.SoftSelectedStructureMembers.Contains(x))
             .ToArray();
-        
+
         bool isHoldingShift = args.KeyModifiers.HasFlag(KeyModifiers.Shift);
 
         if (nonSelected.Any())
@@ -177,10 +175,10 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
                 Guid? nextMain = document.SoftSelectedStructureMembers.FirstOrDefault().Id;
                 List<Guid> softSelected = document.SoftSelectedStructureMembers
                     .Select(x => x.Id).Where(x => x != nextMain.Value).ToList();
-                    
+
                 document.Operations.ClearSoftSelectedMembers();
                 document.Operations.SetSelectedMember(nextMain.Value);
-                    
+
                 foreach (var guid in softSelected)
                 {
                     document.Operations.AddSoftSelectedMember(guid);
@@ -190,9 +188,9 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             {
                 List<Guid> softSelected = document.SoftSelectedStructureMembers
                     .Select(x => x.Id).Where(x => x != topMost.Id).ToList();
-                    
+
                 document.Operations.ClearSoftSelectedMembers();
-                    
+
                 foreach (var guid in softSelected)
                 {
                     document.Operations.AddSoftSelectedMember(guid);
@@ -243,6 +241,14 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         if (!isInProgress)
             return;
 
+        if (!movedOnce)
+        {
+            internals!.ActionAccumulator.AddActions(
+                new TransformSelected_Action(lastCorners, tool.KeepOriginalImage, memberCorners, false,
+                    document.AnimationHandler.ActiveFrameBindable));
+            movedOnce = true;
+        }
+
         internals!.ActionAccumulator.AddActions(
             new TransformSelected_Action(corners, tool!.KeepOriginalImage, memberCorners, false,
                 document!.AnimationHandler.ActiveFrameBindable));

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

@@ -11,7 +11,7 @@ internal interface IAnimationHandler
     public int OnionFramesBindable { get; set; }
     public double OnionOpacityBindable { get; set; }
     public bool IsPlayingBindable { get; set; }
-    public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null, int? frameToCopyFrom = null);
+    public void 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);

+ 6 - 5
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -70,7 +70,7 @@ internal class MemberPreviewUpdater
         var previewSize = StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size);
         float scaling = (float)previewSize.X / doc.SizeBindable.X;
 
-        doc.PreviewPainter = new PreviewPainter(doc.Renderer, doc.AnimationHandler.ActiveFrameTime,
+        doc.PreviewPainter = new PreviewPainter(doc.Renderer, doc.Renderer, doc.AnimationHandler.ActiveFrameTime,
             doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
         doc.PreviewPainter.Repaint();
     }
@@ -91,7 +91,7 @@ internal class MemberPreviewUpdater
                         continue;
 
                     structureMemberHandler.PreviewPainter =
-                        new PreviewPainter(previewRenderable,
+                        new PreviewPainter(doc.Renderer, previewRenderable,
                             doc.AnimationHandler.ActiveFrameTime, doc.SizeBindable,
                             internals.Tracker.Document.ProcessingColorSpace);
                     structureMemberHandler.PreviewPainter.Repaint();
@@ -145,7 +145,7 @@ internal class MemberPreviewUpdater
         if (internals.Tracker.Document.AnimationData.TryFindKeyFrame(cel.Id, out KeyFrame _))
         {
             KeyFrameTime frameTime = doc.AnimationHandler.ActiveFrameTime;
-            cel.PreviewPainter = new PreviewPainter(AnimationKeyFramePreviewRenderer, frameTime, doc.SizeBindable,
+            cel.PreviewPainter = new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, doc.SizeBindable,
                 internals.Tracker.Document.ProcessingColorSpace, cel.Id.ToString());
             cel.PreviewPainter.Repaint();
         }
@@ -161,7 +161,7 @@ internal class MemberPreviewUpdater
             VecI documentSize = doc.SizeBindable;
 
             groupHandler.PreviewPainter =
-                new PreviewPainter(AnimationKeyFramePreviewRenderer, frameTime, documentSize, processingColorSpace,
+                new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, documentSize, processingColorSpace,
                     groupHandler.Id.ToString());
             groupHandler.PreviewPainter.Repaint();
         }
@@ -181,6 +181,7 @@ internal class MemberPreviewUpdater
                     continue;
 
                 structureMemberHandler.MaskPreviewPainter = new PreviewPainter(
+                    doc.Renderer,
                     previewRenderable,
                     doc.AnimationHandler.ActiveFrameTime,
                     doc.SizeBindable,
@@ -221,7 +222,7 @@ internal class MemberPreviewUpdater
             {
                 if (nodeVm.ResultPainter == null)
                 {
-                    nodeVm.ResultPainter = new PreviewPainter(renderable, doc.AnimationHandler.ActiveFrameTime,
+                    nodeVm.ResultPainter = new PreviewPainter(doc.Renderer, renderable, doc.AnimationHandler.ActiveFrameTime,
                         doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
                     nodeVm.ResultPainter.Repaint();
                 }

+ 26 - 5
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -1,4 +1,7 @@
-using ChunkyImageLib.DataHolders;
+using Avalonia;
+using ChunkyImageLib.DataHolders;
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using Drawie.Backend.Core.Surfaces;
@@ -16,26 +19,44 @@ public class PreviewPainter
     public event Action RequestRepaint;
     public KeyFrameTime FrameTime { get; set; }
     public VecI DocumentSize { get; set; }
+    public DocumentRenderer Renderer { get; set; }
     
-    public PreviewPainter(IPreviewRenderable previewRenderable, KeyFrameTime frameTime, VecI documentSize, ColorSpace processingColorSpace, string elementToRenderName = "")
+    private Texture renderTexture;
+    
+    public PreviewPainter(DocumentRenderer renderer, IPreviewRenderable previewRenderable, KeyFrameTime frameTime, VecI documentSize, ColorSpace processingColorSpace, string elementToRenderName = "")
     {
         PreviewRenderable = previewRenderable;
         ElementToRenderName = elementToRenderName;
         ProcessingColorSpace = processingColorSpace;
         FrameTime = frameTime;
         DocumentSize = documentSize;
+        Renderer = renderer;
     }
 
-    public void Paint(DrawingSurface renderOn) 
+    public void Paint(DrawingSurface renderOn, VecI boundsSize, Matrix3X3 matrix) 
     {
         if (PreviewRenderable == null)
         {
             return;
         }
 
-        RenderContext context = new(renderOn, FrameTime, ChunkResolution.Full, DocumentSize, ProcessingColorSpace);
+        if (renderTexture == null || renderTexture.Size != boundsSize)
+        {
+            renderTexture?.Dispose();
+            renderTexture = Texture.ForProcessing(boundsSize, ProcessingColorSpace);
+        }
+        
+        renderTexture.DrawingSurface.Canvas.Clear();
+        renderTexture.DrawingSurface.Canvas.Save();
+
+        renderTexture.DrawingSurface.Canvas.SetMatrix(matrix);
+        
+        RenderContext context = new(renderTexture.DrawingSurface, FrameTime, ChunkResolution.Full, DocumentSize, ProcessingColorSpace);
 
-        PreviewRenderable.RenderPreview(renderOn, context, ElementToRenderName);
+        Renderer.RenderNodePreview(PreviewRenderable, renderTexture.DrawingSurface, context, ElementToRenderName);
+        renderTexture.DrawingSurface.Canvas.Restore();
+        
+        renderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
     }
 
     public void Repaint()

+ 1 - 1
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -95,7 +95,7 @@ internal class SceneRenderer
         for (int i = 1; i <= animationData.OnionFrames; i++)
         {
             int frame = DocumentViewModel.AnimationHandler.ActiveFrameTime.Frame + i;
-            if (frame > DocumentViewModel.AnimationHandler.LastFrame)
+            if (frame >= DocumentViewModel.AnimationHandler.LastFrame)
             {
                 break;
             }

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

@@ -111,7 +111,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         ? keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable)
         : DefaultEndFrame;
 
-    public int FramesCount => LastFrame - FirstFrame + 1;
+    public int FramesCount => LastFrame - FirstFrame;
 
     private double ActiveNormalizedTime => (double)(ActiveFrameBindable - FirstFrame) / FramesCount;
 
@@ -126,12 +126,12 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 
     public KeyFrameTime ActiveFrameTime => new KeyFrameTime(ActiveFrameBindable, ActiveNormalizedTime);
 
-    public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null,
+    public void CreateCel(Guid targetLayerGuid, int frame, Guid? toCloneFrom = null,
         int? frameToCopyFrom = null)
     {
         if (!Document.BlockingUpdateableChangeActive)
         {
-            Internals.ActionAccumulator.AddFinishedActions(new CreateRasterKeyFrame_Action(targetLayerGuid,
+            Internals.ActionAccumulator.AddFinishedActions(new CreateCel_Action(targetLayerGuid,
                 Guid.NewGuid(), Math.Max(1, frame),
                 frameToCopyFrom ?? -1, toCloneFrom ?? Guid.Empty));
         }

+ 7 - 9
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -445,7 +445,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                     foreach (var child in group.Children)
                     {
                         acc.AddActions(
-                            new CreateRasterKeyFrame_Action(
+                            new CreateCel_Action(
                                 mappedIds[child.NodeId],
                                 mappedKeyFrameIds[child.KeyFrameId],
                                 -1, -1, default));
@@ -502,16 +502,14 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             Surface finalSurface = null;
             DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
             {
-                using Texture texture = new Texture(renderSize);
-                texture.DrawingSurface.Canvas.Save();
+                finalSurface = new Surface(renderSize);
+                finalSurface.DrawingSurface.Canvas.Save();
                 VecD scaling = new VecD(renderSize.X / (double)SizeBindable.X, renderSize.Y / (double)SizeBindable.Y);
 
-                texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
-                Renderer.RenderDocument(texture.DrawingSurface, frameTime);
+                finalSurface.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+                Renderer.RenderDocument(finalSurface.DrawingSurface, frameTime);
 
-                texture.DrawingSurface.Canvas.Restore();
-                finalSurface = new Surface(renderSize);
-                finalSurface.DrawingSurface.Canvas.DrawImage(texture.DrawingSurface.Snapshot(), 0, 0);
+                finalSurface.DrawingSurface.Canvas.Restore();
             });
 
             return finalSurface;
@@ -699,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);

+ 23 - 17
src/PixiEditor/ViewModels/SubViewModels/AnimationsViewModel.cs

@@ -42,14 +42,15 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
         if (activeDocument is null)
             return;
 
-        activeDocument.AnimationDataViewModel.IsPlayingBindable = !activeDocument.AnimationDataViewModel.IsPlayingBindable;
+        activeDocument.AnimationDataViewModel.IsPlayingBindable =
+            !activeDocument.AnimationDataViewModel.IsPlayingBindable;
     }
 
-    [Command.Basic("PixiEditor.Animation.CreateRasterKeyFrame", "Create Raster Key Frame", "Create a raster key frame",
+    [Command.Basic("PixiEditor.Animation.CreateCel", "CREATE_CEL", "CREATE_CEL_DESCRIPTIVE",
         Parameter = false, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Animation.DuplicateRasterKeyFrame", "Duplicate Raster Key Frame",
-        "Duplicate a raster key frame", Parameter = true, AnalyticsTrack = true)]
-    public void CreateRasterKeyFrame(bool duplicate)
+    [Command.Basic("PixiEditor.Animation.DuplicateCel", "DUPLICATE_CEL",
+        "DUPLICATE_CEL_DESCRIPTIVE", Parameter = true, AnalyticsTrack = true)]
+    public void CreateCel(bool duplicate)
     {
         var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (activeDocument?.SelectedStructureMember is null)
@@ -57,12 +58,12 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
             return;
         }
 
-        int newFrame = GetActiveFrame(activeDocument, activeDocument.SelectedStructureMember.Id);
+        int newFrame = GetFirstEmptyFrame(activeDocument, activeDocument.SelectedStructureMember.Id);
 
         Guid toCloneFrom = duplicate ? activeDocument.SelectedStructureMember.Id : Guid.Empty;
         int frameToCopyFrom = duplicate ? activeDocument.AnimationDataViewModel.ActiveFrameBindable : -1;
 
-        activeDocument.AnimationDataViewModel.CreateRasterKeyFrame(
+        activeDocument.AnimationDataViewModel.CreateCel(
             activeDocument.SelectedStructureMember.Id,
             newFrame,
             toCloneFrom,
@@ -132,23 +133,28 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
     }
 
 
-    private static int GetActiveFrame(DocumentViewModel activeDocument, Guid targetLayer)
+    private static int GetFirstEmptyFrame(DocumentViewModel activeDocument, Guid targetLayer)
     {
         int active = activeDocument.AnimationDataViewModel.ActiveFrameBindable;
-        if (activeDocument.AnimationDataViewModel.TryFindCels<CelGroupViewModel>(targetLayer,
+        if (activeDocument.AnimationDataViewModel.TryFindCels(targetLayer,
                 out CelGroupViewModel groupViewModel))
         {
-            if (groupViewModel.Children.All(x => !x.IsWithinRange(active )))
+            if (groupViewModel.Children.All(x => !x.IsWithinRange(active)))
             {
                 return active;
             }
-            
-            if (groupViewModel.Children.All(x => !x.IsWithinRange(active + 1)))
+
+            for (int i = active + 1; i < activeDocument.AnimationDataViewModel.FramesCount; i++)
             {
-                return active + 1;
+                if (groupViewModel.Children.All(x => !x.IsWithinRange(i)))
+                {
+                    return i;
+                }
             }
-        }
 
+            return activeDocument.AnimationDataViewModel.FramesCount + 1;
+        }
+        
         return active;
     }
 
@@ -191,14 +197,14 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
 
         document.Operations.SetActiveFrame((int)value);
     }
-    
+
     private bool IsTransforming()
     {
         var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (activeDocument is null)
             return false;
-        
+
         return activeDocument.TransformViewModel.TransformActive || activeDocument.LineToolOverlayViewModel.IsEnabled
-            || activeDocument.PathOverlayViewModel.IsActive;
+                                                                 || activeDocument.PathOverlayViewModel.IsActive;
     }
 }

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

@@ -157,6 +157,18 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         await ClipboardController.CopyToClipboard(doc);
     }
 
+    [Command.Basic("PixiEditor.Clipboard.CopyVisible", "COPY_VISIBLE", "COPY_VISIBLE_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanCopy",
+        Key = Key.C, Modifiers = KeyModifiers.Shift,
+        MenuItemPath = "EDIT/COPY_VISIBLE", MenuItemOrder = 3, Icon = PixiPerfectIcons.Copy, AnalyticsTrack = true)]
+    public async Task CopyVisible()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+
+        await ClipboardController.CopyVisibleToClipboard(doc);
+    }
+
     [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsHex", CopyColor.PrimaryHEX, "COPY_COLOR_HEX",
         "COPY_COLOR_HEX_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon", AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Clipboard.CopyPrimaryColorAsRgb", CopyColor.PrimaryRGB, "COPY_COLOR_RGB",
@@ -168,7 +180,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE", IconEvaluator = "PixiEditor.Clipboard.CopyColorIcon",
         AnalyticsTrack = true)]
     [Command.Filter("PixiEditor.Clipboard.CopyColorToClipboard", "COPY_COLOR_TO_CLIPBOARD", "COPY_COLOR", Key = Key.C,
-        Modifiers = KeyModifiers.Shift, AnalyticsTrack = true)]
+        Modifiers = KeyModifiers.Shift | KeyModifiers.Alt, AnalyticsTrack = true)]
     public async Task CopyColorAsHex(CopyColor color)
     {
         var targetColor = color switch

+ 12 - 9
src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs

@@ -147,7 +147,7 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
 
     private void ProcessShortcutDown(bool isRepeat, Key key, KeyModifiers argsModifiers)
     {
-        if (argsModifiers == KeyModifiers.None)
+        if (argsModifiers == KeyModifiers.None && !isRepeat)
         {
             HandleTransientKey(key);
         }
@@ -177,6 +177,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         {
             Owner.ToolsSubViewModel.HandleToolShortcutUp();
         }
+
+        ShortcutController.UnblockShortcutExecution("ShortcutDown");
     }
 
     private void OnMouseDown(object? sender, MouseOnCanvasEventArgs args)
@@ -240,18 +242,18 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is null)
             return;
-        
-        if(Owner.ColorsSubViewModel.ColorsTempSwapped)
+
+        if (Owner.ColorsSubViewModel.ColorsTempSwapped)
             return;
 
         var tools = Owner.ToolsSubViewModel;
-        
+
         if (tools is { RightClickMode: RightClickMode.SecondaryColor, ActiveTool.UsesColor: true })
         {
             Owner.ColorsSubViewModel.SwapColors(true);
         }
     }
-    
+
     private void HandleRightMouseEraseDown(IToolsHandler tools)
     {
         EraserToolViewModel? eraserTool = tools.GetTool<EraserToolViewModel>();
@@ -259,13 +261,13 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         {
             return;
         }
-        
+
         var currentToolSize = tools.ActiveTool.Toolbar.Settings.FirstOrDefault(x => x.Name == "ToolSize");
         hadSharedToolbar = tools.EnableSharedToolbar;
         if (currentToolSize != null)
         {
             tools.EnableSharedToolbar = false;
-            
+
             var toolSize = eraserTool.Toolbar.Settings.First(x => x.Name == "ToolSize");
             previousEraseSize = (double)toolSize.Value;
             toolSize.Value = tools.ActiveTool is PenToolViewModel { PixelPerfectEnabled: true }
@@ -307,7 +309,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
             return;
         var tools = Owner.ToolsSubViewModel;
 
-        var rightCanUp = (button == MouseButton.Right) && tools.RightClickMode is RightClickMode.Erase or RightClickMode.SecondaryColor;
+        var rightCanUp = (button == MouseButton.Right) &&
+                         tools.RightClickMode is RightClickMode.Erase or RightClickMode.SecondaryColor;
 
         if (button == MouseButton.Left || rightCanUp)
         {
@@ -351,7 +354,7 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
                 break;
         }
     }
-    
+
     private void ToolSessionFinished()
     {
         Owner.ColorsSubViewModel.SwapColors(null);

+ 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;

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -223,6 +223,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         if (ActiveTool == tool)
         {
             ActiveTool.IsTransient = transient;
+            LastActionTool = ActiveTool;
             return;
         }
 
@@ -367,7 +368,6 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         if (ActiveTool == null) return;
         if (ActiveTool.IsTransient && LastActionTool is { } tool)
             SetActiveTool(tool, false);
-        ShortcutController.UnblockShortcutExecution("ShortcutDown");
     }
 
     public void UseToolEventInlet(VecD canvasPos, MouseButton button)

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -13,7 +13,7 @@ using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.V)]
+[Command.Tool(Key = Key.V, Transient = Key.Space)]
 internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
 {
     private string defaultActionDisplay = "MOVE_TOOL_ACTION_DISPLAY";

+ 2 - 2
src/PixiEditor/Views/Dock/TimelineDockView.axaml

@@ -15,14 +15,14 @@
     <animations:Timeline 
         KeyFrames="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.KeyFrames}" 
         ActiveFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, Mode=TwoWay}"
-        NewKeyFrameCommand="{xaml:Command PixiEditor.Animation.CreateRasterKeyFrame}"
+        NewKeyFrameCommand="{xaml:Command PixiEditor.Animation.CreateCel}"
         Fps="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.FrameRateBindable, Mode=TwoWay}"
         DefaultEndFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.DefaultEndFrame}"
         OnionSkinningEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.OnionSkinningEnabledBindable, Mode=TwoWay}"
         OnionFrames="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.OnionFramesBindable, Mode=TwoWay}"
         OnionOpacity="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.OnionOpacityBindable, Mode=TwoWay}"
         IsPlaying="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.IsPlayingBindable, Mode=TwoWay}"
-        DuplicateKeyFrameCommand="{xaml:Command PixiEditor.Animation.DuplicateRasterKeyFrame}"
+        DuplicateKeyFrameCommand="{xaml:Command PixiEditor.Animation.DuplicateCel}"
         DeleteKeyFrameCommand="{xaml:Command PixiEditor.Animation.DeleteCels, UseProvided=True}"
         ChangeKeyFramesLengthCommand="{xaml:Command PixiEditor.Animation.ChangeKeyFramesStartPos, UseProvided=True}"/>
 </UserControl>

+ 2 - 0
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -397,6 +397,8 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         if (Document is null || e.Source != Scene)
             return;
 
+        Scene.Focus(NavigationMethod.Pointer);
+
         bool isMiddle = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;
         HandleMiddleMouse(isMiddle);
 

+ 7 - 5
src/PixiEditor/Views/Visuals/PreviewPainterControl.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using ChunkyImageLib.DataHolders;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Interop.Avalonia.Core.Controls;
 using PixiEditor.Models.Rendering;
@@ -74,17 +75,18 @@ public class PreviewPainterControl : DrawieControl
 
         surface.Canvas.Save();
 
+        Matrix3X3 matrix = Matrix3X3.Identity;
         if (previewBounds != null)
         {
-            UniformScale(x, y, surface, previewBounds.Value);
+            matrix = UniformScale(x, y, previewBounds.Value);
         }
 
-        PreviewPainter.Paint(surface);
+        PreviewPainter.Paint(surface, new VecI((int)Bounds.Size.Width, (int)Bounds.Size.Height), matrix);
 
         surface.Canvas.Restore();
     }
 
-    private void UniformScale(float x, float y, DrawingSurface target, RectD previewBounds)
+    private Matrix3X3 UniformScale(float x, float y,  RectD previewBounds)
     {
         float scaleX = (float)Bounds.Width / x;
         float scaleY = (float)Bounds.Height / y;
@@ -93,7 +95,7 @@ public class PreviewPainterControl : DrawieControl
         dX -= (float)previewBounds.X;
         float dY = (float)Bounds.Height / 2 / scale - y / 2;
         dY -= (float)previewBounds.Y;
-        target.Canvas.Scale(scale, scale);
-        target.Canvas.Translate(dX, dY);
+        Matrix3X3 matrix = Matrix3X3.CreateScale(scale, scale);
+        return matrix.Concat(Matrix3X3.CreateTranslation(dX, dY));
     }
 }