Browse Source

Merge branch 'master' into pixiauth

Krzysztof Krysiński 2 months ago
parent
commit
0be6a7d153
100 changed files with 1347 additions and 790 deletions
  1. 1 1
      samples/Directory.Build.props
  2. 1 1
      src/ColorPicker
  3. 9 1
      src/Directory.Build.props
  4. 1 1
      src/Drawie
  5. 2 2
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  6. 61 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  7. 99 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/ComposeMatrixNode.cs
  8. 47 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/DecomposeMatrixNode.cs
  9. 12 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs
  10. 0 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/MatrixNode.cs
  11. 30 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/OffsetNode.cs
  12. 42 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/RotateNode.cs
  13. 39 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/ScaleNode.cs
  14. 30 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/SkewNode.cs
  15. 28 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/TransformNode.cs
  16. 3 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  17. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  18. 18 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  19. 3 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  20. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs
  21. 5 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/DeleteKeyFrame_Change.cs
  22. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs
  23. 25 2
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConversionTable.cs
  24. 89 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ImportNode_Change.cs
  25. 69 1
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  26. 52 28
      src/PixiEditor.ChangeableDocument/Changes/Structure/ImportFolder_Change.cs
  27. 8 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/ImportLayer_Change.cs
  28. 1 1
      src/PixiEditor.Desktop/PixiEditor.Desktop.csproj
  29. 7 1
      src/PixiEditor.Desktop/Program.cs
  30. 0 256
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.deps.json
  31. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  32. 0 244
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.deps.json
  33. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  34. 6 1
      src/PixiEditor.SVG/Elements/SvgPolyline.cs
  35. 2 1
      src/PixiEditor.UI.Common/Accents/Base.axaml
  36. BIN
      src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf
  37. 2 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  38. 2 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs
  39. 0 0
      src/PixiEditor.UI.Common/Fonts/defs.svg
  40. 15 1
      src/PixiEditor/Data/Localization/Languages/en.json
  41. 26 0
      src/PixiEditor/Helpers/Converters/AnyTrueConverter.cs
  42. 23 4
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  43. 4 4
      src/PixiEditor/Helpers/SupportedFilesHelper.cs
  44. 12 5
      src/PixiEditor/Models/Commands/CommandController.cs
  45. 28 5
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  46. 57 17
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  47. 2 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs
  48. 4 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  49. 1 1
      src/PixiEditor/Models/Handlers/IDocumentOperations.cs
  50. 35 0
      src/PixiEditor/Models/Serialization/Factories/VecD4SerializationFactory.cs
  51. 11 7
      src/PixiEditor/PixiEditor.csproj
  52. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  53. 2 2
      src/PixiEditor/Styles/Templates/NodeView.axaml
  54. 3 0
      src/PixiEditor/ViewLocator.cs
  55. 9 5
      src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs
  56. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/LerpColorNodeViewModel.cs
  57. 10 0
      src/PixiEditor/ViewModels/Document/Nodes/Matrix/ComposeMatrixNodeViewModel.cs
  58. 10 0
      src/PixiEditor/ViewModels/Document/Nodes/Matrix/DecomposeMatrixNodeViewModel.cs
  59. 3 1
      src/PixiEditor/ViewModels/Nodes/NodePropertyViewModel.cs
  60. 59 0
      src/PixiEditor/ViewModels/Nodes/Properties/SinglePropertyViewModel.cs
  61. 29 11
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  62. 1 1
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  63. 5 5
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  64. 4 5
      src/PixiEditor/ViewModels/SubViewModels/MiscViewModel.cs
  65. 4 0
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  66. 4 3
      src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs
  67. 4 4
      src/PixiEditor/Views/Layers/ReferenceLayer.axaml
  68. 17 2
      src/PixiEditor/Views/Main/DocumentPreview.axaml
  69. 23 53
      src/PixiEditor/Views/Main/Tools/ToolsPicker.axaml
  70. 1 0
      src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml
  71. 27 0
      src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml.cs
  72. 66 22
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml
  73. 7 1
      src/PixiEditor/Views/Nodes/ConnectionView.cs
  74. 2 2
      src/PixiEditor/Views/Nodes/NodeGraphView.cs
  75. 42 6
      src/PixiEditor/Views/Nodes/NodeView.cs
  76. 7 0
      src/PixiEditor/Views/Nodes/Properties/ColorMatrixPropertyView.axaml.cs
  77. 7 0
      src/PixiEditor/Views/Nodes/Properties/GenericEnumPropertyView.axaml.cs
  78. 7 0
      src/PixiEditor/Views/Nodes/Properties/KernelPropertyView.axaml.cs
  79. 7 0
      src/PixiEditor/Views/Nodes/Properties/Matrix4x5FPropertyView.axaml.cs
  80. 47 11
      src/PixiEditor/Views/Visuals/PreviewPainterControl.cs
  81. 1 1
      tests/Directory.Build.props
  82. 17 3
      tests/PixiEditor.Tests/RenderTests.cs
  83. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/ComposeMatrix.pixi
  84. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/ComposeMatrix.png
  85. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuMatrixChain.pixi
  86. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuMatrixChain.png
  87. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuOffset.pixi
  88. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuScale.pixi
  89. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuScale.png
  90. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuSkew.pixi
  91. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/GpuSkew.png
  92. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/MatrixChain.pixi
  93. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/MatrixChain.png
  94. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Offset.pixi
  95. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Offset.png
  96. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Rotation.pixi
  97. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Rotation.png
  98. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Scale.pixi
  99. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Scale.png
  100. BIN
      tests/PixiEditor.Tests/TestFiles/RenderTests/Skew.pixi

+ 1 - 1
samples/Directory.Build.props

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

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 66bae8cf20153b9273b10c7d37ac90dc57ef15bb
+Subproject commit 3136a8ee9b682d889df8e3ccd7b793df4e0ead0b

+ 9 - 1
src/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.3.0</AvaloniaVersion>
+		    <AvaloniaVersion>11.3.2</AvaloniaVersion>
     </PropertyGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
@@ -63,6 +63,14 @@
     <Optimize>true</Optimize>
   </PropertyGroup>
 
+
+  <PropertyGroup Condition="'$(Configuration)'=='ReleaseNoUpdate'">
+    <DefineConstants>TRACE;RELEASE</DefineConstants>
+    <DebugSymbols>False</DebugSymbols>
+    <DebugType>None</DebugType>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
   <PropertyGroup Condition="'$(Configuration)'=='Debug'">
     <DebugType>full</DebugType>
     <DebugSymbols>true</DebugSymbols>

+ 1 - 1
src/Drawie

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

+ 2 - 2
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -24,7 +24,7 @@ public class FFMpegRenderer : IAnimationRenderer
     {
         string path = $"ThirdParty/{IOperatingSystem.Current.Name}/ffmpeg";
 
-        string binaryPath = Path.Combine(Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory), path);
+        string binaryPath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), path);
 
         GlobalFFOptions.Configure(new FFOptions() { BinaryFolder = binaryPath });
 
@@ -89,7 +89,7 @@ public class FFMpegRenderer : IAnimationRenderer
     {
         string path = $"ThirdParty/{IOperatingSystem.Current.Name}/ffmpeg";
 
-        string binaryPath = Path.Combine(Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory), path);
+        string binaryPath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), path);
 
         GlobalFFOptions.Configure(new FFOptions() { BinaryFolder = binaryPath });
 

+ 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 Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
@@ -55,10 +56,10 @@ public class FuncContext
         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);
-        return Builder.Sample(texName, pos);
+        return Builder.Sample(texName, pos, normalizedCoordinates);
     }
 
     public Float2 NewFloat2(Expression x, Expression y)
@@ -329,4 +330,62 @@ public class FuncContext
 
         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.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 
