Browse Source

Merge branch 'master' into localization-debug-fixes

CPK 7 months ago
parent
commit
3de3e42cbe
100 changed files with 2057 additions and 694 deletions
  1. 5 4
      src/ChunkyImageLib/Operations/BresenhamLineHelper.cs
  2. 1 1
      src/ChunkyImageLib/Operations/BresenhamLineOperation.cs
  3. 10 0
      src/ChunkyImageLib/Operations/EllipseCache.cs
  4. 136 7
      src/ChunkyImageLib/Operations/EllipseHelper.cs
  5. 25 13
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  6. 1 1
      src/Directory.Build.props
  7. 1 1
      src/Drawie
  8. 1 1
      src/PixiDocks
  9. 4 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs
  10. 4 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs
  11. 2 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs
  12. 51 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs
  13. 48 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  14. 23 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  15. 57 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  16. 0 57
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs
  17. 81 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/BlurNode.cs
  18. 6 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  19. 6 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  20. 56 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  21. 13 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  22. 10 46
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  23. 14 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  24. 23 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  25. 37 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TextureCache.cs
  26. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Animation/KeyFramesStartPos_UpdateableChange.cs
  27. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkCache.cs
  29. 45 15
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  30. 13 8
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  31. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PixelPerfectPen_UpdateableChange.cs
  32. 46 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectionsData.cs
  33. 47 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DuplicateNode_Change.cs
  34. 2 2
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs
  35. 139 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs
  36. 2 1
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs
  37. 63 1
      src/PixiEditor.ChangeableDocument/Enums/MathNodeMode.cs
  38. 76 26
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  39. 10 0
      src/PixiEditor.Common/MathEx.cs
  40. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  41. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  42. 1 1
      src/PixiEditor.SVG/Elements/SvgImage.cs
  43. 1 1
      src/PixiEditor.SVG/SvgDocument.cs
  44. 11 3
      src/PixiEditor.SVG/SvgElement.cs
  45. 12 1
      src/PixiEditor.SVG/SvgProperty.cs
  46. 2 1
      src/PixiEditor.UI.Common/Controls/ComboBox.axaml
  47. 1 1
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  48. 0 31
      src/PixiEditor.sln
  49. 34 3
      src/PixiEditor/Data/Localization/Languages/en.json
  50. 2 2
      src/PixiEditor/Data/ShortcutActionMaps/AsepriteShortcutMap.json
  51. 3 1
      src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs
  52. 2 2
      src/PixiEditor/Helpers/Extensions/MethodExtension.cs
  53. 0 14
      src/PixiEditor/Helpers/MathUtil.cs
  54. 2 2
      src/PixiEditor/Helpers/VersionHelpers.cs
  55. 1 1
      src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs
  56. 1 1
      src/PixiEditor/Models/Commands/CommandController.cs
  57. 1 1
      src/PixiEditor/Models/Commands/Commands/Command.cs
  58. 4 2
      src/PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs
  59. 4 1
      src/PixiEditor/Models/Commands/Search/CommandSearchResult.cs
  60. 1 1
      src/PixiEditor/Models/Commands/Search/FileSearchResult.cs
  61. 92 17
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  62. 1 1
      src/PixiEditor/Models/Controllers/ShortcutController.cs
  63. 31 5
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  64. 1 1
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  65. 22 0
      src/PixiEditor/Models/DocumentModels/Public/ChangeBlock.cs
  66. 74 6
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  67. 7 2
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  68. 2 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  69. 2 2
      src/PixiEditor/Models/Handlers/IAnimationHandler.cs
  70. 1 1
      src/PixiEditor/Models/Handlers/IDocumentOperations.cs
  71. 9 0
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  72. 3 10
      src/PixiEditor/Models/Rendering/CanvasUpdater.cs
  73. 65 17
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  74. 1 1
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  75. 40 18
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  76. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  77. 136 128
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  78. 1 1
      src/PixiEditor/Styles/Templates/NodeView.axaml
  79. 4 4
      src/PixiEditor/ViewModels/Dock/DocumentPreviewDockViewModel.cs
  80. 3 3
      src/PixiEditor/ViewModels/Dock/LayoutManager.cs
  81. 38 20
      src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs
  82. 44 11
      src/PixiEditor/ViewModels/Document/CelGroupViewModel.cs
  83. 1 1
      src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs
  84. 2 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  85. 36 43
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  86. 0 7
      src/PixiEditor/ViewModels/Document/Nodes/DebugBlendModeNodeViewModel.cs
  87. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/BlurNodeViewModel.cs
  88. 20 2
      src/PixiEditor/ViewModels/Document/Nodes/MathNodeViewModel.cs
  89. 4 4
      src/PixiEditor/ViewModels/SubViewModels/AnimationsViewModel.cs
  90. 222 6
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  91. 1 1
      src/PixiEditor/ViewModels/SubViewModels/ColorsViewModel.cs
  92. 5 7
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  93. 1 1
      src/PixiEditor/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs
  94. 4 4
      src/PixiEditor/ViewModels/SubViewModels/SelectionViewModel.cs
  95. 2 2
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  96. 2 2
      src/PixiEditor/ViewModels/SubViewModels/WindowViewModel.cs
  97. 9 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/Setting.cs
  98. 1 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/Toolbar.cs
  99. 11 6
      src/PixiEditor/ViewModels/Tools/ToolViewModel.cs
  100. 3 3
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

+ 5 - 4
src/ChunkyImageLib/Operations/BresenhamLineHelper.cs

@@ -2,19 +2,20 @@
 using Drawie.Numerics;
 
 namespace ChunkyImageLib.Operations;
+
 public static class BresenhamLineHelper
 {
-    public static VecF[] GetBresenhamLine(VecI start, VecI end)
+    public static VecI[] GetBresenhamLine(VecI start, VecI end)
     {
         int count = Math.Abs((start - end).LongestAxis) + 1;
         if (count > 100000)
-            return Array.Empty<VecF>();
-        VecF[] output = new VecF[count];
+            return [];
+        VecI[] output = new VecI[count];
         CalculateBresenhamLine(start, end, output);
         return output;
     }
 
-    private static void CalculateBresenhamLine(VecI start, VecI end, VecF[] output)
+    private static void CalculateBresenhamLine(VecI start, VecI end, VecI[] output)
     {
         int index = 0;
 

+ 1 - 1
src/ChunkyImageLib/Operations/BresenhamLineOperation.cs

@@ -23,7 +23,7 @@ internal class BresenhamLineOperation : IMirroredDrawOperation
         this.color = color;
         this.blendMode = blendMode;
         paint = new Paint() { BlendMode = blendMode };
-        points = BresenhamLineHelper.GetBresenhamLine(from, to);
+        points = BresenhamLineHelper.GetBresenhamLine(from, to).Select(v => new VecF(v)).ToArray();
     }
 
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)

+ 10 - 0
src/ChunkyImageLib/Operations/EllipseCache.cs

@@ -0,0 +1,10 @@
+using System.Collections.Concurrent;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+
+namespace ChunkyImageLib.Operations;
+
+public static class EllipseCache
+{
+    public static readonly ConcurrentDictionary<VecI, VectorPath> Ellipses = new();
+}

+ 136 - 7
src/ChunkyImageLib/Operations/EllipseHelper.cs

@@ -106,7 +106,7 @@ public class EllipseHelper
         float radiusY = (rect.Height - 1) / 2.0f;
         if (rotationRad == 0)
             return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y);
-        
+
         return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y, rotationRad);
     }
 
@@ -185,6 +185,130 @@ public class EllipseHelper
         return listToFill;
     }
 
+    /// <summary>
+    ///     Constructs pixel-perfect ellipse outline represented as a vector path.
+    ///  This function is quite heavy, for less precise but faster results use <see cref="GenerateEllipseVectorFromRect"/>.
+    /// </summary>
+    /// <param name="rectangle">The rectangle that the ellipse should fit into.</param>
+    /// <returns>A vector path that represents an ellipse outline.</returns>
+    public static VectorPath ConstructEllipseOutline(RectI rectangle)
+    {
+        if (EllipseCache.Ellipses.TryGetValue(rectangle.Size, out var cachedPath))
+        {
+            VectorPath finalPath = new(cachedPath);
+            finalPath.Transform(Matrix3X3.CreateTranslation(rectangle.TopLeft.X, rectangle.TopLeft.Y));
+            
+            return finalPath;
+        }
+        
+        if (rectangle.Width < 3 || rectangle.Height < 3)
+        {
+            VectorPath rectPath = new();
+            rectPath.AddRect((RectD)rectangle);
+
+            return rectPath;
+        }
+
+        if (rectangle is { Width: 3, Height: 3 })
+        {
+            return CreateThreePixelCircle((VecI)rectangle.Center);
+        }
+
+        var center = rectangle.Size / 2d;
+        RectI rect = new RectI(0, 0, rectangle.Width, rectangle.Height);
+        var points = GenerateEllipseFromRect(rect, 0).ToList();
+        points.Sort((vec, vec2) => Math.Sign((vec - center).Angle - (vec2 - center).Angle));
+        List<VecI> finalPoints = new();
+        for (int i = 0; i < points.Count; i++)
+        {
+            VecI prev = points[Mod(i - 1, points.Count)];
+            VecI point = points[i];
+            VecI next = points[Mod(i + 1, points.Count)];
+
+            bool atBottom = point.Y >= center.Y;
+            bool onRight = point.X >= center.X;
+            if (atBottom)
+            {
+                if (onRight)
+                {
+                    if (prev.Y != point.Y)
+                        finalPoints.Add(new(point.X + 1, point.Y));
+                    finalPoints.Add(new(point.X + 1, point.Y + 1));
+                    if (next.X != point.X)
+                        finalPoints.Add(new(point.X, point.Y + 1));
+                }
+                else
+                {
+                    if (prev.X != point.X)
+                        finalPoints.Add(new(point.X + 1, point.Y + 1));
+                    finalPoints.Add(new(point.X, point.Y + 1));
+                    if (next.Y != point.Y)
+                        finalPoints.Add(point);
+                }
+            }
+            else
+            {
+                if (onRight)
+                {
+                    if (prev.X != point.X)
+                        finalPoints.Add(point);
+                    finalPoints.Add(new(point.X + 1, point.Y));
+                    if (next.Y != point.Y)
+                        finalPoints.Add(new(point.X + 1, point.Y + 1));
+                }
+                else
+                {
+                    if (prev.Y != point.Y)
+                        finalPoints.Add(new(point.X, point.Y + 1));
+                    finalPoints.Add(point);
+                    if (next.X != point.X)
+                        finalPoints.Add(new(point.X + 1, point.Y));
+                }
+            }
+        }
+
+        VectorPath path = new();
+
+        path.MoveTo(new VecF(finalPoints[0].X, finalPoints[0].Y));
+        for (var index = 1; index < finalPoints.Count; index++)
+        {
+            var point = finalPoints[index];
+            path.LineTo(new VecF(point.X, point.Y));
+        }
+
+        path.Close();
+        
+        EllipseCache.Ellipses[rectangle.Size] = new VectorPath(path);
+        
+        path.Transform(Matrix3X3.CreateTranslation(rectangle.TopLeft.X, rectangle.TopLeft.Y));
+        return path;
+    }
+
+    public static VectorPath CreateThreePixelCircle(VecI rectanglePos)
+    {
+        var path = new VectorPath();
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(0, -1));
+        path.LineTo(new VecF(1, -1));
+        path.LineTo(new VecF(1, 0));
+        path.LineTo(new VecF(2, 0));
+        path.LineTo(new VecF(2, 1));
+        path.LineTo(new VecF(2, 1));
+        path.LineTo(new VecF(1, 1));
+        path.LineTo(new VecF(1, 2));
+        path.LineTo(new VecF(0, 2));
+        path.LineTo(new VecF(0, 1));
+        path.LineTo(new VecF(-1, 1));
+        path.LineTo(new VecF(-1, 0));
+        path.Close();
+        
+        path.Transform(Matrix3X3.CreateTranslation(rectanglePos.X, rectanglePos.Y));
+        
+        return path;
+    }
+
+    private static int Mod(int x, int m) => (x % m + m) % m;
+
     // This function works, but honestly Skia produces better results, and it doesn't require so much
     // computation on the CPU. I'm leaving this, because once I (or someone else) figure out how to
     // make it better, and it will be useful.
@@ -203,7 +327,7 @@ public class EllipseHelper
 
         // less than, because y grows downwards
         //VecD actualTopmost = possiblyTopmostPoint.Y < possiblyMinPoint.Y ? possiblyTopmostPoint : possiblyMinPoint;
-        
+
         //rotationRad = double.Round(rotationRad, 1);
 
         double currentTetha = 0;
@@ -221,13 +345,13 @@ public class EllipseHelper
 
             currentTetha += tethaStep;
         } while (currentTetha < Math.PI * 2);
-        
+
         return listToFill;
     }
 
     private static void AddPoint(HashSet<VecI> listToFill, VecI floored, VecI[] lastPoints)
     {
-        if(!listToFill.Add(floored)) return;
+        if (!listToFill.Add(floored)) return;
 
         if (lastPoints[0] == default)
         {
@@ -247,7 +371,7 @@ public class EllipseHelper
 
             lastPoints[0] = floored;
             lastPoints[1] = default;
-            
+
             return;
         }
 
@@ -345,13 +469,18 @@ public class EllipseHelper
         }
     }
 
+    /// <summary>
+    ///     This function generates a vector path that represents an oval. For pixel-perfect circle use <see cref="ConstructEllipseOutline"/>.
+    /// </summary>
+    /// <param name="location">The rectangle that the ellipse should fit into.</param>
+    /// <returns>A vector path that represents an oval.</returns>
     public static VectorPath GenerateEllipseVectorFromRect(RectD location)
     {
         VectorPath path = new();
         path.AddOval(location);
-       
+
         path.Close();
-        
+
         return path;
     }
 }

+ 25 - 13
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -46,14 +46,22 @@ internal class EllipseOperation : IMirroredDrawOperation
         {
             if (Math.Abs(rotation) < 0.001)
             {
-                var ellipseList = EllipseHelper.GenerateEllipseFromRect((RectI)location);
+                if (strokeWidth == 0)
+                {
+                    ellipseOutline = EllipseHelper.ConstructEllipseOutline((RectI)location);
+                }
+                else
+                {
+                    var ellipseList = EllipseHelper.GenerateEllipseFromRect((RectI)location);
 
-                ellipse = ellipseList.Select(a => new VecF(a)).ToArray();
+                    ellipse = ellipseList.Select(a => new VecF(a)).ToArray();
 
-                if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
-                {
-                    (var fill, ellipseFillRect) = EllipseHelper.SplitEllipseFillIntoRegions(ellipseList.ToList(), (RectI)location);
-                    ellipseFill = fill.Select(a => new VecF(a)).ToArray();
+                    if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
+                    {
+                        (var fill, ellipseFillRect) =
+                            EllipseHelper.SplitEllipseFillIntoRegions(ellipseList.ToList(), (RectI)location);
+                        ellipseFill = fill.Select(a => new VecF(a)).ToArray();
+                    }
                 }
             }
             else
@@ -98,7 +106,7 @@ internal class EllipseOperation : IMirroredDrawOperation
         paint.IsAntiAliased = false;
         if (strokeWidth - 1 < 0.01)
         {
-            if (Math.Abs(rotation) < 0.001)
+            if (Math.Abs(rotation) < 0.001 && strokeWidth > 0)
             {
                 if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
                 {
@@ -122,12 +130,15 @@ internal class EllipseOperation : IMirroredDrawOperation
                     paint.Style = PaintStyle.Fill;
                     surf.Canvas.DrawPath(ellipseOutline!, paint);
                 }
-                
-                paint.Color = strokeColor;
-                paint.Style = PaintStyle.Stroke;
-                paint.StrokeWidth = 1f;
-                
-                surf.Canvas.DrawPath(ellipseOutline!, paint);
+
+                if (strokeWidth > 0)
+                {
+                    paint.Color = strokeColor;
+                    paint.Style = PaintStyle.Stroke;
+                    paint.StrokeWidth = 1;
+
+                    surf.Canvas.DrawPath(ellipseOutline!, paint);
+                }
 
                 surf.Canvas.Restore();
             }
@@ -207,5 +218,6 @@ internal class EllipseOperation : IMirroredDrawOperation
         paint.Dispose();
         outerPath?.Dispose();
         innerPath?.Dispose();
+        ellipseOutline?.Dispose();
     }
 }

+ 1 - 1
src/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.2.2</AvaloniaVersion>
+		    <AvaloniaVersion>11.2.3</AvaloniaVersion>
     </PropertyGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 63c826db08b9ea57cf6ae29718d94fd77951c73a
+Subproject commit 52a989906b7d9254fa9bab4acf172097d9d0dbef

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 9190dfc8d59f5eaae40a80e34e75a1d5667dec83
+Subproject commit 5f14bdf0e46dd470e46a88ce5f58de4e02c68e94

+ 4 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs

@@ -1,4 +1,5 @@
 using System.Collections.Immutable;
+using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.Enums;
@@ -19,9 +20,10 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
         bool maskIsVisible,
         ImmutableArray<NodePropertyInfo> Inputs,
         ImmutableArray<NodePropertyInfo> Outputs,
+        VecD position,
         NodeMetadata metadata
     ) : base(internalName, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
-        maskIsVisible, Inputs, Outputs, metadata)
+        maskIsVisible, Inputs, Outputs, position, metadata)
     {
     }
 
@@ -38,6 +40,7 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
             folder.EmbeddedMask is not null,
             folder.MaskIsVisible.Value, CreatePropertyInfos(folder.InputProperties, true, folder.Id),
             CreatePropertyInfos(folder.OutputProperties, false, folder.Id),
+            folder.Position,
             new NodeMetadata(folder));
     }
 }

+ 4 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs

@@ -1,4 +1,5 @@
 using System.Collections.Immutable;
+using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -22,9 +23,10 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
         bool lockTransparency,
         ImmutableArray<NodePropertyInfo> inputs,
         ImmutableArray<NodePropertyInfo> outputs,
+        VecD position,
         NodeMetadata metadata) :
         base(internalName, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
-            maskIsVisible, inputs, outputs, metadata)
+            maskIsVisible, inputs, outputs, position, metadata)
     {
         LockTransparency = lockTransparency;
     }
@@ -46,6 +48,7 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
             layer is ITransparencyLockable { LockTransparency: true },
             CreatePropertyInfos(layer.InputProperties, true, layer.Id),
             CreatePropertyInfos(layer.OutputProperties, false, layer.Id),
+            layer.Position,
             new NodeMetadata(layer.GetType())
         );
     }

+ 2 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs

