Browse Source

Merge pull request #977 from PixiEditor/func-matrix

Added compose and decompose matrix nodes, func matrix
Krzysztof Krysiński 1 month ago
parent
commit
03ed43683b
54 changed files with 742 additions and 134 deletions
  1. 1 1
      src/Drawie
  2. 1 1
      src/PixiDocks
  3. 61 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  4. 99 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/ComposeMatrixNode.cs
  5. 47 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/DecomposeMatrixNode.cs
  6. 12 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs
  7. 0 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/MatrixNode.cs
  8. 30 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/OffsetNode.cs
  9. 42 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/RotateNode.cs
  10. 39 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/ScaleNode.cs
  11. 30 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/SkewNode.cs
  12. 28 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/TransformNode.cs
  13. 3 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  14. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  15. 18 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  16. 89 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ImportNode_Change.cs
  17. 52 28
      src/PixiEditor.ChangeableDocument/Changes/Structure/ImportFolder_Change.cs
  18. 8 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/ImportLayer_Change.cs
  19. 1 1
      src/PixiEditor.Desktop/PixiEditor.Desktop.csproj
  20. 7 1
      src/PixiEditor.Desktop/Program.cs
  21. 1 0
      src/PixiEditor.UI.Common/Accents/Base.axaml
  22. BIN
      src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf
  23. 2 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  24. 2 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs
  25. 0 0
      src/PixiEditor.UI.Common/Fonts/defs.svg
  26. 14 1
      src/PixiEditor/Data/Localization/Languages/en.json
  27. 26 5
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  28. 57 17
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  29. 1 1
      src/PixiEditor/Models/Handlers/IDocumentOperations.cs
  30. 7 7
      src/PixiEditor/PixiEditor.csproj
  31. 10 0
      src/PixiEditor/ViewModels/Document/Nodes/Matrix/ComposeMatrixNodeViewModel.cs
  32. 10 0
      src/PixiEditor/ViewModels/Document/Nodes/Matrix/DecomposeMatrixNodeViewModel.cs
  33. 23 9
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  34. 1 0
      src/PixiEditor/Views/Nodes/NodeGraphView.cs
  35. 17 3
      tests/PixiEditor.Tests/RenderTests.cs
  36. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/ComposeMatrix.pixi
  37. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/ComposeMatrix.png
  38. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuMatrixChain.pixi
  39. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuMatrixChain.png
  40. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuOffset.pixi
  41. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuScale.pixi
  42. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuScale.png
  43. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuSkew.pixi
  44. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuSkew.png
  45. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/MatrixChain.pixi
  46. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/MatrixChain.png
  47. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Offset.pixi
  48. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Offset.png
  49. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Rotation.pixi
  50. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Rotation.png
  51. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Scale.pixi
  52. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Scale.png
  53. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Skew.pixi
  54. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Skew.png

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 7e9e4ba82f589f6d68589707208c57ee907272a6
+Subproject commit f89aba0bedd4d7800bdd32c4095ee5ec5e5432dd

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit daed2f25fbc530e4f6ebed2baedae4f33c4fa3d4
+Subproject commit 015cfaab052e2b7162d111f3efb5e0da7023b02f

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

@@ -4,6 +4,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
@@ -55,10 +56,10 @@ public class FuncContext
         SamplePosition = Builder.ConstructFloat2(OriginalPosition.X, OriginalPosition.Y);
         SamplePosition = Builder.ConstructFloat2(OriginalPosition.X, OriginalPosition.Y);
     }
     }
 
 
-    public Half4 SampleSurface(DrawingSurface surface, Expression pos, ColorSampleMode sampleMode)
+    public Half4 SampleSurface(DrawingSurface surface, Expression pos, ColorSampleMode sampleMode, bool normalizedCoordinates)
     {
     {
         SurfaceSampler texName = Builder.AddOrGetSurface(surface, sampleMode);
         SurfaceSampler texName = Builder.AddOrGetSurface(surface, sampleMode);
-        return Builder.Sample(texName, pos);
+        return Builder.Sample(texName, pos, normalizedCoordinates);
     }
     }
 
 
     public Float2 NewFloat2(Expression x, Expression y)
     public Float2 NewFloat2(Expression x, Expression y)
@@ -329,4 +330,62 @@ public class FuncContext
 
 
         return val;
         return val;
     }
     }
+
+    public Float3x3 GetValue(FuncInputProperty<Float3x3> getFrom)
+    {
+        if (HasContext)
+        {
+            if (getFrom.Connection == null || !IsFuncType(getFrom))
+            {
+                Float3x3 value = getFrom.Value(this);
+                value.VariableName = $"float3x3_{Builder.GetUniqueNameNumber()}";
+                Builder.AddUniform(value.VariableName, value.ConstantValue);
+                return value;
+            }
+
+            if (_cachedValues.TryGetValue(getFrom, out ShaderExpressionVariable cachedValue))
+            {
+                if (cachedValue is Float3x3 float3x3)
+                {
+                    return float3x3;
+                }
+            }
+        }
+
+        var val = getFrom.Value(this);
+        _cachedValues[getFrom] = val;
+
+        return val;
+    }
+
+    public Float3x3 NewFloat3x3(Expression m00, Expression m01, Expression m02,
+        Expression m10, Expression m11, Expression m12,
+        Expression m20, Expression m21, Expression m22)
+    {
+        if (!HasContext && m00 is Float1 firstFloat && m01 is Float1 secondFloat && m02 is Float1 thirdFloat &&
+            m10 is Float1 fourthFloat && m11 is Float1 fifthFloat && m12 is Float1 sixthFloat &&
+            m20 is Float1 seventhFloat && m21 is Float1 eighthFloat && m22 is Float1 ninthFloat)
+        {
+            Float3x3 constantMatrix = new Float3x3("");
+            constantMatrix.ConstantValue = new Matrix3X3(
+                (float)firstFloat.ConstantValue, (float)secondFloat.ConstantValue, (float)thirdFloat.ConstantValue,
+                (float)fourthFloat.ConstantValue, (float)fifthFloat.ConstantValue, (float)sixthFloat.ConstantValue,
+                (float)seventhFloat.ConstantValue, (float)eighthFloat.ConstantValue, (float)ninthFloat.ConstantValue);
+            return constantMatrix;
+        }
+
+        return Builder.ConstructFloat3x3(m00, m01, m02, m10, m11, m12, m20, m21, m22);
+    }
+
+    public Float3x3 NewFloat3x3(Expression matrixExpression)
+    {
+        if (!HasContext && matrixExpression is Float3x3 float3x3)
+        {
+            Float3x3 constantMatrix = new Float3x3("");
+            constantMatrix.ConstantValue = float3x3.ConstantValue;
+            return constantMatrix;
+        }
+
+        return Builder.AssignNewFloat3x3(matrixExpression);
+    }
 }
 }

+ 99 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/ComposeMatrixNode.cs

@@ -0,0 +1,99 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+
+[NodeInfo("ComposeMatrix")]
+public class ComposeMatrixNode : Node
+{
+    public FuncInputProperty<Float3x3> MatrixInput { get; }
+    public FuncInputProperty<Float1> ScaleX { get; }
+    public FuncInputProperty<Float1> SkewX { get; }
+    public FuncInputProperty<Float1> TransX { get; }
+    public FuncInputProperty<Float1> SkewY { get; }
+    public FuncInputProperty<Float1> ScaleY { get; }
+    public FuncInputProperty<Float1> TransY { get; }
+    public FuncInputProperty<Float1> Persp0 { get; }
+    public FuncInputProperty<Float1> Persp1 { get; }
+    public FuncInputProperty<Float1> Persp2 { get; }
+
+    public FuncOutputProperty<Float3x3> Matrix { get; }
+
+    public ComposeMatrixNode()
+    {
+        MatrixInput = CreateFuncInput<Float3x3>("MatrixInput", "INPUT_MATRIX",
+            new Float3x3("") { ConstantValue = Matrix3X3.Identity });
+
+        ScaleX = CreateFuncInput<Float1>("ScaleX", "SCALE_X", 1.0f);
+        ScaleY = CreateFuncInput<Float1>("ScaleY", "SCALE_Y", 1.0f);
+        SkewX = CreateFuncInput<Float1>("SkewX", "SKEW_X", 0.0f);
+        SkewY = CreateFuncInput<Float1>("SkewY", "SKEW_Y", 0.0f);
+        TransX = CreateFuncInput<Float1>("TranslateX", "TRANSLATE_X", 0.0f);
+        TransY = CreateFuncInput<Float1>("TranslateY", "TRANSLATE_Y", 0.0f);
+        Persp0 = CreateFuncInput<Float1>("Perspective0", "PERSPECTIVE_0", 0.0f);
+        Persp1 = CreateFuncInput<Float1>("Perspective1", "PERSPECTIVE_1", 0.0f);
+        Persp2 = CreateFuncInput<Float1>("Perspective2", "PERSPECTIVE_2", 1.0f);
+
+        Matrix = CreateFuncOutput<Float3x3>("Matrix", "MATRIX", ComposeMatrix);
+    }
+
+    private Float3x3 ComposeMatrix(FuncContext context)
+    {
+        if (context.HasContext)
+        {
+            var composed = context.NewFloat3x3(
+                context.GetValue(ScaleX),
+                context.GetValue(SkewY),
+                context.GetValue(Persp0),
+                context.GetValue(SkewX),
+                context.GetValue(ScaleY),
+                context.GetValue(Persp1),
+                context.GetValue(TransX),
+                context.GetValue(TransY),
+                context.GetValue(Persp2)
+            );
+
+            if (MatrixInput.Connection != null)
+            {
+                return context.NewFloat3x3(ShaderMath.PostConcat(context.GetValue(MatrixInput), composed));
+            }
+
+            return composed;
+        }
+
+        var mtx = new Float3x3("")
+        {
+            ConstantValue
+                = new Matrix3X3(
+                    (float)(context.GetValue(ScaleX).GetConstant() as double? ?? 1.0),
+                    (float)(context.GetValue(SkewX).GetConstant() as double? ?? 0.0),
+                    (float)(context.GetValue(TransX).GetConstant() as double? ?? 0.0),
+                    (float)(context.GetValue(SkewY).GetConstant() as double? ?? 0.0),
+                    (float)(context.GetValue(ScaleY).GetConstant() as double? ?? 1.0),
+                    (float)(context.GetValue(TransY).GetConstant() as double? ?? 0.0),
+                    (float)(context.GetValue(Persp0).GetConstant() as double? ?? 0.0),
+                    (float)(context.GetValue(Persp1).GetConstant() as double? ?? 0.0),
+                    (float)(context.GetValue(Persp2).GetConstant() as double? ?? 1.0))
+        };
+
+        if (MatrixInput.Connection != null)
+        {
+            mtx.ConstantValue = mtx.ConstantValue.PostConcat(
+                (context.GetValue(MatrixInput).ConstantValue as Matrix3X3?) ?? Matrix3X3.Identity);
+        }
+
+        return mtx;
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+    }
+
+    public override Node CreateCopy()
+    {
+        return new ComposeMatrixNode();
+    }
+}