@@ -10,21 +12,22 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
 {
     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()
     {
         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;
         AllowHighDpiRendering = true;
     }
 
     protected override void OnExecute(RenderContext context)
     {
-        Matrix.Value = CalculateMatrix(Input.Value);
         if (Background.Value == null)
             return;
 
@@ -35,7 +38,9 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
     {
         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)
         {
             Background.Value?.Paint(context, surface);
@@ -44,5 +49,5 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
         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.Shaders.Generation.Expressions;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 [NodeInfo("Offset")]
 public class OffsetNode : Matrix3X3BaseNode
 {
-    public InputProperty<VecD> Translation { get; }
+    public FuncInputProperty<Float2> Translation { get; }
 
     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()

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

@@ -1,5 +1,7 @@
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
@@ -7,27 +9,57 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 public class RotateNode : Matrix3X3BaseNode
 {
     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()
     {
         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()
         };
 
-        return input.PostConcat(rotated);
+        return new Float3x3("") { ConstantValue = input.ConstantValue.PostConcat(rotationContextlessMatrix) };
     }
 
     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.Shaders.Generation.Expressions;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 [NodeInfo("Scale")]
 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()
     {
-        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);
         return input.PostConcat(scaled);
-    }
+    }*/
 
     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.Shaders.Generation.Expressions;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 [NodeInfo("Skew")]
 public class SkewNode : Matrix3X3BaseNode
 {
-    public InputProperty<VecD> Skew { get; }
+    public FuncInputProperty<Float2> Skew { get; }
 
     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()

+ 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 PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 [NodeInfo("Transform")]
 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()
     {
         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 InputProperty<ColorSampleMode> SampleMode { get; }
+    public InputProperty<bool> NormalizeCoordinates { get; }
 
     public Guid OtherNode { get; set; }
     
@@ -30,6 +31,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
         Coordinate = CreateFuncOutput("Coordinate", "UV", ctx => ctx.OriginalPosition ?? new Float2(""));
         Color = CreateFuncOutput("Color", "COLOR", GetColor);
         SampleMode = CreateInput("SampleMode", "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
+        NormalizeCoordinates = CreateInput("NormalizeCoordinates", "NORMALIZE_COORDINATES", true);
     }
     
     private Half4 GetColor(FuncContext context)
@@ -41,7 +43,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
             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)

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

@@ -63,7 +63,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
         size = imgSize;
 
-        ShaderBuilder builder = new(size.Value);
+        ShaderBuilder builder = new(size.Value, startNode.NormalizeCoordinates.Value);
         FuncContext context = new(renderContext, builder);
 
         if (Coordinate.Connection != null)
@@ -88,14 +88,14 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
         if (Color.Connection != null)
         {
-            builder.ReturnVar(Color.Value(context));
+            builder.ReturnVar(Color.Value(context), false);
         }
         else
         {
             Half4 color = Color.NonOverridenValue(FuncContext.NoContext);
             color.VariableName = "color";
             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();

+ 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<bool> NormalizedCoordinates { get; }
+
     public SampleImageNode()
     {
-        Image = CreateInput<Texture>(nameof(Texture), "IMAGE", null);
-        Coordinate = CreateFuncInput<Float2>(nameof(Coordinate), "UV", VecD.Zero);
-        Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
-        SampleMode = CreateInput(nameof(SampleMode), "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
+        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)
@@ -39,11 +42,20 @@ public class SampleImageNode : Node
         {
             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;
-        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)
         {
             color = Image.Value.GetSRGBPixel(pixelCoordinate);

+ 3 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs

@@ -122,7 +122,9 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         }
         else
         {
-            path.AddRoundRect(RectD.FromCenterAndSize(Center, Size), new VecD(CornerRadius));
+            double maxRadiusPx = Math.Min(Size.X, Size.Y) / 2f;
+            double radiusPx = CornerRadius * maxRadiusPx;
+            path.AddRoundRect(RectD.FromCenterAndSize(Center, Size), new VecD(radiusPx));
         }
 
         if (transformed)

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

@@ -13,7 +13,7 @@ public class EllipseNode : ShapeNode<EllipseVectorData>
     public InputProperty<VecD> Radius { get; }
     public InputProperty<Paintable> StrokeColor { get; }
     public InputProperty<Paintable> FillColor { get; }
-    public InputProperty<int> StrokeWidth { get; }
+    public InputProperty<double> StrokeWidth { get; }
 
     public EllipseNode()
     {
@@ -22,13 +22,13 @@ public class EllipseNode : ShapeNode<EllipseVectorData>
             v => v.Min(new VecD(0)));
         StrokeColor = CreateInput<Paintable>("StrokeColor", "STROKE_COLOR", new Color(0, 0, 0, 255));
         FillColor = CreateInput<Paintable>("FillColor", "FILL_COLOR", new Color(0, 0, 0, 255));
-        StrokeWidth = CreateInput<int>("StrokeWidth", "STROKE_WIDTH", 1);
+        StrokeWidth = CreateInput<double>("StrokeWidth", "STROKE_WIDTH", 1);
     }
 
     protected override EllipseVectorData? GetShapeData(RenderContext context)
     {
         return new EllipseVectorData(Center.Value, Radius.Value)
-            { Stroke = StrokeColor.Value, FillPaintable = FillColor.Value, StrokeWidth = StrokeWidth.Value };
+            { Stroke = StrokeColor.Value, FillPaintable = FillColor.Value, StrokeWidth = (float)StrokeWidth.Value };
     }
 
     public override Node CreateCopy() => new EllipseNode();

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/DeleteKeyFrame_Change.cs

@@ -35,6 +35,11 @@ internal class DeleteKeyFrame_Change : Change
             
             KeyFrameData data = node.KeyFrames.FirstOrDefault(x => x.KeyFrameGuid == keyFrame.Id);
 
+            if (data is null)
+            {
+                return false;
+            }
+
             savedKeyFrameData = data.Clone();
             
             return true;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs

@@ -4,7 +4,7 @@ using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
-internal class PasteImage_UpdateableChange : UpdateableChange
+internal class PasteImage_UpdateableChange : InterruptableUpdateableChange
 {
     private ShapeCorners corners;
     private readonly Guid memberGuid;

+ 25 - 2
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConversionTable.cs

@@ -16,34 +16,57 @@ public static class ConversionTable
             {
                 typeof(double), [
                     (typeof(int), new TypeConverter<double, int>(DoubleToInt)),
+                    (typeof(float), new TypeConverter<double, float>(d => (float)d)),
                     (typeof(VecD), new TypeConverter<double, VecD>(DoubleToVecD)),
-                    (typeof(VecI), new TypeConverter<double, VecI>(DoubleToVecI))
+                    (typeof(VecI), new TypeConverter<double, VecI>(DoubleToVecI)),
+                    (typeof(Vec3D), new TypeConverter<double, Vec3D>(d => new Vec3D(d, d, d))),
                 ]
             },
             {
                 typeof(int), [
                     (typeof(double), new TypeConverter<int, double>(ConvertIntToDouble)),
+                    (typeof(float), new TypeConverter<int, float>(i => (float)i)),
                     (typeof(VecI), new TypeConverter<int, VecI>(IntToVecI)),
                     (typeof(VecD), new TypeConverter<int, VecD>(IntToVecD)),
+                    (typeof(Vec3D), new TypeConverter<int, Vec3D>(i => new Vec3D(i, i, i))),
                 ]
             },
             {
                 typeof(VecD), [
                     (typeof(double), new TypeConverter<VecD, double>(VecDToDouble)),
                     (typeof(int), new TypeConverter<VecD, int>(VecDToInt)),
+                    (typeof(float), new TypeConverter<VecD, float>(v => (float)v.X)),
                     (typeof(VecI), new TypeConverter<VecD, VecI>(VecDToVecI)),
+                    (typeof(Vec3D), new TypeConverter<VecD, Vec3D>(v => new Vec3D(v.X, v.Y, v.Y)))
                 ]
             },
             {
                 typeof(VecI), [
                     (typeof(double), new TypeConverter<VecI, double>(VecIToDouble)),
                     (typeof(int), new TypeConverter<VecI, int>(VecIToInt)),
-                    (typeof(VecD), new TypeConverter<VecI, VecD>(VecIToVecD))
+                    (typeof(float), new TypeConverter<VecI, float>(v => v.X)),
+                    (typeof(VecD), new TypeConverter<VecI, VecD>(VecIToVecD)),
+                    (typeof(Vec3D), new TypeConverter<VecI, Vec3D>(v => new Vec3D(v.X, v.Y, v.Y)))
                 ]
             },
             {
                 typeof(Color), [
                     (typeof(Paintable), new TypeConverter<Color, Paintable>(c => new ColorPaintable(c))),
+                    (typeof(VecD), new TypeConverter<Color, VecD>(c => new VecD(c.R, c.G))),
+                    (typeof(VecI), new TypeConverter<Color, VecI>(c => new VecI(c.R, c.G))),
+                    (typeof(Vec3D), new TypeConverter<Color, Vec3D>(c => new Vec3D(c.R, c.G, c.B))),
+                    (typeof(double), new TypeConverter<Color, double>(c => c.R)),
+                    (typeof(int), new TypeConverter<Color, int>(c => c.R)),
+                    (typeof(float), new TypeConverter<Color, float>(c => c.R)),
+                ]
+            },
+            {
+                typeof(Vec3D),[
+                    (typeof(double), new TypeConverter<Vec3D, double>(v => v.X)),
+                    (typeof(int), new TypeConverter<Vec3D, int>(v => (int)v.X)),
+                    (typeof(VecD), new TypeConverter<Vec3D, VecD>(v => new VecD(v.X, v.Y))),
+                    (typeof(VecI), new TypeConverter<Vec3D, VecI>(v => new VecI((int)v.X, (int)v.Y))),
+                    (typeof(Color), new TypeConverter<Vec3D, Color>(v => new Color((byte)Math.Clamp(v.X, 0, 255), (byte)Math.Clamp(v.Y, 0, 255), (byte)Math.Clamp(v.Z, 0, 255)))),
                 ]
             }
         };

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

+ 69 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -16,6 +16,8 @@ internal sealed class RotateImage_Change : Change
     private readonly RotationAngle rotation;
     private List<Guid> membersToRotate;
 
+    private Dictionary<Guid, Matrix3X3> originalTransformations = new();
+
     private VecI originalSize;
     private double originalHorAxisY;
     private double originalVerAxisX;
@@ -28,6 +30,7 @@ internal sealed class RotateImage_Change : Change
     {
         this.rotation = rotation;
         membersToRotate ??= new List<Guid>();
+        originalTransformations = new Dictionary<Guid, Matrix3X3>();
         this.membersToRotate = membersToRotate;
         this.frame = frame < 0 ? null : frame;
     }
@@ -143,6 +146,7 @@ internal sealed class RotateImage_Change : Change
 
     private OneOf<None, IChangeInfo, List<IChangeInfo>> Rotate(Document target)
     {
+        originalTransformations.Clear();
         if (membersToRotate.Count == 0)
         {
             return RotateWholeImage(target);
@@ -173,7 +177,13 @@ internal sealed class RotateImage_Change : Change
                 }
                 else if (member is ITransformableObject transformableObject)
                 {
-                    RectD? tightBounds = member.GetTightBounds(frame.Value);
+                    RectD? tightBounds = member.GetTightBounds(frame ?? 0);
+
+                    if (tightBounds is null)
+                        return;
+
+                    originalTransformations[member.Id] = transformableObject.TransformationMatrix;
+
                     transformableObject.TransformationMatrix = transformableObject.TransformationMatrix.PostConcat(
                         Matrix3X3.CreateRotation(
                             RotationAngleToRadians(rotation),
@@ -195,8 +205,12 @@ internal sealed class RotateImage_Change : Change
         int newWidth = rotation == RotationAngle.D180 ? target.Size.X : target.Size.Y;
         int newHeight = rotation == RotationAngle.D180 ? target.Size.Y : target.Size.X;
 
+        VecI oldSize = target.Size;
         VecI newSize = new VecI(newWidth, newHeight);
 
+        VecD imageCenterOld = new VecD(oldSize.X / 2f, oldSize.Y / 2f);
+        VecD imageCenterNew = new VecD(newSize.X / 2f, newSize.Y / 2f);
+
         double normalizedSymmX = originalVerAxisX / Math.Max(target.Size.X, 0.1f);
         double normalizedSymmY = originalHorAxisY / Math.Max(target.Size.Y, 0.1f);
 
@@ -220,6 +234,43 @@ internal sealed class RotateImage_Change : Change
                     });
                 }
             }
+            else
+            {
+                if (member is ITransformableObject transformableObject)
+                {
+                    RectD? tightBounds = member.GetTightBounds(0);
+                    if (tightBounds is null)
+                        return;
+
+                    originalTransformations[member.Id] = transformableObject.TransformationMatrix;
+
+                    float radians = RotationAngleToRadians(rotation);
+
+                    VecD objectCenter = new VecD((float)tightBounds.Value.Center.X, (float)tightBounds.Value.Center.Y);
+
+                    var rotationMatrix =
+                        Matrix3X3.CreateRotation(radians, (float)objectCenter.X, (float)objectCenter.Y);
+
+                    VecD offsetFromCenter = objectCenter - imageCenterOld;
+
+                    VecD rotatedOffset = rotation switch
+                    {
+                        RotationAngle.D90 => new VecD(-offsetFromCenter.Y, offsetFromCenter.X),
+                        RotationAngle.D180 => -offsetFromCenter,
+                        RotationAngle.D270 => new VecD(offsetFromCenter.Y, -offsetFromCenter.X),
+                        _ => offsetFromCenter
+                    };
+
+                    VecD newObjectCenter = imageCenterNew + rotatedOffset;
+
+                    VecD delta = newObjectCenter - objectCenter;
+                    var translationMatrix = Matrix3X3.CreateTranslation(delta.X, delta.Y);
+
+                    transformableObject.TransformationMatrix =
+                        transformableObject.TransformationMatrix.PostConcat(rotationMatrix)
+                            .PostConcat(translationMatrix);
+                }
+            }
 
             if (member.EmbeddedMask is null)
                 return;
@@ -232,6 +283,23 @@ internal sealed class RotateImage_Change : Change
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
+        if (originalTransformations.Count > 0)
+        {
+            foreach (var item in originalTransformations)
+            {
+                var member = item.Key;
+
+                if (!target.HasMember(member))
+                    continue;
+
+                var memberNode = target.FindMember(member);
+                if (memberNode is ITransformableObject transformableObject)
+                {
+                    transformableObject.TransformationMatrix = item.Value;
+                }
+            }
+        }
+
         if (membersToRotate.Count == 0)
         {
             return RevertRotateWholeImage(target);

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

@@ -15,7 +15,10 @@ internal class ImportFolder_Change : Change
     private ICrossDocumentPipe<IReadOnlyFolderNode> sourcefolderPipe;
     private Guid duplicateGuid;
     private Guid[] contentGuids;
-    private Guid[] contentDuplicateGuids;
+
+    private FolderNode? clonedFolderNode;
+    private List<Node> clonedContentNodes = new();
+    private Dictionary<Guid, Guid> contentGuidToNodeMap;
 
     private Guid[]? childGuidsToUse;
 
@@ -55,7 +58,7 @@ internal class ImportFolder_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
     {
-        var readOnlyFolderNode = sourcefolderPipe.TryAccessData();
+        var readOnlyFolderNode = clonedFolderNode ?? sourcefolderPipe.TryAccessData();
 
         if (readOnlyFolderNode is not FolderNode folderNode || target.NodeGraph.OutputNode == null)
         {
@@ -65,6 +68,7 @@ internal class ImportFolder_Change : Change
 
         FolderNode clone = (FolderNode)folderNode.Clone();
         clone.Id = duplicateGuid;
+        clonedFolderNode = clone;
 
         InputProperty<Painter?> targetInput = target.NodeGraph.OutputNode.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter)) as InputProperty<Painter?>;
@@ -99,11 +103,11 @@ internal class ImportFolder_Change : Change
         changes.AddRange(NodeOperations.DetachStructureNode(member));
         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.Add(new DeleteNode_ChangeInfo(contentNode.Id));
 
@@ -127,42 +131,62 @@ internal class ImportFolder_Change : Change
     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;
-        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)
         {
-            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,
                 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 ConnectionsData? connectionsData;
 
+    private LayerNode? clonedLayer;
+
     private Guid duplicateGuid;
 
     [GenerateMakeChangeAction]
@@ -37,9 +39,10 @@ internal class ImportLayer_Change : Change
         if (layer == null || target.NodeGraph.OutputNode == null)
             return false;
 
+        if (target.NodeGraph.OutputNode == null) return false;
+
         connectionsData = NodeOperations.CreateConnectionsData(target.NodeGraph.OutputNode);
 
-        if (target.NodeGraph.OutputNode == null) return false;
 
         return true;
     }
@@ -49,7 +52,7 @@ internal class ImportLayer_Change : Change
     {
         ignoreInUndo = false;
 
-        var layer = sourceDocumentPipe.TryAccessData();
+        var layer = clonedLayer ?? sourceDocumentPipe.TryAccessData();
         if (layer is not LayerNode layerNode)
         {
             ignoreInUndo = true;
@@ -58,6 +61,8 @@ internal class ImportLayer_Change : Change
 
         var clone = (LayerNode)layerNode.Clone();
         clone.Id = duplicateGuid;
+        clonedLayer = clone;
+
         ResizeImageData(clone, target.Size);
 
         var targetInput = target.NodeGraph.OutputNode?.InputProperties.FirstOrDefault(x =>
@@ -108,6 +113,7 @@ internal class ImportLayer_Change : Change
     public override void Dispose()
     {
         sourceDocumentPipe?.Dispose();
+        clonedLayer?.Dispose();
     }
 
     private void ResizeImageData(LayerNode layerNode, VecI docSize)

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

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

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

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

+ 0 - 256
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.deps.json

@@ -1,256 +0,0 @@
-{
-  "runtimeTarget": {
-    "name": ".NETStandard,Version=v2.0/",
-    "signature": ""
-  },
-  "compilationOptions": {},
-  "targets": {
-    ".NETStandard,Version=v2.0": {},
-    ".NETStandard,Version=v2.0/": {
-      "PixiEditor.Api.CGlueMSBuild/1.0.0": {
-        "dependencies": {
-          "Microsoft.Build.Utilities.Core": "17.12.6",
-          "Mono.Cecil": "0.11.6",
-          "NETStandard.Library": "2.0.3",
-          "StyleCop.Analyzers": "1.1.118"
-        },
-        "runtime": {
-          "PixiEditor.Api.CGlueMSBuild.dll": {}
-        }
-      },
-      "Microsoft.Build.Framework/17.12.6": {
-        "dependencies": {
-          "Microsoft.Win32.Registry": "5.0.0",
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "Microsoft.Build.Utilities.Core/17.12.6": {
-        "dependencies": {
-          "Microsoft.Build.Framework": "17.12.6",
-          "Microsoft.NET.StringTools": "17.12.6",
-          "Microsoft.Win32.Registry": "5.0.0",
-          "System.Collections.Immutable": "8.0.0",
-          "System.Configuration.ConfigurationManager": "8.0.0",
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
-          "System.Security.Principal.Windows": "5.0.0",
-          "System.Text.Encoding.CodePages": "7.0.0"
-        }
-      },
-      "Microsoft.NET.StringTools/17.12.6": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "Microsoft.NETCore.Platforms/1.1.0": {},
-      "Microsoft.Win32.Registry/5.0.0": {
-        "dependencies": {
-          "System.Buffers": "4.5.1",
-          "System.Memory": "4.5.5",
-          "System.Security.AccessControl": "5.0.0",
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "Mono.Cecil/0.11.6": {
-        "runtime": {
-          "lib/netstandard2.0/Mono.Cecil.Mdb.dll": {
-            "assemblyVersion": "0.11.6.0",
-            "fileVersion": "0.11.6.0"
-          },
-          "lib/netstandard2.0/Mono.Cecil.Pdb.dll": {
-            "assemblyVersion": "0.11.6.0",
-            "fileVersion": "0.11.6.0"
-          },
-          "lib/netstandard2.0/Mono.Cecil.Rocks.dll": {
-            "assemblyVersion": "0.11.6.0",
-            "fileVersion": "0.11.6.0"
-          },
-          "lib/netstandard2.0/Mono.Cecil.dll": {
-            "assemblyVersion": "0.11.6.0",
-            "fileVersion": "0.11.6.0"
-          }
-        }
-      },
-      "NETStandard.Library/2.0.3": {
-        "dependencies": {
-          "Microsoft.NETCore.Platforms": "1.1.0"
-        }
-      },
-      "StyleCop.Analyzers/1.1.118": {},
-      "System.Buffers/4.5.1": {},
-      "System.Collections.Immutable/8.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "System.Configuration.ConfigurationManager/8.0.0": {
-        "dependencies": {
-          "System.Security.Cryptography.ProtectedData": "8.0.0"
-        }
-      },
-      "System.Memory/4.5.5": {
-        "dependencies": {
-          "System.Buffers": "4.5.1",
-          "System.Numerics.Vectors": "4.4.0",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "System.Numerics.Vectors/4.4.0": {},
-      "System.Runtime.CompilerServices.Unsafe/6.0.0": {},
-      "System.Security.AccessControl/5.0.0": {
-        "dependencies": {
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "System.Security.Cryptography.ProtectedData/8.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5"
-        }
-      },
-      "System.Security.Principal.Windows/5.0.0": {},
-      "System.Text.Encoding.CodePages/7.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      }
-    }
-  },
-  "libraries": {
-    "PixiEditor.Api.CGlueMSBuild/1.0.0": {
-      "type": "project",
-      "serviceable": false,
-      "sha512": ""
-    },
-    "Microsoft.Build.Framework/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-jleteC0seumLGTmTVwob97lcwPj/dfgzL/V3g/VVcMZgo2Ic7jzdy8AYpByPDh8e3uRq0SjCl6HOFCjhy5GzRQ==",
-      "path": "microsoft.build.framework/17.12.6",
-      "hashPath": "microsoft.build.framework.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.Build.Utilities.Core/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-pU3GnHcXp8VRMGKxdJCq+tixfhFn+QwEbpqmZmc/nqFHFyuhlGwjonWZMIWcwuCv/8EHgxoOttFvna1vrN+RrA==",
-      "path": "microsoft.build.utilities.core/17.12.6",
-      "hashPath": "microsoft.build.utilities.core.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.NET.StringTools/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-w8Ehofqte5bJoR+Fa3f6JwkwFEkGtXxqvQHGOVOSHDzgNVySvL5FSNhavbQSZ864el9c3rjdLPLAtBW8dq6fmg==",
-      "path": "microsoft.net.stringtools/17.12.6",
-      "hashPath": "microsoft.net.stringtools.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.NETCore.Platforms/1.1.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
-      "path": "microsoft.netcore.platforms/1.1.0",
-      "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
-    },
-    "Microsoft.Win32.Registry/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
-      "path": "microsoft.win32.registry/5.0.0",
-      "hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512"
-    },
-    "Mono.Cecil/0.11.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==",
-      "path": "mono.cecil/0.11.6",
-      "hashPath": "mono.cecil.0.11.6.nupkg.sha512"
-    },
-    "NETStandard.Library/2.0.3": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
-      "path": "netstandard.library/2.0.3",
-      "hashPath": "netstandard.library.2.0.3.nupkg.sha512"
-    },
-    "StyleCop.Analyzers/1.1.118": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==",
-      "path": "stylecop.analyzers/1.1.118",
-      "hashPath": "stylecop.analyzers.1.1.118.nupkg.sha512"
-    },
-    "System.Buffers/4.5.1": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==",
-      "path": "system.buffers/4.5.1",
-      "hashPath": "system.buffers.4.5.1.nupkg.sha512"
-    },
-    "System.Collections.Immutable/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==",
-      "path": "system.collections.immutable/8.0.0",
-      "hashPath": "system.collections.immutable.8.0.0.nupkg.sha512"
-    },
-    "System.Configuration.ConfigurationManager/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==",
-      "path": "system.configuration.configurationmanager/8.0.0",
-      "hashPath": "system.configuration.configurationmanager.8.0.0.nupkg.sha512"
-    },
-    "System.Memory/4.5.5": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
-      "path": "system.memory/4.5.5",
-      "hashPath": "system.memory.4.5.5.nupkg.sha512"
-    },
-    "System.Numerics.Vectors/4.4.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==",
-      "path": "system.numerics.vectors/4.4.0",
-      "hashPath": "system.numerics.vectors.4.4.0.nupkg.sha512"
-    },
-    "System.Runtime.CompilerServices.Unsafe/6.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
-      "path": "system.runtime.compilerservices.unsafe/6.0.0",
-      "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
-    },
-    "System.Security.AccessControl/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
-      "path": "system.security.accesscontrol/5.0.0",
-      "hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512"
-    },
-    "System.Security.Cryptography.ProtectedData/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg==",
-      "path": "system.security.cryptography.protecteddata/8.0.0",
-      "hashPath": "system.security.cryptography.protecteddata.8.0.0.nupkg.sha512"
-    },
-    "System.Security.Principal.Windows/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==",
-      "path": "system.security.principal.windows/5.0.0",
-      "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512"
-    },
-    "System.Text.Encoding.CodePages/7.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==",
-      "path": "system.text.encoding.codepages/7.0.0",
-      "hashPath": "system.text.encoding.codepages.7.0.0.nupkg.sha512"
-    }
-  }
-}

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