@@ -18,8 +18,9 @@ public abstract record class CreateStructureMember_ChangeInfo(
     bool MaskIsVisible,
     ImmutableArray<NodePropertyInfo> InputProperties,
     ImmutableArray<NodePropertyInfo> OutputProperties,
+    VecD position,
     NodeMetadata Metadata
-) : CreateNode_ChangeInfo(InternalName, Name, new VecD(0, 0), Id, InputProperties, OutputProperties, Metadata)
+) : CreateNode_ChangeInfo(InternalName, Name, position, Id, InputProperties, OutputProperties, Metadata)
 {
     public ImmutableArray<NodePropertyInfo> InputProperties { get; init; } = InputProperties;
     public ImmutableArray<NodePropertyInfo> OutputProperties { get; init; } = OutputProperties;

+ 51 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs

@@ -11,8 +11,10 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncInputProperty
 {
     private T? constantNonOverrideValue;
-    
-    internal FuncInputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node, internalName, displayName, null)
+    private int lastConstantHashCode;
+
+    internal FuncInputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node,
+        internalName, displayName, null)
     {
         constantNonOverrideValue = defaultValue;
         NonOverridenValue = _ => constantNonOverrideValue;
@@ -28,7 +30,7 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
                 shaderExpressionVariable.SetConstantValue(toReturn, ConversionTable.Convert);
                 return (T)(object)shaderExpressionVariable;
             }
-            
+
             return (T)toReturn;
         };
         return func;
@@ -40,12 +42,12 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
         {
             Type targetType = typeof(T);
             bool isShaderExpression = false;
-            if(typeof(T).IsAssignableTo(typeof(ShaderExpressionVariable)))
+            if (typeof(T).IsAssignableTo(typeof(ShaderExpressionVariable)))
             {
                 targetType = targetType.BaseType.GenericTypeArguments[0];
                 isShaderExpression = true;
             }
-            
+
             var sourceObj = delegateToCast.DynamicInvoke(f);
             ConversionTable.TryConvert(sourceObj, targetType, out var result);
             if (isShaderExpression)
@@ -67,12 +69,12 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
 
                 return (T)toReturn;
             }
-            
-            return result == null ? default : (T)result; 
+
+            return result == null ? default : (T)result;
         };
         return func;
     }
-    
+
     private Expression Adjust(Expression expression, object toReturn, out bool adjustNestedVariables)
     {
         adjustNestedVariables = false;
@@ -89,7 +91,7 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
 
         return expression;
     }
-    
+
     private void AdjustNested(IMultiValueVariable toReturn, Expression expression)
     {
         if (toReturn is not ShaderExpressionVariable shaderExpressionVariable)
@@ -133,8 +135,8 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
             shaderExpressionVariable.SetConstantValue(value, ConversionTable.Convert);
             return;
         }
-        
-        if(ConversionTable.TryConvert(value, typeof(T), out var result))
+
+        if (ConversionTable.TryConvert(value, typeof(T), out var result))
         {
             constantNonOverrideValue = (T)result;
             return;
@@ -142,4 +144,42 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
 
         constantNonOverrideValue = default;
     }
+
+    internal override bool CacheChanged
+    {
+        get
+        {
+            if (constantNonOverrideValue == null)
+            {
+                return base.CacheChanged;
+            }
+
+            if (Connection == null && lastConnectionHash != -1)
+            {
+                return true;
+            }
+
+            if (Connection != null && lastConnectionHash != Connection.GetHashCode())
+            {
+                lastConnectionHash = Connection.GetHashCode();
+                return true;
+            }
+
+            if (constantNonOverrideValue is ShaderExpressionVariable expressionVariable)
+            {
+                return expressionVariable.ConstantValueString.GetHashCode() != lastConstantHashCode;
+            }
+
+            return base.CacheChanged;
+        }
+    }
+
+    internal override void UpdateCache()
+    {
+        base.UpdateCache();
+        if (constantNonOverrideValue is ShaderExpressionVariable expressionVariable)
+        {
+            lastConstantHashCode = expressionVariable.ConstantValueString.GetHashCode();
+        }
+    }
 }

+ 48 - 18
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -11,8 +11,12 @@ public class InputProperty : IInputProperty
 {
     private object _internalValue;
     private int _lastExecuteHash = -1;
+    protected int lastConnectionHash = -1;
     private PropertyValidator? validator;
-    
+    private IOutputProperty? connection;
+
+    public event Action ConnectionChanged;
+
     public string InternalPropertyName { get; }
     public string DisplayName { get; }
 
@@ -26,7 +30,7 @@ public class InputProperty : IInputProperty
             }
 
             var connectionValue = Connection.Value;
-            
+
             if (!ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is Delegate connectionField)
             {
                 return connectionField.DynamicInvoke(FuncContext.NoContext);
@@ -40,7 +44,7 @@ public class InputProperty : IInputProperty
             return connectionValue;
         }
     }
-    
+
     public object NonOverridenValue
     {
         get => _internalValue;
@@ -49,7 +53,7 @@ public class InputProperty : IInputProperty
             _internalValue = value;
         }
     }
-    
+
     public PropertyValidator Validator
     {
         get
@@ -73,28 +77,42 @@ public class InputProperty : IInputProperty
     {
         Func<FuncContext, object> func = f =>
         {
-            return ConversionTable.TryConvert(delegateToCast.DynamicInvoke(f), ValueType, out object result) ? result : null;
+            return ConversionTable.TryConvert(delegateToCast.DynamicInvoke(f), ValueType, out object result)
+                ? result
+                : null;
         };
         return func;
     }
 
     public Node Node { get; }
-    public Type ValueType { get; } 
-    internal bool CacheChanged
+    public Type ValueType { get; }
+
+    internal virtual bool CacheChanged
     {
         get
         {
+            if(Connection == null && lastConnectionHash != -1)
+            {
+                return true;
+            }
+            
+            if(Connection != null && lastConnectionHash != Connection.GetHashCode())
+            {
+                lastConnectionHash = Connection.GetHashCode();
+                return true;
+            }
+            
             if (Value is ICacheable cacheable)
             {
                 return cacheable.GetCacheHash() != _lastExecuteHash;
             }
 
-            if(Value is null)
+            if (Value is null)
             {
                 return _lastExecuteHash != 0;
             }
-            
-            if(Value.GetType().IsValueType || Value.GetType() == typeof(string))
+
+            if (Value.GetType().IsValueType || Value.GetType() == typeof(string))
             {
                 return Value.GetHashCode() != _lastExecuteHash;
             }
@@ -103,7 +121,7 @@ public class InputProperty : IInputProperty
         }
     }
 
-    internal void UpdateCache()
+    internal virtual void UpdateCache()
     {
         if (Value is null)
         {
@@ -117,12 +135,25 @@ public class InputProperty : IInputProperty
         {
             _lastExecuteHash = Value.GetHashCode();
         }
+        
+        lastConnectionHash = Connection?.GetHashCode() ?? -1;
     }
-    
+
     IReadOnlyNode INodeProperty.Node => Node;
-    
-    public IOutputProperty? Connection { get; set; }
-    
+
+    public IOutputProperty? Connection
+    {
+        get => connection;
+        set
+        {
+            if (connection != value)
+            {
+                connection = value;
+                ConnectionChanged?.Invoke();
+            }
+        }
+    }
+
     internal InputProperty(Node node, string internalName, string displayName, object defaultValue, Type valueType)
     {
         InternalPropertyName = internalName;
@@ -133,7 +164,6 @@ public class InputProperty : IInputProperty
     }
 }
 
-
 public class InputProperty<T> : InputProperty, IInputProperty<T>
 {
     public new T Value
@@ -150,9 +180,9 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
             {
                 return (T)FuncFactoryDelegate(func);
             }
-            
+
             object target = value;
-            if(value is ShaderExpressionVariable shaderExpression)
+            if (value is ShaderExpressionVariable shaderExpression)
             {
                 target = shaderExpression.GetConstant();
             }

+ 23 - 12
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -1,14 +1,14 @@
-using System.Collections;
-using System.Diagnostics;
+using System.Collections.Immutable;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 {
+    private ImmutableList<IReadOnlyNode>? cachedExecutionList;
+    
     private readonly List<Node> _nodes = new();
     public IReadOnlyCollection<Node> Nodes => _nodes;
     public Node? OutputNode => CustomOutputNode ?? Nodes.OfType<OutputNode>().FirstOrDefault();
@@ -23,8 +23,10 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         {
             return;
         }
-
+        
+        node.ConnectionsChanged += ResetCache;
         _nodes.Add(node);
+        ResetCache();
     }
 
     public void RemoveNode(Node node)
@@ -34,12 +36,19 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
             return;
         }
 
+        node.ConnectionsChanged -= ResetCache;
         _nodes.Remove(node);
+        ResetCache();
     }
 
     public Queue<IReadOnlyNode> CalculateExecutionQueue(IReadOnlyNode outputNode)
     {
-        return GraphUtils.CalculateExecutionQueue(outputNode);
+        return new Queue<IReadOnlyNode>(CalculateExecutionQueueInternal(outputNode));
+    }
+    
+    private ImmutableList<IReadOnlyNode> CalculateExecutionQueueInternal(IReadOnlyNode outputNode)
+    {
+        return cachedExecutionList ??= GraphUtils.CalculateExecutionQueue(outputNode).ToImmutableList();
     }
 
     void IReadOnlyNodeGraph.AddNode(IReadOnlyNode node) => AddNode((Node)node);
@@ -58,11 +67,10 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     {
         if(OutputNode == null) return false;
         
-        var queue = CalculateExecutionQueue(OutputNode);
+        var queue = CalculateExecutionQueueInternal(OutputNode);
         
-        while (queue.Count > 0)
+        foreach (var node in queue)
         {
-            var node = queue.Dequeue();
             action(node);
         }
         
@@ -74,12 +82,10 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         if (OutputNode == null) return;
         if(!CanExecute()) return;
 
-        var queue = CalculateExecutionQueue(OutputNode);
+        var queue = CalculateExecutionQueueInternal(OutputNode);
         
-        while (queue.Count > 0)
+        foreach (var node in queue)
         {
-            var node = queue.Dequeue();
-            
             if (node is Node typedNode)
             {
                 if(typedNode.IsDisposed) continue;
@@ -105,4 +111,9 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 
         return true;
     }
+    
+    private void ResetCache()
+    {
+        cachedExecutionList = null;
+    }
 }

+ 57 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -5,11 +5,12 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("CreateImage")]
-public class CreateImageNode : Node
+public class CreateImageNode : Node, IPreviewRenderable
 {
     public OutputProperty<Texture> Output { get; }
 
@@ -21,6 +22,8 @@ public class CreateImageNode : Node
 
     public RenderOutputProperty RenderOutput { get; }
 
+    private TextureCache textureCache = new();
+
     public CreateImageNode()
     {
         Output = CreateOutput<Texture>(nameof(Output), "IMAGE", null);
@@ -37,7 +40,16 @@ public class CreateImageNode : Node
             return;
         }
 
-        var surface = RequestTexture(0, Size.Value, context.ProcessingColorSpace, false);
+        var surface = Render(context);
+
+        Output.Value = surface;
+
+        RenderOutput.ChainToPainterValue();
+    }
+
+    private Texture Render(RenderContext context)
+    {
+        var surface = textureCache.RequestTexture(0, Size.Value, context.ProcessingColorSpace, false);
 
         surface.DrawingSurface.Canvas.Clear(Fill.Value);
 
@@ -49,16 +61,55 @@ public class CreateImageNode : Node
         Content.Value?.Paint(ctx, surface.DrawingSurface);
 
         surface.DrawingSurface.Canvas.RestoreToCount(saved);
-        
-        Output.Value = surface;
-
-        RenderOutput.ChainToPainterValue();
+        return surface;
     }
 
     private void OnPaint(RenderContext context, DrawingSurface surface)
     {
+        if(Output.Value == null || Output.Value.IsDisposed) return;
+        
         surface.Canvas.DrawSurface(Output.Value.DrawingSurface, 0, 0);
     }
 
     public override Node CreateCopy() => new CreateImageNode();
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        textureCache.Dispose();
+    }
+
+    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        if (Size.Value.X <= 0 || Size.Value.Y <= 0)
+        {
+            return null;
+        }
+
+        return new RectD(0, 0, Size.Value.X, Size.Value.Y);
+    }
+
+    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        if (Size.Value.X <= 0 || Size.Value.Y <= 0)
+        {
+            return false;
+        }
+
+        if (Output.Value == null)
+        {
+            return false;
+        }
+
+        var surface = Render(context);
+        
+        if (surface == null || surface.IsDisposed)
+        {
+            return false;
+        }
+        
+        renderOn.Canvas.DrawSurface(surface.DrawingSurface, 0, 0);
+        
+        return true;
+    }
 }

+ 0 - 57
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs

@@ -1,57 +0,0 @@
-using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
-using Drawie.Backend.Core.Surfaces.PaintImpl;
-using DrawingApiBlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
-using Drawie.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-
-// TODO: Add based on debug mode, not debug build.
-[NodeInfo("DebugBlendMode")]
-public class DebugBlendModeNode : Node
-{
-    private Paint _paint = new();
-    
-    public InputProperty<Texture?> Dst { get; }
-
-    public InputProperty<Texture?> Src { get; }
-
-    public InputProperty<DrawingApiBlendMode> BlendMode { get; }
-
-    public OutputProperty<Texture> Result { get; }
-
-    private Paint blendModeOpacityPaint => new() { BlendMode = DrawingApiBlendMode.SrcOver }; 
-    public DebugBlendModeNode()
-    {
-        Dst = CreateInput<Texture?>(nameof(Dst), "Dst", null);
-        Src = CreateInput<Texture?>(nameof(Src), "Src", null);
-        BlendMode = CreateInput(nameof(BlendMode), "Blend Mode", DrawingApiBlendMode.SrcOver);
-
-        Result = CreateOutput<Texture>(nameof(Result), "Result", null);
-    }
-
-    protected override void OnExecute(RenderContext context)
-    {
-        if (Dst.Value is not { } dst || Src.Value is not { } src)
-            return;
-
-        var size = new VecI(Math.Max(src.Size.X, dst.Size.X), int.Max(src.Size.Y, dst.Size.Y));
-        var workingSurface = RequestTexture(0, size, context.ProcessingColorSpace);
-
-        workingSurface.DrawingSurface.Canvas.DrawSurface(dst.DrawingSurface, 0, 0, blendModeOpacityPaint);
-
-        _paint.BlendMode = BlendMode.Value;
-        workingSurface.DrawingSurface.Canvas.DrawSurface(src.DrawingSurface, 0, 0, _paint);
-        
-        Result.Value = workingSurface;
-    }
-
-
-    public override Node CreateCopy() => new DebugBlendModeNode();
-
-    public override void Dispose()
-    {
-        base.Dispose();
-        _paint.Dispose();
-    }
-}

+ 81 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/BlurNode.cs

@@ -0,0 +1,81 @@
+using System.Diagnostics;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+
+[NodeInfo("BlurFilter")]
+public class BlurNode : FilterNode
+{
+    public InputProperty<bool> PreserveAlpha { get; }
+    
+    public InputProperty<VecD> Radius { get; }
+    
+    public BlurNode()
+    {
+        PreserveAlpha = CreateInput("PreserveAlpha", "PRESERVE_ALPHA", true);
+        Radius = CreateInput("Radius", "RADIUS", new VecD(1, 1)).WithRules(x => x.Min(new VecD(0, 0)));
+    }
+
+    protected override ImageFilter GetImageFilter()
+    {
+        var sigma = (VecF)Radius.Value;
+        var preserveAlpha = PreserveAlpha.Value;
+
+        var xFilter = GetGaussianFilter(sigma.X, true, preserveAlpha, null, out float[] xKernel);
+        
+        // Reuse xKernel if x == y
+        var yKernel = Math.Abs(sigma.Y - sigma.X) < 0.0001f ? xKernel : null;
+        var yFilter = GetGaussianFilter(sigma.Y, false, preserveAlpha, yKernel, out _);
+
+        return (xFilter, yFilter) switch
+        {
+            (null, _) => yFilter,
+            (_, null) => xFilter,
+            (_, _) => ImageFilter.CreateCompose(yFilter, xFilter)
+        };
+    }
+
+    private static ImageFilter? GetGaussianFilter(float sigma, bool isX, bool preserveAlpha, float[]? kernel, out float[] usedKernel)
+    {
+        usedKernel = null;
+        if (sigma < 0.0001f) return null;
+        
+        kernel ??= GenerateGaussianKernel(sigma);
+        usedKernel = kernel;
+        
+        var size = isX ? new VecI(kernel.Length, 1) : new VecI(1, kernel.Length);
+        var offset = isX ? new VecI(kernel.Length / 2, 0) : new VecI(0, kernel.Length / 2);
+        
+        return ImageFilter.CreateMatrixConvolution(size, kernel, 1, 0, offset, TileMode.Repeat, !preserveAlpha);
+    }
+    
+    public static float[] GenerateGaussianKernel(float sigma)
+    {
+        int radius = (int)Math.Ceiling(3 * sigma);
+        radius = Math.Min(radius, 300);
+        int kernelSize = 2 * radius + 1;
+
+        float[] kernel = new float[kernelSize];
+        float sum = 0f;
+        float twoSigmaSquare = 2 * sigma * sigma;
+
+        for (int i = 0; i < kernelSize; i++)
+        {
+            int x = i - radius;
+            kernel[i] = (float)Math.Exp(-(x * x) / twoSigmaSquare);
+            sum += kernel[i];
+        }
+
+        // Normalize the kernel to ensure the sum of elements is 1
+        for (int i = 0; i < kernelSize; i++)
+        {
+            kernel[i] /= sum;
+        }
+
+        return kernel;
+    }
+
+    public override Node CreateCopy() => new BlurNode();
+}

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

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