+ 47 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/DecomposeMatrixNode.cs

@@ -0,0 +1,47 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+
+[NodeInfo("DecomposeMatrix")]
+public class DecomposeMatrixNode : Node
+{
+    public FuncInputProperty<Float3x3> Matrix { get; }
+
+    public FuncOutputProperty<Float1> ScaleX { get; }
+    public FuncOutputProperty<Float1> SkewX { get; }
+    public FuncOutputProperty<Float1> TransX { get; }
+    public FuncOutputProperty<Float1> SkewY { get; }
+    public FuncOutputProperty<Float1> ScaleY { get; }
+    public FuncOutputProperty<Float1> TransY { get; }
+    public FuncOutputProperty<Float1> Persp0 { get; }
+    public FuncOutputProperty<Float1> Persp1 { get; }
+    public FuncOutputProperty<Float1> Persp2 { get; }
+
+    public DecomposeMatrixNode()
+    {
+        Matrix = CreateFuncInput<Float3x3>("Matrix", "MATRIX",
+            new Float3x3("") { ConstantValue = Matrix3X3.Identity });
+        ScaleX = CreateFuncOutput<Float1>("ScaleX", "SCALE_X", context => context.GetValue(Matrix).M11);
+        ScaleY = CreateFuncOutput<Float1>("ScaleY", "SCALE_Y", context => context.GetValue(Matrix).M22);
+        SkewX = CreateFuncOutput<Float1>("SkewX", "SKEW_X", context => context.GetValue(Matrix).M12);
+        SkewY = CreateFuncOutput<Float1>("SkewY", "SKEW_Y", context => context.GetValue(Matrix).M21);
+        TransX = CreateFuncOutput<Float1>("TranslateX", "TRANSLATE_X", context => context.GetValue(Matrix).M13);
+        TransY = CreateFuncOutput<Float1>("TranslateY", "TRANSLATE_Y", context => context.GetValue(Matrix).M23);
+        Persp0 = CreateFuncOutput<Float1>("Perspective0", "PERSPECTIVE_0", context => context.GetValue(Matrix).M31);
+        Persp1 = CreateFuncOutput<Float1>("Perspective1", "PERSPECTIVE_1", context => context.GetValue(Matrix).M32);
+        Persp2 = CreateFuncOutput<Float1>("Perspective2", "PERSPECTIVE_2", context => context.GetValue(Matrix).M33);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+
+    }
+
+    public override Node CreateCopy()
+    {
+        return new DecomposeMatrixNode();
+    }
+}

+ 12 - 7
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs

@@ -1,7 +1,9 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 
 