+ 0 - 244
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.deps.json

@@ -1,244 +0,0 @@
-{
-  "runtimeTarget": {
-    "name": ".NETStandard,Version=v2.0/",
-    "signature": ""
-  },
-  "compilationOptions": {},
-  "targets": {
-    ".NETStandard,Version=v2.0": {},
-    ".NETStandard,Version=v2.0/": {
-      "PixiEditor.Extensions.MSPackageBuilder/1.0.0": {
-        "dependencies": {
-          "Microsoft.Build.Utilities.Core": "17.12.6",
-          "NETStandard.Library": "2.0.3",
-          "Newtonsoft.Json": "13.0.3",
-          "StyleCop.Analyzers": "1.1.118"
-        },
-        "runtime": {
-          "PixiEditor.Extensions.MSPackageBuilder.dll": {}
-        }
-      },
-      "Microsoft.Build.Framework/17.12.6": {
-        "dependencies": {
-          "Microsoft.Win32.Registry": "5.0.0",
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "Microsoft.Build.Utilities.Core/17.12.6": {
-        "dependencies": {
-          "Microsoft.Build.Framework": "17.12.6",
-          "Microsoft.NET.StringTools": "17.12.6",
-          "Microsoft.Win32.Registry": "5.0.0",
-          "System.Collections.Immutable": "8.0.0",
-          "System.Configuration.ConfigurationManager": "8.0.0",
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
-          "System.Security.Principal.Windows": "5.0.0",
-          "System.Text.Encoding.CodePages": "7.0.0"
-        }
-      },
-      "Microsoft.NET.StringTools/17.12.6": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "Microsoft.NETCore.Platforms/1.1.0": {},
-      "Microsoft.Win32.Registry/5.0.0": {
-        "dependencies": {
-          "System.Buffers": "4.5.1",
-          "System.Memory": "4.5.5",
-          "System.Security.AccessControl": "5.0.0",
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "NETStandard.Library/2.0.3": {
-        "dependencies": {
-          "Microsoft.NETCore.Platforms": "1.1.0"
-        }
-      },
-      "Newtonsoft.Json/13.0.3": {
-        "runtime": {
-          "lib/netstandard2.0/Newtonsoft.Json.dll": {
-            "assemblyVersion": "13.0.0.0",
-            "fileVersion": "13.0.3.27908"
-          }
-        }
-      },
-      "StyleCop.Analyzers/1.1.118": {},
-      "System.Buffers/4.5.1": {},
-      "System.Collections.Immutable/8.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "System.Configuration.ConfigurationManager/8.0.0": {
-        "dependencies": {
-          "System.Security.Cryptography.ProtectedData": "8.0.0"
-        }
-      },
-      "System.Memory/4.5.5": {
-        "dependencies": {
-          "System.Buffers": "4.5.1",
-          "System.Numerics.Vectors": "4.4.0",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      },
-      "System.Numerics.Vectors/4.4.0": {},
-      "System.Runtime.CompilerServices.Unsafe/6.0.0": {},
-      "System.Security.AccessControl/5.0.0": {
-        "dependencies": {
-          "System.Security.Principal.Windows": "5.0.0"
-        }
-      },
-      "System.Security.Cryptography.ProtectedData/8.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5"
-        }
-      },
-      "System.Security.Principal.Windows/5.0.0": {},
-      "System.Text.Encoding.CodePages/7.0.0": {
-        "dependencies": {
-          "System.Memory": "4.5.5",
-          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
-        }
-      }
-    }
-  },
-  "libraries": {
-    "PixiEditor.Extensions.MSPackageBuilder/1.0.0": {
-      "type": "project",
-      "serviceable": false,
-      "sha512": ""
-    },
-    "Microsoft.Build.Framework/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-jleteC0seumLGTmTVwob97lcwPj/dfgzL/V3g/VVcMZgo2Ic7jzdy8AYpByPDh8e3uRq0SjCl6HOFCjhy5GzRQ==",
-      "path": "microsoft.build.framework/17.12.6",
-      "hashPath": "microsoft.build.framework.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.Build.Utilities.Core/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-pU3GnHcXp8VRMGKxdJCq+tixfhFn+QwEbpqmZmc/nqFHFyuhlGwjonWZMIWcwuCv/8EHgxoOttFvna1vrN+RrA==",
-      "path": "microsoft.build.utilities.core/17.12.6",
-      "hashPath": "microsoft.build.utilities.core.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.NET.StringTools/17.12.6": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-w8Ehofqte5bJoR+Fa3f6JwkwFEkGtXxqvQHGOVOSHDzgNVySvL5FSNhavbQSZ864el9c3rjdLPLAtBW8dq6fmg==",
-      "path": "microsoft.net.stringtools/17.12.6",
-      "hashPath": "microsoft.net.stringtools.17.12.6.nupkg.sha512"
-    },
-    "Microsoft.NETCore.Platforms/1.1.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
-      "path": "microsoft.netcore.platforms/1.1.0",
-      "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
-    },
-    "Microsoft.Win32.Registry/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
-      "path": "microsoft.win32.registry/5.0.0",
-      "hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512"
-    },
-    "NETStandard.Library/2.0.3": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
-      "path": "netstandard.library/2.0.3",
-      "hashPath": "netstandard.library.2.0.3.nupkg.sha512"
-    },
-    "Newtonsoft.Json/13.0.3": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
-      "path": "newtonsoft.json/13.0.3",
-      "hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
-    },
-    "StyleCop.Analyzers/1.1.118": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==",
-      "path": "stylecop.analyzers/1.1.118",
-      "hashPath": "stylecop.analyzers.1.1.118.nupkg.sha512"
-    },
-    "System.Buffers/4.5.1": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==",
-      "path": "system.buffers/4.5.1",
-      "hashPath": "system.buffers.4.5.1.nupkg.sha512"
-    },
-    "System.Collections.Immutable/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==",
-      "path": "system.collections.immutable/8.0.0",
-      "hashPath": "system.collections.immutable.8.0.0.nupkg.sha512"
-    },
-    "System.Configuration.ConfigurationManager/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==",
-      "path": "system.configuration.configurationmanager/8.0.0",
-      "hashPath": "system.configuration.configurationmanager.8.0.0.nupkg.sha512"
-    },
-    "System.Memory/4.5.5": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
-      "path": "system.memory/4.5.5",
-      "hashPath": "system.memory.4.5.5.nupkg.sha512"
-    },
-    "System.Numerics.Vectors/4.4.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==",
-      "path": "system.numerics.vectors/4.4.0",
-      "hashPath": "system.numerics.vectors.4.4.0.nupkg.sha512"
-    },
-    "System.Runtime.CompilerServices.Unsafe/6.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
-      "path": "system.runtime.compilerservices.unsafe/6.0.0",
-      "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
-    },
-    "System.Security.AccessControl/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
-      "path": "system.security.accesscontrol/5.0.0",
-      "hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512"
-    },
-    "System.Security.Cryptography.ProtectedData/8.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg==",
-      "path": "system.security.cryptography.protecteddata/8.0.0",
-      "hashPath": "system.security.cryptography.protecteddata.8.0.0.nupkg.sha512"
-    },
-    "System.Security.Principal.Windows/5.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==",
-      "path": "system.security.principal.windows/5.0.0",
-      "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512"
-    },
-    "System.Text.Encoding.CodePages/7.0.0": {
-      "type": "package",
-      "serviceable": true,
-      "sha512": "sha512-LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==",
-      "path": "system.text.encoding.codepages/7.0.0",
-      "hashPath": "system.text.encoding.codepages.7.0.0.nupkg.sha512"
-    }
-  }
-}

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