@@ -193,6 +193,11 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     private KeyFrameData GetFrameWithImage(KeyFrameTime frame)
     {
+        if (keyFrames.Count == 1)
+        {
+            return keyFrames[0];
+        }
+        
         var imageFrame = keyFrames.OrderBy(x => x.StartFrame).LastOrDefault(x => x.IsInFrame(frame.Frame));
         if (imageFrame?.Data is not ChunkyImage)
         {
@@ -225,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();

+ 56 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs

@@ -6,6 +6,7 @@ using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
+using PixiEditor.Common;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
@@ -22,7 +23,8 @@ public class MathNode : Node
     
     public FuncInputProperty<Float1> Y { get; }
     
-
+    public FuncInputProperty<Float1> Z { get; }
+    
     public MathNode()
     {
         Result = CreateFuncOutput<Float1>(nameof(Result), "RESULT", Calculate);
@@ -30,11 +32,12 @@ public class MathNode : Node
         Clamp = CreateInput(nameof(Clamp), "CLAMP", false);
         X = CreateFuncInput<Float1>(nameof(X), "X", 0d);
         Y = CreateFuncInput<Float1>(nameof(Y), "Y", 0d);
+        Z = CreateFuncInput<Float1>(nameof(Z), "Z", 0d);
     }
 
     private Float1 Calculate(FuncContext context)
     {
-        var (x, y) = GetValues(context);
+        var (x, y, z) = GetValues(context);
 
         if (context.HasContext)
         {
@@ -47,6 +50,27 @@ public class MathNode : Node
                 MathNodeMode.Sin => ShaderMath.Sin(x),
                 MathNodeMode.Cos => ShaderMath.Cos(x),
                 MathNodeMode.Tan => ShaderMath.Tan(x),
+                MathNodeMode.GreaterThan => ShaderMath.GreaterThan(x, y),
+                MathNodeMode.GreaterThanOrEqual => ShaderMath.GreaterThanOrEqual(x, y),
+                MathNodeMode.LessThan => ShaderMath.LessThan(x, y),
+                MathNodeMode.LessThanOrEqual => ShaderMath.LessThanOrEqual(x, y),
+                MathNodeMode.Compare => ShaderMath.Compare(x, y, z),
+                MathNodeMode.Power => ShaderMath.Power(x, y),
+                MathNodeMode.Logarithm => ShaderMath.Log(x, y),
+                MathNodeMode.NaturalLogarithm => ShaderMath.LogE(x),
+                MathNodeMode.Root => ShaderMath.Root(x, y),
+                MathNodeMode.InverseRoot => ShaderMath.InverseRoot(x, y),
+                MathNodeMode.Fraction => ShaderMath.Fraction(x),
+                MathNodeMode.Absolute => ShaderMath.Abs(x),
+                MathNodeMode.Negate => ShaderMath.Negate(x),
+                MathNodeMode.Floor => ShaderMath.Floor(x),
+                MathNodeMode.Ceil => ShaderMath.Ceil(x),
+                MathNodeMode.Round => ShaderMath.Round(x),
+                MathNodeMode.Modulo => ShaderMath.Modulo(x, y),
+                MathNodeMode.Min => ShaderMath.Min(x, y),
+                MathNodeMode.Max => ShaderMath.Max(x, y),
+                MathNodeMode.Step => ShaderMath.Step(x, y),
+                MathNodeMode.SmoothStep => ShaderMath.SmoothStep(x, y, z),
             };
 
             if (Clamp.Value)
@@ -59,7 +83,8 @@ public class MathNode : Node
 
         var xConst = x.ConstantValue;
         var yConst = y.ConstantValue;
-            
+        var zConst = z.ConstantValue;
+        
         var constValue = Mode.Value switch
         {
             MathNodeMode.Add => xConst + yConst,
@@ -69,14 +94,40 @@ public class MathNode : Node
             MathNodeMode.Sin => Math.Sin(xConst),
             MathNodeMode.Cos => Math.Cos(xConst),
             MathNodeMode.Tan => Math.Tan(xConst),
+            MathNodeMode.GreaterThan => xConst > yConst ? 1 : 0,
+            MathNodeMode.GreaterThanOrEqual => xConst >= yConst ? 1 : 0,
+            MathNodeMode.LessThan => xConst < yConst ? 1 : 0,
+            MathNodeMode.LessThanOrEqual => xConst <= yConst ? 1 : 0,
+            MathNodeMode.Compare => Math.Abs(xConst - yConst) < zConst ? 1 : 0,
+            MathNodeMode.Power => Math.Pow(xConst, yConst),
+            MathNodeMode.Logarithm => Math.Log(xConst, yConst),
+            MathNodeMode.NaturalLogarithm => Math.Log(xConst),
+            MathNodeMode.Root => Math.Pow(xConst, 1.0 / yConst),
+            MathNodeMode.InverseRoot => 1.0 / Math.Pow(xConst, 1.0 / yConst),
+            MathNodeMode.Fraction => 1.0 / xConst,
+            MathNodeMode.Absolute => Math.Abs(xConst),
+            MathNodeMode.Negate => -xConst,
+            MathNodeMode.Floor => Math.Floor(xConst),
+            MathNodeMode.Ceil => Math.Ceiling(xConst),
+            MathNodeMode.Round => Math.Round(xConst),
+            MathNodeMode.Modulo => xConst % yConst,
+            MathNodeMode.Min => Math.Min(xConst, yConst),
+            MathNodeMode.Max => Math.Max(xConst, yConst),
+            MathNodeMode.Step => xConst > yConst ? 1 : 0,
+            MathNodeMode.SmoothStep => MathEx.SmoothStep(xConst, yConst, zConst),
         };
+        
+        if (Clamp.Value)
+        {
+            constValue = Math.Clamp(constValue, 0, 1);
+        }
             
         return new Float1(string.Empty) { ConstantValue = constValue };
     }
 
-    private (Float1 xConst, Float1 y) GetValues(FuncContext context)
+    private (Float1 xConst, Float1 y, Float1 z) GetValues(FuncContext context)
     {
-        return (context.GetValue(X), context.GetValue(Y));
+        return (context.GetValue(X), context.GetValue(Y), context.GetValue(Z));
     }
 
 

+ 13 - 7
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -23,7 +23,11 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
 
     private string _lastSksl;
+    private VecI? size;
 
+    // TODO: Add caching
+    // Caching requires a way to check if any connected node changed, checking inputs for this node works
+    // Also gather uniforms without doing full string builder generation of the shader
 
     public ModifyImageRightNode()
     {
@@ -47,15 +51,17 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         {
             return;
         }
-        
+
         OtherNode = startNode.Id;
 
-        if (startNode.Image.Value is not { Size: var size })
+        if (startNode.Image.Value is not { Size: var imgSize })
         {
             return;
         }
 
-        ShaderBuilder builder = new(size);
+        size = imgSize;
+
+        ShaderBuilder builder = new(size.Value);
         FuncContext context = new(renderContext, builder);
 
         if (Coordinate.Connection != null)
@@ -102,7 +108,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             drawingPaint.Shader = drawingPaint.Shader.WithUpdatedUniforms(builder.Uniforms);
         }
 
-        targetSurface.Canvas.DrawRect(0, 0, size.X, size.Y, drawingPaint);
+        targetSurface.Canvas.DrawRect(0, 0, size.Value.X, size.Value.Y, drawingPaint);
         builder.Dispose();
     }
 
@@ -113,17 +119,17 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         {
             return startNode.GetPreviewBounds(frame, elementToRenderName);
         }
-        
+
         return null;
     }
 
     public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         var startNode = FindStartNode();
-        if (drawingPaint != null && startNode != null && startNode.Image.Value != null)
+        if (drawingPaint != null && startNode is { Image.Value: not null })
         {
             renderOn.Canvas.DrawRect(0, 0, startNode.Image.Value.Size.X, startNode.Image.Value.Size.Y, drawingPaint);
-            
+
             return true;
         }
 

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

@@ -27,7 +27,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     public IReadOnlyList<InputProperty> InputProperties => inputs;
     public IReadOnlyList<OutputProperty> OutputProperties => outputs;
     public IReadOnlyList<KeyFrameData> KeyFrames => keyFrames;
-
+    public event Action ConnectionsChanged;
 
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
@@ -44,9 +44,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected internal bool IsDisposed => _isDisposed;
     private bool _isDisposed;
-
-    private Dictionary<int, Texture> _managedTextures = new();
-
+    
     public void Execute(RenderContext context)
     {
         ExecuteInternal(context);
@@ -84,43 +82,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
     }
 
-    protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
-    {
-        if (_managedTextures.TryGetValue(id, out var texture))
-        {
-            if (texture.Size != size || texture.IsDisposed || texture.ColorSpace != processingCs)
-            {
-                texture.Dispose();
-                texture = new Texture(CreateImageInfo(size, processingCs));
-                _managedTextures[id] = texture;
-                return texture;
-            }
-
-            if (clear)
-            {
-                texture.DrawingSurface.Canvas.Clear(Colors.Transparent);
-            }
-
-            return texture;
-        }
-
-        _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, processingCs) { GpuBacked = true };
-    }
-
     public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action)
     {
         var visited = new HashSet<IReadOnlyNode>();
@@ -309,6 +270,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             throw new InvalidOperationException($"Input with name {propName} already exists.");
         }
 
+        property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
         return property;
     }
@@ -321,6 +283,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             throw new InvalidOperationException($"Input with name {propName} already exists.");
         }
 
+        property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
         return property;
     }
@@ -352,6 +315,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             throw new InvalidOperationException($"Input with name {property.InternalPropertyName} already exists.");
         }
 
+        property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
     }
 
@@ -384,11 +348,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 keyFrame.Dispose();
             }
         }
-
-        foreach (var texture in _managedTextures)
-        {
-            texture.Value.Dispose();
-        }
     }
 
     public void DisconnectAll()
@@ -502,6 +461,11 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
         return new None();
     }
+    
+    private void InvokeConnectionsChanged()
+    {
+        ConnectionsChanged?.Invoke();
+    }
 
     private static object CloneValue(object? value, InputProperty? input)
     {

+ 14 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs

@@ -10,11 +10,13 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [NodeInfo("Output")]
 public class OutputNode : Node, IRenderInput, IPreviewRenderable
 {
+    public const string UniqueName = "PixiEditor.Output";
     public const string InputPropertyName = "Background";
 
-    public RenderInputProperty Input { get; } 
-    
+    public RenderInputProperty Input { get; }
+
     private VecI? lastDocumentSize;
+
     public OutputNode()
     {
         Input = new RenderInputProperty(this, InputPropertyName, "BACKGROUND", null);
@@ -28,26 +30,27 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
 
     protected override void OnExecute(RenderContext context)
     {
-        if(!string.IsNullOrEmpty(context.TargetOutput)) return;
-        
+        if (!string.IsNullOrEmpty(context.TargetOutput)) return;
+
         lastDocumentSize = context.DocumentSize;
-        
+
         int saved = context.RenderSurface.Canvas.Save();
         context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.DocumentSize.X, context.DocumentSize.Y));
         Input.Value?.Paint(context, context.RenderSurface);
-        
+
         context.RenderSurface.Canvas.RestoreToCount(saved);
     }
 
     RenderInputProperty IRenderInput.Background => Input;
+
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
         if (lastDocumentSize == null)
         {
             return null;
         }
-        
-        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y); 
+
+        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y);
     }
 
     public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
@@ -56,12 +59,12 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
         {
             return false;
         }
-        
+
         int saved = renderOn.Canvas.Save();
         Input.Value.Paint(context, renderOn);
-        
+
         renderOn.Canvas.RestoreToCount(saved);
-        
+
         return true;
     }
 }

+ 23 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -1,7 +1,9 @@
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
@@ -12,6 +14,8 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
 
     public bool AllowHighDpiRendering { get; set; } = false;
 
+    private TextureCache textureCache = new();
+
     public RenderNode()
     {
         Painter painter = new Painter(Paint);
@@ -30,22 +34,22 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
             }
         }
     }
-    
+
     private void Paint(RenderContext context, DrawingSurface surface)
     {
         DrawingSurface target = surface;
-        bool useIntermediate = !AllowHighDpiRendering 
-                               && context.DocumentSize is { X: > 0, Y: > 0 } 
+        bool useIntermediate = !AllowHighDpiRendering
+                               && context.DocumentSize is { X: > 0, Y: > 0 }
                                && surface.DeviceClipBounds.Size != context.DocumentSize;
         if (useIntermediate)
         {
-            Texture intermediate = RequestTexture(0, context.DocumentSize, context.ProcessingColorSpace);
+            Texture intermediate = textureCache.RequestTexture(0, context.DocumentSize, context.ProcessingColorSpace);
             target = intermediate.DrawingSurface;
         }
 
         OnPaint(context, target);
-        
-        if(useIntermediate)
+
+        if (useIntermediate)
         {
             surface.Canvas.DrawSurface(target, 0, 0);
         }
@@ -57,4 +61,17 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
 
     public abstract bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName);
+
+    protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
+    {
+        return textureCache.RequestTexture(id, size, processingCs, clear);
+    }
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        textureCache.Dispose(); 
+    }
+
+   
 }

+ 37 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TextureCache.cs

@@ -1,29 +1,56 @@
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-public class TextureCache
+public class TextureCache : IDisposable
 {
-    private Dictionary<ChunkResolution, Texture> _cachedTextures = new();
-    
-    public Texture GetTexture(ChunkResolution resolution, VecI size)
+    private Dictionary<int, Texture> _managedTextures = new();
+
+    public Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
     {
-        if (_cachedTextures.TryGetValue(resolution, out var texture) && texture.Size == size)
+        if (_managedTextures.TryGetValue(id, out var texture))
         {
+            if (texture.Size != size || texture.IsDisposed || texture.ColorSpace != processingCs)
+            {
+                texture.Dispose();
+                texture = new Texture(CreateImageInfo(size, processingCs));
+                _managedTextures[id] = texture;
+                return texture;
+            }
+
+            if (clear)
+            {
+                texture.DrawingSurface.Canvas.Clear(Colors.Transparent);
+            }
+
             return texture;
         }
 
-        texture = new Texture(size);
-        _cachedTextures[resolution] = texture;
-        return texture;
+        _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, processingCs) { GpuBacked = true };
     }
 
     public void Dispose()
     {
-        foreach (var texture in _cachedTextures.Values)
+        foreach (var texture in _managedTextures)
         {
-            texture.Dispose();
+            texture.Value.Dispose();
         }
     }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Animation/KeyFramesStartPos_UpdateableChange.cs

@@ -3,7 +3,7 @@ using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 
 namespace PixiEditor.ChangeableDocument.Changes.Animation;
 
-internal class KeyFramesStartPos_UpdateableChange : UpdateableChange
+internal class KeyFramesStartPos_UpdateableChange : InterruptableUpdateableChange
 {
     public Guid[] KeyFramesGuid { get;  }
     public int Delta { get; set; }

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

+ 45 - 15
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -63,7 +63,18 @@ public static class FloodFillHelper
             static (EmptyChunk _) => Colors.Transparent
         );
 
-        if ((drawingColor.A == 0) || colorToReplace == drawingColor)
+        ulong uLongColor = drawingColor.ToULong();
+        Color colorSpaceCorrectedColor = drawingColor;
+        if (!document.ProcessingColorSpace.IsSrgb)
+        {
+            var srgbTransform = ColorSpace.CreateSrgb().GetTransformFunction();
+
+            var fixedColor = drawingColor.TransformColor(srgbTransform);
+            uLongColor = fixedColor.ToULong();
+            colorSpaceCorrectedColor = fixedColor;
+        }
+
+        if ((colorSpaceCorrectedColor.A == 0) || colorToReplace == colorSpaceCorrectedColor)
             return new();
 
         RectI globalSelectionBounds = (RectI?)selection?.TightBounds ?? new RectI(VecI.Zero, document.Size);
@@ -71,17 +82,6 @@ public static class FloodFillHelper
         // Pre-multiplies the color and convert it to floats. Since floats are imprecise, a range is used.
         // Used for faster pixel checking
         ColorBounds colorRange = new(colorToReplace, tolerance);
-        ulong uLongColor = drawingColor.ToULong();
-        if (chunkAtPos.IsT0 && !chunkAtPos.AsT0.Surface.ImageInfo.ColorSpace.IsSrgb)
-        {
-            if (chunkAtPos.AsT0.Surface?.ImageInfo.ColorSpace != null)
-            {
-                var srgbTransform = ColorSpace.CreateSrgb().GetTransformFunction();
-
-                var fixedColor = drawingColor.TransformColor(srgbTransform);
-                uLongColor = fixedColor.ToULong();
-            }
-        }
 
         Dictionary<VecI, Chunk> drawingChunks = new();
         HashSet<VecI> processedEmptyChunks = new();
@@ -110,7 +110,24 @@ public static class FloodFillHelper
             {
                 if (colorToReplace.A == 0 && !processedEmptyChunks.Contains(chunkPos))
                 {
-                    drawingChunk.Surface.DrawingSurface.Canvas.Clear(drawingColor);
+                    int saved = drawingChunk.Surface.DrawingSurface.Canvas.Save();
+                    if (selection is not null && !selection.IsEmpty)
+                    {
+                        using VectorPath localSelection = new VectorPath(selection);
+                        localSelection.Transform(Matrix3X3.CreateTranslation(-chunkPos.X * chunkSize, -chunkPos.Y * chunkSize));
+                        
+                        drawingChunk.Surface.DrawingSurface.Canvas.ClipPath(localSelection);
+                        if (SelectionIntersectsChunk(selection, chunkPos, chunkSize))
+                        {
+                            drawingChunk.Surface.DrawingSurface.Canvas.Clear(drawingColor);
+                        }
+                    }
+                    else
+                    {
+                        drawingChunk.Surface.DrawingSurface.Canvas.Clear(drawingColor);
+                    }
+
+                    drawingChunk.Surface.DrawingSurface.Canvas.RestoreToCount(saved);
                     for (int i = 0; i < chunkSize; i++)
                     {
                         if (chunkPos.Y > 0)
@@ -139,7 +156,7 @@ public static class FloodFillHelper
                 chunkPos,
                 chunkSize,
                 uLongColor,
-                drawingColor,
+                colorSpaceCorrectedColor,
                 posOnChunk,
                 colorRange,
                 iter != 0);
@@ -175,10 +192,14 @@ public static class FloodFillHelper
         ColorBounds bounds,
         bool checkFirstPixel)
     {
+        // color should be a fixed color
         if (referenceChunk.Surface.GetRawPixel(pos) == color || drawingChunk.Surface.GetRawPixel(pos) == color)
             return null;
         if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
             return null;
+        
+        if(!SelectionIntersectsChunk(selection, chunkPos, chunkSize))
+            return null;
 
         byte[] pixelStates = new byte[chunkSize * chunkSize];
         DrawSelection(pixelStates, selection, globalSelectionBounds, chunkPos, chunkSize);
@@ -248,7 +269,7 @@ public static class FloodFillHelper
         RectI localBounds = globalBounds.Offset(-chunkPos * chunkSize).Intersect(new(0, 0, chunkSize, chunkSize));
         if (localBounds.IsZeroOrNegativeArea)
             return;
-        VectorPath shiftedSelection = new VectorPath(selection);
+        using VectorPath shiftedSelection = new VectorPath(selection);
         shiftedSelection.Transform(Matrix3X3.CreateTranslation(-chunkPos.X * chunkSize, -chunkPos.Y * chunkSize));
 
         fixed (byte* arr = array)
@@ -261,4 +282,13 @@ public static class FloodFillHelper
             drawingSurface.Canvas.Flush();
         }
     }
+    
+    private static bool SelectionIntersectsChunk(VectorPath selection, VecI chunkPos, int chunkSize)
+    {
+        if (selection is null || selection.IsEmpty)
+            return true;
+        
+        RectD chunkBounds = new(chunkPos * chunkSize, new VecI(chunkSize));
+        return selection.Bounds.IntersectsWithInclusive(chunkBounds);
+    }
 }