@@ -10,21 +12,22 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
 public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
 {
 {
     public RenderInputProperty Background { get; }
     public RenderInputProperty Background { get; }
-    public InputProperty<Matrix3X3> Input { get; }
-    public OutputProperty<Matrix3X3> Matrix { get; }
+    public FuncInputProperty<Float3x3> Input { get; }
+    public FuncOutputProperty<Float3x3> Matrix { get; }
 
 
     public Matrix3X3BaseNode()
     public Matrix3X3BaseNode()
     {
     {
         Background = CreateRenderInput("Background", "IMAGE");
         Background = CreateRenderInput("Background", "IMAGE");
-        Input = CreateInput("Input", "INPUT_MATRIX", Matrix3X3.Identity);
-        Matrix = CreateOutput("Matrix", "OUTPUT_MATRIX", Matrix3X3.Identity);
+        Input = CreateFuncInput<Float3x3>("Input", "INPUT_MATRIX",
+            new Float3x3("") { ConstantValue = Matrix3X3.Identity });
+        Matrix = CreateFuncOutput<Float3x3>("Matrix", "OUTPUT_MATRIX",
+            (c) => CalculateMatrix(c, c.GetValue(Input)));
         Output.FirstInChain = null;
         Output.FirstInChain = null;
         AllowHighDpiRendering = true;
         AllowHighDpiRendering = true;
     }
     }
 
 
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
-        Matrix.Value = CalculateMatrix(Input.Value);
         if (Background.Value == null)
         if (Background.Value == null)
             return;
             return;
 
 
@@ -35,7 +38,9 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
     {
     {
         int layer = surface.Canvas.Save();
         int layer = surface.Canvas.Save();
 
 
-        surface.Canvas.SetMatrix(surface.Canvas.TotalMatrix.Concat(Matrix.Value));
+        Float3x3 mtx = Matrix.Value.Invoke(FuncContext.NoContext);
+
+        surface.Canvas.SetMatrix(surface.Canvas.TotalMatrix.Concat(mtx.GetConstant() as Matrix3X3? ?? Matrix3X3.Identity));
         if (!surface.LocalClipBounds.IsZeroOrNegativeArea)
         if (!surface.LocalClipBounds.IsZeroOrNegativeArea)
         {
         {
             Background.Value?.Paint(context, surface);
             Background.Value?.Paint(context, surface);
@@ -44,5 +49,5 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
         surface.Canvas.RestoreToCount(layer);
         surface.Canvas.RestoreToCount(layer);
     }
     }
 
 
-    protected abstract Matrix3X3 CalculateMatrix(Matrix3X3 input);
+    protected abstract Float3x3 CalculateMatrix(FuncContext ctx, Float3x3 input);
 }
 }

+ 0 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/MatrixNode.cs

@@ -1,6 +0,0 @@
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
-
-public class MatrixNode
-{
-
-}

+ 30 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/OffsetNode.cs

@@ -1,22 +1,47 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 
 [NodeInfo("Offset")]
 [NodeInfo("Offset")]
 public class OffsetNode : Matrix3X3BaseNode
 public class OffsetNode : Matrix3X3BaseNode
 {
 {
-    public InputProperty<VecD> Translation { get; }
+    public FuncInputProperty<Float2> Translation { get; }
 
 
     public OffsetNode()
     public OffsetNode()
     {
     {
-        Translation = CreateInput("Offset", "OFFSET", VecD.Zero);
+        Translation = CreateFuncInput<Float2>("Offset", "OFFSET", VecD.Zero);
     }
     }
 
 
-    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
+    protected override Float3x3 CalculateMatrix(FuncContext ctx, Float3x3 input)
     {
     {
-        Matrix3X3 matrix = Matrix3X3.CreateTranslation((float)(Translation.Value.X), (float)(Translation.Value.Y));
-        return input.PostConcat(matrix);
+        Float2 translation = ctx.GetValue(Translation);
+
+        Float1 one = new Float1("") { ConstantValue = 1.0 };
+        Float1 zero = new Float1("") { ConstantValue = 0.0 };
+
+
+        if (ctx.HasContext)
+        {
+            var translationMatrix = ctx.NewFloat3x3(
+                one, zero, zero,
+                zero, one, zero,
+                translation.X, translation.Y, one
+            );
+
+            return ctx.NewFloat3x3(ShaderMath.PostConcat(input, translationMatrix));
+        }
+
+        Matrix3X3 contextlessTranslationMatrix = Matrix3X3.CreateTranslation(
+            (float)(translation.X.GetConstant() as double? ?? 0.0),
+            (float)(translation.Y.GetConstant() as double? ?? 0.0)
+        );
+        return new Float3x3("")
+        {
+            ConstantValue = input.ConstantValue.PostConcat(contextlessTranslationMatrix)
+        };
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

+ 42 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/RotateNode.cs

@@ -1,5 +1,7 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 
@@ -7,27 +9,57 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 public class RotateNode : Matrix3X3BaseNode
 public class RotateNode : Matrix3X3BaseNode
 {
 {
     public InputProperty<RotationType> RotationType { get; }
     public InputProperty<RotationType> RotationType { get; }
-    public InputProperty<double> Angle { get; }
-    public InputProperty<VecD> Center { get; }
+    public FuncInputProperty<Float1> Angle { get; }
+    public FuncInputProperty<Float2> Center { get; }
 
 
     public RotateNode()
     public RotateNode()
     {
     {
         RotationType = CreateInput("RotationType", "UNIT", Nodes.Matrix.RotationType.Degrees);
         RotationType = CreateInput("RotationType", "UNIT", Nodes.Matrix.RotationType.Degrees);
-        Angle = CreateInput("Angle", "ANGLE", 0.0);
-        Center = CreateInput("Center", "CENTER", new VecD(0, 0));
+        Angle = CreateFuncInput<Float1>("Angle", "ANGLE", 0.0);
+        Center = CreateFuncInput<Float2>("Center", "CENTER", new VecD(0, 0));
     }
     }
 
 
-    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
+    protected override Float3x3 CalculateMatrix(FuncContext ctx, Float3x3 input)
     {
     {
-        VecD scaledCenter = new VecD(Center.Value.X, Center.Value.Y);
-        Matrix3X3 rotated = RotationType.Value switch
+        Float1 angle = ctx.GetValue(Angle);
+
+        Float2 center = ctx.GetValue(Center);
+
+        Float1 one = new Float1("") { ConstantValue = 1.0 };
+        Float1 zero = new Float1("") { ConstantValue = 0.0 };
+
+        if (ctx.HasContext)
+        {
+            if (RotationType.Value == Nodes.Matrix.RotationType.Degrees)
+            {
+                angle = ctx.NewFloat1(ShaderMath.DegreesToRadians(angle));
+            }
+
+            var rotationMatrix = ctx.NewFloat3x3(
+                ShaderMath.Cos(angle), ShaderMath.Sin(angle), zero,
+                new Expression($"-{ShaderMath.Sin(angle).ExpressionValue}"), ShaderMath.Cos(angle), zero,
+                new Expression(
+                    $"{center.X.ExpressionValue} * (1.0 - {ShaderMath.Cos(angle)}) + {center.Y.ExpressionValue} * {ShaderMath.Sin(angle)}"), // m02 → col 2, row 0
+                new Expression(
+                    $"{center.Y.ExpressionValue} * (1.0 - {ShaderMath.Cos(angle)}) - {center.X.ExpressionValue} * {ShaderMath.Sin(angle)}"), // m12 → col 2, row 1
+                one
+            );
+
+            return ctx.NewFloat3x3(ShaderMath.PostConcat(input, rotationMatrix));
+        }
+
+        Matrix3X3 rotationContextlessMatrix = RotationType.Value switch
         {
         {
-            Nodes.Matrix.RotationType.Degrees => Matrix3X3.CreateRotationDegrees((float)Angle.Value, (float)scaledCenter.X, (float)scaledCenter.Y),
-            Nodes.Matrix.RotationType.Radians => Matrix3X3.CreateRotation((float)Angle.Value, (float)scaledCenter.X, (float)scaledCenter.Y),
+            Nodes.Matrix.RotationType.Degrees => Matrix3X3.CreateRotationDegrees(
+                (float)(angle.GetConstant() as double? ?? 0.0), (float)(center.X.GetConstant() as double? ?? 0),
+                (float)(center.Y.GetConstant() as double? ?? 0)),
+            Nodes.Matrix.RotationType.Radians => Matrix3X3.CreateRotation(
+                (float)(angle.GetConstant() as double? ?? 0.0), (float)(center.X.GetConstant() as double? ?? 0),
+                (float)(center.Y.GetConstant() as double? ?? 0)),
             _ => throw new ArgumentOutOfRangeException()
             _ => throw new ArgumentOutOfRangeException()
         };
         };
 
 
-        return input.PostConcat(rotated);
+        return new Float3x3("") { ConstantValue = input.ConstantValue.PostConcat(rotationContextlessMatrix) };
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

+ 39 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/ScaleNode.cs

@@ -1,25 +1,58 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 
 [NodeInfo("Scale")]
 [NodeInfo("Scale")]
 public class ScaleNode : Matrix3X3BaseNode
 public class ScaleNode : Matrix3X3BaseNode
 {
 {
-    public InputProperty<VecD> Scale { get; }
-    public InputProperty<VecD> Center { get; }
+    public FuncInputProperty<Float2> Scale { get; }
+    public FuncInputProperty<Float2> Center { get; }
 
 
     public ScaleNode()
     public ScaleNode()
     {
     {
-        Scale = CreateInput("Scale", "SCALE", new VecD(1, 1));
-        Center = CreateInput("Center", "CENTER", new VecD(0, 0));
+        Scale = CreateFuncInput<Float2>("Scale", "SCALE", new VecD(1, 1));
+        Center = CreateFuncInput<Float2>("Center", "CENTER", new VecD(0, 0));
     }
     }
 
 
-    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
+    protected override Float3x3 CalculateMatrix(FuncContext ctx, Float3x3 input)
+    {
+        Float2 scale = ctx.GetValue(Scale);
+        Float2 center = ctx.GetValue(Center);
+
+        Float1 one = new Float1("") { ConstantValue = 1.0 };
+        Float1 zero = new Float1("") { ConstantValue = 0.0 };
+
+        if (ctx.HasContext)
+        {
+            var scaleMatrix = ctx.NewFloat3x3(
+                scale.X, zero, zero,
+                zero, scale.Y, zero,
+                new Expression($"{center.X.ExpressionValue} * (1.0 - {scale.X.ExpressionValue})"),
+                new Expression($"{center.Y.ExpressionValue} * (1.0 - {scale.Y.ExpressionValue})"),
+                one
+            );
+
+            return ctx.NewFloat3x3(ShaderMath.PostConcat(input, scaleMatrix));
+        }
+
+        Matrix3X3 scaleContextlessMatrix = Matrix3X3.CreateScale(
+            (float)(scale.X.GetConstant() as double? ?? 1.0f),
+            (float)(scale.Y.GetConstant() as double? ?? 1.0f),
+            (float)(center.X.GetConstant() as double? ?? 0.0f),
+            (float)(center.Y.GetConstant() as double? ?? 0.0f)
+        );
+
+        return new Float3x3("") { ConstantValue = input.ConstantValue.PostConcat(scaleContextlessMatrix) };
+    }
+
+    /*protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
     {
     {
         Matrix3X3 scaled = Matrix3X3.CreateScale((float)Scale.Value.X, (float)Scale.Value.Y, (float)Center.Value.X, (float)Center.Value.Y);
         Matrix3X3 scaled = Matrix3X3.CreateScale((float)Scale.Value.X, (float)Scale.Value.Y, (float)Center.Value.X, (float)Center.Value.Y);
         return input.PostConcat(scaled);
         return input.PostConcat(scaled);
-    }
+    }*/
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()
     {
     {

+ 30 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/SkewNode.cs

@@ -1,22 +1,47 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 
 [NodeInfo("Skew")]
 [NodeInfo("Skew")]
 public class SkewNode : Matrix3X3BaseNode
 public class SkewNode : Matrix3X3BaseNode
 {
 {
-    public InputProperty<VecD> Skew { get; }
+    public FuncInputProperty<Float2> Skew { get; }
 
 
     public SkewNode()
     public SkewNode()
     {
     {
-        Skew = CreateInput("Skew", "SKEW", VecD.Zero);
+        Skew = CreateFuncInput<Float2>("Skew", "SKEW", VecD.Zero);
     }
     }
 
 
-    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
+    protected override Float3x3 CalculateMatrix(FuncContext ctx, Float3x3 input)
     {
     {
-        Matrix3X3 matrix = Matrix3X3.CreateSkew((float)Skew.Value.X, (float)Skew.Value.Y);
-        return input.PostConcat(matrix);
+        Float2 skew = ctx.GetValue(Skew);
+
+        Float1 one = new Float1("") { ConstantValue = 1.0 };
+        Float1 zero = new Float1("") { ConstantValue = 0.0 };
+
+        if (ctx.HasContext)
+        {
+            var skewMatrix = ctx.NewFloat3x3(
+                one, skew.Y, zero,
+                skew.X, one, zero,
+                zero, zero, one
+            );
+
+            return ctx.NewFloat3x3(ShaderMath.PostConcat(input, skewMatrix));
+        }
+
+        Matrix3X3 skewContextlessMatrix = Matrix3X3.CreateSkew(
+            (float)(skew.X.GetConstant() as double? ?? 0.0),
+            (float)(skew.Y.GetConstant() as double? ?? 0.0)
+        );
+
+        return new Float3x3("")
+        {
+            ConstantValue = input.ConstantValue.PostConcat(skewContextlessMatrix)
+        };
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

+ 28 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/TransformNode.cs

@@ -1,21 +1,43 @@
-using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Numerics;
 using Drawie.Numerics;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 
 [NodeInfo("Transform")]
 [NodeInfo("Transform")]
 public class TransformNode : Matrix3X3BaseNode
 public class TransformNode : Matrix3X3BaseNode
 {
 {
-    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
+    public FuncInputProperty<Float2> Position { get; }
+    public FuncOutputProperty<Float2> TransformedPosition { get; }
+
+    public TransformNode()
+    {
+        Position = CreateFuncInput<Float2>("Position", "POSITION", VecD.Zero);
+        TransformedPosition =
+            CreateFuncOutput<Float2>("TransformedPosition", "TRANSFORMED_POSITION", TransformPosition);
+    }
+
+    private Float2 TransformPosition(FuncContext arg)
     {
     {
-        return Input.Value;
+        if (arg.HasContext)
+        {
+            Float3x3 matrix = CalculateMatrix(arg, arg.GetValue(Input));
+            Float2 position = arg.GetValue(Position);
+            Float3 toTransform = arg.Builder.ConstructFloat3(position.X, position.Y, new Float1("") { ConstantValue = 1 });
+            Float3 transformed = arg.Builder.AssignNewFloat3(new Expression($"{matrix.ExpressionValue} * {toTransform.ExpressionValue}"));
+            return arg.Builder.AssignNewFloat2(new Expression($"{transformed.ExpressionValue}.xy / {transformed.ExpressionValue}.z"));
+        }
+
+        return null;
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()
     {
     {
         return new TransformNode();
         return new TransformNode();
     }
     }
+
+    protected override Float3x3 CalculateMatrix(FuncContext ctx, Float3x3 input)
+    {
+        return input;
+    }
 }
 }

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

@@ -21,6 +21,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
     public FuncOutputProperty<Half4> Color { get; }
     public FuncOutputProperty<Half4> Color { get; }
     
     
     public InputProperty<ColorSampleMode> SampleMode { get; }
     public InputProperty<ColorSampleMode> SampleMode { get; }
+    public InputProperty<bool> NormalizeCoordinates { get; }
 
 
     public Guid OtherNode { get; set; }
     public Guid OtherNode { get; set; }
     
     
@@ -30,6 +31,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
         Coordinate = CreateFuncOutput("Coordinate", "UV", ctx => ctx.OriginalPosition ?? new Float2(""));
         Coordinate = CreateFuncOutput("Coordinate", "UV", ctx => ctx.OriginalPosition ?? new Float2(""));
         Color = CreateFuncOutput("Color", "COLOR", GetColor);
         Color = CreateFuncOutput("Color", "COLOR", GetColor);
         SampleMode = CreateInput("SampleMode", "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
         SampleMode = CreateInput("SampleMode", "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
+        NormalizeCoordinates = CreateInput("NormalizeCoordinates", "NORMALIZE_COORDINATES", true);
     }
     }
     
     
     private Half4 GetColor(FuncContext context)
     private Half4 GetColor(FuncContext context)
@@ -41,7 +43,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
             return new Half4("") { ConstantValue = Colors.Transparent };
             return new Half4("") { ConstantValue = Colors.Transparent };
         }
         }
 
 
-        return context.SampleSurface(Image.Value.DrawingSurface, context.SamplePosition, SampleMode.Value);
+        return context.SampleSurface(Image.Value.DrawingSurface, context.SamplePosition, SampleMode.Value, NormalizeCoordinates.Value);
     }
     }
 
 
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)

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

@@ -63,7 +63,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
 
         size = imgSize;
         size = imgSize;
 
 
-        ShaderBuilder builder = new(size.Value);
+        ShaderBuilder builder = new(size.Value, startNode.NormalizeCoordinates.Value);
         FuncContext context = new(renderContext, builder);
         FuncContext context = new(renderContext, builder);
 
 
         if (Coordinate.Connection != null)
         if (Coordinate.Connection != null)
@@ -88,14 +88,14 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
 
         if (Color.Connection != null)
         if (Color.Connection != null)
         {
         {
-            builder.ReturnVar(Color.Value(context));
+            builder.ReturnVar(Color.Value(context), false);
         }
         }
         else
         else
         {
         {
             Half4 color = Color.NonOverridenValue(FuncContext.NoContext);
             Half4 color = Color.NonOverridenValue(FuncContext.NoContext);
             color.VariableName = "color";
             color.VariableName = "color";
             builder.AddUniform(color.VariableName, color.ConstantValue);
             builder.AddUniform(color.VariableName, color.ConstantValue);
-            builder.ReturnVar(color);
+            builder.ReturnVar(color, false); // Do not premultiply, since we are modifying already premultiplied image
         }
         }
 
 
         string sksl = builder.ToSkSl();
         string sksl = builder.ToSkSl();

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

@@ -20,12 +20,15 @@ public class SampleImageNode : Node
 
 
     public InputProperty<ColorSampleMode> SampleMode { get; }
     public InputProperty<ColorSampleMode> SampleMode { get; }
 
 
+    public InputProperty<bool> NormalizedCoordinates { get; }
+
     public SampleImageNode()
     public SampleImageNode()
     {
     {
-        Image = CreateInput<Texture>(nameof(Texture), "IMAGE", null);
-        Coordinate = CreateFuncInput<Float2>(nameof(Coordinate), "UV", VecD.Zero);
-        Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
-        SampleMode = CreateInput(nameof(SampleMode), "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
+        Image = CreateInput<Texture>("Texture", "IMAGE", null);
+        Coordinate = CreateFuncInput<Float2>("Coordinate", "UV", VecD.Zero);
+        Color = CreateFuncOutput("Color", "COLOR", GetColor);
+        SampleMode = CreateInput("SampleMode", "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
+        NormalizedCoordinates = CreateInput("NormalizedCoordinates", "NORMALIZE_COORDINATES", true);
     }
     }
 
 
     private Half4 GetColor(FuncContext context)
     private Half4 GetColor(FuncContext context)
@@ -39,11 +42,20 @@ public class SampleImageNode : Node
         {
         {
             Expression uv = context.GetValue(Coordinate);
             Expression uv = context.GetValue(Coordinate);
 
 
-            return context.SampleSurface(Image.Value.DrawingSurface, uv, SampleMode.Value);
+            return context.SampleSurface(Image.Value.DrawingSurface, uv, SampleMode.Value, NormalizedCoordinates.Value);
         }
         }
 
 
         Color color;
         Color color;
-        VecI pixelCoordinate = (VecI)context.GetValue(Coordinate).ConstantValue.Round();
+
+        VecD coordinate = context.GetValue(Coordinate).ConstantValue;
+        VecI pixelCoordinate = (VecI)coordinate.Round();
+
+        if(NormalizedCoordinates.Value)
+        {
+            VecD size = Image.Value.Size;
+            pixelCoordinate = (VecI)(new VecD(coordinate.X * size.X, coordinate.Y * size.Y)).Round();
+        }
+
         if (SampleMode.Value == ColorSampleMode.ColorManaged)
         if (SampleMode.Value == ColorSampleMode.ColorManaged)
         {
         {
             color = Image.Value.GetSRGBPixel(pixelCoordinate);
             color = Image.Value.GetSRGBPixel(pixelCoordinate);

+ 89 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ImportNode_Change.cs

@@ -0,0 +1,89 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class ImportNode_Change : Change
+{
+    private ICrossDocumentPipe<IReadOnlyNode> sourceDocumentPipe;
+    private Guid duplicateGuid;
+    private ConnectionsData connectionsData;
+    private Node? cloned;
+
+    [GenerateMakeChangeAction]
+    public ImportNode_Change(ICrossDocumentPipe<IReadOnlyNode> pipe, Guid newGuid)
+    {
+        sourceDocumentPipe = pipe;
+        duplicateGuid = newGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (sourceDocumentPipe is not { CanOpen: true })
+            return false;
+
+        if (!sourceDocumentPipe.IsOpen)
+        {
+            sourceDocumentPipe.Open();
+        }
+
+        IReadOnlyNode? node = sourceDocumentPipe.TryAccessData();
+        if (node == null || target.NodeGraph.OutputNode == null)
+            return false;
+
+        connectionsData = NodeOperations.CreateConnectionsData(target.NodeGraph.OutputNode);
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+
+        var node = cloned ?? sourceDocumentPipe.TryAccessData();
+        if (node is not Node graphNode)
+        {
+            ignoreInUndo = true;
+            return new None();
+        }
+
+        var clone = (Node)graphNode.Clone();
+        clone.Id = duplicateGuid;
+        cloned = clone;
+
+        target.NodeGraph.AddNode(clone);
+
+        return CreateNode_ChangeInfo.CreateFromNode(clone);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var node = target.FindNode(duplicateGuid);
+
+        target.NodeGraph.RemoveNode(node);
+        node.Dispose();
+
+        List<IChangeInfo> changes = new();
+
+        changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, node));
+        changes.Add(new DeleteNode_ChangeInfo(node.Id));
+
+        if (connectionsData is not null)
+        {
+            Node originalNode = target.NodeGraph.OutputNode;
+            changes.AddRange(
+                NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
+        }
+
+        return changes;
+    }
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        sourceDocumentPipe.Dispose();
+        cloned?.Dispose();
+    }
+}

+ 52 - 28
src/PixiEditor.ChangeableDocument/Changes/Structure/ImportFolder_Change.cs

@@ -15,7 +15,10 @@ internal class ImportFolder_Change : Change
     private ICrossDocumentPipe<IReadOnlyFolderNode> sourcefolderPipe;
     private ICrossDocumentPipe<IReadOnlyFolderNode> sourcefolderPipe;
     private Guid duplicateGuid;
     private Guid duplicateGuid;
     private Guid[] contentGuids;
     private Guid[] contentGuids;
-    private Guid[] contentDuplicateGuids;
+
+    private FolderNode? clonedFolderNode;
+    private List<Node> clonedContentNodes = new();
+    private Dictionary<Guid, Guid> contentGuidToNodeMap;
 
 
     private Guid[]? childGuidsToUse;
     private Guid[]? childGuidsToUse;
 
 
@@ -55,7 +58,7 @@ internal class ImportFolder_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
         out bool ignoreInUndo)
     {
     {
-        var readOnlyFolderNode = sourcefolderPipe.TryAccessData();
+        var readOnlyFolderNode = clonedFolderNode ?? sourcefolderPipe.TryAccessData();
 
 
         if (readOnlyFolderNode is not FolderNode folderNode || target.NodeGraph.OutputNode == null)
         if (readOnlyFolderNode is not FolderNode folderNode || target.NodeGraph.OutputNode == null)
         {
         {
@@ -65,6 +68,7 @@ internal class ImportFolder_Change : Change
 
 
         FolderNode clone = (FolderNode)folderNode.Clone();
         FolderNode clone = (FolderNode)folderNode.Clone();
         clone.Id = duplicateGuid;
         clone.Id = duplicateGuid;
+        clonedFolderNode = clone;
 
 
         InputProperty<Painter?> targetInput = target.NodeGraph.OutputNode.InputProperties.FirstOrDefault(x =>
         InputProperty<Painter?> targetInput = target.NodeGraph.OutputNode.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter)) as InputProperty<Painter?>;
             x.ValueType == typeof(Painter)) as InputProperty<Painter?>;
@@ -99,11 +103,11 @@ internal class ImportFolder_Change : Change
         changes.AddRange(NodeOperations.DetachStructureNode(member));
         changes.AddRange(NodeOperations.DetachStructureNode(member));
         changes.Add(new DeleteStructureMember_ChangeInfo(member.Id));
         changes.Add(new DeleteStructureMember_ChangeInfo(member.Id));
 
 
-        if (contentDuplicateGuids is not null)
+        if (clonedContentNodes is not null)
         {
         {
-            foreach (Guid contentGuid in contentDuplicateGuids)
+            foreach (var content in clonedContentNodes)
             {
             {
-                Node contentNode = target.FindNodeOrThrow<Node>(contentGuid);
+                Node contentNode = target.FindNodeOrThrow<Node>(content.Id);
                 changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
                 changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
                 changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
                 changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
 
 
@@ -127,42 +131,62 @@ internal class ImportFolder_Change : Change
     private void DuplicateContent(Document target, FolderNode clone, FolderNode existingLayer,
     private void DuplicateContent(Document target, FolderNode clone, FolderNode existingLayer,
         List<IChangeInfo> operations)
         List<IChangeInfo> operations)
     {
     {
-        Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
-
-        nodeMap[existingLayer.Id] = clone.Id;
-        int counter = 0;
-        List<Guid> contentGuidList = new();
-
-        existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
+        if (contentGuidToNodeMap == null)
         {
         {
-            if (x is not Node targetNode)
-                return false;
+            contentGuidToNodeMap = new Dictionary<Guid, Guid>();
 
 
-            Node? node = targetNode.Clone();
+            contentGuidToNodeMap[existingLayer.Id] = clone.Id;
+            int counter = 0;
 
 
-            if (node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+            existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
             {
             {
-                node.Id = childGuidsToUse[counter];
-                counter++;
-            }
+                if (x is not Node targetNode)
+                    return false;
 
 
-            nodeMap[x.Id] = node.Id;
-            contentGuidList.Add(node.Id);
+                Node? node = targetNode.Clone();
+                clonedContentNodes.Add(node.Clone(true));
 
 
-            target.NodeGraph.AddNode(node);
+                if (node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+                {
+                    node.Id = childGuidsToUse[counter];
+                    counter++;
+                }
 
 
-            operations.Add(CreateNode_ChangeInfo.CreateFromNode(node));
-            return true;
-        });
+                contentGuidToNodeMap[x.Id] = node.Id;
+
+                target.NodeGraph.AddNode(node);
+
+                operations.Add(CreateNode_ChangeInfo.CreateFromNode(node));
+                return true;
+            });
+        }
+        else
+        {
+            foreach (var clonedContentNode in clonedContentNodes)
+            {
+                var toAdd = clonedContentNode.Clone(true);
+                target.NodeGraph.AddNode(toAdd);
+                operations.Add(CreateNode_ChangeInfo.CreateFromNode(toAdd));
+            }
+        }
 
 
         foreach (var data in contentConnectionsData)
         foreach (var data in contentConnectionsData)
         {
         {
-            var updatedData = data.Value.WithUpdatedIds(nodeMap);
-            Guid targetNodeId = nodeMap[data.Key];
+            var updatedData = data.Value.WithUpdatedIds(contentGuidToNodeMap);
+            Guid targetNodeId = contentGuidToNodeMap[data.Key];
             operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
             operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
                 target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
                 target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
         }
         }
+    }
 
 
-        contentDuplicateGuids = contentGuidList.ToArray();
+    public override void Dispose()
+    {
+        base.Dispose();
+        sourcefolderPipe.Dispose();
+        clonedFolderNode?.Dispose();
+        foreach (var node in clonedContentNodes)
+        {
+            node?.Dispose();
+        }
     }
     }
 }
 }

+ 8 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/ImportLayer_Change.cs

@@ -14,6 +14,8 @@ internal class ImportLayer_Change : Change
     private Dictionary<Guid, VecD> originalPositions;
     private Dictionary<Guid, VecD> originalPositions;
     private ConnectionsData? connectionsData;
     private ConnectionsData? connectionsData;
 
 
+    private LayerNode? clonedLayer;
+
     private Guid duplicateGuid;
     private Guid duplicateGuid;
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
@@ -37,9 +39,10 @@ internal class ImportLayer_Change : Change
         if (layer == null || target.NodeGraph.OutputNode == null)
         if (layer == null || target.NodeGraph.OutputNode == null)
             return false;
             return false;
 
 
+        if (target.NodeGraph.OutputNode == null) return false;
+
         connectionsData = NodeOperations.CreateConnectionsData(target.NodeGraph.OutputNode);
         connectionsData = NodeOperations.CreateConnectionsData(target.NodeGraph.OutputNode);
 
 
-        if (target.NodeGraph.OutputNode == null) return false;
 
 
         return true;
         return true;
     }
     }
@@ -49,7 +52,7 @@ internal class ImportLayer_Change : Change
     {
     {
         ignoreInUndo = false;
         ignoreInUndo = false;
 
 
-        var layer = sourceDocumentPipe.TryAccessData();
+        var layer = clonedLayer ?? sourceDocumentPipe.TryAccessData();
         if (layer is not LayerNode layerNode)
         if (layer is not LayerNode layerNode)
         {
         {
             ignoreInUndo = true;
             ignoreInUndo = true;
@@ -58,6 +61,8 @@ internal class ImportLayer_Change : Change
 
 
         var clone = (LayerNode)layerNode.Clone();
         var clone = (LayerNode)layerNode.Clone();
         clone.Id = duplicateGuid;
         clone.Id = duplicateGuid;
+        clonedLayer = clone;
+
         ResizeImageData(clone, target.Size);
         ResizeImageData(clone, target.Size);
 
 
         var targetInput = target.NodeGraph.OutputNode?.InputProperties.FirstOrDefault(x =>
         var targetInput = target.NodeGraph.OutputNode?.InputProperties.FirstOrDefault(x =>
@@ -108,6 +113,7 @@ internal class ImportLayer_Change : Change
     public override void Dispose()
     public override void Dispose()
     {
     {
         sourceDocumentPipe?.Dispose();
         sourceDocumentPipe?.Dispose();
+        clonedLayer?.Dispose();
     }
     }
 
 
     private void ResizeImageData(LayerNode layerNode, VecI docSize)
     private void ResizeImageData(LayerNode layerNode, VecI docSize)

+ 1 - 1
src/PixiEditor.Desktop/PixiEditor.Desktop.csproj

@@ -26,7 +26,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0"/>
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 7 - 1
src/PixiEditor.Desktop/Program.cs

@@ -22,14 +22,20 @@ public class Program
             .With(new Win32PlatformOptions()
             .With(new Win32PlatformOptions()
             {
             {
                 RenderingMode = new Win32RenderingMode[] { Win32RenderingMode.Vulkan },
                 RenderingMode = new Win32RenderingMode[] { Win32RenderingMode.Vulkan },
-                OverlayPopups = true
+                OverlayPopups = true,
             })
             })
             .With(new X11PlatformOptions()
             .With(new X11PlatformOptions()
             {
             {
                 RenderingMode = new X11RenderingMode[] { X11RenderingMode.Vulkan },
                 RenderingMode = new X11RenderingMode[] { X11RenderingMode.Vulkan },
                 OverlayPopups = true,
                 OverlayPopups = true,
             })
             })
+            .With(new SkiaOptions()
+            {
+                MaxGpuResourceSizeBytes = 1024 * 600 * 4 * 12 * 4 // quadruple the default size
+            })
             .WithDrawie()
             .WithDrawie()
+#if DEBUG
             .LogToTrace(LogEventLevel.Verbose, "Vulkan")
             .LogToTrace(LogEventLevel.Verbose, "Vulkan")
+#endif
             .LogToTrace();
             .LogToTrace();
 }
 }

+ 1 - 0
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -165,6 +165,7 @@
             <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}" />
             <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}" />
             <SolidColorBrush x:Key="StringSocketBrush" Color="{StaticResource StringSocketColor}" />
             <SolidColorBrush x:Key="StringSocketBrush" Color="{StaticResource StringSocketColor}" />
             <SolidColorBrush x:Key="Matrix3X3SocketBrush" Color="{StaticResource Matrix3X3SocketColor}" />
             <SolidColorBrush x:Key="Matrix3X3SocketBrush" Color="{StaticResource Matrix3X3SocketColor}" />
+            <SolidColorBrush x:Key="Float3x3SocketBrush" Color="{StaticResource Matrix3X3SocketColor}" />
 
 
             <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush"
             <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush"
                                 GradientStops="{StaticResource ShapeDataSocketGradient}" />
                                 GradientStops="{StaticResource ShapeDataSocketGradient}" />

BIN
src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf


+ 2 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml

@@ -102,6 +102,7 @@
             <system:String x:Key="icon-layers-bottom">&#xE96A;</system:String>
             <system:String x:Key="icon-layers-bottom">&#xE96A;</system:String>
             <system:String x:Key="icon-layers-double">&#xE909;</system:String>
             <system:String x:Key="icon-layers-double">&#xE909;</system:String>
             <system:String x:Key="icon-layers-top">&#xE994;</system:String>
             <system:String x:Key="icon-layers-top">&#xE994;</system:String>
+            <system:String x:Key="icon-leafy-green">&#xE9C8;</system:String>
             <system:String x:Key="icon-letter-spacing">&#xE9B2;</system:String>
             <system:String x:Key="icon-letter-spacing">&#xE9B2;</system:String>
             <system:String x:Key="icon-line">&#xE95A;</system:String>
             <system:String x:Key="icon-line">&#xE95A;</system:String>
             <system:String x:Key="icon-line-height">&#xE9B3;</system:String>
             <system:String x:Key="icon-line-height">&#xE9B3;</system:String>
@@ -150,6 +151,7 @@
             <system:String x:Key="icon-reset">&#xE9AA;</system:String>
             <system:String x:Key="icon-reset">&#xE9AA;</system:String>
             <system:String x:Key="icon-resize">&#xE96B;</system:String>
             <system:String x:Key="icon-resize">&#xE96B;</system:String>
             <system:String x:Key="icon-rotate-view">&#xE96C;</system:String>
             <system:String x:Key="icon-rotate-view">&#xE96C;</system:String>
+            <system:String x:Key="icon-salad">&#xE9C7;</system:String>
             <system:String x:Key="icon-save">&#xE96E;</system:String>
             <system:String x:Key="icon-save">&#xE96E;</system:String>
             <system:String x:Key="icon-scissors">&#xE96F;</system:String>
             <system:String x:Key="icon-scissors">&#xE96F;</system:String>
             <system:String x:Key="icon-search">&#xE996;</system:String>
             <system:String x:Key="icon-search">&#xE996;</system:String>

+ 2 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs

@@ -98,6 +98,7 @@ public static partial class PixiPerfectIcons
     public const string LayersBottom = "\uE96A";
     public const string LayersBottom = "\uE96A";
     public const string LayersDouble = "\uE909";
     public const string LayersDouble = "\uE909";
     public const string LayersTop = "\uE994";
     public const string LayersTop = "\uE994";
+    public const string LeafyGreen = "\uE9C8";
     public const string LetterSpacing = "\uE9B2";
     public const string LetterSpacing = "\uE9B2";
     public const string Line = "\uE95A";
     public const string Line = "\uE95A";
     public const string LineHeight = "\uE9B3";
     public const string LineHeight = "\uE9B3";
@@ -146,6 +147,7 @@ public static partial class PixiPerfectIcons
     public const string Reset = "\uE9AA";
     public const string Reset = "\uE9AA";
     public const string Resize = "\uE96B";
     public const string Resize = "\uE96B";
     public const string RotateView = "\uE96C";
     public const string RotateView = "\uE96C";
+    public const string Salad = "\uE9C7";
     public const string Save = "\uE96E";
     public const string Save = "\uE96E";
     public const string Scissors = "\uE96F";
     public const string Scissors = "\uE96F";
     public const string Search = "\uE996";
     public const string Search = "\uE996";

File diff suppressed because it is too large
+ 0 - 0
src/PixiEditor.UI.Common/Fonts/defs.svg


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

@@ -1076,5 +1076,18 @@
   "NEW_MAX": "New Max",
   "NEW_MAX": "New Max",
   "REMAP_NODE": "Remap",
   "REMAP_NODE": "Remap",
   "TEXT_TOOL_ACTION_DISPLAY": "Click on the canvas to add a new text (drag while clicking to set the size). Click on existing text to edit it.",
   "TEXT_TOOL_ACTION_DISPLAY": "Click on the canvas to add a new text (drag while clicking to set the size). Click on existing text to edit it.",
-  "PASTE_CELS": "Paste cels"
+  "PASTE_CELS": "Paste cels",
+  "SCALE_X": "Scale X",
+  "SCALE_Y": "Scale Y",
+  "TRANSLATE_X": "Translate X",
+  "TRANSLATE_Y": "Translate Y",
+  "SKEW_X": "Skew X",
+  "SKEW_Y": "Skew Y",
+  "PERSPECTIVE_0": "Perspective 0",
+  "PERSPECTIVE_1": "Perspective 1",
+  "PERSPECTIVE_2": "Perspective 2",
+  "COMPOSE_MATRIX": "Compose Matrix",
+  "DECOMPOSE_MATRIX": "Decompose Matrix",
+  "NORMALIZE_COORDINATES": "Normalize Coordinates",
+  "TRANSFORMED_POSITION": "Transformed Position"
 }
 }

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

@@ -598,9 +598,9 @@ internal static class ClipboardController
         return false;
         return false;
     }
     }
 
 
-    public static async Task CopyNodes(Guid[] nodeIds)
+    public static async Task CopyNodes(Guid[] nodeIds, Guid docId)
     {
     {
-        await CopyIds(nodeIds, ClipboardDataFormats.NodeIdList);
+        await CopyIds(nodeIds, ClipboardDataFormats.NodeIdList, docId);
     }
     }
 
 
     public static async Task<Guid[]> GetNodeIds()
     public static async Task<Guid[]> GetNodeIds()
@@ -653,21 +653,42 @@ internal static class ClipboardController
         return formats.Contains(format);
         return formats.Contains(format);
     }
     }
 
 
-    public static async Task CopyCels(Guid[] celIds)
+    public static async Task CopyCels(Guid[] celIds, Guid docId)
     {
     {
-        await CopyIds(celIds, ClipboardDataFormats.CelIdList);
+        await CopyIds(celIds, ClipboardDataFormats.CelIdList, docId);
     }
     }
 
 
-    public static async Task CopyIds(Guid[] ids, string format)
+    public static async Task CopyIds(Guid[] ids, string format, Guid docId)
     {
     {
         await Clipboard.ClearAsync();
         await Clipboard.ClearAsync();
 
 
         DataObject data = new DataObject();
         DataObject data = new DataObject();
 
 
+        data.Set(ClipboardDataFormats.DocumentFormat, Encoding.UTF8.GetBytes(docId.ToString()));
+
         byte[] idsBytes = Encoding.UTF8.GetBytes(string.Join(";", ids.Select(x => x.ToString())));
         byte[] idsBytes = Encoding.UTF8.GetBytes(string.Join(";", ids.Select(x => x.ToString())));
 
 
         data.Set(format, idsBytes);
         data.Set(format, idsBytes);
 
 
         await Clipboard.SetDataObjectAsync(data);
         await Clipboard.SetDataObjectAsync(data);
     }
     }
+
+    public static async Task<Guid> GetDocumentId()
+    {
+        var data = await TryGetDataObject();
+        if (data == null)
+            return Guid.Empty;
+
+        foreach (var dataObject in data)
+        {
+            if (dataObject.Contains(ClipboardDataFormats.DocumentFormat))
+            {
+                byte[] guidBytes = (byte[])dataObject.Get(ClipboardDataFormats.DocumentFormat);
+                string guidString = System.Text.Encoding.UTF8.GetString(guidBytes);
+                return Guid.Parse(guidString);
+            }
+        }
+
+        return Guid.Empty;
+    }
 }
 }

+ 57 - 17
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -1,7 +1,4 @@
-using System.Collections;
-using System.Collections.Immutable;
-using System.Reactive.Disposables;
-using ChunkyImageLib.DataHolders;
+using System.Collections.Immutable;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
@@ -24,7 +21,6 @@ using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.ViewModels.Document.Nodes;
 
 
 namespace PixiEditor.Models.DocumentModels.Public;
 namespace PixiEditor.Models.DocumentModels.Public;
 #nullable enable
 #nullable enable
@@ -204,60 +200,98 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// Duplicates the member with the <paramref name="guidValue"/>
     /// Duplicates the member with the <paramref name="guidValue"/>
     /// </summary>
     /// </summary>
     /// <param name="guidValue">The Guid of the member</param>
     /// <param name="guidValue">The Guid of the member</param>
-    public void DuplicateMember(Guid guidValue)
+    public Guid? DuplicateMember(Guid guidValue)
     {
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
         if (Internals.ChangeController.IsBlockingChangeActive)
-            return;
+            return null;
 
 
         Internals.ChangeController.TryStopActiveExecutor();
         Internals.ChangeController.TryStopActiveExecutor();
 
 
         bool isFolder = Document.StructureHelper.Find(guidValue) is IFolderHandler;
         bool isFolder = Document.StructureHelper.Find(guidValue) is IFolderHandler;
+        Guid newGuid = Guid.NewGuid();
         if (!isFolder)
         if (!isFolder)
         {
         {
-            Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
             Internals.ActionAccumulator.AddFinishedActions(
                 new DuplicateLayer_Action(guidValue, newGuid),
                 new DuplicateLayer_Action(guidValue, newGuid),
                 new CreateAnimationDataFromLayer_Action(newGuid));
                 new CreateAnimationDataFromLayer_Action(newGuid));
         }
         }
         else
         else
         {
         {
-            Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
             Internals.ActionAccumulator.AddFinishedActions(
                 new DuplicateFolder_Action(guidValue, newGuid, null),
                 new DuplicateFolder_Action(guidValue, newGuid, null),
                 new SetSelectedMember_PassthroughAction(newGuid));
                 new SetSelectedMember_PassthroughAction(newGuid));
         }
         }
+
+        return newGuid;
     }
     }
 
 
-    public void ImportMember(Guid layerId, IDocument sourceDocument)
+    public Guid? ImportMember(Guid layerId, IDocument sourceDocument)
     {
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
         if (Internals.ChangeController.IsBlockingChangeActive)
-            return;
+            return null;
 
 
         Internals.ChangeController.TryStopActiveExecutor();
         Internals.ChangeController.TryStopActiveExecutor();
 
 
         if (sourceDocument == this.Document)
         if (sourceDocument == this.Document)
         {
         {
-            DuplicateMember(layerId);
-            return;
+            return DuplicateMember(layerId);
         }
         }
 
 
         if (!sourceDocument.StructureHelper.TryFindNode(layerId, out IStructureMemberHandler? member))
         if (!sourceDocument.StructureHelper.TryFindNode(layerId, out IStructureMemberHandler? member))
-            return;
+            return null;
 
 
+        Guid newGuid = Guid.NewGuid();
         if (member is ILayerHandler layer)
         if (member is ILayerHandler layer)
         {
         {
-            Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
             Internals.ActionAccumulator.AddFinishedActions(
                 new ImportLayer_Action(sourceDocument.ShareNode<IReadOnlyLayerNode>(layer.Id), newGuid),
                 new ImportLayer_Action(sourceDocument.ShareNode<IReadOnlyLayerNode>(layer.Id), newGuid),
                 new CreateAnimationDataFromLayer_Action(newGuid));
                 new CreateAnimationDataFromLayer_Action(newGuid));
         }
         }
         else if (member is IFolderHandler folder)
         else if (member is IFolderHandler folder)
         {
         {
-            Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
             Internals.ActionAccumulator.AddFinishedActions(
                 new ImportFolder_Action(sourceDocument.ShareNode<IReadOnlyFolderNode>(folder.Id), newGuid, null),
                 new ImportFolder_Action(sourceDocument.ShareNode<IReadOnlyFolderNode>(folder.Id), newGuid, null),
                 new SetSelectedMember_PassthroughAction(newGuid));
                 new SetSelectedMember_PassthroughAction(newGuid));
         }
         }
+
+        return newGuid;
+    }
+
+    public Guid? ImportNode(Guid nodeId, IDocument sourceDocument)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return null;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+
+        if (sourceDocument == this.Document)
+        {
+            return DuplicateNode(nodeId);
+        }
+
+        if (!sourceDocument.StructureHelper.TryFindNode(nodeId, out INodeHandler? node))
+            return null;
+
+        Guid newGuid = Guid.NewGuid();
+        if (node is ILayerHandler)
+        {
+            Internals.ActionAccumulator.AddFinishedActions(
+                new ImportLayer_Action(sourceDocument.ShareNode<IReadOnlyLayerNode>(nodeId), newGuid),
+                new CreateAnimationDataFromLayer_Action(newGuid));
+        }
+        else if (node is IFolderHandler)
+        {
+            Internals.ActionAccumulator.AddFinishedActions(
+                new ImportFolder_Action(sourceDocument.ShareNode<IReadOnlyFolderNode>(nodeId), newGuid, null),
+                new SetSelectedMember_PassthroughAction(newGuid));
+        }
+        else
+        {
+            Internals.ActionAccumulator.AddFinishedActions(
+                new ImportNode_Action(sourceDocument.ShareNode<IReadOnlyNode>(nodeId), newGuid));
+        }
+
+        return newGuid;
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -311,7 +345,8 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <param name="anchor">Where the existing content should be put</param>
     /// <param name="anchor">Where the existing content should be put</param>
     public void ResizeCanvas(VecI newSize, ResizeAnchor anchor)
     public void ResizeCanvas(VecI newSize, ResizeAnchor anchor)
     {
     {
-        if (Internals.ChangeController.IsBlockingChangeActive || newSize.X > Constants.MaxCanvasSize || newSize.Y > Constants.MaxCanvasSize ||
+        if (Internals.ChangeController.IsBlockingChangeActive || newSize.X > Constants.MaxCanvasSize ||
+            newSize.Y > Constants.MaxCanvasSize ||
             newSize.X < 1 ||
             newSize.X < 1 ||
             newSize.Y < 1)
             newSize.Y < 1)
             return;
             return;
@@ -908,6 +943,11 @@ internal class DocumentOperationsModule : IDocumentOperations
             node.InternalName == OutputNode.UniqueName)
             node.InternalName == OutputNode.UniqueName)
             return null;
             return null;
 
 
+        if (node is IStructureMemberHandler)
+        {
+            return ImportMember(nodeId, Document);
+        }
+
         Guid newGuid = Guid.NewGuid();
         Guid newGuid = Guid.NewGuid();
 
 
         Internals.ActionAccumulator.AddFinishedActions(new DuplicateNode_Action(nodeId, newGuid));
         Internals.ActionAccumulator.AddFinishedActions(new DuplicateNode_Action(nodeId, newGuid));

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

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

+ 7 - 7
src/PixiEditor/PixiEditor.csproj

@@ -90,20 +90,20 @@
     <PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0"/>
     <PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0"/>
     <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Headless" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Headless" Version="$(AvaloniaVersion)"/>
-    <PackageReference Include="Avalonia.Labs.Lottie" Version="11.2.0"/>
+    <PackageReference Include="Avalonia.Labs.Lottie" Version="11.3.0" />
     <PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Skia" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="Avalonia.Skia" Version="$(AvaloniaVersion)"/>
-    <PackageReference Include="Avalonia.Svg.Skia" Version="11.2.0.2"/>
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
     <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)"/>
     <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="ByteSize" Version="2.1.2"/>
     <PackageReference Include="ByteSize" Version="2.1.2"/>
     <PackageReference Include="CLSEncoderDecoder" Version="1.0.0"/>
     <PackageReference Include="CLSEncoderDecoder" Version="1.0.0"/>
-    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2"/>
-    <PackageReference Include="DiscordRichPresence" Version="1.2.1.24"/>
-    <PackageReference Include="Hardware.Info" Version="101.0.0"/>
-    <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.11.0"/>
-    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0"/>
+    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
+    <PackageReference Include="DiscordRichPresence" Version="1.3.0.28" />
+    <PackageReference Include="Hardware.Info" Version="101.0.1.1" />
+    <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
     <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
     <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
+    <PackageReference Include="Svg.Controls.Skia.Avalonia" Version="11.3.0.1" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 10 - 0
src/PixiEditor/ViewModels/Document/Nodes/Matrix/ComposeMatrixNodeViewModel.cs

@@ -0,0 +1,10 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Matrix;
+
+[NodeViewModel("COMPOSE_MATRIX", "MATRIX", PixiPerfectIcons.Salad)]
+internal class ComposeMatrixNodeViewModel : NodeViewModel<ComposeMatrixNode>
+{
+
+}

+ 10 - 0
src/PixiEditor/ViewModels/Document/Nodes/Matrix/DecomposeMatrixNodeViewModel.cs

@@ -0,0 +1,10 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Matrix;
+
+[NodeViewModel("DECOMPOSE_MATRIX", "MATRIX", PixiPerfectIcons.LeafyGreen)]
+internal class DecomposeMatrixNodeViewModel : NodeViewModel<DecomposeMatrixNode>
+{
+
+}

+ 23 - 9
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -21,6 +21,8 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Layers;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Helpers.Constants;
 using PixiEditor.Helpers.Constants;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.UI.Common.Fonts;
@@ -203,6 +205,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
 
         Dispatcher.UIThread.InvokeAsync(async () =>
         Dispatcher.UIThread.InvokeAsync(async () =>
         {
         {
+            Guid documentId = await ClipboardController.GetDocumentId();
             Guid[] toDuplicate = await ClipboardController.GetNodeIds();
             Guid[] toDuplicate = await ClipboardController.GetNodeIds();
 
 
             List<Guid> newIds = new();
             List<Guid> newIds = new();
@@ -211,9 +214,20 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
 
             using var block = doc.Operations.StartChangeBlock();
             using var block = doc.Operations.StartChangeBlock();
 
 
+            DocumentViewModel targetDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
+
+            if (documentId != Owner.DocumentManagerSubViewModel.ActiveDocument.Id)
+            {
+                targetDocument = Owner.DocumentManagerSubViewModel.Documents.FirstOrDefault(x => x.Id == documentId);
+                if (targetDocument == null)
+                {
+                    return;
+                }
+            }
+
             foreach (var nodeId in toDuplicate)
             foreach (var nodeId in toDuplicate)
             {
             {
-                Guid? newId = doc.Operations.DuplicateNode(nodeId);
+                Guid? newId = doc.Operations.ImportNode(nodeId, targetDocument);
                 if (newId != null)
                 if (newId != null)
                 {
                 {
                     newIds.Add(newId.Value);
                     newIds.Add(newId.Value);
@@ -226,7 +240,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
 
             block.ExecuteQueuedActions();
             block.ExecuteQueuedActions();
 
 
-            ConnectRelatedNodes(doc, nodeMapping);
+            ConnectRelatedNodes(targetDocument, doc, nodeMapping);
 
 
             doc.Operations.InvokeCustomAction(() =>
             doc.Operations.InvokeCustomAction(() =>
             {
             {
@@ -364,7 +378,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         if (selectedNodes.Length == 0)
         if (selectedNodes.Length == 0)
             return;
             return;
 
 
-        await ClipboardController.CopyNodes(selectedNodes);
+        await ClipboardController.CopyNodes(selectedNodes, doc.Id);
 
 
         areNodesInClipboard = true;
         areNodesInClipboard = true;
         ClearHasImageInClipboard();
         ClearHasImageInClipboard();
@@ -384,7 +398,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         if (selectedCels.Length == 0)
         if (selectedCels.Length == 0)
             return;
             return;
 
 
-        await ClipboardController.CopyCels(selectedCels);
+        await ClipboardController.CopyCels(selectedCels, doc.Id);
 
 
         areCelsInClipboard = true;
         areCelsInClipboard = true;
         ClearHasImageInClipboard();
         ClearHasImageInClipboard();
@@ -548,15 +562,15 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         return ColorSearchResult.GetIcon(targetColor.ToOpaqueMediaColor().ToOpaqueColor());
         return ColorSearchResult.GetIcon(targetColor.ToOpaqueMediaColor().ToOpaqueColor());
     }
     }
 
 
-    private void ConnectRelatedNodes(DocumentViewModel doc, Dictionary<Guid, Guid> nodeMapping)
+    private void ConnectRelatedNodes(IDocument sourceDoc, DocumentViewModel targetDoc, Dictionary<Guid, Guid> nodeMapping)
     {
     {
-        foreach (var connection in doc.NodeGraph.Connections)
+        foreach (var connection in sourceDoc.NodeGraphHandler.Connections)
         {
         {
             if (nodeMapping.TryGetValue(connection.InputNode.Id, out var inputNode) &&
             if (nodeMapping.TryGetValue(connection.InputNode.Id, out var inputNode) &&
                 nodeMapping.TryGetValue(connection.OutputNode.Id, out var outputNode))
                 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);
+                var inputNodeInstance = targetDoc.NodeGraph.AllNodes.FirstOrDefault(x => x.Id == inputNode);
+                var outputNodeInstance = targetDoc.NodeGraph.AllNodes.FirstOrDefault(x => x.Id == outputNode);
 
 
                 if (inputNodeInstance == null || outputNodeInstance == null)
                 if (inputNodeInstance == null || outputNodeInstance == null)
                     continue;
                     continue;
@@ -571,7 +585,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
                 if (inputProperty == null || outputProperty == null)
                 if (inputProperty == null || outputProperty == null)
                     continue;
                     continue;
 
 
-                doc.NodeGraph.ConnectProperties(inputProperty, outputProperty);
+                targetDoc.NodeGraph.ConnectProperties(inputProperty, outputProperty);
             }
             }
         }
         }
     }
     }

+ 1 - 0
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -241,6 +241,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
         {
             nodeViewsCache = nodeItemsControl.ItemsPanelRoot.Children.ToList();
             nodeViewsCache = nodeItemsControl.ItemsPanelRoot.Children.ToList();
             HandleNodesAdded(nodeViewsCache);
             HandleNodesAdded(nodeViewsCache);
+            nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged += NodeItems_CollectionChanged;
         });
         });
     }
     }
 
 

+ 17 - 3
tests/PixiEditor.Tests/RenderTests.cs

@@ -35,7 +35,16 @@ public class RenderTests : FullPixiEditorTest
     [InlineData("VectorWithSepiaFilter")]
     [InlineData("VectorWithSepiaFilter")]
     [InlineData("VectorWithSepiaFilterSrgb")]
     [InlineData("VectorWithSepiaFilterSrgb")]
     [InlineData("VectorWithSepiaFilterChained")]
     [InlineData("VectorWithSepiaFilterChained")]
-    public void TestThatPixiFilesRenderTheSameResultAsSavedPng(string fileName)
+    [InlineData("Offset")]
+    [InlineData("Scale")]
+    [InlineData("Skew")]
+    [InlineData("Rotation")]
+    [InlineData("MatrixChain")]
+    [InlineData("GpuOffset", "Offset")]
+    [InlineData("GpuScale")]
+    [InlineData("GpuSkew")]
+    [InlineData("GpuMatrixChain")]
+    public void TestThatPixiFilesRenderTheSameResultAsSavedPng(string fileName, string? resultName = null)
     {
     {
         if (!DrawingBackendApi.Current.IsHardwareAccelerated)
         if (!DrawingBackendApi.Current.IsHardwareAccelerated)
         {
         {
@@ -44,7 +53,7 @@ public class RenderTests : FullPixiEditorTest
         }
         }
 
 
         string pixiFile = Path.Combine("TestFiles", "RenderTests", fileName + ".pixi");
         string pixiFile = Path.Combine("TestFiles", "RenderTests", fileName + ".pixi");
-        string pngFile = Path.Combine("TestFiles", "RenderTests", fileName + ".png");
+        string pngFile = Path.Combine("TestFiles", "RenderTests", (resultName ?? fileName) + ".png");
         var document = Importer.ImportDocument(pixiFile);
         var document = Importer.ImportDocument(pixiFile);
 
 
         Assert.NotNull(pngFile);
         Assert.NotNull(pngFile);
@@ -55,11 +64,16 @@ public class RenderTests : FullPixiEditorTest
 
 
         using var image = result.AsT1;
         using var image = result.AsT1;
 
 
+        using var snapshot = image.DrawingSurface.Snapshot();
+        using var encoded = snapshot.Encode();
+
+        using var renderedToCompare = Surface.Load(encoded.AsSpan().ToArray());
+
         using var toCompareTo = Importer.ImportImage(pngFile, document.SizeBindable);
         using var toCompareTo = Importer.ImportImage(pngFile, document.SizeBindable);
 
 
         Assert.NotNull(toCompareTo);
         Assert.NotNull(toCompareTo);
 
 
-        Assert.True(PixelCompare(image, toCompareTo));
+        Assert.True(PixelCompare(renderedToCompare, toCompareTo));
     }
     }
 
 
     [AvaloniaTheory]
     [AvaloniaTheory]

BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/ComposeMatrix.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/ComposeMatrix.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/GpuMatrixChain.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/GpuMatrixChain.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/GpuOffset.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/GpuScale.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/GpuScale.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/GpuSkew.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/GpuSkew.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/MatrixChain.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/MatrixChain.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Offset.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Offset.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Rotation.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Rotation.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Scale.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Scale.png


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Skew.pixi


BIN
tests/PixiEditor.Tests/TestFiles/RenderTests/Skew.png


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