+ 6 - 1
src/PixiEditor.SVG/Elements/SvgPolyline.cs

@@ -83,6 +83,11 @@ public class SvgPolyline() : SvgPrimitive("polyline")
 
     private static double ParseNumber(string currentNumberString)
     {
-        return double.Parse(currentNumberString, System.Globalization.CultureInfo.InvariantCulture);
+        if (double.TryParse(currentNumberString, System.Globalization.CultureInfo.InvariantCulture, out var parsed))
+        {
+            return parsed;
+        }
+
+        return 0;
     }
 }

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

@@ -51,7 +51,7 @@
             <Color x:Key="FilterSocketColor">#CC5C5C</Color>
             <Color x:Key="BoolSocketColor">#68abdf</Color>
             <Color x:Key="FloatSocketColor">#ffc66d</Color>
-            <Color x:Key="DoubleSocketColor">#efb66d</Color>
+            <Color x:Key="DoubleSocketColor">#eea55c</Color>
             <Color x:Key="ColorSocketColor">#8cf2dd</Color>
             <Color x:Key="PaintableSocketColor">#48b099</Color>
             <Color x:Key="VecDSocketColor">#c984ca</Color>
@@ -165,6 +165,7 @@
             <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}" />
             <SolidColorBrush x:Key="StringSocketBrush" Color="{StaticResource StringSocketColor}" />
             <SolidColorBrush x:Key="Matrix3X3SocketBrush" Color="{StaticResource Matrix3X3SocketColor}" />
+            <SolidColorBrush x:Key="Float3x3SocketBrush" Color="{StaticResource Matrix3X3SocketColor}" />
 
             <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush"
                                 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-double">&#xE909;</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-line">&#xE95A;</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-resize">&#xE96B;</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-scissors">&#xE96F;</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 LayersDouble = "\uE909";
     public const string LayersTop = "\uE994";
+    public const string LeafyGreen = "\uE9C8";
     public const string LetterSpacing = "\uE9B2";
     public const string Line = "\uE95A";
     public const string LineHeight = "\uE9B3";
@@ -146,6 +147,7 @@ public static partial class PixiPerfectIcons
     public const string Reset = "\uE9AA";
     public const string Resize = "\uE96B";
     public const string RotateView = "\uE96C";
+    public const string Salad = "\uE9C7";
     public const string Save = "\uE96E";
     public const string Scissors = "\uE96F";
     public const string Search = "\uE996";

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


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

@@ -1107,5 +1107,19 @@
   "NEW_MIN": "New Min",
   "NEW_MAX": "New Max",
   "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",
+  "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 - 0
src/PixiEditor/Helpers/Converters/AnyTrueConverter.cs

@@ -0,0 +1,26 @@
+using System.Globalization;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class AnyTrueConverter : SingleInstanceMultiValueConverter<AnyTrueConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is IEnumerable<bool> bools)
+        {
+            return bools.Any(x => x);
+        }
+
+        return false;
+    }
+
+    public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (values.All(x => x is bool b))
+        {
+            return values.Cast<bool>().Any(x => x);
+        }
+
+        return false;
+    }
+}

+ 23 - 4
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -164,19 +164,38 @@ internal class DocumentViewModelBuilder
 
         foreach (var node in documentGraph.AllNodes)
         {
-            if (node.KeyFrames.Length > 1 && data.All(x => x.NodeId != node.Id))
+            if (node.KeyFrames.Length > 1)
             {
-                GroupKeyFrameBuilder builder = new GroupKeyFrameBuilder()
-                    .WithNodeId(node.Id);
+                var existingGroup = groups.FirstOrDefault(x => x.NodeId == node.Id);
+                GroupKeyFrameBuilder builder = null;
+                if (existingGroup != null)
+                {
+                    builder = data
+                        .OfType<GroupKeyFrameBuilder>()
+                        .FirstOrDefault(x => x.NodeId == existingGroup.NodeId);
+                }
+
+                if (builder == null)
+                {
+                    builder = new GroupKeyFrameBuilder()
+                        .WithNodeId(node.Id);
+                }
+
 
                 foreach (var keyFrame in node.KeyFrames)
                 {
+                    if (builder.Children.Any(x => x.KeyFrameId == keyFrame.Id))
+                    {
+                        continue; // Skip if the keyframe already exists in the group
+                    }
+
                     builder.WithChild<KeyFrameBuilder>(x => x
                         .WithKeyFrameId(keyFrame.Id)
                         .WithNodeId(node.Id));
                 }
 
-                data.Add(builder);
+                if(existingGroup == null)
+                    data.Add(builder);
             }
         }
     }

+ 4 - 4
src/PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -53,12 +53,12 @@ internal class SupportedFilesHelper
 
     public static bool IsExtensionSupported(string fileExtension)
     {
-        return AllSupportedExtensions.Contains(fileExtension);
+        return AllSupportedExtensions.Contains(fileExtension, StringComparer.OrdinalIgnoreCase);
     }
     public static IoFileType? ParseImageFormat(string extension)
     {
         var allExts = FileTypes;
-        var fileData = allExts.SingleOrDefault(i => i.Extensions.Contains(extension));
+        var fileData = allExts.SingleOrDefault(i => i.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
         return fileData;
     }
 
@@ -84,7 +84,7 @@ internal class SupportedFilesHelper
             return null;
 
         string extension = Path.GetExtension(file.Path.LocalPath);
-        return allSupportedExtensions.Single(i => i.CanSave && i.Extensions.Contains(extension));
+        return allSupportedExtensions.Single(i => i.CanSave && i.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
     }
 
     public static List<FilePickerFileType> BuildOpenFilter()
@@ -95,6 +95,6 @@ internal class SupportedFilesHelper
 
     public static bool IsRasterFormat(string fileExtension)
     {
-        return FileTypes.Any(i => i.Extensions.Contains(fileExtension) && i.SetKind == FileTypeDialogDataSet.SetKind.Image);
+        return FileTypes.Any(i => i.Extensions.Contains(fileExtension, StringComparer.OrdinalIgnoreCase) && i.SetKind == FileTypeDialogDataSet.SetKind.Image);
     }
 }

+ 12 - 5
src/PixiEditor/Models/Commands/CommandController.cs

@@ -5,9 +5,11 @@ using System.Reflection;
 using System.Threading.Tasks;
 using Avalonia.Input;
 using Avalonia.Media;
+using Avalonia.Threading;
 using Microsoft.Extensions.DependencyInjection;
 using Newtonsoft.Json;
 using PixiEditor.Exceptions;
+using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.AnalyticsAPI;
 using PixiEditor.Models.Commands.Attributes.Commands;
@@ -21,6 +23,7 @@ using PixiEditor.Models.Input;
 using PixiEditor.Models.Structures;
 using PixiEditor.OperatingSystem;
 using PixiEditor.UI.Common.Localization;
+using PixiEditor.ViewModels;
 using Command = PixiEditor.Models.Commands.Commands.Command;
 using CommandAttribute = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
@@ -446,11 +449,14 @@ internal class CommandController
 
         return;
 
-        static async void ActionOnException(Task faultedTask)
+        static void ActionOnException(Task faultedTask)
         {
-            // since this method is "async void" and not "async Task", the runtime will propagate exceptions out if it
-            // (instead of putting them into the returned task and forgetting about them)
-            await faultedTask; // this instantly throws the exception from the already faulted task
+            if (faultedTask.Exception == null)
+            {
+                return;
+            }
+
+            Dispatcher.UIThread.Post(() => throw faultedTask.Exception); // Re-throw the exception on the UI thread
         }
 
         ValueTask ReportEndTime(Task originalTask)
@@ -706,7 +712,8 @@ internal class CommandController
         if (IOperatingSystem.Current.IsMacOs)
         {
             KeyCombination newCombination = combination;
-            if (combination.Modifiers.HasFlag(KeyModifiers.Control) && !combination.Modifiers.HasFlag(KeyModifiers.Meta))
+            if (combination.Modifiers.HasFlag(KeyModifiers.Control) &&
+                !combination.Modifiers.HasFlag(KeyModifiers.Meta))
             {
                 newCombination.Modifiers &= ~KeyModifiers.Control;
                 newCombination.Modifiers |= KeyModifiers.Meta;

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

@@ -215,6 +215,8 @@ internal static class ClipboardController
                 }
             }
 
+            manager.Owner.ToolsSubViewModel.SetActiveTool<MoveToolViewModel>(false);
+
             return true;
         }
 
@@ -596,9 +598,9 @@ internal static class ClipboardController
         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()
@@ -651,21 +653,42 @@ internal static class ClipboardController
         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();
 
         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())));
 
         data.Set(format, idsBytes);
 
         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.Actions;
 using PixiEditor.Helpers.Extensions;
@@ -24,7 +21,6 @@ using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.ViewModels.Document.Nodes;
 
 namespace PixiEditor.Models.DocumentModels.Public;
 #nullable enable
@@ -204,60 +200,98 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// Duplicates the member with the <paramref name="guidValue"/>
     /// </summary>
     /// <param name="guidValue">The Guid of the member</param>
-    public void DuplicateMember(Guid guidValue)
+    public Guid? DuplicateMember(Guid guidValue)
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
-            return;
+            return null;
 
         Internals.ChangeController.TryStopActiveExecutor();
 
         bool isFolder = Document.StructureHelper.Find(guidValue) is IFolderHandler;
+        Guid newGuid = Guid.NewGuid();
         if (!isFolder)
         {
-            Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
                 new DuplicateLayer_Action(guidValue, newGuid),
                 new CreateAnimationDataFromLayer_Action(newGuid));
         }
         else
         {
-            Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
                 new DuplicateFolder_Action(guidValue, newGuid, null),
                 new SetSelectedMember_PassthroughAction(newGuid));
         }
+
+        return newGuid;
     }
 
-    public void ImportMember(Guid layerId, IDocument sourceDocument)
+    public Guid? ImportMember(Guid layerId, IDocument sourceDocument)
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
-            return;
+            return null;
 
         Internals.ChangeController.TryStopActiveExecutor();
 
         if (sourceDocument == this.Document)
         {
-            DuplicateMember(layerId);
-            return;
+            return DuplicateMember(layerId);
         }
 
         if (!sourceDocument.StructureHelper.TryFindNode(layerId, out IStructureMemberHandler? member))
-            return;
+            return null;
 