+ 13 - 8
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -24,6 +24,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     private readonly List<VecI> points = new();
     private int frame;
     private VecF lastPos;
+    private int lastAppliedPointIndex = -1;
 
     [GenerateUpdateableChangeActions]
     public LineBasedPen_UpdateableChange(Guid memberGuid, Color color, VecI pos, float strokeWidth, bool erasing,
@@ -59,7 +60,12 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     [UpdateChangeMethod]
     public void Update(VecI pos, float strokeWidth)
     {
-        points.Add(pos);
+        if (points.Count > 0)
+        {
+            var bresenham = BresenhamLineHelper.GetBresenhamLine(points[^1], pos);
+            points.AddRange(bresenham);
+        }
+        
         this.strokeWidth = strokeWidth;
     }
 
@@ -81,16 +87,13 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     {
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
 
-        var (from, to) = points.Count > 1 ? (points[^2], points[^1]) : (points[0], points[0]);
-
         int opCount = image.QueueLength;
 
-        var bresenham = BresenhamLineHelper.GetBresenhamLine(from, to);
-
         float spacingPixels = strokeWidth * spacing;
-
-        foreach (var point in bresenham)
+        
+        for(int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
         {
+            var point = points[i];
             if (points.Count > 1 && VecF.Distance(lastPos, point) < spacingPixels)
                 continue;
 
@@ -103,6 +106,8 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
             image.EnqueueDrawEllipse((RectD)rect, color, color, 0, 0, antiAliasing, srcPaint);
         }
+        
+        lastAppliedPointIndex = points.Count - 1;
 
         var affChunks = image.FindAffectedArea(opCount);
 
@@ -126,7 +131,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         VecF lastPos = points[0];
 
         float spacingInPixels = strokeWidth * this.spacing;
-
+        
         for (int i = 0; i < points.Count; i++)
         {
             if (i > 0 && VecF.Distance(lastPos, points[i]) < spacingInPixels)

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

@@ -73,8 +73,8 @@ internal class PixelPerfectPen_UpdateableChange : UpdateableChange
         (pixelsToConfirm2, pixelsToConfirm) = (pixelsToConfirm, pixelsToConfirm2);
         pixelsToConfirm.Clear();
 
-        VecF[] line = BresenhamLineHelper.GetBresenhamLine(incomingPoints[pointsCount - 2], incomingPoints[pointsCount - 1]);
-        foreach (VecF pixel in line)
+        VecI[] line = BresenhamLineHelper.GetBresenhamLine(incomingPoints[pointsCount - 2], incomingPoints[pointsCount - 1]);
+        foreach (VecI pixel in line)
         {
             pixelsToConfirm.Add(pixel);
         }

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

+ 47 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DuplicateNode_Change.cs

@@ -0,0 +1,47 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class DuplicateNode_Change : Change
+{
+    private Guid nodeGuid;
+    
+    private Guid createdNodeGuid;
+
+    [GenerateMakeChangeAction]
+    public DuplicateNode_Change(Guid nodeGuid, Guid newGuid)
+    {
+        this.nodeGuid = nodeGuid;
+        createdNodeGuid = newGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        return target.TryFindNode(nodeGuid, out Node node) && node.GetNodeTypeUniqueName() != OutputNode.UniqueName;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        Node existingNode = target.FindNode(nodeGuid);
+        Node clone = existingNode.Clone();
+        clone.Id = createdNodeGuid;
+
+        target.NodeGraph.AddNode(clone);
+
+        ignoreInUndo = false;
+
+        return CreateNode_ChangeInfo.CreateFromNode(clone);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var node = target.FindNode(createdNodeGuid);
+        target.NodeGraph.RemoveNode(node);
+        
+        node.Dispose();
+
+        return new DeleteNode_ChangeInfo(node.Id);
+    }
+}

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

@@ -94,7 +94,7 @@ public static class NodeOperations
 
         changes.Add(new ConnectProperty_ChangeInfo(memberId, parentInput.Node.Id,
             toAddOutput.InternalPropertyName, parentInput.InternalPropertyName));
-
+        
         return changes;
     }
 
@@ -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();
+    }
+}

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs

@@ -1,5 +1,6 @@
 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;
 
@@ -47,7 +48,7 @@ internal class DuplicateLayer_Change : Change
         target.NodeGraph.AddNode(clone);
 
         operations.Add(CreateLayer_ChangeInfo.FromLayer(clone));
-
+        
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
 
         ignoreInUndo = false;

+ 63 - 1
src/PixiEditor.ChangeableDocument/Enums/MathNodeMode.cs

@@ -18,9 +18,71 @@ public enum MathNodeMode
     Cos,
     [Description("TAN")]
     Tan,
+    [Description("GREATER_THAN")]
+    GreaterThan,
+    [Description("GREATER_THAN_OR_EQUAL")]
+    GreaterThanOrEqual,
+    [Description("LESS_THAN")]
+    LessThan,
+    [Description("LESS_THAN_OR_EQUAL")]
+    LessThanOrEqual,
+    [Description("COMPARE")]
+    Compare,
+    [Description("MATH_POWER")]
+    Power,
+    [Description("LOGARITHM")]
+    Logarithm,
+    [Description("NATURAL_LOGARITHM")]
+    NaturalLogarithm,
+    [Description("ROOT")]
+    Root,
+    [Description("INVERSE_ROOT")]
+    InverseRoot,
+    [Description("FRACTION")]
+    Fraction,
+    [Description("ABSOLUTE")]
+    Absolute,
+    [Description("NEGATE")]
+    Negate,
+    [Description("FLOOR")]
+    Floor,
+    [Description("CEIL")]
+    Ceil,
+    [Description("ROUND")]
+    Round,
+    [Description("MODULO")]
+    Modulo,
+    [Description("MIN")]
+    Min,
+    [Description("MAX")]
+    Max,
+    [Description("STEP")]
+    Step,
+    [Description("SMOOTH_STEP")]
+    SmoothStep,
 }
 
 public static class MathNodeModeExtensions
 {
-    public static bool UsesYValue(this MathNodeMode mode) => !(mode is >= MathNodeMode.Sin and <= MathNodeMode.Tan);
+    public static bool UsesYValue(this MathNodeMode mode) =>
+        mode != MathNodeMode.Sin &&
+        mode != MathNodeMode.Cos &&
+        mode != MathNodeMode.Tan &&
+        mode != MathNodeMode.Fraction &&
+        mode != MathNodeMode.Absolute &&
+        mode != MathNodeMode.Negate &&
+        mode != MathNodeMode.Floor &&
+        mode != MathNodeMode.Ceil &&
+        mode != MathNodeMode.Round &&
+        mode != MathNodeMode.NaturalLogarithm;
+
+
+    public static bool UsesZValue(this MathNodeMode mode) =>
+        mode is MathNodeMode.Compare or MathNodeMode.SmoothStep;
+
+    public static (string x, string y, string z) GetNaming(this MathNodeMode mode) => mode switch
+    {
+        MathNodeMode.Compare => ("VALUE", "TARGET", "EPSILON"),
+        _ => ("X", "Y", "Z")
+    };
 }

+ 76 - 26
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -5,6 +5,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 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;
@@ -20,7 +21,7 @@ public class DocumentRenderer : IPreviewRenderable
     };
 
     private Texture renderTexture;
-    
+
     public DocumentRenderer(IReadOnlyDocument document)
     {
         Document = document;
@@ -46,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);
 
@@ -78,24 +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;
+
         previewRenderable.RenderPreview(renderOn, context, elementToRenderName);
-        
+
         IsBusy = false;
     }
 
@@ -129,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;
             }
@@ -141,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;
             }
@@ -177,44 +217,54 @@ public class DocumentRenderer : IPreviewRenderable
         return true;
     }
 
-    public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime)
+    public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime, VecI renderSize)
     {
         IsBusy = true;
 
-        if (renderTexture == null || renderTexture.Size != Document.Size)
+        if (renderTexture == null || renderTexture.Size != renderSize)
         {
             renderTexture?.Dispose();
-            renderTexture = Texture.ForProcessing(Document.Size, Document.ProcessingColorSpace);
+            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, 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;
@@ -222,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;
     }
 }

+ 10 - 0
src/PixiEditor.Common/MathEx.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.Common;
+
+public static class MathEx
+{
+    public static double SmoothStep(double edge0, double edge1, double x)
+    {
+        x = Math.Clamp((x - edge0) / (edge1 - edge0), 0, 1);
+        return x * x * (3 - 2 * x);
+    }
+}

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

+ 2 - 1
src/PixiEditor.UI.Common/Controls/ComboBox.axaml

@@ -33,7 +33,8 @@
             <ContentControl Margin="{TemplateBinding Padding}"
                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                             VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
-                            Content="{TemplateBinding SelectionBoxItem}"
+                            Content="{Binding SelectionBoxItem,
+                                              RelativeSource={RelativeSource TemplatedParent}}"
                             ContentTemplate="{TemplateBinding ItemTemplate}" />
             <ToggleButton Name="toggle"
                           Grid.Column="1"

+ 1 - 1
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml

@@ -120,7 +120,7 @@
             <system:String x:Key="icon-paste-as-new-layer">&#xe978;</system:String>
             <system:String x:Key="icon-star">&#xe97c;</system:String>
             <system:String x:Key="icon-star-filled">&#xe979;</system:String>
-            <system:String x:Key="icon-reset">R</system:String>
+            <system:String x:Key="icon-reset">&#xE98A;</system:String>
             <system:String x:Key="icon-message">&#xE96F;</system:String>
             <system:String x:Key="icon-download">&#xE969;</system:String>
             <system:String x:Key="icon-youtube">&#xE975;</system:String>

+ 0 - 31
src/PixiEditor.sln

@@ -124,8 +124,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.RenderApi", "Drawie\
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.RenderApi.Vulkan", "Drawie\src\Drawie.RenderApi.Vulkan\Drawie.RenderApi.Vulkan.csproj", "{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpirvCompiler", "Drawie\src\SpirvCompiler\SpirvCompiler.csproj", "{475C7BBF-B10B-456A-A095-4395E49CF4B2}"
-EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Interop.Avalonia", "Drawie\src\Drawie.Interop.Avalonia\Drawie.Interop.Avalonia.csproj", "{6D79C3E3-E31F-43B4-B173-3E6959230923}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Interop.Avalonia.OpenGl", "Drawie\src\Drawie.Interop.Avalonia.OpenGl\Drawie.Interop.Avalonia.OpenGl.csproj", "{843F55B4-987B-45A9-BDBD-1A0A86CB883E}"
@@ -1075,34 +1073,6 @@ Global
 		{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2}.Steam|x64.Build.0 = Debug|Any CPU
 		{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2}.Steam|ARM64.ActiveCfg = Debug|Any CPU
 		{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2}.Steam|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Debug|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Debug|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Debug|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Debug|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevRelease|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevRelease|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevRelease|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevSteam|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevSteam|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.DevSteam|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX Debug|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.MSIX|ARM64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Release|x64.ActiveCfg = Release|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Release|x64.Build.0 = Release|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Release|ARM64.ActiveCfg = Release|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Release|ARM64.Build.0 = Release|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Steam|x64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Steam|x64.Build.0 = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Steam|ARM64.ActiveCfg = Debug|Any CPU
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2}.Steam|ARM64.Build.0 = Debug|Any CPU
 		{6D79C3E3-E31F-43B4-B173-3E6959230923}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{6D79C3E3-E31F-43B4-B173-3E6959230923}.Debug|x64.Build.0 = Debug|Any CPU
 		{6D79C3E3-E31F-43B4-B173-3E6959230923}.Debug|ARM64.ActiveCfg = Debug|Any CPU
@@ -1327,7 +1297,6 @@ Global
 		{467EDEB3-6004-46B8-8448-2F5C4F131D75} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{21152CEE-F9D7-452F-9FF5-B15FF80F2CED} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{1346AE27-CCC8-4BC6-A52D-05B6A657ACD2} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
-		{475C7BBF-B10B-456A-A095-4395E49CF4B2} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{6D79C3E3-E31F-43B4-B173-3E6959230923} = {E8A74431-F76F-43B1-BC66-CA05E249E6F4}
 		{843F55B4-987B-45A9-BDBD-1A0A86CB883E} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}
 		{7BD495CA-2EB5-4ABC-BDDB-0E1765C40C19} = {03CFB32D-E797-41B1-B072-A4FEBA5F8813}

+ 34 - 3
src/PixiEditor/Data/Localization/Languages/en.json

@@ -260,7 +260,7 @@
   "OPEN_STARTUP_WINDOW": "Open startup window",
   "OPEN_SHORTCUT_WINDOW": "Open shortcuts window",
   "OPEN_ABOUT_WINDOW": "Open about window",
-  "OPEN_NAVIGATION_WINDOW": "Open navigation window",
+  "OPEN_PREVIEW_WINDOW": "Open preview window",
   "ERROR": "Error",
   "INTERNAL_ERROR": "Internal error",
   "ERROR_SAVE_LOCATION": "Couldn't save the file to the specified location",
@@ -381,7 +381,7 @@
   "PALETTE_TITLE": "Palette",
   "SWATCHES_TITLE": "Swatches",
   "LAYERS_TITLE": "Layers",
-  "NAVIGATION_TITLE": "Navigation",
+  "PREVIEW_TITLE": "Preview",
   "NORMAL_BLEND_MODE": "Normal",
   "ERASE_BLEND_MODE": "Erase",
   "DARKEN_BLEND_MODE": "Darken",
@@ -743,6 +743,23 @@
   "SIN": "Sin",
   "COS": "Cos",
   "TAN": "Tan",
+  "GREATER_THAN": "Greater than",
+  "LESS_THAN": "Less than",
+  "LESS_THAN_OR_EQUAL": "Less than or equal",
+  "COMPARE": "Compare",
+  "MATH_POWER": "Power",
+  "LOGARITHM": "Logarithm",
+  "NATURAL_LOGARITHM": "Natural logarithm",
+  "ROOT": "Root",
+  "INVERSE_ROOT": "Inverse root",
+  "FRACTION": "Fraction",
+  "NEGATE": "Negate",
+  "FLOOR": "Floor",
+  "CEIL": "Ceil",
+  "ROUND": "Round",
+  "MODULO": "Modulo",
+  "STEP": "Step",
+  "SMOOTH_STEP": "Smoothstep",
   "PIXEL_ART_TOOLSET": "Pixel Art",
   "VECTOR_TOOLSET": "Vector",
   "VECTOR_LAYER": "Vector Layer",
@@ -803,5 +820,19 @@
   "TOGGLE_HUD": "Toggle HUD",
   "OPEN_TIMELINE": "Open timeline",
   "OPEN_NODE_GRAPH": "Open node graph",
-  "TOGGLE_PLAY": "Play/Pause animation"
+  "TOGGLE_PLAY": "Play/Pause animation",
+  "COPY_NODES": "Copy nodes",
+  "COPY_NODES_DESCRIPTIVE": "Copy selected nodes",
+  "PASTE_NODES": "Paste nodes",
+  "PASTE_NODES_DESCRIPTIVE": "Paste copied nodes",
+  "COPY_CELS": "Copy cels",
+  "COPY_CELS_DESCRIPTIVE": "Copy selected cels",
+  "TOGGLE_ONION_SKINNING_DESCRIPTIVE": "Toggle onion skinning",
+  "VALUE": "Value",
+  "TARGET": "Target",
+  "EPSILON": "Epsilon",
+  "PRESERVE_ALPHA": "Preserve alpha",
+  "BLUR_FILTER_NODE": "Gaussian Blur",
+  "LENGTH": "Length",
+  "GREATER_THAN_OR_EQUAL": "Greater than or equal"
 }

+ 2 - 2
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
@@ -436,7 +436,7 @@
       "Parameters": []
     },
     "TogglePreview": {
-      "Command": "PixiEditor.Window.OpenNavigationWindow",
+      "Command": "PixiEditor.Window.OpenPreviewWindow",
       "DefaultShortcut": {
         "key": "F7",
         "modifiers": null

+ 3 - 1
src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs

@@ -6,5 +6,7 @@ public static class ClipboardDataFormats
     public const string LayerIdList = "PixiEditor.LayerIdList";
     public const string PositionFormat = "PixiEditor.Position";
     public const string ImageSlashPng = "image/png";
-    public const string DocumentFormat = "PixiEditor.Document"; 
+    public const string DocumentFormat = "PixiEditor.Document";
+    public const string NodeIdList = "PixiEditor.NodeIdList";
+    public const string CelIdList = "PixiEditor.CelIdList";
 }

+ 2 - 2
src/PixiEditor/Helpers/Extensions/MethodExtension.cs

@@ -9,13 +9,13 @@ public static class MethodExtension
     public static async Task<T> InvokeAsync<T>(this MethodInfo @this, object obj, params object[] parameters)
     {
         //TODO: uhh, make sure this is ok?
-        Dispatcher.UIThread.InvokeAsync(async () => await Task.Run(async () =>
+        Dispatcher.UIThread.InvokeAsync(async () =>
         {
             var task = (Task)@this.Invoke(obj, parameters);
             await task.ConfigureAwait(false);
             var resultProperty = task.GetType().GetProperty("Result");
             return (T)resultProperty.GetValue(task);
-        }));
+        });
 
         return default;
     }

+ 0 - 14
src/PixiEditor/Helpers/MathUtil.cs

@@ -1,14 +0,0 @@
-namespace PixiEditor.Helpers;
-
-public static class MathUtil
-{
-    public static double DegreesToRadians(double angle)
-    {
-        return angle * Math.PI / 180;
-    }
-
-    public static double RadiansToDegrees(double angle)
-    {
-        return angle * 180 / Math.PI;
-    }
-}

+ 2 - 2
src/PixiEditor/Helpers/VersionHelpers.cs

@@ -42,10 +42,10 @@ internal static class VersionHelpers
         return "BetaDebug";
 #elif DEVRELEASE
         return "BetaDevRelease";
-#elif RELEASE
-        return "BetaRelease";
 #elif STEAM
         return "BetaSteam";
+#elif RELEASE
+        return "BetaRelease";
 #elif MSIX
         return "BetaMSIX";
 #else

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -17,7 +17,7 @@ internal partial class Command
 
         public string CanExecute { get; set; }
         
-        public Type? ShortcutContext { get; set; }
+        public Type[]? ShortcutContexts { get; set; }
 
         /// <summary>
         /// Gets or sets the default shortcut key for this command

+ 1 - 1
src/PixiEditor/Models/Commands/CommandController.cs

@@ -260,7 +260,7 @@ internal class CommandController
                                 Parameter = basic.Parameter,
                                 MenuItemPath = basic.MenuItemPath,
                                 MenuItemOrder = basic.MenuItemOrder,
-                                ShortcutContext = basic.ShortcutContext
+                                ShortcutContexts = basic.ShortcutContexts
                             });
                     }
                     else if (attribute is Attributes.Commands.Command.FilterAttribute menu)

+ 1 - 1
src/PixiEditor/Models/Commands/Commands/Command.cs

@@ -42,7 +42,7 @@ internal abstract partial class Command : PixiObservableObject
         }
     }
     
-    public Type? ShortcutContext { get; init; }
+    public Type[]? ShortcutContexts { get; init; }
 
     public string? MenuItemPath { get; init; }
 

+ 4 - 2
src/PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs

@@ -16,8 +16,10 @@ internal class IconEvaluator : Evaluator<IImage>
 {
     public static IconEvaluator Default { get; } = new FontIconEvaluator();
 
-    public override IImage? CallEvaluate(Command command, object parameter) =>
-        base.CallEvaluate(command, parameter is CommandSearchResult or Command ? parameter : command);
+    public override IImage? CallEvaluate(Command command, object parameter)
+    {
+        return base.CallEvaluate(command, parameter is CommandSearchResult or Command ? parameter : command);
+    }
 
     [DebuggerDisplay("IconEvaluator.Default")]
     private class FontIconEvaluator : IconEvaluator

+ 4 - 1
src/PixiEditor/Models/Commands/Search/CommandSearchResult.cs

@@ -17,7 +17,10 @@ internal class CommandSearchResult : SearchResult
 
     public override KeyCombination Shortcut => Command.Shortcut;
 
-    public CommandSearchResult(Command command) => Command = command;
+    public CommandSearchResult(Command command)
+    {
+        Command = command;
+    }
 
     public override void Execute()
     {

+ 1 - 1
src/PixiEditor/Models/Commands/Search/FileSearchResult.cs

@@ -30,7 +30,7 @@ internal class FileSearchResult : SearchResult
     {
         FilePath = path;
         var drawing = new GeometryDrawing() { Brush = FileExtensionToColorConverter.GetBrush(FilePath) };
-        var geometry = new RectangleGeometry(new Rect(0, 0, 10, 10)) { }; // TODO: Avalonia 11.1 introduces rounded rectangle geometry, let's make this round again then 
+        var geometry = new RectangleGeometry(new Rect(0, 0, 10, 10)) { RadiusX = 2, RadiusY = 2 };
         drawing.Geometry = geometry;
         icon = new DrawingImage(drawing);
         this.asReferenceLayer = asReferenceLayer;

+ 92 - 17
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -27,6 +27,7 @@ using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers;
 using PixiEditor.Parser;
 using PixiEditor.ViewModels.Document;
 using Bitmap = Avalonia.Media.Imaging.Bitmap;
@@ -126,7 +127,7 @@ internal static class ClipboardController
 
         await Clipboard.SetDataObjectAsync(data);
     }
-    
+
     public static async Task CopyVisibleToClipboard(DocumentViewModel document)
     {
         await Clipboard.ClearAsync();
@@ -134,7 +135,7 @@ internal static class ClipboardController
         DataObject data = new DataObject();
 
         RectD copyArea = new RectD(VecD.Zero, document.SizeBindable);
-        
+
         if (!document.SelectionPathBindable.IsEmpty)
         {
             copyArea = document.SelectionPathBindable.TightBounds;
@@ -145,15 +146,16 @@ internal static class ClipboardController
         }
 
         using Surface documentSurface = new Surface(document.SizeBindable);
-        
-        document.Renderer.RenderDocument(documentSurface.DrawingSurface, document.AnimationDataViewModel.ActiveFrameTime);
-        
+
+        document.Renderer.RenderDocument(documentSurface.DrawingSurface,
+            document.AnimationDataViewModel.ActiveFrameTime, document.SizeBindable);
+
         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);
+            documentSurface.DrawingSurface.Snapshot(),
+            copyArea, new RectD(0, 0, copyArea.Size.X, copyArea.Size.Y), paint);
 
         await AddImageToClipboard(surfaceToCopy, data);
 
@@ -184,9 +186,9 @@ internal static class ClipboardController
     /// </summary>
     public static bool TryPaste(DocumentViewModel document, IEnumerable<IDataObject> data, bool pasteAsNew = false)
     {
-        Guid sourceDocument = GetSourceDocument(data); 
+        Guid sourceDocument = GetSourceDocument(data);
         Guid[] layerIds = GetLayerIds(data);
-        
+
         if (sourceDocument != document.Id)
         {
             layerIds = [];
@@ -194,11 +196,11 @@ internal static class ClipboardController
 
         bool hasPos = data.Any(x => x.Contains(ClipboardDataFormats.PositionFormat));
 
-        if (layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, document)))
+        if (pasteAsNew && layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, document)))
         {
             foreach (var layerId in layerIds)
             {
-                document.Operations.DuplicateLayer(layerId);
+                document.Operations.DuplicateMember(layerId);
             }
 
             return true;
@@ -242,11 +244,11 @@ internal static class ClipboardController
         document.Operations.PasteImagesAsLayers(images, document.AnimationDataViewModel.ActiveFrameBindable);
         return true;
     }
-    
+
     private static bool AllMatchesPos(Guid[] layerIds, IEnumerable<IDataObject> data, DocumentViewModel doc)
     {
         var dataObjects = data as IDataObject[] ?? data.ToArray();
-        
+
         var dataObjectWithPos = dataObjects.FirstOrDefault(x => x.Contains(ClipboardDataFormats.PositionFormat));
         VecD pos = VecD.Zero;
 
@@ -254,7 +256,7 @@ internal static class ClipboardController
         {
             pos = dataObjectWithPos.GetVecD(ClipboardDataFormats.PositionFormat);
         }
-        
+
         for (var i = 0; i < layerIds.Length; i++)
         {
             var layerId = layerIds[i];
@@ -263,7 +265,7 @@ internal static class ClipboardController
             if (layer is not { TightBounds: not null } || !layer.TightBounds.Value.Pos.AlmostEquals(pos))
                 return false;
         }
-        
+
         return true;
     }
 
@@ -281,7 +283,7 @@ internal static class ClipboardController
 
         return [];
     }
-    
+
     private static Guid GetSourceDocument(IEnumerable<IDataObject> data)
     {
         foreach (var dataObject in data)
@@ -569,4 +571,77 @@ internal static class ClipboardController
         result = null;
         return false;
     }
+
+    public static async Task CopyNodes(Guid[] nodeIds)
+    {
+        await CopyIds(nodeIds, ClipboardDataFormats.NodeIdList);
+    }
+
+    public static async Task<Guid[]> GetNodeIds()
+    {
+        return await GetIds(ClipboardDataFormats.NodeIdList);
+    }
+
+    public static async Task<Guid[]> GetCelIds()
+    {
+        return await GetIds(ClipboardDataFormats.CelIdList);
+    }
+    
+    public static async Task<Guid[]> GetIds(string format)
+    {
+        var data = await TryGetDataObject();
+        return GetIds(data, format);
+    }
+
+    private static Guid[] GetIds(IEnumerable<IDataObject?> data, string format)
+    {
+        foreach (var dataObject in data)
+        {
+            if (dataObject.Contains(format))
+            {
+                byte[] nodeIds = (byte[])dataObject.Get(format);
+                string nodeIdsString = System.Text.Encoding.UTF8.GetString(nodeIds);
+                return nodeIdsString.Split(';').Select(Guid.Parse).ToArray();
+            }
+        }
+
+        return [];
+    }
+
+    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(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();
+
+        DataObject data = new DataObject();
+
+        byte[] idsBytes = Encoding.UTF8.GetBytes(string.Join(";", ids.Select(x => x.ToString())));
+
+        data.Set(format, idsBytes);
+
+        await Clipboard.SetDataObjectAsync(data);
+    }
 }

+ 1 - 1
src/PixiEditor/Models/Controllers/ShortcutController.cs

@@ -57,7 +57,7 @@ internal class ShortcutController
             return;
         }
 
-        var commands = CommandController.Current.Commands[shortcut].Where(x => x.ShortcutContext is null || x.ShortcutContext == ActiveContext).ToList();
+        var commands = CommandController.Current.Commands[shortcut].Where(x => x.ShortcutContexts is null || x.ShortcutContexts.Contains(ActiveContext)).ToList();
 
         if (!commands.Any())
         {

+ 31 - 5
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -23,6 +23,8 @@ internal class ActionAccumulator
     private CanvasUpdater canvasUpdater;
     private MemberPreviewUpdater previewUpdater;
 
+    private bool isChangeBlockActive = false;
+
     public ActionAccumulator(IDocument doc, DocumentInternalParts internals)
     {
         this.document = doc;
@@ -32,6 +34,21 @@ internal class ActionAccumulator
         previewUpdater = new(doc, internals);
     }
 
+    public void StartChangeBlock()
+    {
+        if (isChangeBlockActive)
+            throw new InvalidOperationException("Change block is already active");
+
+        isChangeBlockActive = true;
+    }
+
+    public void EndChangeBlock()
+    {
+        isChangeBlockActive = false;
+        queuedActions.Add((ActionSource.Automated, new ChangeBoundary_Action()));
+        TryExecuteAccumulatedActions();
+    }
+
     public void AddFinishedActions(params IAction[] actions)
     {
         foreach (var action in actions)
@@ -39,8 +56,11 @@ internal class ActionAccumulator
             queuedActions.Add((ActionSource.User, action));
         }
 
-        queuedActions.Add((ActionSource.Automated, new ChangeBoundary_Action()));
-        TryExecuteAccumulatedActions();
+        if (!isChangeBlockActive)
+        {
+            queuedActions.Add((ActionSource.Automated, new ChangeBoundary_Action()));
+            TryExecuteAccumulatedActions();
+        }
     }
 
     public void AddActions(params IAction[] actions)
@@ -50,7 +70,10 @@ internal class ActionAccumulator
             queuedActions.Add((ActionSource.User, action));
         }
 
-        TryExecuteAccumulatedActions();
+        if (!isChangeBlockActive)
+        {
+            TryExecuteAccumulatedActions();
+        }
     }
 
     public void AddActions(ActionSource source, IAction action)
@@ -59,7 +82,7 @@ internal class ActionAccumulator
         TryExecuteAccumulatedActions();
     }
 
-    private async void TryExecuteAccumulatedActions()
+    internal async Task TryExecuteAccumulatedActions()
     {
         if (executing || queuedActions.Count == 0)
             return;
@@ -92,6 +115,8 @@ internal class ActionAccumulator
                 toExecute.Any(static action => action.action is ChangeBoundary_Action or Redo_Action or Undo_Action);
             bool viewportRefreshRequest =
                 toExecute.Any(static action => action.action is RefreshViewport_PassthroughAction);
+            bool changeFrameRequest =
+                toExecute.Any(static action => action.action is SetActiveFrame_PassthroughAction);
             foreach (IChangeInfo info in optimizedChanges)
             {
                 internals.Updater.ApplyChangeFromChangeInfo(info);
@@ -114,7 +139,8 @@ internal class ActionAccumulator
                     undoBoundaryPassed || viewportRefreshRequest);
             }
 
-            previewUpdater.UpdatePreviews(undoBoundaryPassed, affectedAreas.ImagePreviewAreas.Keys, affectedAreas.MaskPreviewAreas.Keys,
+            previewUpdater.UpdatePreviews(undoBoundaryPassed || changeFrameRequest || viewportRefreshRequest, affectedAreas.ImagePreviewAreas.Keys,
+                affectedAreas.MaskPreviewAreas.Keys,
                 affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames);
 
             // force refresh viewports for better responsiveness

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

+ 22 - 0
src/PixiEditor/Models/DocumentModels/Public/ChangeBlock.cs

@@ -0,0 +1,22 @@
+namespace PixiEditor.Models.DocumentModels.Public;
+
+public class ChangeBlock : IDisposable
+{
+    private ActionAccumulator Accumulator { get; }
+    
+    internal ChangeBlock(ActionAccumulator accumulator)
+    {
+        Accumulator = accumulator;
+        Accumulator.StartChangeBlock();
+    }
+    
+    public async Task ExecuteQueuedActions()
+    {
+        await Accumulator.TryExecuteAccumulatedActions();
+    }
+    
+    public void Dispose()
+    {
+        Accumulator.EndChangeBlock();
+    }
+}

+ 74 - 6
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -1,4 +1,6 @@
-using System.Collections.Immutable;
+using System.Collections;
+using System.Collections.Immutable;
+using System.Reactive.Disposables;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Actions;
@@ -34,6 +36,11 @@ internal class DocumentOperationsModule : IDocumentOperations
         Internals = internals;
     }
 
+    public ChangeBlock StartChangeBlock()
+    {
+        return new ChangeBlock(Internals.ActionAccumulator);
+    }
+
     /// <summary>
     /// Creates a new selection with the size of the document
     /// </summary>
@@ -190,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>
@@ -446,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;
@@ -837,4 +855,54 @@ internal class DocumentOperationsModule : IDocumentOperations
         Internals.ActionAccumulator.AddFinishedActions(
             new ChangeProcessingColorSpace_Action(ColorSpace.CreateSrgb()));
     }
+
+    public Guid? DuplicateNode(Guid nodeId)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return null;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+
+        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());
+    }
 }

+ 7 - 2
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -26,6 +26,11 @@ internal class DocumentStructureModule
         return doc.NodeGraphHandler.AllNodes.FirstOrDefault(x => x.Id == guid && x is T) as T;
     }
 
+    public bool TryFindNode<T>(Guid guid, out T found) where T : class, INodeHandler
+    {
+        found = FindNode<T>(guid);
+        return found != null;
+    }
 
     public Guid FindClosestMember(IReadOnlyList<Guid> guids)
     {
@@ -127,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>();
 
@@ -140,7 +145,7 @@ internal class DocumentStructureModule
 
         return layers;
     }
-    
+
     public List<IStructureMemberHandler> GetAllMembers()
     {
         List<IStructureMemberHandler> members = new List<IStructureMemberHandler>();

+ 2 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -45,7 +45,8 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
         members = document.SoftSelectedStructureMembers
             .Append(document.SelectedStructureMember)
-            .Where(static m => m is ILayerHandler).ToList();
+            .Where(static m => m is ILayerHandler)
+            .Distinct().ToList();
 
         if (!members.Any())
             return ExecutionState.Error;

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

+ 9 - 0
src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -150,6 +150,7 @@ internal class AffectedAreasGatherer
                 case SetActiveFrame_PassthroughAction:
                     AddWholeCanvasToMainImage();
                     AddWholeCanvasToEveryImagePreview();
+                    AddAllNodesToImagePreviews();
                     break;
                 case KeyFrameLength_ChangeInfo:
                     AddWholeCanvasToMainImage();
@@ -210,6 +211,14 @@ internal class AffectedAreasGatherer
         if (!ChangedNodes.Contains(nodeId))
             ChangedNodes.Add(nodeId);
     }
+    
+    private void AddAllNodesToImagePreviews()
+    {
+        foreach (var node in tracker.Document.NodeGraph.AllNodes)
+        {
+            AddToNodePreviews(node.Id);
+        }
+    }
 
     private void AddAllToImagePreviews(Guid memberGuid, KeyFrameTime frame, bool ignoreSelf = false)
     {

+ 3 - 10
src/PixiEditor/Models/Rendering/CanvasUpdater.cs

@@ -22,13 +22,6 @@ internal class CanvasUpdater
     private int lastOnionKeyFrames = -1;
     private double lastOnionOpacity = -1;
 
-    private static readonly Paint ReplacingPaint = new() { BlendMode = BlendMode.Src };
-
-    private static readonly Paint ClearPaint = new()
-    {
-        BlendMode = BlendMode.Src, Color = Colors.Transparent
-    };
-
     /// <summary>
     /// Affected chunks that have not been rerendered yet.
     /// </summary>
@@ -65,7 +58,7 @@ internal class CanvasUpdater
     /// </summary>
     public async Task UpdateGatheredChunks
         (AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
-    { 
+    {
         await Task.Run(() => Render(chunkGatherer, rerenderDelayed)).ConfigureAwait(true);
     }
 
@@ -157,7 +150,7 @@ internal class CanvasUpdater
     {
         Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender =
             FindGlobalChunksToRerender(chunkGatherer, rerenderDelayed);
-        
+
         ChunkResolution onionSkinResolution = chunksToRerender.Min(x => x.Key);
 
         bool updatingStoredChunks = false;
@@ -199,7 +192,7 @@ internal class CanvasUpdater
             if (globalClippingRectangle is not null)
                 globalScaledClippingRectangle =
                     (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
-            
+
             foreach (var chunkPos in chunks)
             {
                 RenderChunk(chunkPos, resolution);

+ 65 - 17
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -37,7 +37,9 @@ internal class MemberPreviewUpdater
         IEnumerable<Guid> masksToUpdate, IEnumerable<Guid> nodesToUpdate, IEnumerable<Guid> keyFramesToUpdate)
     {
         if (!rerenderPreviews)
+        {
             return;
+        }
 
         UpdatePreviewPainters(membersToUpdate, masksToUpdate, nodesToUpdate, keyFramesToUpdate);
     }
@@ -58,7 +60,9 @@ internal class MemberPreviewUpdater
         RenderWholeCanvasPreview();
         RenderLayersPreview(memberGuids);
         RenderMaskPreviews(maskGuids);
+
         RenderAnimationPreviews(memberGuids, keyFramesGuids);
+
         RenderNodePreviews(nodesGuids);
     }
 
@@ -68,10 +72,17 @@ internal class MemberPreviewUpdater
     private void RenderWholeCanvasPreview()
     {
         var previewSize = StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size);
-        float scaling = (float)previewSize.X / doc.SizeBindable.X;
+        //float scaling = (float)previewSize.X / doc.SizeBindable.X;
 
-        doc.PreviewPainter = new PreviewPainter(doc.Renderer, doc.Renderer, doc.AnimationHandler.ActiveFrameTime,
-            doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
+        if (doc.PreviewPainter == null)
+        {
+            doc.PreviewPainter = new PreviewPainter(doc.Renderer, doc.Renderer, doc.AnimationHandler.ActiveFrameTime,
+                doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
+        }
+
+        doc.PreviewPainter.DocumentSize = doc.SizeBindable;
+        doc.PreviewPainter.ProcessingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
+        doc.PreviewPainter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
         doc.PreviewPainter.Repaint();
     }
 
@@ -102,7 +113,7 @@ internal class MemberPreviewUpdater
                     structureMemberHandler.PreviewPainter.DocumentSize = doc.SizeBindable;
                     structureMemberHandler.PreviewPainter.ProcessingColorSpace =
                         internals.Tracker.Document.ProcessingColorSpace;
-                    
+
                     structureMemberHandler.PreviewPainter.Repaint();
                 }
             }
@@ -145,8 +156,19 @@ internal class MemberPreviewUpdater
         if (internals.Tracker.Document.AnimationData.TryFindKeyFrame(cel.Id, out KeyFrame _))
         {
             KeyFrameTime frameTime = doc.AnimationHandler.ActiveFrameTime;
-            cel.PreviewPainter = new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, doc.SizeBindable,
-                internals.Tracker.Document.ProcessingColorSpace, cel.Id.ToString());
+            if (cel.PreviewPainter == null)
+            {
+                cel.PreviewPainter = new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime,
+                    doc.SizeBindable,
+                    internals.Tracker.Document.ProcessingColorSpace, cel.Id.ToString());
+            }
+            else
+            {
+                cel.PreviewPainter.FrameTime = frameTime;
+                cel.PreviewPainter.DocumentSize = doc.SizeBindable;
+                cel.PreviewPainter.ProcessingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
+            }
+
             cel.PreviewPainter.Repaint();
         }
     }