+        Guid newGuid = Guid.NewGuid();
         if (member is ILayerHandler layer)
         {
-            Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
                 new ImportLayer_Action(sourceDocument.ShareNode<IReadOnlyLayerNode>(layer.Id), newGuid),
                 new CreateAnimationDataFromLayer_Action(newGuid));
         }
         else if (member is IFolderHandler folder)
         {
-            Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
                 new ImportFolder_Action(sourceDocument.ShareNode<IReadOnlyFolderNode>(folder.Id), newGuid, null),
                 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>
@@ -311,7 +345,8 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <param name="anchor">Where the existing content should be put</param>
     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.Y < 1)
             return;
@@ -908,6 +943,11 @@ internal class DocumentOperationsModule : IDocumentOperations
             node.InternalName == OutputNode.UniqueName)
             return null;
 
+        if (node is IStructureMemberHandler)
+        {
+            return ImportMember(nodeId, Document);
+        }
+
         Guid newGuid = Guid.NewGuid();
 
         Internals.ActionAccumulator.AddFinishedActions(new DuplicateNode_Action(nodeId, newGuid));

+ 2 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -17,6 +17,8 @@ internal class PasteImageExecutor : UpdateableChangeExecutor, ITransformableExec
     private bool drawOnMask;
     private Guid? memberGuid;
 
+    public override bool BlocksOtherActions => false;
+
     public PasteImageExecutor(Surface image, VecI pos)
     {
         this.image = image;

+ 4 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -471,6 +471,10 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         }
 
         internals!.ActionAccumulator.AddActions(new EndPreviewShiftLayers_Action());
+        if (!movedOnce)
+        {
+            DoTransform(lastCorners);
+        }
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();

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

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

+ 35 - 0
src/PixiEditor/Models/Serialization/Factories/VecD4SerializationFactory.cs

@@ -0,0 +1,35 @@
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.Serialization.Factories;
+
+public class VecD4SerializationFactory : SerializationFactory<byte[], Vec4D>
+{
+    public override string DeserializationId { get; } = "PixiEditor.VecD4";
+
+    public override byte[] Serialize(Vec4D original)
+    {
+        byte[] result = new byte[sizeof(double) * 4];
+        BitConverter.GetBytes(original.X).CopyTo(result, 0);
+        BitConverter.GetBytes(original.Y).CopyTo(result, sizeof(double));
+        BitConverter.GetBytes(original.Z).CopyTo(result, sizeof(double) * 2);
+        BitConverter.GetBytes(original.W).CopyTo(result, sizeof(double) * 3);
+        return result;
+    }
+
+    public override bool TryDeserialize(object serialized, out Vec4D original,
+        (string serializerName, string serializerVersion) serializerData)
+    {
+        if (serialized is byte[] { Length: sizeof(double) * 4 } bytes)
+        {
+            original = new Vec4D(
+                BitConverter.ToDouble(bytes, 0),
+                BitConverter.ToDouble(bytes, sizeof(double)),
+                BitConverter.ToDouble(bytes, sizeof(double) * 2),
+                BitConverter.ToDouble(bytes, sizeof(double) * 3));
+            return true;
+        }
+
+        original = default;
+        return false; 
+    }
+}

+ 11 - 7
src/PixiEditor/PixiEditor.csproj

@@ -65,6 +65,10 @@
     <ProjectReference Include="..\PixiEditor.Platform.Standalone\PixiEditor.Platform.Standalone.csproj"/>
   </ItemGroup>
 
+  <ItemGroup Condition=" '$(Configuration)' == 'ReleaseNoUpdate' ">
+    <ProjectReference Include="..\PixiEditor.Platform.Standalone\PixiEditor.Platform.Standalone.csproj"/>
+  </ItemGroup>
+
   <ItemGroup Condition=" '$(Configuration)' == 'DevRelease' ">
     <ProjectReference Include="..\PixiEditor.Platform.Standalone\PixiEditor.Platform.Standalone.csproj"/>
   </ItemGroup>
@@ -91,21 +95,21 @@
     <PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0"/>
     <PackageReference Include="Avalonia" 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.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.-->
     <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)"/>
     <PackageReference Condition="'$(Configuration)' == 'DebugSteam'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)"/>
     <PackageReference Include="ByteSize" Version="2.1.2"/>
     <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="Svg.Controls.Skia.Avalonia" Version="11.3.0.1" />
   </ItemGroup>
 
   <ItemGroup>

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

@@ -43,5 +43,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.95")]
-[assembly: AssemblyFileVersion("2.0.0.95")]
+[assembly: AssemblyVersion("2.0.0.99")]
+[assembly: AssemblyFileVersion("2.0.0.99")]

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

@@ -39,7 +39,7 @@
                         </Border>
                         <Border Grid.Row="1" Background="{DynamicResource ThemeControlMidBrush}">
                             <StackPanel>
-                                <ItemsControl ItemsSource="{TemplateBinding Outputs}"
+                                <ItemsControl Name="PART_Outputs" ItemsSource="{TemplateBinding Outputs}"
                                               ClipToBounds="False">
                                     <ItemsControl.ItemContainerTheme>
                                         <ControlTheme TargetType="ContentPresenter">
@@ -47,7 +47,7 @@
                                         </ControlTheme>
                                     </ItemsControl.ItemContainerTheme>
                                 </ItemsControl>
-                                <ItemsControl ItemsSource="{TemplateBinding Inputs}" ClipToBounds="False">
+                                <ItemsControl Name="PART_Inputs" ItemsSource="{TemplateBinding Inputs}" ClipToBounds="False">
                                     <ItemsControl.ItemContainerTheme>
                                         <ControlTheme TargetType="ContentPresenter">
                                             <Setter Property="DataContext" Value="." />

+ 3 - 0
src/PixiEditor/ViewLocator.cs

@@ -4,9 +4,11 @@ using Avalonia.Controls.Templates;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiDocks.Core.Docking;
 using PixiEditor.ViewModels.Dock;
+using PixiEditor.ViewModels.Nodes.Properties;
 using PixiEditor.ViewModels.SubViewModels;
 using PixiEditor.Views.Dock;
 using PixiEditor.Views.Layers;
+using PixiEditor.Views.Nodes.Properties;
 
 namespace PixiEditor;
 
@@ -17,6 +19,7 @@ public class ViewLocator : IDataTemplate
         [typeof(ViewportWindowViewModel)] = typeof(DocumentTemplate),
         [typeof(LazyViewportWindowViewModel)] = typeof(LazyDocumentTemplate),
         [typeof(LayersDockViewModel)] = typeof(LayersManager),
+        [typeof(SinglePropertyViewModel)] = typeof(DoublePropertyView),
     };
 
     public Control Build(object? data)

+ 9 - 5
src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs

@@ -92,11 +92,12 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
 
     [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "FLIP_LAYERS_HORIZONTALLY",
         "FLIP_LAYERS_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_HORIZONTALLY", MenuItemOrder = 16, Icon = PixiPerfectIcons.MirrorHorizontal,
+        MenuItemPath = "LAYER/FLIP/FLIP_LAYERS_HORIZONTALLY", MenuItemOrder = 16,
+        Icon = PixiPerfectIcons.MirrorHorizontal,
         AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.FlipLayersVertical", FlipType.Vertical, "FLIP_LAYERS_VERTICALLY",
         "FLIP_LAYERS_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_VERTICALLY", MenuItemOrder = 17, Icon = PixiPerfectIcons.MirrorVertical,
+        MenuItemPath = "LAYER/FLIP/FLIP_LAYERS_VERTICALLY", MenuItemOrder = 17, Icon = PixiPerfectIcons.MirrorVertical,
         AnalyticsTrack = true)]
     public void FlipLayers(FlipType type)
     {
@@ -123,15 +124,15 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
 
     [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "ROT_LAYERS_90",
         "ROT_LAYERS_90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90,
-        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_90_D", MenuItemOrder = 11, Icon = PixiPerfectIcons.File90,
+        MenuItemPath = "LAYER/ROTATION/ROT_LAYERS_90_D", MenuItemOrder = 11, Icon = PixiPerfectIcons.File90,
         AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate180DegLayers", "ROT_LAYERS_180",
         "ROT_LAYERS_180", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180,
-        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_180_D", MenuItemOrder = 12, Icon = PixiPerfectIcons.File180,
+        MenuItemPath = "LAYER/ROTATION/ROT_LAYERS_180_D", MenuItemOrder = 12, Icon = PixiPerfectIcons.File180,
         AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate270DegLayers", "ROT_LAYERS_-90",
         "ROT_LAYERS_-90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270,
-        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_-90_D", MenuItemOrder = 13, Icon = PixiPerfectIcons.FileMinus90,
+        MenuItemPath = "LAYER/ROTATION/ROT_LAYERS_-90_D", MenuItemOrder = 13, Icon = PixiPerfectIcons.FileMinus90,
         AnalyticsTrack = true)]
     public void RotateLayers(RotationAngle angle)
     {
@@ -195,6 +196,9 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         MenuItemPath = "EDIT/DELETE_SELECTED", MenuItemOrder = 6, AnalyticsTrack = true)]
     public void DeleteSelected()
     {
+        if (ActiveDocument is null)
+            return;
+
         if (ActiveDocument.SelectionPathBindable is { IsEmpty: false })
         {
             Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels(activeDocument

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/LerpColorNodeViewModel.cs

@@ -4,5 +4,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("LERP_NODE", "NUMBERS", PixiPerfectIcons.SunCold)]
+[NodeViewModel("LERP_NODE", "COLOR", PixiPerfectIcons.SunCold)]
 internal class LerpColorNodeViewModel : NodeViewModel<LerpColorNode>;

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

+ 3 - 1
src/PixiEditor/ViewModels/Nodes/NodePropertyViewModel.cs

@@ -179,7 +179,9 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
             propertyType = type.GetMethod("Invoke").ReturnType.BaseType.GenericTypeArguments[0];
         }
 
-        string name = $"{propertyType.Name}PropertyViewModel";
+        string typeName = propertyType.Name;
+
+        string name = $"{typeName}PropertyViewModel";
 
         Type viewModelType = Type.GetType($"PixiEditor.ViewModels.Nodes.Properties.{name}");
         if (viewModelType == null)

+ 59 - 0
src/PixiEditor/ViewModels/Nodes/Properties/SinglePropertyViewModel.cs

@@ -0,0 +1,59 @@
+using System.ComponentModel;
+using Avalonia;
+using Avalonia.Media;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace PixiEditor.ViewModels.Nodes.Properties;
+
+internal class SinglePropertyViewModel : NodePropertyViewModel<float>
+{
+    private double min = double.MinValue;
+    private double max = double.MaxValue;
+
+    private NumberPickerMode numberPickerMode = NumberPickerMode.NumberInput;
+
+    private SliderSettings sliderSettings = new SliderSettings();
+
+    public NumberPickerMode NumberPickerMode
+    {
+        get => numberPickerMode;
+        set => SetProperty(ref numberPickerMode, value);
+    }
+
+    public double DoubleValue
+    {
+        get => Value;
+        set => Value = (float)value;
+    }
+
+    public double Min
+    {
+        get => min;
+        set => SetProperty(ref min, value);
+    }
+
+    public double Max
+    {
+        get => max;
+        set => SetProperty(ref max, value);
+    }
+
+    public SliderSettings SliderSettings
+    {
+        get => sliderSettings;
+        set => SetProperty(ref sliderSettings, value);
+    }
+
+    public SinglePropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+    }
+
+    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+    {
+        base.OnPropertyChanged(e);
+        if (e.PropertyName == nameof(Value))
+        {
+            OnPropertyChanged(nameof(DoubleValue));
+        }
+    }
+}

+ 29 - 11
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -21,6 +21,8 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Helpers.Constants;
 using PixiEditor.Models.Commands;
 using PixiEditor.UI.Common.Fonts;
@@ -90,7 +92,11 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
             Guid[] guids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
             await ClipboardController.TryPasteFromClipboard(doc, Owner.DocumentManagerSubViewModel, pasteAsNewLayer);
 
-            doc.Operations.InvokeCustomAction(() =>
+            // Leaving the code below commented out in case something breaks.
+            // It instantly ended paste image operation after I made it interruptable,
+            // I did test it, and it seems everything works fine without it.
+            /*doc.Operations.InvokeCustomAction(
+                () =>
             {
                 Guid[] newGuids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
 
@@ -105,7 +111,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
                         doc.Operations.AddSoftSelectedMember(diff[i]);
                     }
                 }
-            });
+            }, false);*/
         });
     }
 