@@ -160,9 +182,20 @@ internal class MemberPreviewUpdater
             ColorSpace processingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
             VecI documentSize = doc.SizeBindable;
 
-            groupHandler.PreviewPainter =
-                new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, documentSize, processingColorSpace,
-                    groupHandler.Id.ToString());
+            if (groupHandler.PreviewPainter == null)
+            {
+                groupHandler.PreviewPainter =
+                    new PreviewPainter(doc.Renderer, AnimationKeyFramePreviewRenderer, frameTime, documentSize,
+                        processingColorSpace,
+                        groupHandler.Id.ToString());
+            }
+            else
+            {
+                groupHandler.PreviewPainter.FrameTime = frameTime;
+                groupHandler.PreviewPainter.DocumentSize = documentSize;
+                groupHandler.PreviewPainter.ProcessingColorSpace = processingColorSpace;
+            }
+
             groupHandler.PreviewPainter.Repaint();
         }
     }
@@ -180,13 +213,24 @@ internal class MemberPreviewUpdater
                 if (member is not IPreviewRenderable previewRenderable)
                     continue;
 
-                structureMemberHandler.MaskPreviewPainter = new PreviewPainter(
-                    doc.Renderer,
-                    previewRenderable,
-                    doc.AnimationHandler.ActiveFrameTime,
-                    doc.SizeBindable,
-                    internals.Tracker.Document.ProcessingColorSpace,
-                    nameof(StructureNode.EmbeddedMask));
+                if (structureMemberHandler.MaskPreviewPainter == null)
+                {
+                    structureMemberHandler.MaskPreviewPainter = new PreviewPainter(
+                        doc.Renderer,
+                        previewRenderable,
+                        doc.AnimationHandler.ActiveFrameTime,
+                        doc.SizeBindable,
+                        internals.Tracker.Document.ProcessingColorSpace,
+                        nameof(StructureNode.EmbeddedMask));
+                }
+                else
+                {
+                    structureMemberHandler.MaskPreviewPainter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
+                    structureMemberHandler.MaskPreviewPainter.DocumentSize = doc.SizeBindable;
+                    structureMemberHandler.MaskPreviewPainter.ProcessingColorSpace =
+                        internals.Tracker.Document.ProcessingColorSpace;
+                }
+
                 structureMemberHandler.MaskPreviewPainter.Repaint();
             }
         }
@@ -203,6 +247,9 @@ internal class MemberPreviewUpdater
             internals.Tracker.Document.NodeGraph
                 .AllNodes; //internals.Tracker.Document.NodeGraph.CalculateExecutionQueue(outputNode);
 
+        if (nodesGuids.Length == 0)
+            return;
+
         foreach (var node in executionQueue)
         {
             if (node is null)
@@ -222,7 +269,8 @@ internal class MemberPreviewUpdater
             {
                 if (nodeVm.ResultPainter == null)
                 {
-                    nodeVm.ResultPainter = new PreviewPainter(doc.Renderer, renderable, doc.AnimationHandler.ActiveFrameTime,
+                    nodeVm.ResultPainter = new PreviewPainter(doc.Renderer, renderable,
+                        doc.AnimationHandler.ActiveFrameTime,
                         doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
                     nodeVm.ResultPainter.Repaint();
                 }

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

@@ -33,7 +33,7 @@ public class PreviewPainter
         Renderer = renderer;
     }
 
-    public void Paint(DrawingSurface renderOn, VecI boundsSize, Matrix3X3 matrix) 
+    public void Paint(DrawingSurface renderOn, VecI boundsSize, Matrix3X3 matrix)
     {
         if (PreviewRenderable == null)
         {

+ 40 - 18
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
@@ -11,12 +12,13 @@ using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.Models.Rendering;
 
-internal class SceneRenderer
+internal class SceneRenderer 
 {
     public IReadOnlyDocument Document { get; }
     public IDocument DocumentViewModel { get; }
     public bool HighResRendering { get; set; } = true;
 
+
     public SceneRenderer(IReadOnlyDocument trackerDocument, IDocument documentViewModel)
     {
         Document = trackerDocument;
@@ -25,7 +27,7 @@ internal class SceneRenderer
 
     public void RenderScene(DrawingSurface target, ChunkResolution resolution, string? targetOutput = null)
     {
-        if(Document.Renderer.IsBusy || DocumentViewModel.Busy) return;
+        if (Document.Renderer.IsBusy || DocumentViewModel.Busy) return;
         RenderOnionSkin(target, resolution, targetOutput);
         RenderGraph(target, resolution, targetOutput);
     }
@@ -33,23 +35,41 @@ internal class SceneRenderer
     private void RenderGraph(DrawingSurface target, ChunkResolution resolution, string? targetOutput)
     {
         DrawingSurface renderTarget = target;
-        Texture? texture = null;
-        
+        Texture? renderTexture = null;
+        bool restoreCanvas = false;
+
         if (!HighResRendering || !HighDpiRenderNodePresent(Document.NodeGraph))
         {
-            texture = Texture.ForProcessing(Document.Size, Document.ProcessingColorSpace);
-            renderTarget = texture.DrawingSurface;
+            renderTexture = Texture.ForProcessing(Document.Size, Document.ProcessingColorSpace);
+            renderTarget = renderTexture.DrawingSurface;
+        }
+        else
+        {
+            renderTexture = Texture.ForProcessing(renderTarget.DeviceClipBounds.Size, Document.ProcessingColorSpace);
+            renderTarget = renderTexture.DrawingSurface;
+            
+            target.Canvas.Save();
+            renderTarget.Canvas.Save();
+            
+            renderTarget.Canvas.SetMatrix(target.Canvas.TotalMatrix);
+            target.Canvas.SetMatrix(Matrix3X3.Identity);
+            restoreCanvas = true;
         }
 
         RenderContext context = new(renderTarget, DocumentViewModel.AnimationHandler.ActiveFrameTime,
             resolution, Document.Size, Document.ProcessingColorSpace);
         context.TargetOutput = targetOutput;
         SolveFinalNodeGraph(context.TargetOutput).Execute(context);
-        
-        if(texture != null)
+
+        if (renderTexture != null)
         {
-            target.Canvas.DrawSurface(texture.DrawingSurface, 0, 0);
-            texture.Dispose();
+            target.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
+            renderTexture.Dispose();
+
+            if (restoreCanvas)
+            {
+                target.Canvas.Restore();
+            }
         }
     }
 
@@ -61,7 +81,7 @@ internal class SceneRenderer
         }
 
         CustomOutputNode[] outputNodes = Document.NodeGraph.AllNodes.OfType<CustomOutputNode>().ToArray();
-        
+
         foreach (CustomOutputNode outputNode in outputNodes)
         {
             if (outputNode.OutputName.Value == targetOutput)
@@ -82,10 +102,10 @@ internal class SceneRenderer
             {
                 graph.AddNode(node);
             }
-            
+
             return true;
         });
-        
+
         graph.CustomOutputNode = outputNode;
         return graph;
     }
@@ -98,9 +118,9 @@ internal class SceneRenderer
             if (n is IHighDpiRenderNode { AllowHighDpiRendering: true })
             {
                 highDpiRenderNodePresent = true;
-            } 
+            }
         });
-        
+
         return highDpiRenderNodePresent;
     }
 
@@ -116,7 +136,7 @@ internal class SceneRenderer
         double alphaFalloffMultiplier = 1.0 / animationData.OnionFrames;
 
         var finalGraph = SolveFinalNodeGraph(targetOutput);
-        
+
         // Render previous frames'
         for (int i = 1; i <= animationData.OnionFrames; i++)
         {
@@ -128,7 +148,8 @@ internal class SceneRenderer
 
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
 
-            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace, finalOpacity);
+            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace,
+                finalOpacity);
             onionContext.TargetOutput = targetOutput;
             finalGraph.Execute(onionContext);
         }
@@ -143,7 +164,8 @@ internal class SceneRenderer
             }
 
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
-            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace, finalOpacity);
+            RenderContext onionContext = new(target, frame, resolution, Document.Size, Document.ProcessingColorSpace,
+                finalOpacity);
             onionContext.TargetOutput = targetOutput;
             finalGraph.Execute(onionContext);
         }

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -41,5 +41,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.42")]
-[assembly: AssemblyFileVersion("2.0.0.42")]
+[assembly: AssemblyVersion("2.0.0.47")]
+[assembly: AssemblyFileVersion("2.0.0.47")]

+ 136 - 128
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -7,147 +7,155 @@
         <Setter Property="Template">
             <ControlTemplate>
                 <Grid Background="Transparent">
+
+                    <Rectangle Name="PART_SelectionRectangle" HorizontalAlignment="Left"
+                               VerticalAlignment="Top"
+                               IsVisible="False" ZIndex="100"
+                               Fill="{DynamicResource SelectionFillBrush}" Opacity="1" />
                     <Grid.ContextFlyout>
                         <Flyout>
                             <nodes:NodePicker
                                 AllNodeTypeInfos="{Binding AllNodeTypeInfos, RelativeSource={RelativeSource TemplatedParent}}"
                                 SearchQuery="{Binding SearchQuery, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
-                                SelectNodeCommand="{Binding CreateNodeFromContextCommand, RelativeSource={RelativeSource TemplatedParent}}"
-                                />
+                                SelectNodeCommand="{Binding CreateNodeFromContextCommand, RelativeSource={RelativeSource TemplatedParent}}" />
                         </Flyout>
-                        </Grid.ContextFlyout>
-                        <ItemsControl ZIndex="1" ClipToBounds="False"
-                                      Name="PART_Nodes"
-                                      ItemsSource="{Binding NodeGraph.AllNodes, RelativeSource={RelativeSource TemplatedParent}}">
-                            <ItemsControl.ItemsPanel>
-                                <ItemsPanelTemplate>
-                                    <Canvas RenderTransformOrigin="0, 0">
-                                        <Canvas.RenderTransform>
-                                            <TransformGroup>
-                                                <ScaleTransform
-                                                    ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                    ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                                <TranslateTransform
-                                                    X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                    Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                            </TransformGroup>
-                                        </Canvas.RenderTransform>
-                                    </Canvas>
-                                </ItemsPanelTemplate>
-                            </ItemsControl.ItemsPanel>
-                            <ItemsControl.ItemTemplate>
-                                <DataTemplate>
-                                    <nodes:NodeView
-                                        Node="{Binding}"
-                                        DisplayName="{Binding NodeNameBindable}"
-                                        CategoryBackgroundBrush="{Binding CategoryBackgroundBrush}"
-                                        Inputs="{Binding Inputs}"
-                                        ActiveFrame="{Binding ActiveFrame, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                        BorderBrush="{Binding InternalName, Converter={converters:NodeInternalNameToStyleConverter}, ConverterParameter='BorderBrush'}"
-                                        BorderThickness="2"
-                                        Outputs="{Binding Outputs}"
-                                        IsSelected="{Binding IsNodeSelected}"
-                                        SelectNodeCommand="{Binding SelectNodeCommand,
+                    </Grid.ContextFlyout>
+                    <ItemsControl ZIndex="1" ClipToBounds="False"
+                                  Name="PART_Nodes"
+                                  ItemsSource="{Binding NodeGraph.AllNodes, RelativeSource={RelativeSource TemplatedParent}}">
+                        <ItemsControl.ItemsPanel>
+                            <ItemsPanelTemplate>
+                                <Canvas RenderTransformOrigin="0, 0">
+                                    <Canvas.RenderTransform>
+                                        <TransformGroup>
+                                            <ScaleTransform
+                                                ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                                ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                            <TranslateTransform
+                                                X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                                Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                        </TransformGroup>
+                                    </Canvas.RenderTransform>
+                                </Canvas>
+                            </ItemsPanelTemplate>
+                        </ItemsControl.ItemsPanel>
+                        <ItemsControl.ItemTemplate>
+                            <DataTemplate>
+                                <nodes:NodeView
+                                    Node="{Binding}"
+                                    DisplayName="{Binding NodeNameBindable}"
+                                    CategoryBackgroundBrush="{Binding CategoryBackgroundBrush}"
+                                    Inputs="{Binding Inputs}"
+                                    ActiveFrame="{Binding ActiveFrame, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    BorderBrush="{Binding InternalName, Converter={converters:NodeInternalNameToStyleConverter}, ConverterParameter='BorderBrush'}"
+                                    BorderThickness="2"
+                                    Outputs="{Binding Outputs}"
+                                    IsSelected="{Binding IsNodeSelected}"
+                                    SelectNodeCommand="{Binding SelectNodeCommand,
                                     RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                        StartDragCommand="{Binding StartDraggingCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                        DragCommand="{Binding DraggedCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                        EndDragCommand="{Binding EndDragCommand,
+                                    StartDragCommand="{Binding StartDraggingCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    DragCommand="{Binding DraggedCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    EndDragCommand="{Binding EndDragCommand,
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                        SocketDropCommand="{Binding SocketDropCommand,
+                                    SocketDropCommand="{Binding SocketDropCommand,
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                        ResultPreview="{Binding ResultPainter}" />
-                                </DataTemplate>
-                            </ItemsControl.ItemTemplate>
-                            <ItemsControl.ItemContainerTheme>
-                                <ControlTheme TargetType="ContentPresenter">
-                                    <Setter Property="Canvas.Left" Value="{Binding PositionBindable.X}" />
-                                    <Setter Property="Canvas.Top" Value="{Binding PositionBindable.Y}" />
-                                </ControlTheme>
-                            </ItemsControl.ItemContainerTheme>
-                        </ItemsControl>
-                        <ItemsControl Name="PART_Connections"
-                                      ItemsSource="{Binding NodeGraph.Connections, RelativeSource={RelativeSource TemplatedParent}}">
-                            <ItemsControl.ItemsPanel>
-                                <ItemsPanelTemplate>
-                                    <Canvas RenderTransformOrigin="0, 0">
-                                        <Canvas.RenderTransform>
-                                            <TransformGroup>
-                                                <ScaleTransform
-                                                    ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                    ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                                <TranslateTransform
-                                                    X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                    Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                            </TransformGroup>
-                                        </Canvas.RenderTransform>
-                                    </Canvas>
-                                </ItemsPanelTemplate>
-                            </ItemsControl.ItemsPanel>
-                            <ItemsControl.ItemTemplate>
-                                <DataTemplate>
-                                    <nodes:ConnectionView
-                                        InputNodePosition="{Binding InputNode.PositionBindable}"
-                                        OutputNodePosition="{Binding OutputNode.PositionBindable}"
-                                        InputProperty="{Binding InputProperty}"
-                                        OutputProperty="{Binding OutputProperty}">
-                                        <nodes:ConnectionView.IsVisible>
-                                            <MultiBinding Converter="{x:Static BoolConverters.And}">
-                                                <Binding Path="InputProperty.IsVisible" />
-                                                <Binding Path="OutputProperty.IsVisible" />
-                                            </MultiBinding>
-                                        </nodes:ConnectionView.IsVisible>
-                                    </nodes:ConnectionView>
-                                </DataTemplate>
-                            </ItemsControl.ItemTemplate>
-                        </ItemsControl>
+                                    ResultPreview="{Binding ResultPainter}" />
+                            </DataTemplate>
+                        </ItemsControl.ItemTemplate>
+                        <ItemsControl.ItemContainerTheme>
+                            <ControlTheme TargetType="ContentPresenter">
+                                <Setter Property="Canvas.Left" Value="{Binding PositionBindable.X}" />
+                                <Setter Property="Canvas.Top" Value="{Binding PositionBindable.Y}" />
+                            </ControlTheme>
+                        </ItemsControl.ItemContainerTheme>
+                    </ItemsControl>
+                    <ItemsControl Name="PART_Connections"
+                                  ItemsSource="{Binding NodeGraph.Connections, RelativeSource={RelativeSource TemplatedParent}}">
+                        <ItemsControl.ItemsPanel>
+                            <ItemsPanelTemplate>
+                                <Canvas RenderTransformOrigin="0, 0">
+                                    <Canvas.RenderTransform>
+                                        <TransformGroup>
+                                            <ScaleTransform
+                                                ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                                ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                            <TranslateTransform
+                                                X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                                Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                        </TransformGroup>
+                                    </Canvas.RenderTransform>
+                                </Canvas>
+                            </ItemsPanelTemplate>
+                        </ItemsControl.ItemsPanel>
+                        <ItemsControl.ItemTemplate>
+                            <DataTemplate>
+                                <nodes:ConnectionView
+                                    InputNodePosition="{Binding InputNode.PositionBindable}"
+                                    OutputNodePosition="{Binding OutputNode.PositionBindable}"
+                                    InputProperty="{Binding InputProperty}"
+                                    OutputProperty="{Binding OutputProperty}">
+                                    <nodes:ConnectionView.IsVisible>
+                                        <MultiBinding Converter="{x:Static BoolConverters.And}">
+                                            <Binding Path="InputProperty.IsVisible" />
+                                            <Binding Path="OutputProperty.IsVisible" />
+                                        </MultiBinding>
+                                    </nodes:ConnectionView.IsVisible>
+                                </nodes:ConnectionView>
+                            </DataTemplate>
+                        </ItemsControl.ItemTemplate>
+                    </ItemsControl>
                     <ItemsControl
                         ZIndex="-1"
                         Name="PART_Frames"
                         ItemsSource="{Binding NodeGraph.Frames, RelativeSource={RelativeSource TemplatedParent}}">
-                            <ItemsControl.ItemsPanel>
-                                <ItemsPanelTemplate>
-                                    <Canvas RenderTransformOrigin="0, 0">
-                                        <Canvas.RenderTransform>
-                                            <TransformGroup>
-                                                <ScaleTransform
-                                                    ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                    ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                                <TranslateTransform
-                                                    X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                    Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                            </TransformGroup>
-                                        </Canvas.RenderTransform>
-                                    </Canvas>
-                                </ItemsPanelTemplate>
-                            </ItemsControl.ItemsPanel>
-                            <ItemsControl.ItemTemplate>
-                                <DataTemplate>
-                                    <nodes:NodeFrameView
-                                        TopLeft="{Binding TopLeft}"
-                                        BottomRight="{Binding BottomRight}"
-                                        Size="{Binding Size}">
-                                        <nodes:NodeFrameView.Background>
-                                            <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
-                                                 <Binding Path="InternalName" Converter="{converters:NodeInternalNameToStyleConverter}" ConverterParameter="BackgroundBrush" />
-                                                 <DynamicResource ResourceKey="NodeFrameBackgroundBrush"/>
-                                            </MultiBinding>
-                                        </nodes:NodeFrameView.Background>
-                                        <nodes:NodeFrameView.BorderBrush>
-                                            <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
-                                                <Binding Path="InternalName" Converter="{converters:NodeInternalNameToStyleConverter}" ConverterParameter="BorderBrush" />
-                                                <DynamicResource ResourceKey="NodeFrameBorderBrush"/>
-                                            </MultiBinding>
-                                        </nodes:NodeFrameView.BorderBrush>
-                                    </nodes:NodeFrameView>
-                                </DataTemplate>
-                            </ItemsControl.ItemTemplate>
-                            <ItemsControl.ItemContainerTheme>
-                                <ControlTheme TargetType="ContentPresenter">
-                                    <Setter Property="Canvas.Left" Value="{Binding TopLeft.X}" />
-                                    <Setter Property="Canvas.Top" Value="{Binding TopLeft.Y}" />
-                                </ControlTheme>
-                            </ItemsControl.ItemContainerTheme>
-                        </ItemsControl>
+                        <ItemsControl.ItemsPanel>
+                            <ItemsPanelTemplate>
+                                <Canvas RenderTransformOrigin="0, 0">
+                                    <Canvas.RenderTransform>
+                                        <TransformGroup>
+                                            <ScaleTransform
+                                                ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                                ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                            <TranslateTransform
+                                                X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                                Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                        </TransformGroup>
+                                    </Canvas.RenderTransform>
+                                </Canvas>
+                            </ItemsPanelTemplate>
+                        </ItemsControl.ItemsPanel>
+                        <ItemsControl.ItemTemplate>
+                            <DataTemplate>
+                                <nodes:NodeFrameView
+                                    TopLeft="{Binding TopLeft}"
+                                    BottomRight="{Binding BottomRight}"
+                                    Size="{Binding Size}">
+                                    <nodes:NodeFrameView.Background>
+                                        <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
+                                            <Binding Path="InternalName"
+                                                     Converter="{converters:NodeInternalNameToStyleConverter}"
+                                                     ConverterParameter="BackgroundBrush" />
+                                            <DynamicResource ResourceKey="NodeFrameBackgroundBrush" />
+                                        </MultiBinding>
+                                    </nodes:NodeFrameView.Background>
+                                    <nodes:NodeFrameView.BorderBrush>
+                                        <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
+                                            <Binding Path="InternalName"
+                                                     Converter="{converters:NodeInternalNameToStyleConverter}"
+                                                     ConverterParameter="BorderBrush" />
+                                            <DynamicResource ResourceKey="NodeFrameBorderBrush" />
+                                        </MultiBinding>
+                                    </nodes:NodeFrameView.BorderBrush>
+                                </nodes:NodeFrameView>
+                            </DataTemplate>
+                        </ItemsControl.ItemTemplate>
+                        <ItemsControl.ItemContainerTheme>
+                            <ControlTheme TargetType="ContentPresenter">
+                                <Setter Property="Canvas.Left" Value="{Binding TopLeft.X}" />
+                                <Setter Property="Canvas.Top" Value="{Binding TopLeft.Y}" />
+                            </ControlTheme>
+                        </ItemsControl.ItemContainerTheme>
+                    </ItemsControl>
                 </Grid>
             </ControlTemplate>
         </Setter>

+ 1 - 1
src/PixiEditor/Styles/Templates/NodeView.axaml

@@ -60,7 +60,7 @@
                                     <ImageBrush Source="/Images/CheckerTile.png"
                                                 TileMode="Tile" DestinationRect="0, 0, 25, 25" />
                                 </Panel.Background>
-                            <visuals:PreviewPainterControl
+                            <visuals:PreviewPainterControl 
                                                            PreviewPainter="{TemplateBinding ResultPreview}"
                                                            FrameToRender="{TemplateBinding ActiveFrame}"
                                                            RenderOptions.BitmapInterpolationMode="None">

+ 4 - 4
src/PixiEditor/ViewModels/Dock/NavigationDockViewModel.cs → src/PixiEditor/ViewModels/Dock/DocumentPreviewDockViewModel.cs

@@ -7,12 +7,12 @@ using PixiEditor.ViewModels.SubViewModels;
 
 namespace PixiEditor.ViewModels.Dock;
 
-internal class NavigationDockViewModel : DockableViewModel
+internal class DocumentPreviewDockViewModel : DockableViewModel
 {
-    public const string TabId = "Navigator";
+    public const string TabId = "DocumentPreview";
 
     public override string Id => TabId;
-    public override string Title => new LocalizedString("NAVIGATION_TITLE");
+    public override string Title => new LocalizedString("PREVIEW_TITLE");
     public override bool CanFloat => true;
     public override bool CanClose => true;
 
@@ -32,7 +32,7 @@ internal class NavigationDockViewModel : DockableViewModel
         set => SetProperty(ref documentManagerSubViewModel, value);
     }
 
-    public NavigationDockViewModel(ColorsViewModel colorsSubViewModel, DocumentManagerViewModel documentManagerViewModel)
+    public DocumentPreviewDockViewModel(ColorsViewModel colorsSubViewModel, DocumentManagerViewModel documentManagerViewModel)
     {
         ColorsSubViewModel = colorsSubViewModel;
         DocumentManagerSubViewModel = documentManagerViewModel;

+ 3 - 3
src/PixiEditor/ViewModels/Dock/LayoutManager.cs

@@ -33,7 +33,7 @@ internal class LayoutManager
         LayersDockViewModel layersDockViewModel = new(mainViewModel.DocumentManagerSubViewModel);
         ColorPickerDockViewModel colorPickerDockViewModel = new(mainViewModel.ColorsSubViewModel);
         ColorSlidersDockViewModel colorSldersDockViewModel = new(mainViewModel.ColorsSubViewModel);
-        NavigationDockViewModel navigationDockViewModel =
+        DocumentPreviewDockViewModel documentPreviewDockViewModel =
             new(mainViewModel.ColorsSubViewModel, mainViewModel.DocumentManagerSubViewModel);
         SwatchesDockViewModel swatchesDockViewModel = new(mainViewModel.DocumentManagerSubViewModel);
         PaletteViewerDockViewModel paletteViewerDockViewModel =
@@ -48,7 +48,7 @@ internal class LayoutManager
         RegisterDockable(layersDockViewModel);
         RegisterDockable(colorPickerDockViewModel);
         RegisterDockable(colorSldersDockViewModel);
-        RegisterDockable(navigationDockViewModel);
+        RegisterDockable(documentPreviewDockViewModel);
         RegisterDockable(swatchesDockViewModel);
         RegisterDockable(paletteViewerDockViewModel);
         RegisterDockable(timelineDockViewModel);
@@ -101,7 +101,7 @@ internal class LayoutManager
                     SplitDirection = DockingDirection.Bottom,
                     Second = new DockableArea
                     {
-                        Id = "NavigatorArea", ActiveDockable = DockContext.CreateDockable(navigationDockViewModel)
+                        Id = "DocumentPreviewArea", ActiveDockable = DockContext.CreateDockable(documentPreviewDockViewModel)
                     }
                 }
             }

+ 38 - 20
src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs

@@ -17,7 +17,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     private int frameRateBindable = 60;
     private int onionFrames = 1;
     private double onionOpacity = 50;
-    
+
     public DocumentViewModel Document { get; }
     protected DocumentInternalParts Internals { get; }
     public IReadOnlyCollection<ICelHandler> KeyFrames => keyFrames;
@@ -25,12 +25,15 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     public IReadOnlyCollection<ICelHandler> AllCels => allCels;
 
     public event Action<int, int> ActiveFrameChanged;
-    
+
     private KeyFrameCollection keyFrames = new KeyFrameCollection();
     private List<ICelHandler> allCels = new List<ICelHandler>();
     private bool onionSkinningEnabled;
     private bool isPlayingBindable;
 
+    private int? cachedFirstFrame;
+    private int? cachedLastFrame;
+
     public int ActiveFrameBindable
     {
         get => _activeFrameBindable;
@@ -68,7 +71,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
             Internals.ActionAccumulator.AddFinishedActions(new ToggleOnionSkinning_PassthroughAction(value));
         }
     }
-    
+
     public int OnionFramesBindable
     {
         get => onionFrames;
@@ -80,7 +83,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
             Internals.ActionAccumulator.AddFinishedActions(new SetOnionSettings_Action(value, OnionOpacityBindable));
         }
     }
-    
+
     public double OnionOpacityBindable
     {
         get => onionOpacity;
@@ -89,10 +92,10 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
             if (Document.BlockingUpdateableChangeActive)
                 return;
 
-            Internals.ActionAccumulator.AddFinishedActions(new SetOnionSettings_Action(OnionFramesBindable, value)); 
+            Internals.ActionAccumulator.AddFinishedActions(new SetOnionSettings_Action(OnionFramesBindable, value));
         }
     }
-    
+
     public bool IsPlayingBindable
     {
         get => isPlayingBindable;
@@ -105,9 +108,9 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         }
     }
 
-    public int FirstFrame => keyFrames.Count > 0 ? keyFrames.Min(x => x.StartFrameBindable) : 0;
+    public int FirstFrame => cachedFirstFrame ??= keyFrames.Count > 0 ? keyFrames.Min(x => x.StartFrameBindable) : 0;
 
-    public int LastFrame => keyFrames.Count > 0
+    public int LastFrame => cachedLastFrame ??= keyFrames.Count > 0
         ? keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable)
         : DefaultEndFrame;
 
@@ -126,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)
@@ -196,19 +203,19 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         ActiveFrameChanged?.Invoke(previousFrame, newFrame);
         OnPropertyChanged(nameof(ActiveFrameBindable));
     }
-    
+
     public void SetPlayingState(bool value)
     {
         isPlayingBindable = value;
         OnPropertyChanged(nameof(IsPlayingBindable));
     }
-    
+
     public void SetOnionSkinning(bool value)
     {
         onionSkinningEnabled = value;
         OnPropertyChanged(nameof(OnionSkinningEnabledBindable));
     }
-    
+
     public void SetOnionFrames(int frames, double opacity)
     {
         onionFrames = frames;
@@ -217,13 +224,17 @@ 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))
         {
+            cachedFirstFrame = null;
+            cachedLastFrame = null;
+            
             keyFrame.SetStartFrame(newStartFrame);
             keyFrame.SetDuration(newDuration);
             keyFrames.NotifyCollectionChanged();
+
             OnPropertyChanged(nameof(FirstFrame));
             OnPropertyChanged(nameof(LastFrame));
             OnPropertyChanged(nameof(FramesCount));
@@ -261,8 +272,12 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         {
             allCels.Add(iCel);
         }
-        
+
         SortByLayers();
+        
+        cachedFirstFrame = null;
+        cachedLastFrame = null;
+        
         OnPropertyChanged(nameof(FirstFrame));
         OnPropertyChanged(nameof(LastFrame));
         OnPropertyChanged(nameof(FramesCount));
@@ -290,6 +305,9 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 
         allCels.RemoveAll(x => x.Id == keyFrameId);
         
+        cachedFirstFrame = null;
+        cachedLastFrame = null;
+
         OnPropertyChanged(nameof(FirstFrame));
         OnPropertyChanged(nameof(LastFrame));
         OnPropertyChanged(nameof(FramesCount));
@@ -376,7 +394,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         result = default;
         return false;
     }
-    
+
     public void SortByLayers()
     {
         var allLayers = Document.StructureHelper.GetAllLayers();
@@ -384,8 +402,9 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         var layerKeyFrames = new List<CelGroupViewModel>();
         foreach (var layer in allLayers)
         {
-            var group = unsortedKeyFrames.FirstOrDefault(x => x is CelGroupViewModel group && group.LayerGuid == layer.Id) as CelGroupViewModel; 
-            if(group != null)
+            var group = unsortedKeyFrames.FirstOrDefault(x =>
+                x is CelGroupViewModel group && group.LayerGuid == layer.Id) as CelGroupViewModel;
+            if (group != null)
             {
                 layerKeyFrames.Insert(0, group);
             }
@@ -398,9 +417,8 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
                 layerKeyFrames.Add(group);
             }
         }
-        
+
         this.keyFrames = new KeyFrameCollection(layerKeyFrames);
         OnPropertyChanged(nameof(KeyFrames));
     }
-
 }

+ 44 - 11
src/PixiEditor/ViewModels/Document/CelGroupViewModel.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
+using System.ComponentModel;
 using System.Reactive.Linq;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
@@ -8,10 +9,16 @@ namespace PixiEditor.ViewModels.Document;
 
 internal class CelGroupViewModel : CelViewModel, ICelGroupHandler
 {
+    private int? cachedStartFrame;
+    private int? cachedDuration;
     public ObservableCollection<ICelHandler> Children { get; } = new ObservableCollection<ICelHandler>();
 
-    public override int StartFrameBindable => Children.Count > 0 ? Children.Min(x => x.StartFrameBindable) : 0;
-    public override int DurationBindable => Children.Count > 0 ? Children.Max(x => x.StartFrameBindable + x.DurationBindable) - StartFrameBindable : 0;
+    public override int StartFrameBindable =>
+        cachedStartFrame ??= (Children.Count > 0 ? Children.Min(x => x.StartFrameBindable) : 0);
+
+    public override int DurationBindable => cachedDuration ??= (Children.Count > 0
+        ? Children.Max(x => x.StartFrameBindable + x.DurationBindable) - StartFrameBindable
+        : 0);
 
     public string LayerName => Document.StructureHelper.Find(LayerGuid).NodeNameBindable;
 
@@ -37,16 +44,17 @@ internal class CelGroupViewModel : CelViewModel, ICelGroupHandler
     {
         foreach (var child in Children)
         {
-            if(child is CelViewModel keyFrame)
+            if (child is CelViewModel keyFrame)
             {
                 keyFrame.SetVisibility(isVisible);
             }
         }
-        
+
         base.SetVisibility(isVisible);
     }
 
-    public CelGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id, DocumentViewModel doc, DocumentInternalParts internalParts) 
+    public CelGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id, DocumentViewModel doc,
+        DocumentInternalParts internalParts)
         : base(startFrame, duration, layerGuid, id, doc, internalParts)
     {
         Children.CollectionChanged += ChildrenOnCollectionChanged;
@@ -61,19 +69,44 @@ internal class CelGroupViewModel : CelViewModel, ICelGroupHandler
 
     private void ChildrenOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
     {
-        OnPropertyChanged(nameof(StartFrameBindable));
-        OnPropertyChanged(nameof(DurationBindable));
-        
+        cachedStartFrame = null;
+        cachedDuration = null;
+
         if (e.Action == NotifyCollectionChangedAction.Add)
         {
             foreach (var item in e.NewItems)
             {
-                if (item is CelViewModel keyFrame)
+                if (item is CelViewModel cel)
+                {
+                    cel.IsCollapsed = IsCollapsed;
+                    cel.SetVisibility(IsVisible);
+                    cel.PropertyChanged += CelOnPropertyChanged;
+                }
+            }
+        }
+        else if (e.Action == NotifyCollectionChangedAction.Remove)
+        {
+            foreach (var item in e.OldItems)
+            {
+                if (item is CelViewModel cel)
                 {
-                    keyFrame.IsCollapsed = IsCollapsed;
-                    keyFrame.SetVisibility(IsVisible);
+                    cel.PropertyChanged -= CelOnPropertyChanged;
                 }
             }
         }
+
+        OnPropertyChanged(nameof(StartFrameBindable));
+        OnPropertyChanged(nameof(DurationBindable));
+    }
+    
+    private void CelOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName is nameof(ICelHandler.StartFrameBindable) or nameof(ICelHandler.DurationBindable))
+        {
+            cachedStartFrame = null;
+            cachedDuration = null;
+            OnPropertyChanged(nameof(StartFrameBindable));
+            OnPropertyChanged(nameof(DurationBindable));
+        }
     }
 }

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

@@ -162,7 +162,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
 
     [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE", 
         CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete,
-        ShortcutContext = typeof(ViewportWindowViewModel),
+        ShortcutContexts = [typeof(ViewportWindowViewModel)],
         Icon = PixiPerfectIcons.Eraser,
         MenuItemPath = "EDIT/DELETE_SELECTED_PIXELS", MenuItemOrder = 6, AnalyticsTrack = true)]
     public void DeletePixels()

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

+ 36 - 43
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>();
 
@@ -507,7 +507,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 VecD scaling = new VecD(renderSize.X / (double)SizeBindable.X, renderSize.Y / (double)SizeBindable.Y);
 
                 finalSurface.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
-                Renderer.RenderDocument(finalSurface.DrawingSurface, frameTime);
+                Renderer.RenderDocument(finalSurface.DrawingSurface, frameTime, renderSize);
 
                 finalSurface.DrawingSurface.Canvas.Restore();
             });
@@ -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)
@@ -807,18 +787,21 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     {
         softSelectedStructureMembers.Add(member);
         Internals.ChangeController.MembersSelectedInlet(GetSelectedMembers());
+        OnPropertyChanged(nameof(SoftSelectedStructureMembers));
     }
 
     public void RemoveSoftSelectedMember(IStructureMemberHandler member)
     {
         softSelectedStructureMembers.Remove(member);
         Internals.ChangeController.MembersSelectedInlet(GetSelectedMembers());
+        OnPropertyChanged(nameof(SoftSelectedStructureMembers));
     }
 
     public void ClearSoftSelectedMembers()
     {
         softSelectedStructureMembers.Clear();
         Internals.ChangeController.MembersSelectedInlet(GetSelectedMembers());
+        OnPropertyChanged(nameof(SoftSelectedStructureMembers));
     }
 
     #endregion
@@ -845,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;
@@ -867,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);
@@ -891,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];

+ 0 - 7
src/PixiEditor/ViewModels/Document/Nodes/DebugBlendModeNodeViewModel.cs

@@ -1,7 +0,0 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-using PixiEditor.ViewModels.Nodes;
-
-namespace PixiEditor.ViewModels.Document.Nodes;
-
-[NodeViewModel("Debug Blend Mode", "", null)]
-internal class DebugBlendModeNodeViewModel : NodeViewModel<DebugBlendModeNode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/BlurNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
+
+[NodeViewModel("BLUR_FILTER_NODE", "FILTERS", null)] // TODO: Blur Icon
+internal class BlurNodeViewModel : NodeViewModel<BlurNode>;

+ 20 - 2
src/PixiEditor/ViewModels/Document/Nodes/MathNodeViewModel.cs