@@ -202,6 +208,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
         Dispatcher.UIThread.InvokeAsync(async () =>
         {
+            Guid documentId = await ClipboardController.GetDocumentId();
             Guid[] toDuplicate = await ClipboardController.GetNodeIds();
 
             List<Guid> newIds = new();
@@ -210,9 +217,20 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
             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)
             {
-                Guid? newId = doc.Operations.DuplicateNode(nodeId);
+                Guid? newId = doc.Operations.ImportNode(nodeId, targetDocument);
                 if (newId != null)
                 {
                     newIds.Add(newId.Value);
@@ -225,7 +243,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
             block.ExecuteQueuedActions();
 
-            ConnectRelatedNodes(doc, nodeMapping);
+            ConnectRelatedNodes(targetDocument, doc, nodeMapping);
 
             doc.Operations.InvokeCustomAction(() =>
             {
@@ -363,7 +381,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         if (selectedNodes.Length == 0)
             return;
 
-        await ClipboardController.CopyNodes(selectedNodes);
+        await ClipboardController.CopyNodes(selectedNodes, doc.Id);
 
         areNodesInClipboard = true;
         ClearHasImageInClipboard();
@@ -383,7 +401,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         if (selectedCels.Length == 0)
             return;
 
-        await ClipboardController.CopyCels(selectedCels);
+        await ClipboardController.CopyCels(selectedCels, doc.Id);
 
         areCelsInClipboard = true;
         ClearHasImageInClipboard();
@@ -547,15 +565,15 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         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) &&
                 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)
                     continue;
@@ -570,7 +588,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
                 if (inputProperty == null || outputProperty == null)
                     continue;
 
-                doc.NodeGraph.ConnectProperties(inputProperty, outputProperty);
+                targetDoc.NodeGraph.ConnectProperties(inputProperty, outputProperty);
             }
         }
     }

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

@@ -620,7 +620,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
                     {
                         Dispatcher.UIThread.Post(() =>
                         {
-                            if (IPreferences.Current.GetPreference<bool>(PreferencesConstants.OpenDirectoryOnExport))
+                            if (IPreferences.Current.GetPreference<bool>(PreferencesConstants.OpenDirectoryOnExport, true))
                             {
                                 IOperatingSystem.Current.OpenFolder(result.finalPath);
                             }

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

@@ -614,7 +614,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
 
     [Command.Basic("PixiEditor.Layer.Rasterize", "RASTERIZE_ACTIVE_LAYER", "RASTERIZE_ACTIVE_LAYER_DESCRIPTIVE",
         CanExecute = "PixiEditor.Layer.AnySelectedLayerIsRasterizable",
-        Icon = PixiPerfectIcons.LowresCircle, MenuItemPath = "LAYER/VECTOR/RASTERIZE_ACTIVE_LAYER",
+        Icon = PixiPerfectIcons.LowresCircle, MenuItemPath = "LAYER/RASTERIZE_ACTIVE_LAYER",
         AnalyticsTrack = true)]
     public void RasterizeActiveLayer()
     {
@@ -635,7 +635,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
 
     [Command.Basic("PixiEditor.Layer.ConvertToCurve", "CONVERT_TO_CURVE", "CONVERT_TO_CURVE_DESCRIPTIVE",
         CanExecute = "PixiEditor.Layer.AnySelectedMemberIsVectorLayer",
-        MenuItemPath = "LAYER/VECTOR/CONVERT_TO_CURVE", AnalyticsTrack = true)]
+        MenuItemPath = "LAYER/CONVERT_TO_CURVE", AnalyticsTrack = true)]
     public void ConvertActiveLayersToCurve()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -655,7 +655,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
 
     [Command.Basic("PixiEditor.Layer.SeparateShapes", "SEPARATE_SHAPES", "SEPARATE_SHAPES_DESCRIPTIVE",
         CanExecute = "PixiEditor.Layer.AnySelectedMemberIsVectorLayer",
-        MenuItemPath = "LAYER/VECTOR/SEPARATE_SHAPES", AnalyticsTrack = true)]
+        MenuItemPath = "LAYER/SEPARATE_SHAPES", AnalyticsTrack = true)]
     public void SeparateShapes()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -678,14 +678,14 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         Key = Key.X, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
         Parameter = false,
         ShortcutContexts = [typeof(ViewportWindowViewModel), typeof(TextOverlay)],
-        MenuItemPath = "LAYER/TEXT/EXTRACT_SELECTED_TEXT", AnalyticsTrack = true)]
+        MenuItemPath = "LAYER/EXTRACT_SELECTED_TEXT", AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Layer.ExtractSelectedCharacters", "EXTRACT_SELECTED_CHARACTERS",
         "EXTRACT_SELECTED_CHARACTERS_DESCRIPTIVE",
         CanExecute = "PixiEditor.Layer.SelectedMemberIsSelectedText",
         Key = Key.X, Modifiers = KeyModifiers.Control | KeyModifiers.Shift | KeyModifiers.Alt,
         Parameter = true,
         ShortcutContexts = [typeof(ViewportWindowViewModel), typeof(TextOverlay)],
-        MenuItemPath = "LAYER/TEXT/EXTRACT_SELECTED_CHARACTERS", AnalyticsTrack = true)]
+        MenuItemPath = "LAYER/EXTRACT_SELECTED_CHARACTERS", AnalyticsTrack = true)]
     public void ExtractSelectedText(bool extractEachCharacter)
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;

+ 4 - 5
src/PixiEditor/ViewModels/SubViewModels/MiscViewModel.cs

@@ -20,8 +20,7 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Internal("PixiEditor.Links.OpenHyperlink")]
-    [Command.Basic("PixiEditor.Links.OpenDocumentation", "https://pixieditor.net/docs/introduction", "DOCUMENTATION",
-        "OPEN_DOCUMENTATION", Icon = PixiPerfectIcons.Globe,
+    [Command.Basic("PixiEditor.Links.OpenDocumentation", "https://pixieditor.net/docs/", "DOCUMENTATION", "OPEN_DOCUMENTATION", Icon = PixiPerfectIcons.Globe,
         MenuItemPath = "HELP/DOCUMENTATION", MenuItemOrder = 0, AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Links.OpenWebsite", "https://pixieditor.net", "WEBSITE", "OPEN_WEBSITE",
         Icon = PixiPerfectIcons.Globe,
@@ -29,8 +28,7 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Links.OpenRepository", "https://github.com/PixiEditor/PixiEditor", "REPOSITORY",
         "OPEN_REPOSITORY", Icon = PixiPerfectIcons.Globe,
         MenuItemPath = "HELP/REPOSITORY", MenuItemOrder = 2, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Links.OpenLicense", "{BaseDir}LICENSE", "LICENSE", "OPEN_LICENSE",
-        Icon = PixiPerfectIcons.Folder,
+    [Command.Basic("PixiEditor.Links.OpenLicense", "{BaseDir}/LICENSE", "LICENSE", "OPEN_LICENSE", Icon = PixiPerfectIcons.Folder,
         MenuItemPath = "HELP/LICENSE", MenuItemOrder = 3, AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Links.OpenOtherLicenses", "{BaseDir}/Third Party Licenses", "THIRD_PARTY_LICENSES",
         "OPEN_THIRD_PARTY_LICENSES", Icon = PixiPerfectIcons.Folder,
@@ -41,7 +39,8 @@ internal class MiscViewModel : SubViewModel<ViewModelMain>
         {
             if (uri.StartsWith("{BaseDir}"))
             {
-                uri = uri.Replace("{BaseDir}", AppDomain.CurrentDomain.BaseDirectory);
+                string exeDir = Path.GetDirectoryName(Environment.ProcessPath);
+                uri = uri.Replace("{BaseDir}", exeDir ?? string.Empty);
             }
 
             IOperatingSystem.Current.OpenUri(uri);

+ 4 - 0
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -98,6 +98,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
 
     public ObservableCollection<IToolSetHandler> AllToolSets { get; } = new();
     public List<string> AllToolSetNames => AllToolSets.Select(x => x.Name).ToList();
+    public List<IToolSetHandler> NonSelectedToolSets => AllToolSets.Where(x => x != ActiveToolSet).ToList();
 
     public event EventHandler<SelectedToolEventArgs>? SelectedToolChanged;
 
@@ -160,6 +161,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         SetActiveToolSet(AllToolSets.First());
     }
 
+    [Command.Internal("PixiEditor.Tools.SetActiveToolSet", AnalyticsTrack = true)]
     public void SetActiveToolSet(IToolSetHandler toolSetHandler)
     {
         ActiveTool?.OnToolDeselected(false);
@@ -168,6 +170,8 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         UpdateEnabledState();
 
         ActiveTool?.OnToolSelected(false);
+
+        OnPropertyChanged(nameof(NonSelectedToolSets));
     }
 
     public void SetupToolsTooltipShortcuts()

+ 4 - 3
src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -140,7 +140,8 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     {
         if (IOperatingSystem.Current.IsLinux)
         {
-            if (File.Exists("no-updates"))
+            string exeDir = Path.GetDirectoryName(Environment.ProcessPath);
+            if (File.Exists(Path.Combine(exeDir, "no-updates")))
             {
                 UpdateState = UpdateState.UnableToCheck;
                 return;
@@ -283,7 +284,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         UpdateChecker.SetLatestReleaseInfo(new ReleaseInfo(true) { TagName = "2.2.2.2" });
         Install(true);
     }
-    
+
     [Command.Debug("PixiEditor.Update.DebugDownload", "Debug Download Update",
         "(DEBUG) Download update file")]
     public void DebugDownload()
@@ -379,7 +380,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
     private static bool InstallDirReadOnly()
     {
-        string installDir = AppDomain.CurrentDomain.BaseDirectory;
+        string installDir = Path.GetDirectoryName(Environment.ProcessPath);
         DirectoryInfo dirInfo = new DirectoryInfo(installDir);
         return dirInfo.Attributes.HasFlag(FileAttributes.ReadOnly);
     }

+ 4 - 4
src/PixiEditor/Views/Layers/ReferenceLayer.axaml

@@ -16,10 +16,11 @@
              mc:Ignorable="d"
              d:DesignHeight="60" d:DesignWidth="350" VerticalAlignment="Center" x:Name="uc">
     <UserControl.Styles>
+        <Style Selector="Button#topmostBtn">
+            <Setter Property="Content" Value="{DynamicResource icon-layers-top}"></Setter>
+        </Style>
         <Style Selector=":topmost Button#topmostBtn">
-            <Setter Property="RenderTransform">
-                <RotateTransform Angle="180" />
-            </Setter>
+            <Setter Property="Content" Value="{DynamicResource icon-layers-bottom}"/>
         </Style>
     </UserControl.Styles>
     <Border BorderThickness="0 2 0 0" MinWidth="60"
@@ -67,7 +68,6 @@
                                 Command="{cmds:Command PixiEditor.Layer.ToggleReferenceLayerTopMost}"
                                 Name="topmostBtn"
                                 Classes="pixi-icon"
-                                Content="{DynamicResource icon-reference-layer}"
                                 ToolTip.Tip="{Binding Document.ReferenceLayerViewModel.IsTopMost, ElementName=uc, Converter={converters:BoolToValueConverter FalseValue='localized:PUT_REFERENCE_LAYER_ABOVE', TrueValue='localized:PUT_REFERENCE_LAYER_BELOW'}}"
                                 RenderOptions.BitmapInterpolationMode="HighQuality"
                                 Width="24" Height="24" HorizontalAlignment="Right"/>

+ 17 - 2
src/PixiEditor/Views/Main/DocumentPreview.axaml

@@ -7,6 +7,7 @@
              xmlns:input="clr-namespace:PixiEditor.Views.Input"
              xmlns:ui1="clr-namespace:PixiEditor.Helpers.UI"
              xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+             xmlns:localization="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              Name="uc"
              x:Class="PixiEditor.Views.Main.DocumentPreview">
@@ -21,9 +22,11 @@
               IsVisible="{Binding !!Document, ElementName=uc}"
               Margin="10" Background="Transparent"
               d:Width="8" d:Height="8">
+
             <viewportControls:FixedViewport
                 Delayed="True"
                 x:Name="viewport"
+                RenderInDocSize="{Binding ElementName=highDpiButton, Path=IsChecked}"
                 Document="{Binding Document, ElementName=uc}"
                 Background="{Binding ActiveItem.Value, ElementName=backgroundButton}"/>
         </Grid>
@@ -54,8 +57,20 @@
             </TextBlock>
         </StackPanel>
         <Grid Grid.Row="2" HorizontalAlignment="Right" Margin="0,0,5,0" ui:RenderOptionsBindable.BitmapInterpolationMode="{Binding ElementName=backgroundButton, Path=ActiveItem.ScalingMode}">
-            <StackPanel Orientation="Horizontal">
-                <input:ListSwitchButton x:Name="formatButton" Margin="0,0,5,0" Height="20">
+            <StackPanel Spacing="5" Orientation="Horizontal">
+                <StackPanel.Styles>
+                    <Style Selector="ToggleButton#highDpiButton">
+                        <Setter Property="Content" Value="{DynamicResource icon-circle}"/>
+                    </Style>
+                    <Style Selector="ToggleButton#highDpiButton:checked">
+                        <Setter Property="Content" Value="{DynamicResource icon-lowres-circle}"/>
+                        <Setter Property="Background" Value="Transparent"/>
+                        <Setter Property="BorderThickness" Value="0"/>
+                    </Style>
+                </StackPanel.Styles>
+
+                <ToggleButton x:Name="highDpiButton" Classes="pixi-icon" localization:Translator.TooltipKey="TOGGLE_HIGH_RES_PREVIEW"/>
+                <input:ListSwitchButton x:Name="formatButton" Height="20">
                     <input:ListSwitchButton.Items>
                         <input:SwitchItemObservableCollection>
                             <input:SwitchItem Content="RGBA" Background="{DynamicResource ThemeControlMidBrush}" Value="RGBA"/>

+ 23 - 53
src/PixiEditor/Views/Main/Tools/ToolsPicker.axaml

@@ -10,58 +10,28 @@
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              ClipToBounds="False"
              x:Class="PixiEditor.Views.Main.Tools.ToolsPicker" Name="picker">
-    <Border CornerRadius="{DynamicResource ControlCornerRadius}"
-            BorderBrush="{DynamicResource ThemeBorderMidBrush}"
-            BorderThickness="{DynamicResource ThemeBorderThickness}"
-            Cursor="Arrow"
-            Width="48"
-            Background="{DynamicResource ThemeBackgroundBrush1}">
-        <Grid>
-            <Grid.RowDefinitions>
-                <RowDefinition Height="25" />
-                <RowDefinition Height="*" />
-            </Grid.RowDefinitions>
-            
-            <Border Grid.Row="0" Background="{DynamicResource ThemeBackgroundBrush2}">
-                <StackPanel Orientation="Horizontal">
-                    <Button Classes="pixi-icon" Content="{DynamicResource icon-chevron-left}"
-                            Command="{Binding SwitchToolSetCommand, ElementName=picker}">
-                        <Button.CommandParameter>
-                            <system:Boolean>False</system:Boolean>
-                        </Button.CommandParameter>
-                    </Button>
-                    <Button Classes="pixi-icon" Content="{DynamicResource icon-chevron-right}"
-                            Command="{Binding SwitchToolSetCommand, ElementName=picker}">
-                        <Button.CommandParameter>
-                            <system:Boolean>True</system:Boolean>
-                        </Button.CommandParameter>
-                    </Button>
-                    <!--<ComboBox ItemsSource="{Binding ElementName=picker, Path=ToolSets}"
-                              SelectedItem="{Binding ElementName=picker, Path=ToolSet}"
-                              SelectedIndex="0">
-                        <ComboBox.ItemTemplate>
-                            <DataTemplate>
-                                <TextBlock ui:Translator.Key="{Binding Name}" />
-                            </DataTemplate>
-                        </ComboBox.ItemTemplate>
-                    </ComboBox>-->
-                </StackPanel>
-            </Border>
-            <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
-                <ItemsControl ItemsSource="{Binding ElementName=picker, Path=ToolSet.Tools}" Padding="0 2">
-                    <ItemsControl.ItemTemplate>
-                        <DataTemplate DataType="tools:ToolViewModel">
-                            <tools1:ToolPickerButton DataContext="{Binding}"
-                                                     IsSelected="{Binding IsActive}" />
-                        </DataTemplate>
-                    </ItemsControl.ItemTemplate>
-                    <ItemsControl.ItemsPanel>
-                        <ItemsPanelTemplate>
-                            <StackPanel Orientation="Vertical" />
-                        </ItemsPanelTemplate>
-                    </ItemsControl.ItemsPanel>
-                </ItemsControl>
-            </ScrollViewer>
-        </Grid>
+    <Border
+        ClipToBounds="True"
+        Cursor="Arrow" Width="48"
+        BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+        CornerRadius="{DynamicResource ControlCornerRadius}"
+        Padding="0, 5"
+        BorderThickness="{DynamicResource ThemeBorderThickness}"
+        Background="{DynamicResource ThemeBackgroundBrush1}">
+        <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
+            <ItemsControl ItemsSource="{Binding ElementName=picker, Path=ToolSet.Tools}" Padding="0 2">
+                <ItemsControl.ItemTemplate>
+                    <DataTemplate DataType="tools:ToolViewModel">
+                        <tools1:ToolPickerButton DataContext="{Binding}"
+                                                 IsSelected="{Binding IsActive}" />
+                    </DataTemplate>
+                </ItemsControl.ItemTemplate>
+                <ItemsControl.ItemsPanel>
+                    <ItemsPanelTemplate>
+                        <StackPanel Orientation="Vertical" />
+                    </ItemsPanelTemplate>
+                </ItemsControl.ItemsPanel>
+            </ItemsControl>
+        </ScrollViewer>
     </Border>
 </UserControl>

+ 1 - 0
src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml

@@ -18,6 +18,7 @@
             x:Name="mainImage"
             Focusable="True"
             PreviewPainter="{Binding Document.PreviewPainter, ElementName=uc}"
+            CustomRenderSize="{Binding CustomRenderSize, ElementName=uc}"
             FrameToRender="{Binding Document.AnimationDataViewModel.ActiveFrameBindable, ElementName=uc}"
             SizeChanged="OnImageSizeChanged">
             <ui1:RenderOptionsBindable.BitmapInterpolationMode>

+ 27 - 0
src/PixiEditor/Views/Main/ViewportControls/FixedViewport.axaml.cs

@@ -23,6 +23,24 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     public static readonly StyledProperty<bool> DelayedProperty =
         AvaloniaProperty.Register<FixedViewport, bool>(nameof(Delayed), false);
 
+    public static readonly StyledProperty<bool> RenderInDocSizeProperty = AvaloniaProperty.Register<FixedViewport, bool>(
+        nameof(RenderInDocSize));
+
+    public static readonly StyledProperty<VecI> CustomRenderSizeProperty = AvaloniaProperty.Register<FixedViewport, VecI>(
+        nameof(CustomRenderSize));
+
+    public VecI CustomRenderSize
+    {
+        get => GetValue(CustomRenderSizeProperty);
+        set => SetValue(CustomRenderSizeProperty, value);
+    }
+
+    public bool RenderInDocSize
+    {
+        get => GetValue(RenderInDocSizeProperty);
+        set => SetValue(RenderInDocSizeProperty, value);
+    }
+
     public event PropertyChangedEventHandler? PropertyChanged;
 
     public bool Delayed
@@ -42,6 +60,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     static FixedViewport()
     {
         DocumentProperty.Changed.Subscribe(OnDocumentChange);
+        RenderInDocSizeProperty.Changed.Subscribe(OnRenderInDocSizeChanged);
     }
 
     public FixedViewport()
@@ -132,6 +151,14 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
         viewport.ForceRefreshFinalImage();
     }
 
+    private static void OnRenderInDocSizeChanged(AvaloniaPropertyChangedEventArgs<bool> args)
+    {
+        FixedViewport? viewport = (FixedViewport)args.Sender;
+        viewport.CustomRenderSize = args.NewValue.Value ? viewport.Document?.SizeBindable ?? VecI.Zero : VecI.Zero;
+        viewport.InvalidateMeasure();
+        viewport.ForceRefreshFinalImage();
+    }
+
     private void DocSizeChanged(object? sender, DocumentSizeChangedEventArgs e)
     {
         Document?.Operations.AddOrUpdateViewport(GetLocation());

+ 66 - 22
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml

@@ -19,6 +19,7 @@
     xmlns:input="clr-namespace:PixiEditor.Views.Input"
     xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
     xmlns:ui1="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+    xmlns:handlers="clr-namespace:PixiEditor.Models.Handlers"
     mc:Ignorable="d"
     x:Name="vpUc"
     d:DesignHeight="450"
@@ -96,16 +97,16 @@
                         </StackPanel>
                         <Separator />
                         <StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Spacing="10">
-                            <ToggleButton Width="32" Height="32" ui:Translator.TooltipKey="TOGGLE_VERTICAL_SYMMETRY"
+                            <ToggleButton Width="32" Height="32" ui:Translator.TooltipKey="TOGGLE_HORIZONTAL_SYMMETRY"
                                           Classes="OverlayToggleButton pixi-icon"
                                           IsChecked="{Binding Document.VerticalSymmetryAxisEnabledBindable, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
-                                          Content="{DynamicResource icon-y-symmetry}"
+                                          Content="{DynamicResource icon-x-symmetry}"
                                           Cursor="Hand" />
                             <ToggleButton Width="32" Height="32"
-                                          ui:Translator.TooltipKey="TOGGLE_HORIZONTAL_SYMMETRY"
+                                          ui:Translator.TooltipKey="TOGGLE_VERTICAL_SYMMETRY"
                                           Classes="OverlayToggleButton pixi-icon"
                                           IsChecked="{Binding Document.HorizontalSymmetryAxisEnabledBindable, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
-                                          Content="{DynamicResource icon-x-symmetry}"
+                                          Content="{DynamicResource icon-y-symmetry}"
                                           Cursor="Hand" />
                         </StackPanel>
                         <Separator />
@@ -140,11 +141,11 @@
                         <TextBlock HorizontalAlignment="Center"
                                    ui:Translator.Key="GRIDLINES_SIZE" Margin="0 0 0 10" />
                         <controls:NumberInput Min="1"
-                                           Max="1024"
-                                           Value="{Binding GridLinesXSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}" />
+                                              Max="1024"
+                                              Value="{Binding GridLinesXSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}" />
                         <controls:NumberInput Min="1"
-                                           Max="1024"
-                                           Value="{Binding GridLinesYSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}" />
+                                              Max="1024"
+                                              Value="{Binding GridLinesYSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}" />
                         <Separator />
                         <TextBlock ui:Translator.Key="RENDER_PREVIEW" />
                         <ComboBox
@@ -168,26 +169,69 @@
             ZIndex="100" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="10">
             <Grid.RowDefinitions>
                 <RowDefinition MinHeight="40" MaxHeight="120" />
-                <RowDefinition Height="35" />
+                <RowDefinition Height="30" />
                 <RowDefinition Height="*" />
             </Grid.RowDefinitions>
             <tools:Toolbar
                 DataContext="{Binding Source={viewModels:MainVM}, Path=.}" />
-            <Border Grid.Row="1" ClipToBounds="False"
-                    HorizontalAlignment="Left"
-                    Padding="5 0"
-                    Margin="0 2.5 0 5"
-                    Height="25"
-                    BorderBrush="{DynamicResource ThemeBorderMidBrush}"
-                    BorderThickness="1"
-                    Background="{DynamicResource ThemeBackgroundBrush2}"
-                    CornerRadius="{DynamicResource ControlCornerRadius}">
-                <TextBlock
-                    ui:Translator.Key="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet.Name}"
-                    VerticalAlignment="Center" />
-            </Border>
+            <StackPanel Margin="0, 5, 0, 0" Background="Transparent" Grid.Row="1" Orientation="Horizontal" Name="toolsetsPanel">
+                <Border ClipToBounds="False"
+                        HorizontalAlignment="Left"
+                        Padding="5 0"
+                        Name="activeToolSetChip"
+                        Height="25"
+                        BorderBrush="{DynamicResource ThemeBorderHighBrush}"
+                        BorderThickness="1"
+                        Background="{DynamicResource ThemeBackgroundBrush2}"
+                        CornerRadius="{DynamicResource ControlCornerRadius}">
+                    <TextBlock>
+                        <Run Classes="pixi-icon"
+                             Text="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet.Icon}" />
+                        <Run
+                            ui:Translator.Key="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet.Name}" />
+                    </TextBlock>
+                </Border>
+                <ItemsControl
+                    Name="otherToolSets"
+                    Background="Transparent"
+                    ItemsSource="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.NonSelectedToolSets, Mode=OneWay}">
+                    <ItemsControl.ItemsPanel>
+                        <ItemsPanelTemplate>
+                            <StackPanel Margin="5, 0, 0, 0" Spacing="5"
+                                        Orientation="Horizontal" />
+                        </ItemsPanelTemplate>
+                    </ItemsControl.ItemsPanel>
+                    <ItemsControl.ItemTemplate>
+                        <DataTemplate x:DataType="{x:Type handlers:IToolSetHandler}">
+                            <Button
+                                Command="{xaml:Command PixiEditor.Tools.SetActiveToolSet, UseProvided=True}"
+                                CommandParameter="{Binding}"
+                                FontSize="{DynamicResource FontSizeMedium}"
+                                VerticalAlignment="Center">
+                                <TextBlock>
+                                    <Run Classes="pixi-icon" Text="{Binding Icon}" />
+                                    <Run ui:Translator.Key="{Binding Name}" />
+                                </TextBlock>
+                            </Button>
+                        </DataTemplate>
+                    </ItemsControl.ItemTemplate>
+                    <ItemsControl.IsVisible>
+                        <MultiBinding Converter="{converters:AnyTrueConverter}">
+                            <MultiBinding.Bindings>
+                                <Binding Path="IsPointerOver" ElementName="toolsPicker" />
+                                <Binding Path="IsPointerOver" ElementName="activeToolSetChip" />
+                                <Binding Path="IsPointerOver" ElementName="otherToolSets" />
+                            </MultiBinding.Bindings>
+                        </MultiBinding>
+                    </ItemsControl.IsVisible>
+                </ItemsControl>
+            </StackPanel>
             <tools:ToolsPicker Grid.Row="2" IsHitTestVisible="True"
+                               Name="toolsPicker"
+                               Padding="0, 5, 0, 0"
+                               Background="Transparent"
                                HorizontalAlignment="Left"
+                               Margin="0, 0, 0, -95"
                                ToolSet="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet, Mode=TwoWay}"
                                ToolSets="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.AllToolSets, Mode=OneWay}"
                                SwitchToolSetCommand="{xaml:Command Name=PixiEditor.Tools.SwitchToolSet, UseProvided=True}" />

+ 7 - 1
src/PixiEditor/Views/Nodes/ConnectionView.cs

@@ -67,6 +67,7 @@ internal class ConnectionView : TemplatedControl
         set { SetValue(OutputNodePositionProperty, value); }
     }
 
+    private Canvas? mainCanvas;
 
     static ConnectionView()
     {
@@ -102,7 +103,12 @@ internal class ConnectionView : TemplatedControl
             return default;
         }
 
-        Canvas canvas = this.FindAncestorOfType<NodeGraphView>().FindDescendantOfType<Canvas>();
+        if(mainCanvas != null && !mainCanvas.IsAttachedToVisualTree())
+        {
+            mainCanvas = null;
+        }
+
+        Canvas canvas = mainCanvas ??= this.FindAncestorOfType<NodeGraphView>().FindDescendantOfType<Canvas>();
 
         if (property.Node is null || canvas is null)
         {

+ 2 - 2
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -239,9 +239,9 @@ internal class NodeGraphView : Zoombox.Zoombox
 
         Dispatcher.UIThread.Post(() =>
         {
-            nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged += NodeItems_CollectionChanged;
             nodeViewsCache = nodeItemsControl.ItemsPanelRoot.Children.ToList();
             HandleNodesAdded(nodeViewsCache);
+            nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged += NodeItems_CollectionChanged;
         });
     }
 
@@ -274,7 +274,7 @@ internal class NodeGraphView : Zoombox.Zoombox
     protected override void OnKeyDown(KeyEventArgs e)
     {
         base.OnKeyDown(e);
-        if (e.Key == Key.Space)
+        if (e.Key == Key.Space && e.Source.Equals(rootPanel))
         {
             rootPanel.ContextFlyout?.ShowAt(rootPanel);
             e.Handled = true;

+ 42 - 6
src/PixiEditor/Views/Nodes/NodeView.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
@@ -6,6 +7,7 @@ using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Media;
+using Avalonia.Threading;
 using Avalonia.VisualTree;
 using ChunkyImageLib;
 using PixiEditor.Helpers;
@@ -19,6 +21,8 @@ using PixiEditor.Views.Nodes.Properties;
 namespace PixiEditor.Views.Nodes;
 
 [PseudoClasses(":selected")]
+[TemplatePart("PART_Inputs", typeof(ItemsControl))]
+[TemplatePart("PART_Outputs", typeof(ItemsControl))]
 public class NodeView : TemplatedControl
 {
     public static readonly StyledProperty<INodeHandler> NodeProperty =
@@ -155,11 +159,41 @@ public class NodeView : TemplatedControl
     public static readonly StyledProperty<int> ActiveFrameProperty =
         AvaloniaProperty.Register<NodeView, int>("ActiveFrame");
 
+    private Dictionary<INodePropertyHandler, NodePropertyView> propertyViews = new();
+
+    private ItemsControl inputsControl;
+    private ItemsControl outputsControl;
+
     static NodeView()
     {
         IsSelectedProperty.Changed.Subscribe(NodeSelectionChanged);
     }
 
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+
+        inputsControl = e.NameScope.Find<ItemsControl>("PART_Inputs");
+        outputsControl = e.NameScope.Find<ItemsControl>("PART_Outputs");
+
+        Dispatcher.UIThread.Post(
+            () =>
+        {
+            inputsControl.ItemsPanelRoot.Children.CollectionChanged += ChildrenOnCollectionChanged;
+            outputsControl.ItemsPanelRoot.Children.CollectionChanged += ChildrenOnCollectionChanged;
+
+            propertyViews.Clear();
+            propertyViews = this.GetVisualDescendants().OfType<NodePropertyView>()
+                .ToDictionary(x => (INodePropertyHandler)x.DataContext, x => x);
+        }, DispatcherPriority.Render);
+    }
+
+    private void ChildrenOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+    {
+        propertyViews = this.GetVisualDescendants().OfType<NodePropertyView>()
+            .ToDictionary(x => (INodePropertyHandler)x.DataContext, x => x);
+    }
+
     protected override void OnPointerPressed(PointerPressedEventArgs e)
     {
         base.OnPointerPressed(e);
@@ -233,15 +267,17 @@ public class NodeView : TemplatedControl
 
     public NodeSocket GetSocket(INodePropertyHandler property)
     {
-        NodePropertyView propertyView = this.GetVisualDescendants().OfType<NodePropertyView>()
-            .FirstOrDefault(x => x.DataContext == property);
-
-        if (propertyView is null)
+        if (propertyViews.TryGetValue(property, out var view))
         {
-            return default;
+            if (view is null)
+            {
+                return default;
+            }
+
+            return property.IsInput ? view.InputSocket : view.OutputSocket;
         }
 
-        return property.IsInput ? propertyView.InputSocket : propertyView.OutputSocket;
+        return null;
     }
 
     public Point GetSocketPoint(INodePropertyHandler property, Canvas canvas)

+ 7 - 0
src/PixiEditor/Views/Nodes/Properties/ColorMatrixPropertyView.axaml.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
 using Avalonia.Markup.Xaml;
 
 namespace PixiEditor.Views.Nodes.Properties;
@@ -10,5 +11,11 @@ public partial class ColorMatrixPropertyView : NodePropertyView
     {
         InitializeComponent();
     }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        HideSocket(true, false);
+    }
 }
 

+ 7 - 0
src/PixiEditor/Views/Nodes/Properties/GenericEnumPropertyView.axaml.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Markup.Xaml;
 
@@ -12,6 +13,12 @@ public partial class GenericEnumPropertyView : NodePropertyView
         InitializeComponent();
     }
 
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        HideSocket(true, false);
+    }
+
     private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
     {
         e.Handled = true;

+ 7 - 0
src/PixiEditor/Views/Nodes/Properties/KernelPropertyView.axaml.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
 using Avalonia.Markup.Xaml;
 
 namespace PixiEditor.Views.Nodes.Properties;
@@ -9,5 +10,11 @@ public partial class KernelPropertyView : NodePropertyView
     {
         InitializeComponent();
     }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        HideSocket(true, false);
+    }
 }
 

+ 7 - 0
src/PixiEditor/Views/Nodes/Properties/Matrix4x5FPropertyView.axaml.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
 using Avalonia.Markup.Xaml;
 
 namespace PixiEditor.Views.Nodes.Properties;
@@ -10,5 +11,11 @@ public partial class Matrix4x5FPropertyView : NodePropertyView
     {
         InitializeComponent();
     }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        HideSocket(true, false);
+    }
 }
 

+ 47 - 11
src/PixiEditor/Views/Visuals/PreviewPainterControl.cs

@@ -17,6 +17,15 @@ public class PreviewPainterControl : DrawieControl
         AvaloniaProperty.Register<PreviewPainterControl, PreviewPainter>(
             nameof(PreviewPainter));
 
+    public static readonly StyledProperty<VecI> CustomRenderSizeProperty = AvaloniaProperty.Register<PreviewPainterControl, VecI>(
+        nameof(CustomRenderSize));
+
+    public VecI CustomRenderSize
+    {
+        get => GetValue(CustomRenderSizeProperty);
+        set => SetValue(CustomRenderSizeProperty, value);
+    }
+
     public PreviewPainter PreviewPainter
     {
         get => GetValue(PreviewPainterProperty);
@@ -35,6 +44,7 @@ public class PreviewPainterControl : DrawieControl
     {
         PreviewPainterProperty.Changed.Subscribe(PainterChanged);
         BoundsProperty.Changed.Subscribe(UpdatePainterBounds);
+        CustomRenderSizeProperty.Changed.Subscribe(UpdatePainterBounds);
     }
 
     public PreviewPainterControl()
@@ -64,10 +74,10 @@ public class PreviewPainterControl : DrawieControl
         if (args.NewValue.Value != null)
         {
             sender.painterInstance = args.NewValue.Value.AttachPainterInstance();
-            if (sender.Bounds is { Width: > 0, Height: > 0 })
+            VecI finalSize = sender.GetFinalSize();
+            if (finalSize is { X: > 0, Y: > 0 })
             {
-                sender.PreviewPainter.ChangeRenderTextureSize(sender.painterInstance.RequestId,
-                    new VecI((int)sender.Bounds.Width, (int)sender.Bounds.Height));
+                sender.PreviewPainter.ChangeRenderTextureSize(sender.painterInstance.RequestId, finalSize);
             }
 
             sender.painterInstance.RequestMatrix = sender.OnPainterRequestMatrix;
@@ -93,23 +103,49 @@ public class PreviewPainterControl : DrawieControl
             return;
         }
 
+        if (CustomRenderSize.ShortestAxis > 0)
+        {
+            surface.Canvas.Save();
+            VecI finalSize = GetFinalSize();
+            surface.Canvas.Scale(
+                (float)Bounds.Width / finalSize.X,
+                (float)Bounds.Height / finalSize.Y);
+        }
+
         PreviewPainter.Paint(surface, painterInstance.RequestId);
+
+        if (CustomRenderSize.ShortestAxis > 0)
+        {
+            surface.Canvas.Restore();
+        }
     }
 
     private Matrix3X3 UniformScale(float x, float y, RectD previewBounds)
     {
-        float scaleX = (float)Bounds.Width / x;
-        float scaleY = (float)Bounds.Height / y;
+        VecI finalSize = GetFinalSize();
+        float scaleX = finalSize.X / x;
+        float scaleY = finalSize.Y / y;
         var scale = Math.Min(scaleX, scaleY);
-        float dX = (float)Bounds.Width / 2 / scale - x / 2;
+        float dX = (float)finalSize.X / 2 / scale - x / 2;
         dX -= (float)previewBounds.X;
-        float dY = (float)Bounds.Height / 2 / scale - y / 2;
+        float dY = (float)finalSize.Y / 2 / scale - y / 2;
         dY -= (float)previewBounds.Y;
         Matrix3X3 matrix = Matrix3X3.CreateScale(scale, scale);
         return matrix.Concat(Matrix3X3.CreateTranslation(dX, dY));
     }
 
-    private static void UpdatePainterBounds(AvaloniaPropertyChangedEventArgs<Rect> args)
+    private VecI GetFinalSize()
+    {
+        VecI finalSize = CustomRenderSize.ShortestAxis > 0 ? CustomRenderSize : new VecI((int)Bounds.Width, (int)Bounds.Height);
+        if(Bounds.Width < finalSize.X && Bounds.Height < finalSize.Y)
+        {
+            finalSize = new VecI((int)Bounds.Width, (int)Bounds.Height);
+        }
+
+        return finalSize;
+    }
+
+    private static void UpdatePainterBounds(AvaloniaPropertyChangedEventArgs args)
     {
         var sender = args.Sender as PreviewPainterControl;
 
@@ -120,10 +156,10 @@ public class PreviewPainterControl : DrawieControl
 
         if (sender.painterInstance != null)
         {
-            if (args.NewValue.Value is { Width: > 0, Height: > 0 })
+            VecI finalSize = sender.GetFinalSize();
+            if (finalSize is { X: > 0, Y: > 0 })
             {
-                sender.PreviewPainter.ChangeRenderTextureSize(sender.painterInstance.RequestId,
-                    new VecI((int)args.NewValue.Value.Width, (int)args.NewValue.Value.Height));
+                sender.PreviewPainter.ChangeRenderTextureSize(sender.painterInstance.RequestId, finalSize);
                 sender.PreviewPainter.RepaintFor(sender.painterInstance.RequestId);
             }
         }

+ 1 - 1
tests/Directory.Build.props

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

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

@@ -35,7 +35,16 @@ public class RenderTests : FullPixiEditorTest
     [InlineData("VectorWithSepiaFilter")]
     [InlineData("VectorWithSepiaFilterSrgb")]
     [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)
         {
@@ -44,7 +53,7 @@ public class RenderTests : FullPixiEditorTest
         }
 
         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);
 
         Assert.NotNull(pngFile);
@@ -55,11 +64,16 @@ public class RenderTests : FullPixiEditorTest
 
         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);
 
         Assert.NotNull(toCompareTo);
 
-        Assert.True(PixelCompare(image, toCompareTo));
+        Assert.True(PixelCompare(renderedToCompare, toCompareTo));
     }
 
     [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


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