@@ -14,22 +14,40 @@ internal class MathNodeViewModel : NodeViewModel<MathNode>
 {
     private GenericEnumPropertyViewModel Mode { get; set; }
     
+    private NodePropertyViewModel X { get; set; }
+    
     private NodePropertyViewModel Y { get; set; }
     
+    private NodePropertyViewModel Z { get; set; }
+    
     public override void OnInitialized()
     {
         Mode = FindInputProperty("Mode") as GenericEnumPropertyViewModel;
+        X = FindInputProperty("X");
         Y = FindInputProperty("Y");
+        Z = FindInputProperty("Z");
         
-        Mode.ValueChanged += ModeChanged;
+        Mode.ValueChanged += (_, _) => ModeChanged();
+        ModeChanged();
     }
 
-    private void ModeChanged(INodePropertyHandler property, NodePropertyValueChangedArgs args)
+    private void ModeChanged()
     {
         if (Mode.Value is not MathNodeMode mode)
             return;
 
         DisplayName = mode.GetDescription();
         Y.IsVisible = mode.UsesYValue();
+        Z.IsVisible = mode.UsesZValue();
+
+        var (x, y, z) = mode.GetNaming();
+
+        x = new LocalizedString(x);
+        y = new LocalizedString(y);
+        z = new LocalizedString(z);
+
+        X.DisplayName = x;
+        Y.DisplayName = y;
+        Z.DisplayName = z;
     }
 }

+ 4 - 4
src/PixiEditor/ViewModels/SubViewModels/AnimationsViewModel.cs

@@ -151,7 +151,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
 
     [Command.Basic("PixiEditor.Animation.ToggleOnionSkinning", "TOGGLE_ONION_SKINNING",
         "TOGGLE_ONION_SKINNING_DESCRIPTIVE",
-        ShortcutContext = typeof(TimelineDockViewModel), Key = Key.O, AnalyticsTrack = true)]
+        ShortcutContexts = [typeof(TimelineDockViewModel)], Key = Key.O, AnalyticsTrack = true)]
     public void ToggleOnionSkinning(bool value)
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is null)
@@ -161,7 +161,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.Animation.DeleteCels", "DELETE_CELS", "DELETE_CELS_DESCRIPTIVE",
-        ShortcutContext = typeof(TimelineDockViewModel), Key = Key.Delete, AnalyticsTrack = true)]
+        ShortcutContexts = [typeof(TimelineDockViewModel)], Key = Key.Delete, AnalyticsTrack = true)]
     public void DeleteCels()
     {
         var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -214,7 +214,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
                 return active;
             }
 
-            for (int i = active + 1; i < activeDocument.AnimationDataViewModel.FramesCount; i++)
+            for (int i = active + 1; i < groupViewModel.StartFrameBindable + groupViewModel.DurationBindable; i++)
             {
                 if (groupViewModel.Children.All(x => !x.IsWithinRange(i)))
                 {
@@ -222,7 +222,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
                 }
             }
 
-            return activeDocument.AnimationDataViewModel.FramesCount + 1;
+            return groupViewModel.StartFrameBindable + groupViewModel.DurationBindable;
         }
 
         return active;

+ 222 - 6
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -19,6 +19,8 @@ using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
 using Drawie.Numerics;
 using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Dock;
+using PixiEditor.ViewModels.Document;
 
 namespace PixiEditor.ViewModels.SubViewModels;
 #nullable enable
@@ -46,12 +48,13 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         doc.Operations.DeleteSelectedPixels(doc.AnimationDataViewModel.ActiveFrameBindable, true);
     }
 
-    [Command.Basic("PixiEditor.Clipboard.Paste", false, "PASTE", "PASTE_DESCRIPTIVE",
-        CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Shift,
-        MenuItemPath = "EDIT/PASTE", MenuItemOrder = 4, Icon = PixiPerfectIcons.Paste, AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Clipboard.PasteAsNewLayer", true, "PASTE_AS_NEW_LAYER", "PASTE_AS_NEW_LAYER_DESCRIPTIVE",
         CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Control,
+        ShortcutContexts = [typeof(ViewportWindowViewModel), typeof(LayersDockViewModel)],
         Icon = PixiPerfectIcons.PasteAsNewLayer, AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Clipboard.Paste", false, "PASTE", "PASTE_DESCRIPTIVE",
+        CanExecute = "PixiEditor.Clipboard.CanPaste", Key = Key.V, Modifiers = KeyModifiers.Shift,
+        MenuItemPath = "EDIT/PASTE", MenuItemOrder = 4, Icon = PixiPerfectIcons.Paste, AnalyticsTrack = true)]
     public void Paste(bool pasteAsNewLayer)
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is null)
@@ -145,8 +148,127 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         }
     }
 
+    [Command.Basic("PixiEditor.Clipboard.PasteNodes", "PASTE_NODES", "PASTE_NODES_DESCRIPTIVE",
+        ShortcutContexts = [typeof(NodeGraphDockViewModel)], Key = Key.V, Modifiers = KeyModifiers.Control,
+        CanExecute = "PixiEditor.Clipboard.CanPasteNodes", Icon = PixiPerfectIcons.Paste, AnalyticsTrack = true)]
+    public async Task PasteNodes()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+
+        Guid[] toDuplicate = await ClipboardController.GetNodeIds();
+
+        List<Guid> newIds = new();
+
+        Dictionary<Guid, Guid> nodeMapping = new();
+
+        using var block = doc.Operations.StartChangeBlock();
+
+        foreach (var nodeId in toDuplicate)
+        {
+            Guid? newId = doc.Operations.DuplicateNode(nodeId);
+            if (newId != null)
+            {
+                newIds.Add(newId.Value);
+                nodeMapping.Add(nodeId, newId.Value);
+            }
+        }
+
+        if (newIds.Count == 0)
+            return;
+
+        await block.ExecuteQueuedActions();
+
+        ConnectRelatedNodes(doc, nodeMapping);
+
+        doc.Operations.InvokeCustomAction(() =>
+        {
+            foreach (var node in doc.NodeGraph.AllNodes)
+            {
+                node.IsNodeSelected = false;
+            }
+
+            foreach (var node in newIds)
+            {
+                var nodeInstance = doc.NodeGraph.AllNodes.FirstOrDefault(x => x.Id == node);
+                if (nodeInstance != null)
+                {
+                    nodeInstance.IsNodeSelected = true;
+                }
+            }
+        });
+    }
+
+    [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,
+        ShortcutContexts = [typeof(ViewportWindowViewModel), typeof(LayersDockViewModel)],
         MenuItemPath = "EDIT/COPY", MenuItemOrder = 3, Icon = PixiPerfectIcons.Copy, AnalyticsTrack = true)]
     public async Task Copy()
     {
@@ -157,7 +279,8 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         await ClipboardController.CopyToClipboard(doc);
     }
 
-    [Command.Basic("PixiEditor.Clipboard.CopyVisible", "COPY_VISIBLE", "COPY_VISIBLE_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanCopy",
+    [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()
@@ -169,6 +292,42 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         await ClipboardController.CopyVisibleToClipboard(doc);
     }
 
+    [Command.Basic("PixiEditor.Clipboard.CopyNodes", "COPY_NODES", "COPY_NODES_DESCRIPTIVE",
+        Key = Key.C, Modifiers = KeyModifiers.Control,
+        ShortcutContexts = [typeof(NodeGraphDockViewModel)],
+        CanExecute = "PixiEditor.Clipboard.CanCopyNodes",
+        Icon = PixiPerfectIcons.Copy, AnalyticsTrack = true)]
+    public async Task CopySelectedNodes()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+
+        var selectedNodes = doc.NodeGraph.AllNodes.Where(x => x.IsNodeSelected).Select(x => x.Id).ToArray();
+        if (selectedNodes.Length == 0)
+            return;
+
+        await ClipboardController.CopyNodes(selectedNodes);
+    }
+
+    [Command.Basic("PixiEditor.Clipboard.CopyCels", "COPY_CELS",
+        "COPY_CELS_DESCRIPTIVE", CanExecute = "PixiEditor.Clipboard.CanCopyCels",
+        ShortcutContexts = [typeof(TimelineDockViewModel)],
+        Key = Key.C, Modifiers = KeyModifiers.Control, Icon = PixiPerfectIcons.Copy, AnalyticsTrack = true)]
+    public async Task CopySelectedCels()
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+
+        var selectedCels = doc.AnimationDataViewModel.AllCels.Where(x => x.IsSelected).Select(x => x.Id).ToArray();
+        if (selectedCels.Length == 0)
+            return;
+
+        await ClipboardController.CopyCels(selectedCels);
+    }
+
+
     [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",
@@ -210,9 +369,38 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
             : ClipboardController.IsImageInClipboard().Result;
     }
 
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopyCels")]
+    public bool CanCopyCels()
+    {
+        return Owner.DocumentIsNotNull(null) &&
+               Owner.DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.AllCels.Any(x => x.IsSelected);
+    }
+
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopyNodes")]
+    public bool CanCopyNodes()
+    {
+        return Owner.DocumentIsNotNull(null) &&
+               Owner.DocumentManagerSubViewModel.ActiveDocument.NodeGraph.AllNodes.Any(x => x.IsNodeSelected);
+    }
+
+    [Evaluator.CanExecute("PixiEditor.Clipboard.CanPasteNodes")]
+    public bool CanPasteNodes()
+    {
+        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() =>
-        ColorHelper.ParseAnyFormat((await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out _);
+    public static async Task<bool> CanPasteColor()
+    {
+        return ColorHelper.ParseAnyFormat(
+            (await ClipboardController.Clipboard.GetTextAsync())?.Trim() ?? string.Empty, out _);
+    }
 
     [Evaluator.CanExecute("PixiEditor.Clipboard.CanCopy")]
     public bool CanCopy()
@@ -264,6 +452,34 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         return ColorSearchResult.GetIcon(targetColor.ToOpaqueMediaColor().ToOpaqueColor());
     }
 
+    private void ConnectRelatedNodes(DocumentViewModel doc, Dictionary<Guid, Guid> nodeMapping)
+    {
+        foreach (var connection in doc.NodeGraph.Connections)
+        {
+            if (nodeMapping.TryGetValue(connection.InputNode.Id, out var inputNode) &&
+                nodeMapping.TryGetValue(connection.OutputNode.Id, out var outputNode))
+            {
+                var inputNodeInstance = doc.NodeGraph.AllNodes.FirstOrDefault(x => x.Id == inputNode);
+                var outputNodeInstance = doc.NodeGraph.AllNodes.FirstOrDefault(x => x.Id == outputNode);
+
+                if (inputNodeInstance == null || outputNodeInstance == null)
+                    continue;
+
+                var inputProperty =
+                    inputNodeInstance.Inputs.FirstOrDefault(
+                        x => x.PropertyName == connection.InputProperty.PropertyName);
+                var outputProperty =
+                    outputNodeInstance.Outputs.FirstOrDefault(x =>
+                        x.PropertyName == connection.OutputProperty.PropertyName);
+
+                if (inputProperty == null || outputProperty == null)
+                    continue;
+
+                doc.NodeGraph.ConnectProperties(inputProperty, outputProperty);
+            }
+        }
+    }
+
     public enum CopyColor
     {
         PrimaryHEX,

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

@@ -408,7 +408,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
         PrimaryColor = color.ToColor();
     }
 
-    [Commands_Command.Basic("PixIEditor.Colors.AddPrimaryToPalettes", "ADD_PRIMARY_COLOR_TO_PALETTE",
+    [Commands_Command.Basic("PixiEditor.Colors.AddPrimaryToPalettes", "ADD_PRIMARY_COLOR_TO_PALETTE",
         "ADD_PRIMARY_COLOR_TO_PALETTE_DESCRIPTIVE", CanExecute = "PixiEditor.HasDocument",
         Icon = PixiPerfectIcons.CopyAdd, AnalyticsTrack = true)]
     public void AddPrimaryColorToPalette()

+ 5 - 7
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -92,7 +92,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Layer.DeleteAllSelected", "LAYER_DELETE_ALL_SELECTED",
         "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE", CanExecute = "PixiEditor.Layer.HasSelectedMembers",
         Icon = PixiPerfectIcons.Trash, AnalyticsTrack = true, Key = Key.Delete,
-        ShortcutContext = typeof(LayersDockViewModel))]
+        ShortcutContexts = [typeof(LayersDockViewModel)])]
     public void DeleteAllSelected()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -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")]

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

@@ -15,7 +15,7 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.NodeGraph.DeleteSelectedNodes", "DELETE_NODES", "DELETE_NODES_DESCRIPTIVE", 
-        Key = Key.Delete, ShortcutContext = typeof(NodeGraphDockViewModel), AnalyticsTrack = true)]
+        Key = Key.Delete, ShortcutContexts = [typeof(NodeGraphDockViewModel)], AnalyticsTrack = true)]
     public void DeleteSelectedNodes()
     {
         var nodes = Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.AllNodes

+ 4 - 4
src/PixiEditor/ViewModels/SubViewModels/SelectionViewModel.cs

@@ -65,13 +65,13 @@ internal class SelectionViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectLeft", "NUDGE_SELECTED_LEFT", "NUDGE_SELECTED_LEFT", Key = Key.Left, Parameter = new int[] { -1, 0 }, Icon = PixiPerfectIcons.ChevronLeft, CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject",
-        ShortcutContext = typeof(ViewportWindowViewModel))]
+        ShortcutContexts = [typeof(ViewportWindowViewModel)])]
     [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectRight", "NUDGE_SELECTED_RIGHT", "NUDGE_SELECTED_RIGHT", Key = Key.Right, Parameter = new int[] { 1, 0 }, Icon = PixiPerfectIcons.ChevronRight, CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject",
-        ShortcutContext = typeof(ViewportWindowViewModel))]
+        ShortcutContexts = [typeof(ViewportWindowViewModel)])]
     [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectUp", "NUDGE_SELECTED_UP", "NUDGE_SELECTED_UP", Key = Key.Up, Parameter = new int[] { 0, -1 }, Icon = PixiPerfectIcons.ChevronUp, CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject",
-        ShortcutContext = typeof(ViewportWindowViewModel))]
+        ShortcutContexts = [typeof(ViewportWindowViewModel)])]
     [Command.Basic("PixiEditor.Selection.NudgeSelectedObjectDown", "NUDGE_SELECTED_DOWN", "NUDGE_SELECTED_DOWN", Key = Key.Down, Parameter = new int[] { 0, 1 }, Icon = PixiPerfectIcons.ChevronDown, CanExecute = "PixiEditor.Selection.CanNudgeSelectedObject",
-        ShortcutContext = typeof(ViewportWindowViewModel))]
+        ShortcutContexts = [typeof(ViewportWindowViewModel)])]
     public void NudgeSelectedObject(int[] dist)
     {
         VecI distance = new(dist[0], dist[1]);

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

@@ -504,7 +504,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
 
     private void DocumentOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
     {
-        if (e.PropertyName == nameof(DocumentViewModel.SelectedStructureMember))
+        if (e.PropertyName is nameof(DocumentViewModel.SelectedStructureMember) or nameof(DocumentViewModel.SoftSelectedStructureMembers))
         {
             UpdateEnabledState();
         }
@@ -530,7 +530,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
                     doc.SelectedStructureMember
                 };
 
-                selectedLayers.AddRange(doc.SoftSelectedStructureMembers);
+                selectedLayers.AddRange(doc.SoftSelectedStructureMembers.Except(selectedLayers));
                 tool.SelectedLayersChanged(selectedLayers.ToArray());
             }
         }

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

@@ -210,8 +210,8 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
     }
 
     [Commands_Command.Internal("PixiEditor.Window.ShowDockWindow")]
-    [Commands_Command.Basic("PixiEditor.Window.OpenNavigationWindow", "Navigator", "OPEN_NAVIGATION_WINDOW",
-        "OPEN_NAVIGATION_WINDOW")]
+    [Commands_Command.Basic("PixiEditor.Window.OpenPreviewWindow", "DocumentPreview", "OPEN_PREVIEW_WINDOW",
+        "OPEN_PREVIEW_WINDOW")]
     public void ShowDockWindow(string id)
     {
         Owner.LayoutSubViewModel.LayoutManager.ShowDockable(id);

+ 9 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/Setting.cs

@@ -27,7 +27,15 @@ internal abstract class Setting<T> : Setting
 
     public new virtual T Value
     {
-        get => (T)base.Value;
+        get
+        {
+            if(base.Value != null && base.Value is not T value)
+            {
+                return default;
+            }
+            
+            return (T)base.Value;
+        }
         set
         {
             T oldValue = default;

+ 1 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/Toolbar.cs

@@ -44,7 +44,7 @@ internal abstract class Toolbar : ObservableObject, IToolbar
     {
         Setting setting = Settings.FirstOrDefault(currentSetting => string.Equals(currentSetting.Name, name, StringComparison.CurrentCultureIgnoreCase));
 
-        if (setting is null || setting is not T convertedSetting)
+        if (setting is not T convertedSetting)
         {
             return null;
         }

+ 11 - 6
src/PixiEditor/ViewModels/Tools/ToolViewModel.cs

@@ -77,9 +77,9 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
             OnPropertyChanged(nameof(ActionDisplay));
         }
     }
-    
+
     public string IconOverwrite { get; set; }
-    
+
     public string IconToUse => IconOverwrite ?? DefaultIcon;
 
     private bool isActive;
@@ -110,6 +110,11 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
         if (layers.Length is > 1 or 0)
         {
             CanBeUsedOnActiveLayer = SupportedLayerTypes == null;
+            if (IsActive)
+            {
+                OnSelectedLayersChanged(layers);
+            }
+
             return;
         }
 
@@ -151,7 +156,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
     public virtual void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) { }
 
     public virtual void UseTool(VecD pos) { }
-    
+
     protected virtual void OnSelected(bool restoring) { }
 
     public void OnToolSelected(bool restoring)
@@ -179,7 +184,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
     protected virtual void OnDeselecting(bool transient)
     {
     }
-    
+
     public virtual void OnPostUndo() { }
     public virtual void OnPostRedo() { }
     public virtual void OnActiveFrameChanged(int newFrame) { }
@@ -209,8 +214,8 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
         {
             toolbarSetting.ResetOverwrite();
         }
-        
-        if(toolset.IconOverwrites.TryGetValue(this, out var icon))
+
+        if (toolset.IconOverwrites.TryGetValue(this, out var icon))
         {
             IconOverwrite = icon;
         }

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

@@ -100,7 +100,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
     {
         if (IsActive)
         {
-            OnSelected(false);
+           OnToolSelected(false);
         }
     }
 
@@ -108,7 +108,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
     {
         if (IsActive)
         {
-            OnSelected(false);
+            OnToolSelected(false);
         }
     }
 
@@ -124,7 +124,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
 
     private void UpdateSelection()
     {
-        OnDeselecting(false);
+        OnToolDeselected(false);
         OnToolSelected(false);
     }
 

Some files were not shown because too many files changed in this diff