Browse Source

Merge branch 'master' into pixiauth

Krzysztof Krysiński 2 months ago
parent
commit
e430fbd85a
100 changed files with 2891 additions and 608 deletions
  1. 62 63
      pipelines/Windows/tests-windows.yml
  2. 1 1
      samples/Directory.Build.props
  3. 18 0
      samples/PixiEditorExtensionSamples.sln
  4. 1 1
      samples/Sample1_HelloWorld/Sample1_HelloWorld.csproj
  5. 1 1
      samples/Sample2_LocalizationSample/Sample2_LocalizationSample.csproj
  6. 1 1
      samples/Sample3_Preferences/Sample3_Preferences.csproj
  7. 1 1
      samples/Sample4_CreatePopup/Sample4_CreatePopup.csproj
  8. 1 1
      samples/Sample5_Resources/Sample5_Resources.csproj
  9. 1 1
      samples/Sample6_Palettes/Sample6_Palettes.csproj
  10. 1 1
      samples/Sample7_FlyUI/Sample7_FlyUI.csproj
  11. 48 30
      samples/Sample7_FlyUI/WindowContentElement.cs
  12. 55 0
      samples/Sample8_CommandLibrary/CommandLibraryExtension.cs
  13. 9 0
      samples/Sample8_CommandLibrary/Program.cs
  14. 39 0
      samples/Sample8_CommandLibrary/Sample8_CommandLibrary.csproj
  15. 41 0
      samples/Sample8_CommandLibrary/extension.json
  16. 75 0
      samples/Sample8_Commands/CommandsSampleExtension.cs
  17. 3 0
      samples/Sample8_Commands/Localization/en.json
  18. 3 0
      samples/Sample8_Commands/Localization/pl.json
  19. 9 0
      samples/Sample8_Commands/Program.cs
  20. 39 0
      samples/Sample8_Commands/Sample8_Commands.csproj
  21. 41 0
      samples/Sample8_Commands/extension.json
  22. 24 0
      samples/Sample9_Document/CommandsSampleExtension.cs
  23. 8 0
      samples/Sample9_Document/Program.cs
  24. BIN
      samples/Sample9_Document/Resources/cs.png
  25. 45 0
      samples/Sample9_Document/Sample9_Document.csproj
  26. 44 0
      samples/Sample9_Document/extension.json
  27. 7 0
      samples/global.json
  28. 5 1
      src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs
  29. 16 1
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  30. 1 1
      src/ColorPicker
  31. 10 10
      src/Directory.Build.props
  32. 1 1
      src/Drawie
  33. 1 1
      src/PixiDocks
  34. 0 29
      src/PixiEditor.Beta/BetaExtension.cs
  35. 0 36
      src/PixiEditor.Beta/PixiEditor.Beta.csproj
  36. 0 9
      src/PixiEditor.Beta/Program.cs
  37. 0 15
      src/PixiEditor.Beta/WelcomeMessage.cs
  38. 0 83
      src/PixiEditor.Beta/WelcomeMessageState.cs
  39. 0 20
      src/PixiEditor.Beta/extension.json
  40. 3 2
      src/PixiEditor.Browser/Program.cs
  41. 4 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/ComputedPropertyValue_ChangeInfo.cs
  42. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Root/ReferenceLayerChangeInfos/SetReferenceLayer_ChangeInfo.cs
  43. 11 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/GraphUtils.cs
  44. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  45. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs
  46. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs
  47. 6 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  48. 0 70
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CustomOutputNode.cs
  49. 8 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DocumentInfoNode.cs
  50. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/OutlineNode.cs
  51. 76 20
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  52. 17 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  53. 53 16
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  54. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  55. 4 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs
  56. 3 32
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/TransformNode.cs
  57. 8 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  58. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  59. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  60. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  61. 6 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs
  62. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs
  63. 32 19
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  64. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TextureCache.cs
  65. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs
  66. 51 28
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  67. 98 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs
  68. 8 8
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  69. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterLine_UpdateableChange.cs
  70. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs
  71. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  72. 49 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/EvaluateGraph_Change.cs
  73. 80 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/GetComputedPropertyValue_Change.cs
  74. 2 4
      src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs
  75. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Vectors/ConvertToCurve_Change.cs
  76. 9 9
      src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs
  77. 48 7
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  78. 5 3
      src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs
  79. 2 1
      src/PixiEditor.ChangeableDocument/Rendering/RenderingUtils.cs
  80. 1 0
      src/PixiEditor.Desktop/Program.cs
  81. 14 0
      src/PixiEditor.Extensions.CommonApi/Commands/CommandMetadata.Impl.cs
  82. 9 0
      src/PixiEditor.Extensions.CommonApi/Commands/ICommandProvider.cs
  83. 1144 0
      src/PixiEditor.Extensions.CommonApi/Commands/Shortcut.Impl.cs
  84. 43 0
      src/PixiEditor.Extensions.CommonApi/DataContracts/CommandMetadata.proto
  85. 10 0
      src/PixiEditor.Extensions.CommonApi/DataContracts/Shortcut.proto
  86. 7 0
      src/PixiEditor.Extensions.CommonApi/Documents/IDocument.cs
  87. 6 3
      src/PixiEditor.Extensions.CommonApi/FlyUI/ByteMap.cs
  88. 72 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/Cursor.cs
  89. 37 4
      src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ElementEventArgs.cs
  90. 3 1
      src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ElementEventHandler.cs
  91. 19 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/Events/NumberEventArgs.cs
  92. 19 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/Events/TextEventArgs.cs
  93. 9 1
      src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ToggleEventArgs.cs
  94. 12 1
      src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/Color.cs
  95. 0 8
      src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/FontStyle.cs
  96. 148 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/TextStyle.cs
  97. 10 0
      src/PixiEditor.Extensions.CommonApi/IO/IDocumentProvider.cs
  98. 3 3
      src/PixiEditor.Extensions.CommonApi/Palettes/FilteringSettings.Impl.cs
  99. 66 0
      src/PixiEditor.Extensions.CommonApi/ProtoAutogen/CommandMetadata.cs
  100. 30 0
      src/PixiEditor.Extensions.CommonApi/ProtoAutogen/Shortcut.cs

+ 62 - 63
pipelines/Windows/tests-windows.yml

@@ -1,7 +1,7 @@
 trigger:
 trigger:
-- development
-- master
-- 2.0-cicd
+  - development
+  - master
+  - 2.0-cicd
 
 
 pool:
 pool:
   vmImage: 'windows-latest'
   vmImage: 'windows-latest'
@@ -14,73 +14,72 @@ variables:
   wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/$(wasiVer).tar.gz'
   wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/$(wasiVer).tar.gz'
 
 
 steps:
 steps:
-- task: UseDotNet@2
-  displayName: 'Install .NET SDK'
-  inputs:
-    packageType: 'sdk'
-    version: '$(dotnetVersion)'
-    
-- task: CmdLine@2
-  displayName: 'Download WASI SDK'
-  inputs:
-    script: |
-      curl -L -o $(wasiVer).tar.gz $(wasiUrl)
+  - task: UseDotNet@2
+    displayName: 'Install .NET SDK'
+    inputs:
+      packageType: 'sdk'
+      version: '$(dotnetVersion)'
 
 
-- task: CmdLine@2
-  displayName: 'Unpack WASI SDK'
-  inputs:
-    script: |
-      tar -xzf $(wasiVer).tar.gz
-      echo "Contents of directory after extraction:"
-      dir $(wasiVer)
+  - task: CmdLine@2
+    displayName: 'Download WASI SDK'
+    inputs:
+      script: |
+        curl -L -o $(wasiVer).tar.gz $(wasiUrl)
 
 
-- task: PowerShell@2
-  displayName: 'Set Environment Path for WASI SDK'
-  inputs:
-    targetType: 'inline'
-    script: |
-      $env:WASI_SDK_PATH = "$(Get-Location)\$(wasiVer)"
-      Write-Host "##vso[task.setvariable variable=WASI_SDK_PATH]$env:WASI_SDK_PATH"
+  - task: CmdLine@2
+    displayName: 'Unpack WASI SDK'
+    inputs:
+      script: |
+        tar -xzf $(wasiVer).tar.gz
+        echo "Contents of directory after extraction:"
+        dir $(wasiVer)
 
 
-- task: PowerShell@2
-  displayName: 'Verify Environment Path'
-  inputs:
-    targetType: 'inline'
-    script: |
-      Write-Host "Environment path set to: $env:WASI_SDK_PATH"
+  - task: PowerShell@2
+    displayName: 'Set Environment Path for WASI SDK'
+    inputs:
+      targetType: 'inline'
+      script: |
+        $env:WASI_SDK_PATH = "$(Get-Location)\$(wasiVer)"
+        Write-Host "##vso[task.setvariable variable=WASI_SDK_PATH]$env:WASI_SDK_PATH"
 
 
+  - task: PowerShell@2
+    displayName: 'Verify Environment Path'
+    inputs:
+      targetType: 'inline'
+      script: |
+        Write-Host "Environment path set to: $env:WASI_SDK_PATH"
 
 
-- task: NuGetToolInstaller@1
+  - task: NuGetToolInstaller@1
 
 
-- task: DotNetCoreCLI@2
-  displayName: Install wasi-wasm
-  inputs:
-    command: 'custom'
-    custom: 'workload'
-    arguments: 'install wasi-experimental'
+  - task: DotNetCoreCLI@2
+    displayName: Install wasi-wasm
+    inputs:
+      command: 'custom'
+      custom: 'workload'
+      arguments: 'install wasi-experimental'
 
 
-- task: DotNetCoreCLI@2
-  displayName: Install wasm-tools
-  inputs:
-    command: 'custom'
-    custom: 'workload'
-    arguments: 'install wasm-tools'
+  - task: DotNetCoreCLI@2
+    displayName: Install wasm-tools
+    inputs:
+      command: 'custom'
+      custom: 'workload'
+      arguments: 'install wasm-tools'
 
 
-- task: NuGetCommand@2
-  displayName: 'Restore solution'
-  inputs:
-    restoreSolution: '$(solution)'
+  - task: NuGetCommand@2
+    displayName: 'Restore solution'
+    inputs:
+      restoreSolution: '$(solution)'
 
 
-- task: DotNetCoreCLI@2
-  displayName: Build
-  inputs:
-    command: 'build'
-    projects: '**/*.csproj'
-    arguments: '--configuration Release -r $(buildPlatform)'
+  - task: DotNetCoreCLI@2
+    displayName: Build
+    inputs:
+      command: 'build'
+      projects: '**/*.csproj'
+      arguments: '--configuration Release -r $(buildPlatform)'
 
 
-- task: DotNetCoreCLI@2
-  displayName: Tests
-  inputs:
-    command: test
-    projects: '**/*Tests/*.csproj'
-    arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'
+  - task: DotNetCoreCLI@2
+    displayName: Tests
+    inputs:
+      command: test
+      projects: '**/*Tests/*.csproj'
+      arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'

+ 1 - 1
samples/Directory.Build.props

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

+ 18 - 0
samples/PixiEditorExtensionSamples.sln

@@ -21,6 +21,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample6_Palettes", "Sample6
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample7_FlyUI", "Sample7_FlyUI\Sample7_FlyUI.csproj", "{432A224A-8035-47C1-AC41-6715021B3AA3}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample7_FlyUI", "Sample7_FlyUI\Sample7_FlyUI.csproj", "{432A224A-8035-47C1-AC41-6715021B3AA3}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample8_Commands", "Sample8_Commands\Sample8_Commands.csproj", "{25DA4758-9F82-494E-96A3-B9C48637C0E0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample8_CommandLibrary", "Sample8_CommandLibrary\Sample8_CommandLibrary.csproj", "{3559A288-DF82-4429-B23C-CFF9E55B372E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample9_Document", "Sample9_Document\Sample9_Document.csproj", "{E018D2C3-2DD7-4BC7-AAAF-91DA949789E4}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
@@ -62,6 +68,18 @@ Global
 		{432A224A-8035-47C1-AC41-6715021B3AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{432A224A-8035-47C1-AC41-6715021B3AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{432A224A-8035-47C1-AC41-6715021B3AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{432A224A-8035-47C1-AC41-6715021B3AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{432A224A-8035-47C1-AC41-6715021B3AA3}.Release|Any CPU.Build.0 = Release|Any CPU
 		{432A224A-8035-47C1-AC41-6715021B3AA3}.Release|Any CPU.Build.0 = Release|Any CPU
+		{25DA4758-9F82-494E-96A3-B9C48637C0E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{25DA4758-9F82-494E-96A3-B9C48637C0E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{25DA4758-9F82-494E-96A3-B9C48637C0E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{25DA4758-9F82-494E-96A3-B9C48637C0E0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3559A288-DF82-4429-B23C-CFF9E55B372E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3559A288-DF82-4429-B23C-CFF9E55B372E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3559A288-DF82-4429-B23C-CFF9E55B372E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3559A288-DF82-4429-B23C-CFF9E55B372E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E018D2C3-2DD7-4BC7-AAAF-91DA949789E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E018D2C3-2DD7-4BC7-AAAF-91DA949789E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E018D2C3-2DD7-4BC7-AAAF-91DA949789E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E018D2C3-2DD7-4BC7-AAAF-91DA949789E4}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 	GlobalSection(NestedProjects) = preSolution
 		{FD9B4C32-4D2E-410E-BC6B-787779BEB6E2} = {7CC35BC4-829F-4EF4-8EB6-E1D46206E7DC}
 		{FD9B4C32-4D2E-410E-BC6B-787779BEB6E2} = {7CC35BC4-829F-4EF4-8EB6-E1D46206E7DC}

+ 1 - 1
samples/Sample1_HelloWorld/Sample1_HelloWorld.csproj

@@ -6,7 +6,7 @@
         <PublishTrimmed>true</PublishTrimmed>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <RootNamespace>HelloWorld</RootNamespace>
         <RootNamespace>HelloWorld</RootNamespace>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>

+ 1 - 1
samples/Sample2_LocalizationSample/Sample2_LocalizationSample.csproj

@@ -6,7 +6,7 @@
         <PublishTrimmed>true</PublishTrimmed>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <RootNamespace>LocalizationSample</RootNamespace>
         <RootNamespace>LocalizationSample</RootNamespace>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>

+ 1 - 1
samples/Sample3_Preferences/Sample3_Preferences.csproj

@@ -6,7 +6,7 @@
         <PublishTrimmed>true</PublishTrimmed>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <RootNamespace>Preferences</RootNamespace>
         <RootNamespace>Preferences</RootNamespace>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>

+ 1 - 1
samples/Sample4_CreatePopup/Sample4_CreatePopup.csproj

@@ -6,7 +6,7 @@
         <PublishTrimmed>true</PublishTrimmed>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <RootNamespace>CreatePopupSample</RootNamespace>
         <RootNamespace>CreatePopupSample</RootNamespace>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>

+ 1 - 1
samples/Sample5_Resources/Sample5_Resources.csproj

@@ -6,7 +6,7 @@
         <PublishTrimmed>true</PublishTrimmed>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <RootNamespace>ResourcesSample</RootNamespace>
         <RootNamespace>ResourcesSample</RootNamespace>

+ 1 - 1
samples/Sample6_Palettes/Sample6_Palettes.csproj

@@ -6,7 +6,7 @@
         <PublishTrimmed>true</PublishTrimmed>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <RootNamespace>PalettesSample</RootNamespace>
         <RootNamespace>PalettesSample</RootNamespace>

+ 1 - 1
samples/Sample7_FlyUI/Sample7_FlyUI.csproj

@@ -6,7 +6,7 @@
         <PublishTrimmed>true</PublishTrimmed>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <RootNamespace>FlyUISample</RootNamespace>
         <RootNamespace>FlyUISample</RootNamespace>

+ 48 - 30
samples/Sample7_FlyUI/WindowContentElement.cs

@@ -1,47 +1,65 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using System.Diagnostics.CodeAnalysis;
+using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 using PixiEditor.Extensions.Sdk;
 using PixiEditor.Extensions.Sdk;
 using PixiEditor.Extensions.Sdk.Api.FlyUI;
 using PixiEditor.Extensions.Sdk.Api.FlyUI;
 using PixiEditor.Extensions.Sdk.Api.Window;
 using PixiEditor.Extensions.Sdk.Api.Window;
 
 
 namespace FlyUISample;
 namespace FlyUISample;
 
 
+[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines",
+    Justification = "FlyUI style")]
 public class WindowContentElement : StatelessElement
 public class WindowContentElement : StatelessElement
 {
 {
     public PopupWindow Window { get; set; }
     public PopupWindow Window { get; set; }
-    public override CompiledControl BuildNative()
+
+    public override ControlDefinition BuildNative()
     {
     {
+        SizeInputField field = new SizeInputField();
+        field.SizeChanged += args =>
+        {
+            PixiEditorExtension.Api.Logger.Log(field.Value.ToString());
+        };
+
         Layout layout = new Layout(body:
         Layout layout = new Layout(body:
             new Container(margin: Edges.All(25), child:
             new Container(margin: Edges.All(25), child:
                 new Column(
                 new Column(
-                    new Center(
-                        new Text(
-                            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae neque nibh. Duis sed pharetra dolor. Donec dui sapien, aliquam id sodales in, ornare et urna. Mauris nunc odio, sagittis eget lectus at, imperdiet ornare quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam euismod pellentesque blandit. Vestibulum sagittis, ligula non finibus lobortis, dolor lacus consectetur turpis, id facilisis ligula dolor vitae augue.",
-                            wrap: TextWrap.Wrap,
-                            fontSize: 16)
-                    ),
-                    new Align(
-                        alignment: Alignment.CenterRight,
-                        child: new Text("- Paulo Coelho, The Alchemist (1233)", fontStyle: FontStyle.Italic)
-                    ),
-                    new Container(
-                        margin: Edges.Symmetric(25, 0),
-                        backgroundColor: Color.FromRgba(25, 25, 25, 255),
-                        child: new Column(
-                            new Image(
-                                "/Pizza.png",
-                                filterQuality: FilterQuality.None,
-                                width: 256, height: 256))
-                    ),
-                    new CheckBox(new Text("heloo"), onCheckedChanged: args =>
-                    {
-                        PixiEditorExtension.Api.Logger.Log(((CheckBox)args.Sender).IsChecked ? "Checked" : "Unchecked");
-                    }),
-                    new Center(
-                        new Button(
-                            child: new Text("Close"), onClick: _ =>
+                    crossAxisAlignment: CrossAxisAlignment.Center,
+                    mainAxisAlignment: MainAxisAlignment.SpaceEvenly,
+                    children:
+                    [
+                        new Center(
+                            new Text(
+                                "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae neque nibh. Duis sed pharetra dolor. Donec dui sapien, aliquam id sodales in, ornare et urna. Mauris nunc odio, sagittis eget lectus at, imperdiet ornare quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam euismod pellentesque blandit. Vestibulum sagittis, ligula non finibus lobortis, dolor lacus consectetur turpis, id facilisis ligula dolor vitae augue.",
+                                wrap: TextWrap.Wrap,
+                                textStyle: new TextStyle(fontSize: 16))
+                        ),
+                        new Align(
+                            alignment: Alignment.CenterRight,
+                            child: new Text("- Paulo Coelho, The Alchemist (1233)",
+                                textStyle: new TextStyle(fontStyle: FontStyle.Italic))
+                        ),
+                        new Container(
+                            margin: Edges.Symmetric(25, 0),
+                            backgroundColor: Color.FromRgba(25, 25, 25, 255),
+                            child: new Column(
+                                new Image(
+                                    "/Pizza.png",
+                                    filterQuality: FilterQuality.None,
+                                    width: 256, height: 256))
+                        ),
+                        new CheckBox(new Text("heloo"),
+                            onCheckedChanged: args =>
                             {
                             {
-                                Window.Close();
-                            }))
+                                PixiEditorExtension.Api.Logger.Log(((CheckBox)args.Sender).IsChecked
+                                    ? "Checked"
+                                    : "Unchecked");
+                            }),
+                        field,
+                        new Center(
+                            new Button(
+                                child: new Text("Close"), onClick: _ => { Window.Close(); }))
+                    ]
                 )
                 )
             )
             )
         );
         );

+ 55 - 0
samples/Sample8_CommandLibrary/CommandLibraryExtension.cs

@@ -0,0 +1,55 @@
+using PixiEditor.Extensions.CommonApi.Commands;
+using PixiEditor.Extensions.Sdk;
+
+namespace Sample8_CommandLibrary;
+
+public class CommandLibraryExtension : PixiEditorExtension
+{
+    public override void OnInitialized()
+    {
+        CommandMetadata publicCommand = new CommandMetadata("PrintHelloWorld")
+        {
+            // All extensions can invoke this command
+            InvokePermissions = InvokePermissions.Public
+        };
+
+        CommandMetadata internalCommand = new CommandMetadata("PrintHelloWorldFamily")
+        {
+            // All extensions with unique name starting with "yourCompany" can invoke this command
+            InvokePermissions = InvokePermissions.Family
+        };
+
+        CommandMetadata privateCommand = new CommandMetadata("PrintHelloWorldPrivate")
+        {
+            // Only this extension can invoke this command
+            InvokePermissions = InvokePermissions.Owner
+        };
+
+        CommandMetadata explicitCommand = new CommandMetadata("PrintHelloWorldExplicit")
+        {
+            // Only this extension and the ones listed in ExplicitlyAllowedExtensions can invoke this command
+            InvokePermissions = InvokePermissions.Explicit,
+            ExplicitlyAllowedExtensions = "yourCompany.Samples.Commands" // You can put multiple extensions by separating with ;
+        };
+
+        Api.Commands.RegisterCommand(publicCommand, () =>
+        {
+            Api.Logger.Log("Hello World from public command!");
+        });
+
+        Api.Commands.RegisterCommand(internalCommand, () =>
+        {
+            Api.Logger.Log("Hello World from internal command!");
+        });
+
+        Api.Commands.RegisterCommand(privateCommand, () =>
+        {
+            Api.Logger.Log("Hello World from private command!");
+        });
+
+        Api.Commands.RegisterCommand(explicitCommand, () =>
+        {
+            Api.Logger.Log("Hello World from explicit command!");
+        });
+    }
+}

+ 9 - 0
samples/Sample8_CommandLibrary/Program.cs

@@ -0,0 +1,9 @@
+namespace Sample8_CommandLibrary;
+
+public static class Program
+{
+    public static void Main()
+    {
+
+    }
+}

+ 39 - 0
samples/Sample8_CommandLibrary/Sample8_CommandLibrary.csproj

@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
+        <OutputType>Exe</OutputType>
+        <PublishTrimmed>true</PublishTrimmed>
+        <WasmSingleFileBundle>true</WasmSingleFileBundle>
+        <GenerateExtensionPackage>true</GenerateExtensionPackage>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\Extensions</PixiExtOutputPath>
+        <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+        <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
+        <RootNamespace>Sample8_CommandLibrary</RootNamespace>
+    </PropertyGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <ItemGroup>
+        <ProjectReference Include="..\..\src\PixiEditor.Extensions.Sdk\PixiEditor.Extensions.Sdk.csproj"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <None Remove="extension.json"/>
+        <Content Include="extension.json">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <ItemGroup>
+        <Content Include="Localization\*">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.props"/>
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.targets"/>
+
+
+</Project>

+ 41 - 0
samples/Sample8_CommandLibrary/extension.json

@@ -0,0 +1,41 @@
+{
+  "displayName": "Sample Extension - Command Library",
+  "uniqueName": "yourCompany.Samples.CommandLibrary",
+  "description": "Commands Library that can be invoked by other extensions for PixiEditor",
+  "version": "1.0.0",
+  "localization": {
+    "languages": [
+      {
+        "name": "English",
+        "code": "en",
+        "localeFileName": "Localization/en.json"
+      },
+      {
+        "name": "Polish",
+        "code": "pl",
+        "localeFileName": "Localization/pl.json"
+      }
+    ]
+  },
+  "author": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "publisher": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "contributors": [
+    {
+      "name": "flabbet",
+      "email": "[email protected]",
+      "website": "https://github.com/flabbet"
+    }
+  ],
+  "license": "MIT",
+  "categories": [
+    "Extension"
+  ]
+}

+ 75 - 0
samples/Sample8_Commands/CommandsSampleExtension.cs

@@ -0,0 +1,75 @@
+using PixiEditor.Extensions.CommonApi.Commands;
+using PixiEditor.Extensions.Sdk;
+
+namespace Sample8_Menu;
+
+public class CommandsSampleExtension : PixiEditorExtension
+{
+    /// <summary>
+    ///     This method is called when extension is loaded.
+    ///  All extensions are first loaded and then initialized. This method is called before <see cref="OnInitialized"/>.
+    /// </summary>
+    public override void OnLoaded()
+    {
+    }
+
+    /// <summary>
+    ///     This method is called when extension is initialized. After this method is called, you can use Api property to access PixiEditor API.
+    /// </summary>
+    public override void OnInitialized()
+    {
+        // A good practice is to use localization keys instead of hardcoded strings.
+        // And add them to the localization file. Check Sample2_LocalizationSample for more information.
+
+        CommandMetadata firstCommand = new CommandMetadata("Loggers.WriteHello");
+        firstCommand.DisplayName = "Write Hello"; // can be localized
+        firstCommand.Description = "Writes Hello to the log"; // can be localized
+
+        // Either an icon key (https://github.com/PixiEditor/PixiEditor/blob/master/src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml)
+        // or unicode character
+        firstCommand.Icon = "icon-terminal";
+        firstCommand.MenuItemPath = "AWESOME_LOGGER/Write Hello"; // AWESOME_LOGGER is taken from localization, same can be done for the rest
+        firstCommand.Shortcut = new Shortcut(Key.H, KeyModifiers.Control | KeyModifiers.Alt);
+
+        Api.Commands.RegisterCommand(firstCommand, () => { Api.Logger.Log("Hello from the command!"); });
+
+        int clickedCount = 0;
+        CommandMetadata secondCommand = new CommandMetadata("Loggers.WriteClickedCount");
+        secondCommand.DisplayName = "Write Clicked Count";
+        secondCommand.Description = "Writes clicked count to the log";
+        secondCommand.Icon = "icon-terminal";
+        secondCommand.MenuItemPath = "EDIT/Write Clicked Count"; // append to EDIT menu
+        secondCommand.Order = 1000; // Last
+
+        secondCommand.Shortcut = new Shortcut(Key.C, KeyModifiers.Control | KeyModifiers.Alt);
+        Api.Commands.RegisterCommand(secondCommand, () =>
+        {
+            clickedCount++;
+            Api.Logger.Log($"Clicked {clickedCount} times");
+        });
+
+
+        Api.Commands.InvokeCommand("PixiEditor.File.New");
+
+        if (Api.Commands.CommandExists("yourCompany.Samples.CommandLibrary:PrintHelloWorld"))
+        {
+            Api.Commands.InvokeCommand("yourCompany.Samples.CommandLibrary:PrintHelloWorld");
+        }
+
+        if (Api.Commands.CommandExists("yourCompany.Samples.CommandLibrary:PrintHelloWorldFamily"))
+        {
+            Api.Commands.InvokeCommand("yourCompany.Samples.CommandLibrary:PrintHelloWorldFamily");
+        }
+
+        if (Api.Commands.CommandExists("yourCompany.Samples.CommandLibrary:PrintHelloWorldPrivate"))
+        {
+            // This will log an error.
+            Api.Commands.InvokeCommand("yourCompany.Samples.CommandLibrary:PrintHelloWorldPrivate");
+        }
+
+        if (Api.Commands.CommandExists("yourCompany.Samples.CommandLibrary:PrintHelloWorldExplicit"))
+        {
+            Api.Commands.InvokeCommand("yourCompany.Samples.CommandLibrary:PrintHelloWorldExplicit");
+        }
+    }
+}

+ 3 - 0
samples/Sample8_Commands/Localization/en.json

@@ -0,0 +1,3 @@
+{
+  "AWESOME_LOGGER": "Awesome Logger"
+}

+ 3 - 0
samples/Sample8_Commands/Localization/pl.json

@@ -0,0 +1,3 @@
+{
+  "AWESOME_LOGGER": "Znakomity rejestrator"
+}

+ 9 - 0
samples/Sample8_Commands/Program.cs

@@ -0,0 +1,9 @@
+namespace Sample8_Commands;
+
+public static class Program
+{
+    public static void Main()
+    {
+        
+    }
+}

+ 39 - 0
samples/Sample8_Commands/Sample8_Commands.csproj

@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
+        <OutputType>Exe</OutputType>
+        <PublishTrimmed>true</PublishTrimmed>
+        <WasmSingleFileBundle>true</WasmSingleFileBundle>
+        <GenerateExtensionPackage>true</GenerateExtensionPackage>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\Extensions</PixiExtOutputPath>
+        <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+        <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
+        <RootNamespace>Sample8_Commands</RootNamespace>
+    </PropertyGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <ItemGroup>
+        <ProjectReference Include="..\..\src\PixiEditor.Extensions.Sdk\PixiEditor.Extensions.Sdk.csproj"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <None Remove="extension.json"/>
+        <Content Include="extension.json">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <ItemGroup>
+        <Content Include="Localization\*">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.props"/>
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.targets"/>
+
+
+</Project>

+ 41 - 0
samples/Sample8_Commands/extension.json

@@ -0,0 +1,41 @@
+{
+  "displayName": "Sample Extension - Commands",
+  "uniqueName": "yourCompany.Samples.Commands",
+  "description": "Sample Commands extension for PixiEditor",
+  "version": "1.0.0",
+  "localization": {
+    "languages": [
+      {
+        "name": "English",
+        "code": "en",
+        "localeFileName": "Localization/en.json"
+      },
+      {
+        "name": "Polish",
+        "code": "pl",
+        "localeFileName": "Localization/pl.json"
+      }
+    ]
+  },
+  "author": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "publisher": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "contributors": [
+    {
+      "name": "flabbet",
+      "email": "[email protected]",
+      "website": "https://github.com/flabbet"
+    }
+  ],
+  "license": "MIT",
+  "categories": [
+    "Extension"
+  ]
+}

+ 24 - 0
samples/Sample9_Document/CommandsSampleExtension.cs

@@ -0,0 +1,24 @@
+using PixiEditor.Extensions.CommonApi.Commands;
+using PixiEditor.Extensions.Sdk;
+
+namespace Sample9_Commands;
+
+public class CommandsSampleExtension : PixiEditorExtension
+{
+    /// <summary>
+    ///     This method is called when extension is loaded.
+    ///  All extensions are first loaded and then initialized. This method is called before <see cref="OnInitialized"/>.
+    /// </summary>
+    public override void OnLoaded()
+    {
+    }
+
+    /// <summary>
+    ///     This method is called when extension is initialized. After this method is called, you can use Api property to access PixiEditor API.
+    /// </summary>
+    public override void OnInitialized()
+    {
+        var doc = Api.Documents.ImportFile("Resources/cs.png", true); // Open file from the extension resources
+        doc?.Resize(128, 128); // Resizes whole document
+    }
+}

+ 8 - 0
samples/Sample9_Document/Program.cs

@@ -0,0 +1,8 @@
+namespace Sample9_Commands;
+
+public static class Program
+{
+    public static void Main()
+    {
+    }
+}

BIN
samples/Sample9_Document/Resources/cs.png


+ 45 - 0
samples/Sample9_Document/Sample9_Document.csproj

@@ -0,0 +1,45 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
+        <OutputType>Exe</OutputType>
+        <PublishTrimmed>true</PublishTrimmed>
+        <WasmSingleFileBundle>true</WasmSingleFileBundle>
+        <GenerateExtensionPackage>true</GenerateExtensionPackage>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\Extensions</PixiExtOutputPath>
+        <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+        <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
+        <RootNamespace>Sample9_Commands</RootNamespace>
+    </PropertyGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <ItemGroup>
+        <ProjectReference Include="..\..\src\PixiEditor.Extensions.Sdk\PixiEditor.Extensions.Sdk.csproj"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <None Remove="extension.json"/>
+        <Content Include="extension.json">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <ItemGroup>
+        <Content Include="Localization\*">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <ItemGroup>
+        <Content Include="Resources\*">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.props"/>
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.targets"/>
+
+
+</Project>

+ 44 - 0
samples/Sample9_Document/extension.json

@@ -0,0 +1,44 @@
+{
+  "displayName": "Sample Extension - Document",
+  "uniqueName": "yourCompany.Samples.Document",
+  "description": "Sample Document extension for PixiEditor",
+  "version": "1.0.0",
+  "localization": {
+    "languages": [
+      {
+        "name": "English",
+        "code": "en",
+        "localeFileName": "Localization/en.json"
+      },
+      {
+        "name": "Polish",
+        "code": "pl",
+        "localeFileName": "Localization/pl.json"
+      }
+    ]
+  },
+  "author": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "publisher": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "contributors": [
+    {
+      "name": "flabbet",
+      "email": "[email protected]",
+      "website": "https://github.com/flabbet"
+    }
+  ],
+  "license": "MIT",
+  "categories": [
+    "Extension"
+  ],
+  "permissions": [
+    "OpenDocuments"
+  ]
+}

+ 7 - 0
samples/global.json

@@ -0,0 +1,7 @@
+{
+  "sdk": {
+    "version": "8.0.405",
+    "rollForward": "latestMinor",
+    "allowPrerelease": false
+  }
+}

+ 5 - 1
src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -68,7 +69,10 @@ internal class DrawingSurfaceLineOperation : IMirroredDrawOperation
             newFrom = (VecI)newFrom.ReflectY((double)horAxisY).Round();
             newFrom = (VecI)newFrom.ReflectY((double)horAxisY).Round();
             newTo = (VecI)newTo.ReflectY((double)horAxisY).Round();
             newTo = (VecI)newTo.ReflectY((double)horAxisY).Round();
         }
         }
-        return new DrawingSurfaceLineOperation(newFrom, newTo, paint.StrokeCap, paint.StrokeWidth, paint.Color, paint.BlendMode);
+
+        Color color = paint.Paintable is ColorPaintable colorPaintable ? colorPaintable.Color : paint.Color;
+
+        return new DrawingSurfaceLineOperation(newFrom, newTo, paint.StrokeCap, paint.StrokeWidth, color, paint.BlendMode);
     }
     }
 
 
     public void Dispose()
     public void Dispose()

+ 16 - 1
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -211,7 +211,22 @@ internal class EllipseOperation : IMirroredDrawOperation
             newLocation = newLocation.ReflectX((double)verAxisX).Round();
             newLocation = newLocation.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
             newLocation = newLocation.ReflectY((double)horAxisY).Round();
             newLocation = newLocation.ReflectY((double)horAxisY).Round();
-        return new EllipseOperation(newLocation, strokePaintable, fillPaintable, strokeWidth, rotation, antialiased, paint);
+
+        Paintable? finalFillPaintable = fillPaintable;
+        Paintable? finalStrokePaintable = strokePaintable;
+        if (fillPaintable.AbsoluteValues && fillPaintable is IPositionPaintable)
+        {
+            finalFillPaintable = fillPaintable.Clone();
+            ((IPositionPaintable)finalFillPaintable).Position = newLocation.Center;
+        }
+
+        if (strokePaintable.AbsoluteValues && strokePaintable is IPositionPaintable)
+        {
+            finalStrokePaintable = strokePaintable.Clone();
+            ((IPositionPaintable)finalStrokePaintable).Position = newLocation.Center;
+        }
+
+        return new EllipseOperation(newLocation, finalStrokePaintable, finalFillPaintable, strokeWidth, rotation, antialiased, paint);
     }
     }
 
 
     public void Dispose()
     public void Dispose()

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 14fd539e1e72dc03d7bb04e14b1d5cbaf1202306
+Subproject commit 78237e9c5d70ebd878b34f4dd63f54b25bafea23

+ 10 - 10
src/Directory.Build.props

@@ -1,14 +1,14 @@
 <Project>
 <Project>
-  <PropertyGroup>
-    <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-    <AvaloniaVersion>11.2.7</AvaloniaVersion>
-  </PropertyGroup>
-  <ItemGroup>
-    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118"/>
-  </ItemGroup>
-  <ItemGroup>
-    <AdditionalFiles Include="$(SolutionDir)/stylecop.json"/>
-  </ItemGroup>
+    <PropertyGroup>
+        <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
+		    <AvaloniaVersion>11.3.0</AvaloniaVersion>
+    </PropertyGroup>
+    <ItemGroup>
+        <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
+    </ItemGroup>
+    <ItemGroup>
+        <AdditionalFiles Include="$(SolutionDir)/stylecop.json" />
+    </ItemGroup>
 
 
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">
     <RuntimeIdentifier>win-x64</RuntimeIdentifier>
     <RuntimeIdentifier>win-x64</RuntimeIdentifier>

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 72824cf4ae68a88c69002f6b2e0e81283ff7905a
+Subproject commit 06d7adbd41c53f0319c07b07d99faab57b376560

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit c4bfc826ebe5c44b72930c27dab0d7661850e58e
+Subproject commit 8c9a1f874ec8d1f7cd773a1f6b044deb0343ad1a

+ 0 - 29
src/PixiEditor.Beta/BetaExtension.cs

@@ -1,29 +0,0 @@
-using PixiEditor.Extensions.Sdk;
-
-namespace PixiEditor.Beta;
-
-public class BetaExtension : PixiEditorExtension
-{
-    public override void OnInitialized()
-    {
-        if (Api.Preferences.GetPreference<bool>("BetaWelcomeShown"))
-        {
-            return;
-        }
-
-        WelcomeMessage welcomeMessage = new();
-        var window = Api.WindowProvider.CreatePopupWindow("Welcome to the PixiEditor 2.0 beta!", welcomeMessage);
-        welcomeMessage.OnContinue += () =>
-        {
-            Api.Preferences.UpdatePreference("BetaWelcomeShown", true);
-            window.Close();
-        };
-
-        window.Width = 800;
-        window.Height = 650;
-
-        window.CanResize = false;
-        window.CanMinimize = false;
-        window.ShowDialog();
-    }
-}

+ 0 - 36
src/PixiEditor.Beta/PixiEditor.Beta.csproj

@@ -1,36 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-  <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
-    <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
-    <OutputType>Exe</OutputType>
-    <ImplicitUsings>enable</ImplicitUsings>
-    <Nullable>disable</Nullable>
-    <PublishTrimmed>true</PublishTrimmed>
-    <WasmSingleFileBundle>true</WasmSingleFileBundle>
-    <EventSourceSupport>false</EventSourceSupport>
-    <UseSystemResourceKeys>true</UseSystemResourceKeys>
-    <EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
-    <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
-    <DebuggerSupport>false</DebuggerSupport>
-    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
-    <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
-    <RootNamespace>PixiEditor.Beta</RootNamespace>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\PixiEditor.Extensions.Sdk\PixiEditor.Extensions.Sdk.csproj"/>
-  </ItemGroup>
-
-  <ItemGroup>
-    <None Update="extension.json">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-  </ItemGroup>
-
-
-  <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
-  <Import Project="..\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.props"/>
-  <Import Project="..\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.targets"/>
-
-</Project>

+ 0 - 9
src/PixiEditor.Beta/Program.cs

@@ -1,9 +0,0 @@
-namespace PixiEditor.Beta;
-
-public static class Program
-{
-    public static void Main(string[] args)
-    {
-
-    }
-}

+ 0 - 15
src/PixiEditor.Beta/WelcomeMessage.cs

@@ -1,15 +0,0 @@
-using PixiEditor.Extensions.Sdk.Api.FlyUI;
-
-namespace PixiEditor.Beta;
-
-public class WelcomeMessage : StatefulElement<WelcomeMessageState>
-{
-    public event Action OnContinue;
-    
-    public override WelcomeMessageState CreateState()
-    { 
-        WelcomeMessageState state = new WelcomeMessageState();
-        state.OnContinue += () => OnContinue?.Invoke();
-        return state;
-    }
-}

+ 0 - 83
src/PixiEditor.Beta/WelcomeMessageState.cs

@@ -1,83 +0,0 @@
-using PixiEditor.Extensions.Sdk.Api.FlyUI;
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
-
-namespace PixiEditor.Beta;
-
-public class WelcomeMessageState : State
-{
-    private const string Body1 = @"
-We are extremely excited to share this version with you, early testers. Before you jump in and test all the new things, we have a few things to note:
-
-- This is a very early version of PixiEditor 2.0. Not every feature promised in the roadmap is
-  implemented yet. 
-
-- App is not production ready! Expect bugs, crashes, unfinished features, placeholders and other signs of development.
-";
-    
-private const string Body2 = "- Your feedback is the most important thing of this beta, please take a moment to report any issues and suggestions on PixiEditor Forum.";
-private const string Body3 = @"
-- We are collecting anonymous usage data to fix bugs, crashes and performance issues. This data will help us to improve the app. During the beta 
-there is no option to opt-out. No personal data is collected.
-
-Click on below checkboxes that you understand what you are getting into and you are ready to test the app.
-
-I understand that:
-";
-
-    private bool[] _checkboxes = new bool[4];
-
-    public event Action OnContinue;
-
-    public override LayoutElement BuildElement()
-    {
-        return new Layout(body:
-            new Align(
-                Alignment.TopCenter,
-                new Column(
-                    new Center(new Text("Welcome to the open beta of PixiEditor 2.0!", TextWrap.Wrap,
-                        FontStyle.Normal,
-                        fontSize: 24)),
-                    new Text(Body1, TextWrap.Wrap, fontSize: 16),
-                    new Hyperlink("https://forum.pixieditor.net", Body2,
-                        fontSize: 16, textWrap: TextWrap.Wrap),
-                    new Text(Body3, TextWrap.Wrap, fontSize: 16),
-                    new CheckBox(
-                        new Text("The app may be unstable, crash or freeze", fontSize: 16,
-                            fontStyle: FontStyle.Italic),
-                        onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 0)),
-                    new CheckBox(
-                        new Text("I may encounter unfinished features and placeholders", fontSize: 16,
-                            fontStyle: FontStyle.Italic),
-                        onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 1)),
-                    new CheckBox(new Text("I may lose my work due to bugs", fontSize: 16, fontStyle: FontStyle.Italic),
-                        onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 2)),
-                    new CheckBox(
-                        new Text("I will have a lot of fun testing the app", fontSize: 16,
-                            fontStyle: FontStyle.Italic),
-                        onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 3)),
-                    new Container(
-                        margin: new Edges(0, 5, 0, 0),
-                        width: AllCheckBoxesChecked() ? 100 : 200,
-                        child:
-                        AllCheckBoxesChecked()
-                            ? new Button(new Text("Continue"), onClick: (args) => { OnContinue?.Invoke(); })
-                            : new Text("Select All Checkboxes to continue")
-                    )
-                )
-            )
-        );
-    }
-
-    void CheckboxChanged(CheckBox checkBox, int index)
-    {
-        SetState(() =>
-        {
-            _checkboxes[index] = checkBox.IsChecked;
-        });
-    }
-
-    private bool AllCheckBoxesChecked()
-    {
-        return _checkboxes.All(x => x);
-    }
-}

+ 0 - 20
src/PixiEditor.Beta/extension.json

@@ -1,20 +0,0 @@
-{
-  "displayName": "PixiEditor Beta",
-  "uniqueName": "PixiEditor.Beta",
-  "description": "Open Beta of PixiEditor 2.0",
-  "version": "1.0.1",
-  "author": {
-    "name": "PixiEditor",
-    "email": "[email protected]",
-    "website": "https://pixieditor.net"
-  },
-  "publisher": {
-    "name": "PixiEditor",
-    "email": "[email protected]",
-    "website": "https://pixieditor.net"
-  },
-  "license": "Copyright PixiEditor Organization",
-  "categories": [
-    "Beta"
-  ]
-}

+ 3 - 2
src/PixiEditor.Browser/Program.cs

@@ -2,10 +2,11 @@
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Avalonia;
 using Avalonia;
 using Avalonia.Browser;
 using Avalonia.Browser;
-using PixiEditor;
 
 
 [assembly: SupportedOSPlatform("browser")]
 [assembly: SupportedOSPlatform("browser")]
 
 
+namespace PixiEditor.Avalonia.Browser;
+
 internal sealed partial class Program
 internal sealed partial class Program
 {
 {
     private static Task Main(string[] args) => BuildAvaloniaApp()
     private static Task Main(string[] args) => BuildAvaloniaApp()
@@ -13,4 +14,4 @@ internal sealed partial class Program
 
 
     public static AppBuilder BuildAvaloniaApp()
     public static AppBuilder BuildAvaloniaApp()
         => AppBuilder.Configure<App>();
         => AppBuilder.Configure<App>();
-}
+}

+ 4 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/ComputedPropertyValue_ChangeInfo.cs

@@ -0,0 +1,4 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record ComputedPropertyValue_ChangeInfo(Guid Node, string PropertyName, bool IsInput, object? Value)
+    : IChangeInfo;

+ 1 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Root/ReferenceLayerChangeInfos/SetReferenceLayer_ChangeInfo.cs

@@ -2,6 +2,6 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
-namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Root.ReferenceLayerChangeInfos;
 
 
 public record class SetReferenceLayer_ChangeInfo(ImmutableArray<byte> ImagePbgra8888Bytes, VecI ImageSize, ShapeCorners Shape) : IChangeInfo;
 public record class SetReferenceLayer_ChangeInfo(ImmutableArray<byte> ImagePbgra8888Bytes, VecI ImageSize, ShapeCorners Shape) : IChangeInfo;

+ 11 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/GraphUtils.cs

@@ -4,7 +4,8 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 
 public static class GraphUtils
 public static class GraphUtils
 {
 {
-    public static Queue<IReadOnlyNode> CalculateExecutionQueue(IReadOnlyNode outputNode)
+    public static Queue<IReadOnlyNode> CalculateExecutionQueue(IReadOnlyNode outputNode,
+        Func<IInputProperty, bool>? branchFilter = null)
     {
     {
         var finalQueue = new HashSet<IReadOnlyNode>();
         var finalQueue = new HashSet<IReadOnlyNode>();
         var queueNodes = new Queue<IReadOnlyNode>();
         var queueNodes = new Queue<IReadOnlyNode>();
@@ -13,12 +14,12 @@ public static class GraphUtils
         while (queueNodes.Count > 0)
         while (queueNodes.Count > 0)
         {
         {
             var node = queueNodes.Dequeue();
             var node = queueNodes.Dequeue();
-            
+
             if (finalQueue.Contains(node))
             if (finalQueue.Contains(node))
             {
             {
                 continue;
                 continue;
             }
             }
-            
+
             bool canAdd = true;
             bool canAdd = true;
 
 
             foreach (var input in node.InputProperties)
             foreach (var input in node.InputProperties)
@@ -33,8 +34,13 @@ public static class GraphUtils
                     continue;
                     continue;
                 }
                 }
 
 
+                if (branchFilter != null && !branchFilter(input))
+                {
+                    continue;
+                }
+
                 canAdd = false;
                 canAdd = false;
-                
+
                 if (finalQueue.Contains(input.Connection.Node))
                 if (finalQueue.Contains(input.Connection.Node))
                 {
                 {
                     finalQueue.Remove(input.Connection.Node);
                     finalQueue.Remove(input.Connection.Node);
@@ -46,7 +52,7 @@ public static class GraphUtils
                     queueNodes.Enqueue(input.Connection.Node);
                     queueNodes.Enqueue(input.Connection.Node);
                 }
                 }
             }
             }
-            
+
             if (canAdd)
             if (canAdd)
             {
             {
                 finalQueue.Add(node);
                 finalQueue.Add(node);

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -37,11 +37,12 @@ public class InputProperty : IInputProperty
                 return null;
                 return null;
             }
             }
 
 
+            object target = connectionValue;
             if (!ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is Delegate connectionField)
             if (!ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is Delegate connectionField)
             {
             {
                 try
                 try
                 {
                 {
-                    return connectionField.DynamicInvoke(FuncContext.NoContext);
+                    target = connectionField.DynamicInvoke(FuncContext.NoContext);
                 }
                 }
                 catch
                 catch
                 {
                 {
@@ -64,7 +65,6 @@ public class InputProperty : IInputProperty
                 return FuncFactoryDelegate(func);
                 return FuncFactoryDelegate(func);
             }
             }
 
 
-            object target = connectionValue;
             if (target is ShaderExpressionVariable shaderExpression)
             if (target is ShaderExpressionVariable shaderExpression)
             {
             {
                 target = shaderExpression.GetConstant();
                 target = shaderExpression.GetConstant();

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs

@@ -28,7 +28,7 @@ public interface IReadOnlyNode : ICacheable
     ///     Traverses the graph backwards from this node. Backwards means towards the input nodes.
     ///     Traverses the graph backwards from this node. Backwards means towards the input nodes.
     /// </summary>
     /// </summary>
     /// <param name="action">The action to perform on each node. Input property is the input that was used to traverse this node.</param>
     /// <param name="action">The action to perform on each node. Input property is the input that was used to traverse this node.</param>
-    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action);
+    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action, Func<IInputProperty, bool>? branchCondition = null);
 
 
     /// <summary>
     /// <summary>
     ///     Traverses the graph forwards from this node. Forwards means towards the output nodes.
     ///     Traverses the graph forwards from this node. Forwards means towards the output nodes.

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs

@@ -13,7 +13,7 @@ public class SceneObjectRenderContext : RenderContext
     public RenderOutputProperty TargetPropertyOutput { get; }
     public RenderOutputProperty TargetPropertyOutput { get; }
 
 
     public SceneObjectRenderContext(RenderOutputProperty targetPropertyOutput, DrawingSurface surface, RectD localBounds, KeyFrameTime frameTime,
     public SceneObjectRenderContext(RenderOutputProperty targetPropertyOutput, DrawingSurface surface, RectD localBounds, KeyFrameTime frameTime,
-        ChunkResolution chunkResolution, VecI docSize, bool renderSurfaceIsScene, ColorSpace processingColorSpace, double opacity) : base(surface, frameTime, chunkResolution, docSize, processingColorSpace, opacity)
+        ChunkResolution chunkResolution, VecI renderOutputSize, VecI documentSize, bool renderSurfaceIsScene, ColorSpace processingColorSpace, double opacity) : base(surface, frameTime, chunkResolution, renderOutputSize, documentSize, processingColorSpace, opacity)
     {
     {
         TargetPropertyOutput = targetPropertyOutput;
         TargetPropertyOutput = targetPropertyOutput;
         LocalBounds = localBounds;
         LocalBounds = localBounds;

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

@@ -3,6 +3,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -22,7 +23,8 @@ public class CreateImageNode : Node, IPreviewRenderable
 
 
     public RenderInputProperty Content { get; }
     public RenderInputProperty Content { get; }
 
 
-    public InputProperty<VecD> ContentOffset { get; }
+    public InputProperty<Matrix3X3> ContentMatrix { get; }
+
 
 
     public RenderOutputProperty RenderOutput { get; }
     public RenderOutputProperty RenderOutput { get; }
 
 
@@ -34,7 +36,7 @@ public class CreateImageNode : Node, IPreviewRenderable
         Size = CreateInput(nameof(Size), "SIZE", new VecI(32, 32)).WithRules(v => v.Min(VecI.One));
         Size = CreateInput(nameof(Size), "SIZE", new VecI(32, 32)).WithRules(v => v.Min(VecI.One));
         Fill = CreateInput<Paintable>(nameof(Fill), "FILL", new ColorPaintable(Colors.Transparent));
         Fill = CreateInput<Paintable>(nameof(Fill), "FILL", new ColorPaintable(Colors.Transparent));
         Content = CreateRenderInput(nameof(Content), "CONTENT");
         Content = CreateRenderInput(nameof(Content), "CONTENT");
-        ContentOffset = CreateInput(nameof(ContentOffset), "CONTENT_OFFSET", VecD.Zero);
+        ContentMatrix = CreateInput<Matrix3X3>(nameof(ContentMatrix), "MATRIX", Matrix3X3.Identity);
         RenderOutput = CreateRenderOutput("RenderOutput", "RENDER_OUTPUT", () => new Painter(OnPaint));
         RenderOutput = CreateRenderOutput("RenderOutput", "RENDER_OUTPUT", () => new Painter(OnPaint));
     }
     }
 
 
@@ -70,9 +72,9 @@ public class CreateImageNode : Node, IPreviewRenderable
         int saved = surface.DrawingSurface.Canvas.Save();
         int saved = surface.DrawingSurface.Canvas.Save();
 
 
         RenderContext ctx = new RenderContext(surface.DrawingSurface, context.FrameTime, context.ChunkResolution,
         RenderContext ctx = new RenderContext(surface.DrawingSurface, context.FrameTime, context.ChunkResolution,
-            context.DocumentSize, context.ProcessingColorSpace);
+            context.RenderOutputSize, context.DocumentSize, context.ProcessingColorSpace);
 
 
-        surface.DrawingSurface.Canvas.Translate((float)-ContentOffset.Value.X, (float)-ContentOffset.Value.Y);
+        surface.DrawingSurface.Canvas.SetMatrix(surface.DrawingSurface.Canvas.TotalMatrix.Concat(ContentMatrix.Value));
 
 
         Content.Value?.Paint(ctx, surface.DrawingSurface);
         Content.Value?.Paint(ctx, surface.DrawingSurface);
 
 

+ 0 - 70
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CustomOutputNode.cs

@@ -1,70 +0,0 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
-using Drawie.Backend.Core.Surfaces;
-using Drawie.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-
-[NodeInfo("CustomOutput")]
-public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
-{
-    public const string OutputNamePropertyName = "OutputName";
-    public RenderInputProperty Input { get; } 
-    public InputProperty<string> OutputName { get; }
-    
-    private VecI? lastDocumentSize;
-    public CustomOutputNode()
-    {
-        Input = new RenderInputProperty(this, OutputNode.InputPropertyName, "BACKGROUND", null);
-        AddInputProperty(Input);
-        
-        OutputName = CreateInput(OutputNamePropertyName, "OUTPUT_NAME", "");
-    }
-
-    public override Node CreateCopy()
-    {
-        return new CustomOutputNode();
-    }
-
-    protected override void OnExecute(RenderContext context)
-    {
-        if (context.TargetOutput == OutputName.Value)
-        {
-            lastDocumentSize = context.DocumentSize;
-
-            int saved = context.RenderSurface.Canvas.Save();
-            context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.DocumentSize.X, context.DocumentSize.Y));
-            Input.Value?.Paint(context, context.RenderSurface);
-
-            context.RenderSurface.Canvas.RestoreToCount(saved);
-        }
-    }
-
-    RenderInputProperty IRenderInput.Background => Input;
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        if (lastDocumentSize == null)
-        {
-            return null;
-        }
-        
-        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y); 
-    }
-
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        if (Input.Value == null)
-        {
-            return false;
-        }
-        
-        int saved = renderOn.Canvas.Save();
-        Input.Value.Paint(context, renderOn);
-        
-        renderOn.Canvas.RestoreToCount(saved);
-        
-        return true;
-    }
-}

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DocumentInfoNode.cs

@@ -9,16 +9,24 @@ public class DocumentInfoNode : Node
     public OutputProperty<VecI> Size { get; }
     public OutputProperty<VecI> Size { get; }
     public OutputProperty<VecD> Center { get; }
     public OutputProperty<VecD> Center { get; }
 
 
+    public OutputProperty<VecI> RenderOutputSize { get; }
+    public OutputProperty<VecI> RenderOutputCenter { get; }
+
     public DocumentInfoNode()
     public DocumentInfoNode()
     {
     {
         Size = CreateOutput("Size", "SIZE", new VecI(0, 0));
         Size = CreateOutput("Size", "SIZE", new VecI(0, 0));
         Center = CreateOutput("Center", "CENTER", new VecD(0, 0));
         Center = CreateOutput("Center", "CENTER", new VecD(0, 0));
+        RenderOutputSize = CreateOutput("RenderOutputSize", "RENDER_OUTPUT_SIZE", new VecI(0, 0));
+        RenderOutputCenter = CreateOutput("RenderOutputCenter", "RENDER_OUTPUT_CENTER", new VecI(0, 0));
     }
     }
 
 
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
         Size.Value = context.DocumentSize;
         Size.Value = context.DocumentSize;
         Center.Value = new VecD(context.DocumentSize.X / 2.0, context.DocumentSize.Y / 2.0);
         Center.Value = new VecD(context.DocumentSize.X / 2.0, context.DocumentSize.Y / 2.0);
+
+        RenderOutputSize.Value = context.RenderOutputSize;
+        RenderOutputCenter.Value = new VecI(context.RenderOutputSize.X / 2, context.RenderOutputSize.Y / 2);
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

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

@@ -52,7 +52,7 @@ public class OutlineNode : RenderNode, IRenderInput
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
         base.OnExecute(context);
         base.OnExecute(context);
-        lastDocumentSize = context.DocumentSize;
+        lastDocumentSize = context.RenderOutputSize;
 
 
         Kernel finalKernel = Type.Value switch
         Kernel finalKernel = Type.Value switch
         {
         {

+ 76 - 20
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -11,7 +11,7 @@ using Drawie.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
 [NodeInfo("Folder")]
 [NodeInfo("Folder")]
-public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPreviewRenderable
+public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
 {
 {
     public const string ContentInternalName = "Content";
     public const string ContentInternalName = "Content";
     private VecI documentSize;
     private VecI documentSize;
@@ -25,21 +25,21 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
 
     public override Node CreateCopy() => new FolderNode
     public override Node CreateCopy() => new FolderNode
     {
     {
-        MemberName = MemberName, 
+        MemberName = MemberName,
         ClipToPreviousMember = this.ClipToPreviousMember,
         ClipToPreviousMember = this.ClipToPreviousMember,
         EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted()
         EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted()
     };
     };
 
 
     public override VecD GetScenePosition(KeyFrameTime time) =>
     public override VecD GetScenePosition(KeyFrameTime time) =>
-        documentSize / 2f; 
+        documentSize / 2f;
 
 
     public override VecD GetSceneSize(KeyFrameTime time) =>
     public override VecD GetSceneSize(KeyFrameTime time) =>
-        documentSize; 
+        documentSize;
 
 
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
         base.OnExecute(context);
         base.OnExecute(context);
-        documentSize = context.DocumentSize;
+        documentSize = context.RenderOutputSize;
     }
     }
 
 
     public override void Render(SceneObjectRenderContext sceneContext)
     public override void Render(SceneObjectRenderContext sceneContext)
@@ -99,7 +99,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
 
         Content.Value?.Paint(sceneContext, outputWorkingSurface.DrawingSurface);
         Content.Value?.Paint(sceneContext, outputWorkingSurface.DrawingSurface);
 
 
-        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, sceneContext);
+        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, sceneContext, sceneContext.ChunkResolution);
 
 
         if (Background.Value != null && sceneContext.TargetPropertyOutput != RawOutput)
         if (Background.Value != null && sceneContext.TargetPropertyOutput != RawOutput)
         {
         {
@@ -143,28 +143,65 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
     {
         RectD? bounds = null;
         RectD? bounds = null;
+        if (!IsVisible.Value)
+            return null;
+
         if (Content.Connection != null)
         if (Content.Connection != null)
         {
         {
-            Content.Connection.Node.TraverseBackwards((n) =>
-            {
-                if (n is StructureNode structureNode)
+            Content.Connection.Node.TraverseBackwards(
+                (n, input) =>
                 {
                 {
-                    RectD? childBounds = structureNode.GetTightBounds(frameTime);
-                    if (childBounds != null)
+                    if (n is StructureNode { IsVisible.Value: true } structureNode)
                     {
                     {
-                        if (bounds == null)
+                        RectD? childBounds = structureNode.GetTightBounds(frameTime);
+                        if (childBounds != null)
                         {
                         {
-                            bounds = childBounds;
+                            if (bounds == null)
+                            {
+                                bounds = childBounds;
+                            }
+                            else
+                            {
+                                bounds = bounds.Value.Union(childBounds.Value);
+                            }
                         }
                         }
-                        else
+                    }
+
+                    return true;
+                }, FilterInvisibleFolders);
+
+            return bounds ?? RectD.Empty;
+        }
+
+        return null;
+    }
+
+    public override RectD? GetApproxBounds(KeyFrameTime frameTime)
+    {
+        RectD? bounds = null;
+        if (Content.Connection != null)
+        {
+            Content.Connection.Node.TraverseBackwards(
+                (n, input) =>
+                {
+                    if (n is StructureNode { IsVisible.Value: true } structureNode)
+                    {
+                        RectD? childBounds = structureNode.GetApproxBounds(frameTime);
+                        if (childBounds != null)
                         {
                         {
-                            bounds = bounds.Value.Union(childBounds.Value);
+                            if (bounds == null)
+                            {
+                                bounds = childBounds;
+                            }
+                            else
+                            {
+                                bounds = bounds.Value.Union(childBounds.Value);
+                            }
                         }
                         }
                     }
                     }
-                }
 
 
-                return true;
-            });
+                    return true;
+                }, FilterInvisibleFolders);
 
 
             return bounds ?? RectD.Empty;
             return bounds ?? RectD.Empty;
         }
         }
@@ -172,6 +209,19 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
         return null;
         return null;
     }
     }
 
 
+    private bool FilterInvisibleFolders(IInputProperty input)
+    {
+        if (input is
+            {
+                Node: IReadOnlyFolderNode folderNode, InternalPropertyName: FolderNode.ContentInternalName
+            })
+        {
+            return folderNode.IsVisible.Value;
+        }
+
+        return true;
+    }
+
     public HashSet<Guid> GetLayerNodeGuids()
     public HashSet<Guid> GetLayerNodeGuids()
     {
     {
         HashSet<Guid> guids = new();
         HashSet<Guid> guids = new();
@@ -195,7 +245,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
             return base.GetPreviewBounds(frame, elementFor);
             return base.GetPreviewBounds(frame, elementFor);
         }
         }
 
 
-        return GetTightBounds(frame);
+        return GetApproxBounds(frame);
     }
     }
 
 
     public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
     public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
@@ -208,10 +258,16 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
 
         if (Content.Connection != null)
         if (Content.Connection != null)
         {
         {
-            var executionQueue = GraphUtils.CalculateExecutionQueue(Content.Connection.Node);
+            var executionQueue = GraphUtils.CalculateExecutionQueue(Content.Connection.Node, FilterInvisibleFolders);
             while (executionQueue.Count > 0)
             while (executionQueue.Count > 0)
             {
             {
                 IReadOnlyNode node = executionQueue.Dequeue();
                 IReadOnlyNode node = executionQueue.Dequeue();
+
+                if (node is IReadOnlyStructureNode { IsVisible.Value: false })
+                {
+                    continue;
+                }
+
                 if (node is IPreviewRenderable previewRenderable)
                 if (node is IPreviewRenderable previewRenderable)
                 {
                 {
                     previewRenderable.RenderPreview(renderOn, context, elementToRenderName);
                     previewRenderable.RenderPreview(renderOn, context, elementToRenderName);

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

@@ -47,6 +47,20 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
         return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
     }
     }
 
 
+    public override RectD? GetApproxBounds(KeyFrameTime frameTime)
+    {
+        var chunkAlignedBounds = GetLayerImageAtFrame(frameTime.Frame).FindChunkAlignedCommittedBounds();
+        if (chunkAlignedBounds == null)
+        {
+            return null;
+        }
+
+        RectD size = new RectD(chunkAlignedBounds.Value.X, chunkAlignedBounds.Value.Y,
+            Math.Min(chunkAlignedBounds.Value.Width, layerImage.LatestSize.X),
+            Math.Min(chunkAlignedBounds.Value.Height, layerImage.LatestSize.Y));
+        return size;
+    }
+
     protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx,
     protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx,
         DrawingSurface workingSurface,
         DrawingSurface workingSurface,
         bool useFilters = true)
         bool useFilters = true)
@@ -62,11 +76,12 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
 
     protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx,
     protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx,
         DrawingSurface workingSurface,
         DrawingSurface workingSurface,
+        ChunkResolution resolution,
         bool useFilters, Paint paint)
         bool useFilters, Paint paint)
     {
     {
         int scaled = workingSurface.Canvas.Save();
         int scaled = workingSurface.Canvas.Save();
-        workingSurface.Canvas.Translate(GetScenePosition(ctx.FrameTime) * ctx.ChunkResolution.Multiplier());
-        workingSurface.Canvas.Scale((float)ctx.ChunkResolution.Multiplier());
+        workingSurface.Canvas.Translate(GetScenePosition(ctx.FrameTime) * resolution.Multiplier());
+        workingSurface.Canvas.Scale((float)resolution.Multiplier());
 
 
         DrawLayerOnto(ctx, workingSurface, useFilters, paint);
         DrawLayerOnto(ctx, workingSurface, useFilters, paint);
 
 

+ 53 - 16
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -44,7 +44,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
                 blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             }
             }
 
 
-            if (AllowHighDpiRendering)
+            if (AllowHighDpiRendering || renderOnto.DeviceClipBounds.Size == context.RenderOutputSize)
             {
             {
                 DrawLayerInScene(context, renderOnto, useFilters);
                 DrawLayerInScene(context, renderOnto, useFilters);
             }
             }
@@ -56,38 +56,59 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                     BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver
                     BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver
                 };
                 };
 
 
-                using var tempSurface = Texture.ForProcessing(context.DocumentSize, context.ProcessingColorSpace);
-                DrawLayerOnTexture(context, tempSurface.DrawingSurface, useFilters, targetPaint);
+                var tempSurface = TryInitWorkingSurface(context.RenderOutputSize, context.ChunkResolution,
+                    context.ProcessingColorSpace, 22);
+
+                DrawLayerOnTexture(context, tempSurface.DrawingSurface, context.ChunkResolution, useFilters, targetPaint);
 
 
-                renderOnto.Canvas.DrawSurface(tempSurface.DrawingSurface, 0, 0, blendPaint);
+                blendPaint.SetFilters(null);
+                DrawWithResolution(tempSurface.DrawingSurface, renderOnto, context.ChunkResolution);
             }
             }
 
 
             return;
             return;
         }
         }
 
 
-        VecI size = renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos;
+        VecI size = AllowHighDpiRendering
+            ? renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos
+            : context.RenderOutputSize;
         int saved = renderOnto.Canvas.Save();
         int saved = renderOnto.Canvas.Save();
 
 
+        var adjustedResolution = AllowHighDpiRendering ? ChunkResolution.Full : context.ChunkResolution;
+
         var outputWorkingSurface =
         var outputWorkingSurface =
-            TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 1);
+            TryInitWorkingSurface(size, adjustedResolution, context.ProcessingColorSpace, 1);
         outputWorkingSurface.DrawingSurface.Canvas.Clear();
         outputWorkingSurface.DrawingSurface.Canvas.Clear();
         outputWorkingSurface.DrawingSurface.Canvas.Save();
         outputWorkingSurface.DrawingSurface.Canvas.Save();
-        outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(renderOnto.Canvas.TotalMatrix);
+        if (AllowHighDpiRendering)
+        {
+            outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(renderOnto.Canvas.TotalMatrix);
+            renderOnto.Canvas.SetMatrix(Matrix3X3.Identity);
+        }
 
 
-        renderOnto.Canvas.SetMatrix(Matrix3X3.Identity);
+        using var paint = new Paint
+        {
+            Color = new Color(255, 255, 255, 255), BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver
+        };
 
 
-        DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface, useFilters, blendPaint);
+        DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface, adjustedResolution, false, paint);
 
 
-        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, context);
+        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, context, adjustedResolution);
 
 
         if (Background.Value != null)
         if (Background.Value != null)
         {
         {
-            Texture tempSurface = TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 4);
+            Texture tempSurface = TryInitWorkingSurface(size, adjustedResolution, context.ProcessingColorSpace, 4);
 
 
             tempSurface.DrawingSurface.Canvas.Save();
             tempSurface.DrawingSurface.Canvas.Save();
-            tempSurface.DrawingSurface.Canvas.SetMatrix(outputWorkingSurface.DrawingSurface.Canvas.TotalMatrix);
-
-            outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+            if (AllowHighDpiRendering)
+            {
+                tempSurface.DrawingSurface.Canvas.SetMatrix(outputWorkingSurface.DrawingSurface.Canvas.TotalMatrix);
+                outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+            }
+            else
+            {
+                tempSurface.DrawingSurface.Canvas.Scale(
+                    (float)context.ChunkResolution.Multiplier());
+            }
 
 
             tempSurface.DrawingSurface.Canvas.Clear();
             tempSurface.DrawingSurface.Canvas.Clear();
             if (Background.Connection is { Node: IClipSource clipSource } && ClipToPreviousMember)
             if (Background.Connection is { Node: IClipSource clipSource } && ClipToPreviousMember)
@@ -99,7 +120,16 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         }
         }
 
 
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
-        DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, context.ChunkResolution);
+        if (useFilters)
+        {
+            blendPaint.SetFilters(Filters.Value);
+        }
+        else
+        {
+            blendPaint.SetFilters(null);
+        }
+
+        DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, adjustedResolution);
 
 
         renderOnto.Canvas.RestoreToCount(saved);
         renderOnto.Canvas.RestoreToCount(saved);
         outputWorkingSurface.DrawingSurface.Canvas.Restore();
         outputWorkingSurface.DrawingSurface.Canvas.Restore();
@@ -107,10 +137,11 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
 
     protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx,
     protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx,
         DrawingSurface workingSurface,
         DrawingSurface workingSurface,
+        ChunkResolution resolution,
         bool useFilters, Paint paint)
         bool useFilters, Paint paint)
     {
     {
         int scaled = workingSurface.Canvas.Save();
         int scaled = workingSurface.Canvas.Save();
-        workingSurface.Canvas.Scale((float)ctx.ChunkResolution.Multiplier());
+        workingSurface.Canvas.Scale((float)resolution.Multiplier());
 
 
         DrawLayerOnto(ctx, workingSurface, useFilters, paint);
         DrawLayerOnto(ctx, workingSurface, useFilters, paint);
 
 
@@ -198,9 +229,15 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
 
         if (!hasSurface || workingSurface.Size != targetSize || workingSurface.IsDisposed)
         if (!hasSurface || workingSurface.Size != targetSize || workingSurface.IsDisposed)
         {
         {
+            workingSurface?.Dispose();
             workingSurfaces[(targetResolution, id)] = Texture.ForProcessing(targetSize, processingCs);
             workingSurfaces[(targetResolution, id)] = Texture.ForProcessing(targetSize, processingCs);
             workingSurface = workingSurfaces[(targetResolution, id)];
             workingSurface = workingSurfaces[(targetResolution, id)];
         }
         }
+        else
+        {
+            workingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+            workingSurface.DrawingSurface.Canvas.Clear();
+        }
 
 
         return workingSurface;
         return workingSurface;
     }
     }

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

@@ -81,9 +81,9 @@ public class MathNode : Node
             return context.NewFloat1(result);
             return context.NewFloat1(result);
         }
         }
 
 
-        var xConst = x.ConstantValue;
-        var yConst = y.ConstantValue;
-        var zConst = z.ConstantValue;
+        var xConst = (double)x.GetConstant();
+        var yConst = (double)y.GetConstant();
+        var zConst = (double)z.GetConstant();
         
         
         var constValue = Mode.Value switch
         var constValue = Mode.Value switch
         {
         {

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

@@ -13,8 +13,6 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
     public InputProperty<Matrix3X3> Input { get; }
     public InputProperty<Matrix3X3> Input { get; }
     public OutputProperty<Matrix3X3> Matrix { get; }
     public OutputProperty<Matrix3X3> Matrix { get; }
 
 
-    private Paint? paint;
-
     public Matrix3X3BaseNode()
     public Matrix3X3BaseNode()
     {
     {
         Background = CreateRenderInput("Background", "IMAGE");
         Background = CreateRenderInput("Background", "IMAGE");
@@ -30,19 +28,18 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
         if (Background.Value == null)
         if (Background.Value == null)
             return;
             return;
 
 
-        paint ??= new();
         base.OnExecute(context);
         base.OnExecute(context);
     }
     }
 
 
     protected override void OnPaint(RenderContext context, DrawingSurface surface)
     protected override void OnPaint(RenderContext context, DrawingSurface surface)
     {
     {
-        if (paint == null)
-            return;
-
         int layer = surface.Canvas.Save();
         int layer = surface.Canvas.Save();
 
 
         surface.Canvas.SetMatrix(surface.Canvas.TotalMatrix.Concat(Matrix.Value));
         surface.Canvas.SetMatrix(surface.Canvas.TotalMatrix.Concat(Matrix.Value));
-        Background.Value?.Paint(context, surface);
+        if (!surface.LocalClipBounds.IsZeroOrNegativeArea)
+        {
+            Background.Value?.Paint(context, surface);
+        }
 
 
         surface.Canvas.RestoreToCount(layer);
         surface.Canvas.RestoreToCount(layer);
     }
     }

+ 3 - 32
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/TransformNode.cs

@@ -7,40 +7,11 @@ using PixiEditor.ChangeableDocument.Rendering;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 
 
 [NodeInfo("Transform")]
 [NodeInfo("Transform")]
-public class TransformNode : RenderNode, IRenderInput
+public class TransformNode : Matrix3X3BaseNode
 {
 {
-    public RenderInputProperty Background { get; }
-    public InputProperty<Matrix3X3> Matrix { get; }
-
-    public TransformNode()
-    {
-        Background = CreateRenderInput("Background", "IMAGE");
-        Matrix = CreateInput("Matrix", "INPUT_MATRIX", Matrix3X3.Identity);
-
-        Output.FirstInChain = null;
-    }
-
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
-    {
-        if (Background.Value == null)
-            return;
-
-        int layer = surface.Canvas.Save();
-
-        surface.Canvas.SetMatrix(surface.Canvas.TotalMatrix.PostConcat(Matrix.Value));
-        Background.Value?.Paint(context, surface);
-
-        surface.Canvas.RestoreToCount(layer);
-    }
-
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        return null;
-    }
-
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
     {
     {
-        return false;
+        return Input.Value;
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

+ 8 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -113,7 +113,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         lastContentCacheHash = GetContentCacheHash();
         lastContentCacheHash = GetContentCacheHash();
     }
     }
 
 
-    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action)
+    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action, Func<IInputProperty, bool>? branchCondition = null)
     {
     {
         var visited = new HashSet<IReadOnlyNode>();
         var visited = new HashSet<IReadOnlyNode>();
         var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
         var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
@@ -135,6 +135,10 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
 
             foreach (var inputProperty in node.Item1.InputProperties)
             foreach (var inputProperty in node.Item1.InputProperties)
             {
             {
+                if (branchCondition != null && !branchCondition(inputProperty))
+                {
+                    continue;
+                }
                 if (inputProperty.Connection != null)
                 if (inputProperty.Connection != null)
                 {
                 {
                     queueNodes.Enqueue((inputProperty.Connection.Node, inputProperty));
                     queueNodes.Enqueue((inputProperty.Connection.Node, inputProperty));
@@ -483,12 +487,12 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
 
     public abstract Node CreateCopy();
     public abstract Node CreateCopy();
 
 
-    public Node Clone()
+    public Node Clone(bool preserveGuids = false)
     {
     {
         var clone = CreateCopy();
         var clone = CreateCopy();
 
 
         clone.DisplayName = DisplayName;
         clone.DisplayName = DisplayName;
-        clone.Id = Guid.NewGuid();
+        clone.Id = preserveGuids ? Id : Guid.NewGuid();
         clone.Position = Position;
         clone.Position = Position;
 
 
         for (var i = 0; i < clone.inputs.Count; i++)
         for (var i = 0; i < clone.inputs.Count; i++)
@@ -509,7 +513,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
 
         foreach (var keyFrame in keyFrames)
         foreach (var keyFrame in keyFrames)
         {
         {
-            Guid newGuid = Guid.NewGuid();
+            Guid newGuid = preserveGuids ? keyFrame.KeyFrameGuid : Guid.NewGuid();
             KeyFrameData newKeyFrame = new KeyFrameData(newGuid, keyFrame.StartFrame, keyFrame.Duration,
             KeyFrameData newKeyFrame = new KeyFrameData(newGuid, keyFrame.StartFrame, keyFrame.Duration,
                 keyFrame.AffectedElement)
                 keyFrame.AffectedElement)
             {
             {

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

@@ -32,10 +32,10 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
     {
     {
         if (!string.IsNullOrEmpty(context.TargetOutput)) return;
         if (!string.IsNullOrEmpty(context.TargetOutput)) return;
 
 
-        lastDocumentSize = context.DocumentSize;
+        lastDocumentSize = context.RenderOutputSize;
 
 
         int saved = context.RenderSurface.Canvas.Save();
         int saved = context.RenderSurface.Canvas.Save();
-        context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.DocumentSize.X, context.DocumentSize.Y));
+        context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y));
         Input.Value?.Paint(context, context.RenderSurface);
         Input.Value?.Paint(context, context.RenderSurface);
 
 
         context.RenderSurface.Canvas.RestoreToCount(saved);
         context.RenderSurface.Canvas.RestoreToCount(saved);

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

@@ -39,18 +39,18 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
             }
             }
         }
         }
 
 
-        lastDocumentSize = context.DocumentSize;
+        lastDocumentSize = context.RenderOutputSize;
     }
     }
 
 
     protected virtual void Paint(RenderContext context, DrawingSurface surface)
     protected virtual void Paint(RenderContext context, DrawingSurface surface)
     {
     {
         DrawingSurface target = surface;
         DrawingSurface target = surface;
         bool useIntermediate = !AllowHighDpiRendering
         bool useIntermediate = !AllowHighDpiRendering
-                               && context.DocumentSize is { X: > 0, Y: > 0 }
-                               && surface.DeviceClipBounds.Size != context.DocumentSize;
+                               && context.RenderOutputSize is { X: > 0, Y: > 0 }
+                               && surface.DeviceClipBounds.Size != context.RenderOutputSize;
         if (useIntermediate)
         if (useIntermediate)
         {
         {
-            Texture intermediate = textureCache.RequestTexture(-6451, context.DocumentSize, context.ProcessingColorSpace);
+            Texture intermediate = textureCache.RequestTexture(-6451, context.RenderOutputSize, context.ProcessingColorSpace);
             target = intermediate.DrawingSurface;
             target = intermediate.DrawingSurface;
         }
         }
 
 

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

@@ -30,7 +30,7 @@ public class SampleImageNode : Node
 
 
     private Half4 GetColor(FuncContext context)
     private Half4 GetColor(FuncContext context)
     {
     {
-        if (Image.Value is null)
+        if (Image.Value is null || Image.Value.IsDisposed)
         {
         {
             return new Half4("");
             return new Half4("");
         }
         }

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

@@ -48,7 +48,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     {
     {
         base.OnExecute(context);
         base.OnExecute(context);
 
 
-        lastDocumentSize = context.DocumentSize;
+        lastDocumentSize = context.RenderOutputSize;
 
 
         if (lastShaderCode != ShaderCode.Value)
         if (lastShaderCode != ShaderCode.Value)
         {
         {
@@ -88,7 +88,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         Uniforms uniforms;
         Uniforms uniforms;
         uniforms = new Uniforms();
         uniforms = new Uniforms();
 
 
-        uniforms.Add("iResolution", new Uniform("iResolution", (VecD)context.DocumentSize));
+        uniforms.Add("iResolution", new Uniform("iResolution", (VecD)context.RenderOutputSize));
         uniforms.Add("iNormalizedTime", new Uniform("iNormalizedTime", (float)context.FrameTime.NormalizedTime));
         uniforms.Add("iNormalizedTime", new Uniform("iNormalizedTime", (float)context.FrameTime.NormalizedTime));
         uniforms.Add("iFrame", new Uniform("iFrame", context.FrameTime.Frame));
         uniforms.Add("iFrame", new Uniform("iFrame", context.FrameTime.Frame));
 
 
@@ -101,7 +101,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
             return uniforms;
             return uniforms;
         }
         }
 
 
-        Texture texture = RequestTexture(50, context.DocumentSize, context.ProcessingColorSpace);
+        Texture texture = RequestTexture(50, context.RenderOutputSize, context.ProcessingColorSpace);
         Background.Value.Paint(context, texture.DrawingSurface);
         Background.Value.Paint(context, texture.DrawingSurface);
 
 
         var snapshot = texture.DrawingSurface.Snapshot();
         var snapshot = texture.DrawingSurface.Snapshot();
@@ -128,17 +128,17 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         {
         {
             if (ColorSpace.Value == ColorSpaceType.Srgb && !context.ProcessingColorSpace.IsSrgb)
             if (ColorSpace.Value == ColorSpaceType.Srgb && !context.ProcessingColorSpace.IsSrgb)
             {
             {
-                targetSurface = RequestTexture(51, context.DocumentSize,
+                targetSurface = RequestTexture(51, context.RenderOutputSize,
                     Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgb()).DrawingSurface;
                     Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgb()).DrawingSurface;
             }
             }
             else if (ColorSpace.Value == ColorSpaceType.LinearSrgb && context.ProcessingColorSpace.IsSrgb)
             else if (ColorSpace.Value == ColorSpaceType.LinearSrgb && context.ProcessingColorSpace.IsSrgb)
             {
             {
-                targetSurface = RequestTexture(51, context.DocumentSize,
+                targetSurface = RequestTexture(51, context.RenderOutputSize,
                     Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgbLinear()).DrawingSurface;
                     Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgbLinear()).DrawingSurface;
             }
             }
         }
         }
 
 
-        targetSurface.Canvas.DrawRect(0, 0, context.DocumentSize.X, context.DocumentSize.Y, paint);
+        targetSurface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
 
 
         if (targetSurface != surface)
         if (targetSurface != surface)
         {
         {

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

@@ -22,7 +22,7 @@ public class DistributePointsNode : ShapeNode<PointsVectorData>
 
 
     protected override PointsVectorData? GetShapeData(RenderContext context)
     protected override PointsVectorData? GetShapeData(RenderContext context)
     {
     {
-        return GetPointsRandomly(context.DocumentSize);
+        return GetPointsRandomly(context.RenderOutputSize);
     }
     }
 
 
     private PointsVectorData GetPointsRandomly(VecI size)
     private PointsVectorData GetPointsRandomly(VecI size)

+ 32 - 19
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -47,13 +47,15 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     public ChunkyImage? EmbeddedMask { get; set; }
     public ChunkyImage? EmbeddedMask { get; set; }
 
 
     protected Texture renderedMask;
     protected Texture renderedMask;
-    protected static readonly Paint replacePaint = new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src };
+
+    protected static readonly Paint replacePaint =
+        new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src };
 
 
     protected static readonly Paint clearPaint = new Paint()
     protected static readonly Paint clearPaint = new Paint()
     {
     {
         BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src, Color = Colors.Transparent
         BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src, Color = Colors.Transparent
     };
     };
-    
+
     protected static readonly Paint clipPaint = new Paint()
     protected static readonly Paint clipPaint = new Paint()
     {
     {
         BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.DstIn
         BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.DstIn
@@ -70,12 +72,16 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         set => DisplayName = value;
         set => DisplayName = value;
     }
     }
 
 
-    protected Paint maskPaint = new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.DstIn, ColorFilter = Nodes.Filters.MaskFilter };
+    protected Paint maskPaint = new Paint()
+    {
+        BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.DstIn, ColorFilter = Nodes.Filters.MaskFilter
+    };
+
     protected Paint blendPaint = new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver };
     protected Paint blendPaint = new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver };
 
 
     protected Paint maskPreviewPaint = new Paint()
     protected Paint maskPreviewPaint = new Paint()
     {
     {
-        BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver, 
+        BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver,
         ColorFilter = ColorFilter.CreateCompose(Nodes.Filters.AlphaGrayscaleFilter, Nodes.Filters.MaskFilter)
         ColorFilter = ColorFilter.CreateCompose(Nodes.Filters.AlphaGrayscaleFilter, Nodes.Filters.MaskFilter)
     };
     };
 
 
@@ -141,22 +147,22 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     {
     {
         RenderForOutput(context, renderTarget, FilterlessOutput);
         RenderForOutput(context, renderTarget, FilterlessOutput);
     }
     }
-    
+
     private void OnRawPaint(RenderContext context, DrawingSurface renderTarget)
     private void OnRawPaint(RenderContext context, DrawingSurface renderTarget)
     {
     {
         RenderForOutput(context, renderTarget, RawOutput);
         RenderForOutput(context, renderTarget, RawOutput);
     }
     }
-    
+
     public abstract VecD GetScenePosition(KeyFrameTime frameTime);
     public abstract VecD GetScenePosition(KeyFrameTime frameTime);
     public abstract VecD GetSceneSize(KeyFrameTime frameTime);
     public abstract VecD GetSceneSize(KeyFrameTime frameTime);
 
 
     public void RenderForOutput(RenderContext context, DrawingSurface renderTarget, RenderOutputProperty output)
     public void RenderForOutput(RenderContext context, DrawingSurface renderTarget, RenderOutputProperty output)
     {
     {
-        if(IsDisposed)
+        if (IsDisposed)
         {
         {
             return;
             return;
         }
         }
-        
+
         var renderObjectContext = CreateSceneContext(context, renderTarget, output);
         var renderObjectContext = CreateSceneContext(context, renderTarget, output);
 
 
         int renderSaved = renderTarget.Canvas.Save();
         int renderSaved = renderTarget.Canvas.Save();
@@ -176,7 +182,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         RectD localBounds = new RectD(0, 0, sceneSize.X, sceneSize.Y);
         RectD localBounds = new RectD(0, 0, sceneSize.X, sceneSize.Y);
 
 
         SceneObjectRenderContext renderObjectContext = new SceneObjectRenderContext(output, renderTarget, localBounds,
         SceneObjectRenderContext renderObjectContext = new SceneObjectRenderContext(output, renderTarget, localBounds,
-            context.FrameTime, context.ChunkResolution, context.DocumentSize, renderTarget == context.RenderSurface,
+            context.FrameTime, context.ChunkResolution, context.RenderOutputSize, context.DocumentSize, renderTarget == context.RenderSurface,
             context.ProcessingColorSpace,
             context.ProcessingColorSpace,
             context.Opacity);
             context.Opacity);
         renderObjectContext.FullRerender = context.FullRerender;
         renderObjectContext.FullRerender = context.FullRerender;
@@ -185,16 +191,16 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
 
     public abstract void Render(SceneObjectRenderContext sceneContext);
     public abstract void Render(SceneObjectRenderContext sceneContext);
 
 
-    protected void ApplyMaskIfPresent(DrawingSurface surface, RenderContext context)
+    protected void ApplyMaskIfPresent(DrawingSurface surface, RenderContext context, ChunkResolution renderResolution)
     {
     {
         if (MaskIsVisible.Value)
         if (MaskIsVisible.Value)
         {
         {
             if (CustomMask.Value != null)
             if (CustomMask.Value != null)
             {
             {
                 int layer = surface.Canvas.SaveLayer(maskPaint);
                 int layer = surface.Canvas.SaveLayer(maskPaint);
-                surface.Canvas.Scale((float)context.ChunkResolution.Multiplier());
+                surface.Canvas.Scale((float)renderResolution.Multiplier());
                 CustomMask.Value.Paint(context, surface);
                 CustomMask.Value.Paint(context, surface);
-                
+
                 surface.Canvas.RestoreToCount(layer);
                 surface.Canvas.RestoreToCount(layer);
             }
             }
             else if (EmbeddedMask != null)
             else if (EmbeddedMask != null)
@@ -206,9 +212,12 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
                         ChunkResolution.Full,
                         ChunkResolution.Full,
                         surface, VecI.Zero, maskPaint);
                         surface, VecI.Zero, maskPaint);
                 }
                 }
-                else if(renderedMask != null)
+                else if (renderedMask != null)
                 {
                 {
+                    int saved = surface.Canvas.Save();
+                    surface.Canvas.Scale((float)renderResolution.Multiplier());
                     surface.Canvas.DrawSurface(renderedMask.DrawingSurface, 0, 0, maskPaint);
                     surface.Canvas.DrawSurface(renderedMask.DrawingSurface, 0, 0, maskPaint);
+                    surface.Canvas.RestoreToCount(saved);
                 }
                 }
             }
             }
         }
         }
@@ -216,10 +225,12 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
 
     protected override int GetContentCacheHash()
     protected override int GetContentCacheHash()
     {
     {
-        return HashCode.Combine(base.GetContentCacheHash(), EmbeddedMask?.GetCacheHash() ?? 0, ClipToPreviousMember ? 1 : 0);
+        return HashCode.Combine(base.GetContentCacheHash(), EmbeddedMask?.GetCacheHash() ?? 0,
+            ClipToPreviousMember ? 1 : 0);
     }
     }
 
 
-    public virtual void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime, ColorSpace processingColorSpace)
+    public virtual void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
+        ColorSpace processingColorSpace)
     {
     {
         RenderChunkyImageChunk(chunkPos, resolution, EmbeddedMask, 55, processingColorSpace, ref renderedMask);
         RenderChunkyImageChunk(chunkPos, resolution, EmbeddedMask, 55, processingColorSpace, ref renderedMask);
     }
     }
@@ -234,11 +245,11 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         }
         }
 
 
         VecI targetSize = img.LatestSize;
         VecI targetSize = img.LatestSize;
-        
+
         renderSurface = RequestTexture(textureId, targetSize, processingColorSpace, false);
         renderSurface = RequestTexture(textureId, targetSize, processingColorSpace, false);
 
 
         int saved = renderSurface.DrawingSurface.Canvas.Save();
         int saved = renderSurface.DrawingSurface.Canvas.Save();
-        
+
         if (!img.DrawMostUpToDateChunkOn(
         if (!img.DrawMostUpToDateChunkOn(
                 chunkPos,
                 chunkPos,
                 ChunkResolution.Full,
                 ChunkResolution.Full,
@@ -250,7 +261,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
             renderSurface.DrawingSurface.Canvas.DrawRect(new RectD(chunkPos * chunkSize, new VecD(chunkSize)),
             renderSurface.DrawingSurface.Canvas.DrawRect(new RectD(chunkPos * chunkSize, new VecD(chunkSize)),
                 clearPaint);
                 clearPaint);
         }
         }
-        
+
         renderSurface.DrawingSurface.Canvas.RestoreToCount(saved);
         renderSurface.DrawingSurface.Canvas.RestoreToCount(saved);
     }
     }
 
 
@@ -279,6 +290,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     }
     }
 
 
     public abstract RectD? GetTightBounds(KeyFrameTime frameTime);
     public abstract RectD? GetTightBounds(KeyFrameTime frameTime);
+    public abstract RectD? GetApproxBounds(KeyFrameTime frameTime);
 
 
     public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
     public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
     {
     {
@@ -287,6 +299,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         {
         {
             additionalData["embeddedMask"] = EmbeddedMask;
             additionalData["embeddedMask"] = EmbeddedMask;
         }
         }
+
         if (ClipToPreviousMember)
         if (ClipToPreviousMember)
         {
         {
             additionalData["clipToPreviousMember"] = ClipToPreviousMember;
             additionalData["clipToPreviousMember"] = ClipToPreviousMember;
@@ -308,7 +321,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
 
             infos.Add(new StructureMemberMask_ChangeInfo(Id, mask != null));
             infos.Add(new StructureMemberMask_ChangeInfo(Id, mask != null));
         }
         }
-        
+
         if (data.TryGetValue("clipToPreviousMember", out var clip))
         if (data.TryGetValue("clipToPreviousMember", out var clip))
         {
         {
             ClipToPreviousMember = (bool)clip;
             ClipToPreviousMember = (bool)clip;

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

@@ -1,5 +1,6 @@
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
@@ -24,6 +25,7 @@ public class TextureCache : IDisposable
             if (clear)
             if (clear)
             {
             {
                 texture.DrawingSurface.Canvas.Clear(Colors.Transparent);
                 texture.DrawingSurface.Canvas.Clear(Colors.Transparent);
+                texture.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
             }
             }
 
 
             return texture;
             return texture;

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

@@ -62,7 +62,7 @@ public class TileNode : RenderNode
         if (paint == null)
         if (paint == null)
             return;
             return;
 
 
-        surface.Canvas.DrawRect(0, 0, context.DocumentSize.X, context.DocumentSize.Y, paint);
+        surface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

+ 51 - 28
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs

@@ -18,44 +18,59 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [NodeInfo("VectorLayer")]
 [NodeInfo("VectorLayer")]
 public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorNode, IRasterizable, IScalable
 public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorNode, IRasterizable, IScalable
 {
 {
+    public InputProperty<ShapeVectorData> InputVector { get; }
     public OutputProperty<ShapeVectorData> Shape { get; }
     public OutputProperty<ShapeVectorData> Shape { get; }
+    public OutputProperty<Matrix3X3> Matrix { get; }
 
 
     public Matrix3X3 TransformationMatrix
     public Matrix3X3 TransformationMatrix
     {
     {
-        get => ShapeData?.TransformationMatrix ?? Matrix3X3.Identity;
+        get => RenderableShapeData?.TransformationMatrix ?? Matrix3X3.Identity;
         set
         set
         {
         {
-            if (ShapeData == null)
+            if (RenderableShapeData == null)
             {
             {
                 return;
                 return;
             }
             }
 
 
-            ShapeData.TransformationMatrix = value;
+            RenderableShapeData.TransformationMatrix = value;
         }
         }
     }
     }
 
 
-    public ShapeVectorData? ShapeData
+    public ShapeVectorData? EmbeddedShapeData
     {
     {
         get => Shape.Value;
         get => Shape.Value;
         set => Shape.Value = value;
         set => Shape.Value = value;
     }
     }
 
 
-    IReadOnlyShapeVectorData IReadOnlyVectorNode.ShapeData => ShapeData;
+    public ShapeVectorData? RenderableShapeData
+    {
+        get => InputVector.Value ?? EmbeddedShapeData;
+    }
+
+    IReadOnlyShapeVectorData IReadOnlyVectorNode.ShapeData => RenderableShapeData;
 
 
 
 
-    public override VecD GetScenePosition(KeyFrameTime time) => ShapeData?.TransformedAABB.Center ?? VecD.Zero;
-    public override VecD GetSceneSize(KeyFrameTime time) => ShapeData?.TransformedAABB.Size ?? VecD.Zero;
+    public override VecD GetScenePosition(KeyFrameTime time) => RenderableShapeData?.TransformedAABB.Center ?? VecD.Zero;
+    public override VecD GetSceneSize(KeyFrameTime time) => RenderableShapeData?.TransformedAABB.Size ?? VecD.Zero;
 
 
     public VectorLayerNode()
     public VectorLayerNode()
     {
     {
         AllowHighDpiRendering = true;
         AllowHighDpiRendering = true;
+        InputVector = CreateInput<ShapeVectorData>("Input", "INPUT", null);
         Shape = CreateOutput<ShapeVectorData>("Shape", "SHAPE", null);
         Shape = CreateOutput<ShapeVectorData>("Shape", "SHAPE", null);
+        Matrix = CreateOutput<Matrix3X3>("Matrix", "MATRIX", Matrix3X3.Identity);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        base.OnExecute(context);
+        Matrix.Value = TransformationMatrix;
     }
     }
 
 
     protected override void DrawWithoutFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
     protected override void DrawWithoutFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
         Paint paint)
         Paint paint)
     {
     {
-        if (ShapeData == null)
+        if (RenderableShapeData == null)
         {
         {
             return;
             return;
         }
         }
@@ -65,7 +80,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
 
 
     protected override void DrawWithFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface, Paint paint)
     protected override void DrawWithFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface, Paint paint)
     {
     {
-        if (ShapeData == null)
+        if (RenderableShapeData == null)
         {
         {
             return;
             return;
         }
         }
@@ -81,7 +96,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         }
         }
         else
         else
         {
         {
-            return ShapeData?.TransformedVisualAABB;
+            return RenderableShapeData?.TransformedVisualAABB;
         }
         }
 
 
         return null;
         return null;
@@ -95,18 +110,18 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
             return base.RenderPreview(renderOn, context, elementToRenderName);
             return base.RenderPreview(renderOn, context, elementToRenderName);
         }
         }
 
 
-        if (ShapeData == null)
+        if (RenderableShapeData == null)
         {
         {
             return false;
             return false;
         }
         }
 
 
         using var paint = new Paint();
         using var paint = new Paint();
 
 
-        VecI tightBoundsSize = (VecI)ShapeData.TransformedVisualAABB.Size;
+        VecI tightBoundsSize = (VecI)RenderableShapeData.TransformedVisualAABB.Size;
 
 
         VecI translation = new VecI(
         VecI translation = new VecI(
-            (int)Math.Max(ShapeData.TransformedAABB.TopLeft.X, 0),
-            (int)Math.Max(ShapeData.TransformedAABB.TopLeft.Y, 0));
+            (int)Math.Max(RenderableShapeData.TransformedAABB.TopLeft.X, 0),
+            (int)Math.Max(RenderableShapeData.TransformedAABB.TopLeft.Y, 0));
 
 
         VecI size = tightBoundsSize + translation;
         VecI size = tightBoundsSize + translation;
 
 
@@ -115,7 +130,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
             return false;
             return false;
         }
         }
 
 
-        Matrix3X3 matrix = ShapeData.TransformationMatrix;
+        Matrix3X3 matrix = RenderableShapeData.TransformationMatrix;
 
 
         if (!context.ProcessingColorSpace.IsSrgb)
         if (!context.ProcessingColorSpace.IsSrgb)
         {
         {
@@ -134,48 +149,56 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         return true;
         return true;
     }
     }
 
 
+    public override RectD? GetApproxBounds(KeyFrameTime frameTime)
+    {
+        return GetTightBounds(frameTime);
+    }
+
     public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
     public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
     {
     {
         base.SerializeAdditionalData(additionalData);
         base.SerializeAdditionalData(additionalData);
-        additionalData["ShapeData"] = ShapeData;
+        additionalData["ShapeData"] = EmbeddedShapeData;
     }
     }
 
 
     internal override void DeserializeAdditionalData(IReadOnlyDocument target,
     internal override void DeserializeAdditionalData(IReadOnlyDocument target,
         IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
         IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
     {
     {
         base.DeserializeAdditionalData(target, data, infos);
         base.DeserializeAdditionalData(target, data, infos);
-        ShapeData = (ShapeVectorData)data["ShapeData"];
+        EmbeddedShapeData = (ShapeVectorData)data["ShapeData"];
 
 
-        if (ShapeData == null)
+        if (EmbeddedShapeData == null)
         {
         {
             return;
             return;
         }
         }
 
 
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
-            (RectI)ShapeData.TransformedAABB, ChunkyImage.FullChunkSize));
+            (RectI)EmbeddedShapeData.TransformedAABB, ChunkyImage.FullChunkSize));
 
 
         infos.Add(new VectorShape_ChangeInfo(Id, affected));
         infos.Add(new VectorShape_ChangeInfo(Id, affected));
     }
     }
 
 
     protected override int GetContentCacheHash()
     protected override int GetContentCacheHash()
     {
     {
-        return HashCode.Combine(base.GetContentCacheHash(), ShapeData?.GetCacheHash() ?? 0);
+        return HashCode.Combine(
+            base.GetContentCacheHash(),
+            EmbeddedShapeData?.GetCacheHash() ?? 0,
+            RenderableShapeData?.GetCacheHash() ?? 0);
     }
     }
 
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
     {
-        return ShapeData?.TransformedVisualAABB ?? null;
+        return RenderableShapeData?.TransformedVisualAABB ?? null;
     }
     }
 
 
     public override ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
     public override ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
     {
     {
-        return ShapeData?.TransformationCorners ?? new ShapeCorners();
+        return RenderableShapeData?.TransformationCorners ?? new ShapeCorners();
     }
     }
 
 
     public void Rasterize(DrawingSurface surface, Paint paint)
     public void Rasterize(DrawingSurface surface, Paint paint)
     {
     {
         int layer = surface.Canvas.SaveLayer(paint);
         int layer = surface.Canvas.SaveLayer(paint);
-        ShapeData?.RasterizeTransformed(surface.Canvas);
+        RenderableShapeData?.RasterizeTransformed(surface.Canvas);
 
 
         surface.Canvas.RestoreToCount(layer);
         surface.Canvas.RestoreToCount(layer);
     }
     }
@@ -184,7 +207,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
     {
     {
         return new VectorLayerNode()
         return new VectorLayerNode()
         {
         {
-            ShapeData = (ShapeVectorData?)ShapeData?.Clone(),
+            EmbeddedShapeData = (ShapeVectorData?)EmbeddedShapeData?.Clone(),
             ClipToPreviousMember = this.ClipToPreviousMember,
             ClipToPreviousMember = this.ClipToPreviousMember,
             EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted(),
             EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted(),
             AllowHighDpiRendering = this.AllowHighDpiRendering
             AllowHighDpiRendering = this.AllowHighDpiRendering
@@ -193,19 +216,19 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
 
 
     public void Resize(VecD multiplier)
     public void Resize(VecD multiplier)
     {
     {
-        if (ShapeData == null)
+        if (EmbeddedShapeData == null)
         {
         {
             return;
             return;
         }
         }
 
 
-        if(ShapeData is IScalable resizable)
+        if(EmbeddedShapeData is IScalable resizable)
         {
         {
             resizable.Resize(multiplier);
             resizable.Resize(multiplier);
         }
         }
         else
         else
         {
         {
-            ShapeData.TransformationMatrix =
-                ShapeData.TransformationMatrix.PostConcat(Matrix3X3.CreateScale((float)multiplier.X,
+            EmbeddedShapeData.TransformationMatrix =
+                EmbeddedShapeData.TransformationMatrix.PostConcat(Matrix3X3.CreateScale((float)multiplier.X,
                     (float)multiplier.Y));
                     (float)multiplier.Y));
         }
         }
     }
     }

+ 98 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs

@@ -0,0 +1,98 @@
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
+
+[NodeInfo("CustomOutput")]
+public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
+{
+    public const string OutputNamePropertyName = "OutputName";
+    public const string IsDefaultExportPropertyName = "IsDefaultExport";
+    public const string SizePropertyName = "Size";
+    public RenderInputProperty Input { get; }
+    public InputProperty<string> OutputName { get; }
+    public InputProperty<bool> IsDefaultExport { get; }
+    public InputProperty<VecI> Size { get; }
+
+    private VecI? lastDocumentSize;
+
+    private TextureCache textureCache = new TextureCache();
+
+    public CustomOutputNode()
+    {
+        Input = new RenderInputProperty(this, OutputNode.InputPropertyName, "BACKGROUND", null);
+        AddInputProperty(Input);
+
+        OutputName = CreateInput(OutputNamePropertyName, "OUTPUT_NAME", "");
+        IsDefaultExport = CreateInput(IsDefaultExportPropertyName, "IS_DEFAULT_EXPORT", false);
+        Size = CreateInput(SizePropertyName, "SIZE", VecI.Zero);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new CustomOutputNode();
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        if (context.TargetOutput == OutputName.Value)
+        {
+            VecI targetSize = Size.Value.ShortestAxis <= 0
+                ? context.RenderOutputSize
+                : Size.Value;
+
+            lastDocumentSize = targetSize;
+
+            DrawingSurface targetSurface = context.RenderSurface;
+
+            if(context.RenderOutputSize != targetSize)
+            {
+                targetSurface = textureCache.RequestTexture(0, targetSize, context.ProcessingColorSpace).DrawingSurface;
+            }
+
+            int saved = targetSurface.Canvas.Save();
+            targetSurface.Canvas.ClipRect(new RectD(0, 0, targetSize.X, targetSize.Y));
+
+            RenderContext outputContext = new RenderContext(context.RenderSurface, context.FrameTime, context.ChunkResolution,
+                targetSize, context.DocumentSize, context.ProcessingColorSpace, context.Opacity) { TargetOutput = OutputName.Value, };
+
+            Input.Value?.Paint(outputContext, targetSurface);
+
+            targetSurface.Canvas.RestoreToCount(saved);
+
+            if (targetSurface != context.RenderSurface)
+            {
+                context.RenderSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+            }
+        }
+    }
+
+    RenderInputProperty IRenderInput.Background => Input;
+
+    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        if (lastDocumentSize == null)
+        {
+            return null;
+        }
+
+        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y);
+    }
+
+    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        if (Input.Value == null)
+        {
+            return false;
+        }
+
+        int saved = renderOn.Canvas.Save();
+        Input.Value.Paint(context, renderOn);
+
+        renderOn.Canvas.RestoreToCount(saved);
+
+        return true;
+    }
+}

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

@@ -187,7 +187,7 @@ internal class CombineStructureMembersOnto_Change : Change
         if (targetLayer is not VectorLayerNode vectorLayer)
         if (targetLayer is not VectorLayerNode vectorLayer)
             throw new InvalidOperationException("Target layer is not a vector layer");
             throw new InvalidOperationException("Target layer is not a vector layer");
 
 
-        ShapeVectorData targetData = vectorLayer.ShapeData ?? null;
+        ShapeVectorData targetData = vectorLayer.EmbeddedShapeData ?? null;
         VectorPath? targetPath = targetData?.ToPath();
         VectorPath? targetPath = targetData?.ToPath();
 
 
         var reversed = toCombine.Reverse().ToHashSet();
         var reversed = toCombine.Reverse().ToHashSet();
@@ -197,16 +197,16 @@ internal class CombineStructureMembersOnto_Change : Change
             if (target.FindMember(guid) is not VectorLayerNode vectorNode)
             if (target.FindMember(guid) is not VectorLayerNode vectorNode)
                 continue;
                 continue;
 
 
-            if (vectorNode.ShapeData == null)
+            if (vectorNode.EmbeddedShapeData == null)
                 continue;
                 continue;
 
 
-            VectorPath path = vectorNode.ShapeData.ToPath();
+            VectorPath path = vectorNode.EmbeddedShapeData.ToPath();
 
 
             if (targetData == null)
             if (targetData == null)
             {
             {
-                targetData = vectorNode.ShapeData;
+                targetData = vectorNode.EmbeddedShapeData;
                 targetPath = new VectorPath();
                 targetPath = new VectorPath();
-                targetPath.AddPath(path, vectorNode.ShapeData.TransformationMatrix, AddPathMode.Append);
+                targetPath.AddPath(path, vectorNode.EmbeddedShapeData.TransformationMatrix, AddPathMode.Append);
 
 
                 if (originalPaths.ContainsKey(frame))
                 if (originalPaths.ContainsKey(frame))
                     originalPaths[frame].Dispose();
                     originalPaths[frame].Dispose();
@@ -215,7 +215,7 @@ internal class CombineStructureMembersOnto_Change : Change
             }
             }
             else
             else
             {
             {
-                targetPath.AddPath(path, vectorNode.ShapeData.TransformationMatrix, AddPathMode.Append);
+                targetPath.AddPath(path, vectorNode.EmbeddedShapeData.TransformationMatrix, AddPathMode.Append);
                 path.Dispose();
                 path.Dispose();
             }
             }
         }
         }
@@ -241,7 +241,7 @@ internal class CombineStructureMembersOnto_Change : Change
             data.Path = targetPath;
             data.Path = targetPath;
         }
         }
 
 
-        vectorLayer.ShapeData = data;
+        vectorLayer.EmbeddedShapeData = data;
 
 
         return new AffectedArea(new HashSet<VecI>());
         return new AffectedArea(new HashSet<VecI>());
     }
     }
@@ -383,7 +383,7 @@ internal class CombineStructureMembersOnto_Change : Change
         if (!originalPaths.TryGetValue(frame, out var path))
         if (!originalPaths.TryGetValue(frame, out var path))
             throw new InvalidOperationException("Original path not found");
             throw new InvalidOperationException("Original path not found");
 
 
-        targetLayer.ShapeData = new PathVectorData(path);
+        targetLayer.EmbeddedShapeData = new PathVectorData(path);
         return new VectorShape_ChangeInfo(targetLayer.Id, new AffectedArea(new HashSet<VecI>()));
         return new VectorShape_ChangeInfo(targetLayer.Id, new AffectedArea(new HashSet<VecI>()));
     }
     }
 
 

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

@@ -6,7 +6,7 @@ using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 
-internal class DrawRasterLine_UpdateableChange : UpdateableChange
+internal class DrawRasterLine_UpdateableChange : InterruptableUpdateableChange
 {
 {
     private readonly Guid memberGuid;
     private readonly Guid memberGuid;
     private VecD from;
     private VecD from;

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs

@@ -47,8 +47,8 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
 
 
             if (layer is VectorLayerNode transformableObject)
             if (layer is VectorLayerNode transformableObject)
             {
             {
-                originalShapes[layerGuid] = transformableObject.ShapeData;
-                transformableObject.ShapeData = null;
+                originalShapes[layerGuid] = transformableObject.EmbeddedShapeData;
+                transformableObject.EmbeddedShapeData = null;
             }
             }
         }
         }
 
 
@@ -78,7 +78,7 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                 StrokeJoin join = StrokeJoin.Miter;
                 StrokeJoin join = StrokeJoin.Miter;
                 StrokeCap cap = StrokeCap.Butt;
                 StrokeCap cap = StrokeCap.Butt;
                 
                 
-                (vectorLayer.ShapeData as PathVectorData)?.Path.Dispose();
+                (vectorLayer.EmbeddedShapeData as PathVectorData)?.Path.Dispose();
 
 
                 var originalShape = originalShapes[layerGuid];
                 var originalShape = originalShapes[layerGuid];
 
 
@@ -107,7 +107,7 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                     StrokeLineCap = cap
                     StrokeLineCap = cap
                 };
                 };
                 
                 
-                vectorLayer.ShapeData = newShapeData;
+                vectorLayer.EmbeddedShapeData = newShapeData;
                 changes.Add(new VectorShape_ChangeInfo(layerGuid, ShiftLayer_UpdateableChange.AffectedAreaFromBounds(target, layerGuid, frame)));
                 changes.Add(new VectorShape_ChangeInfo(layerGuid, ShiftLayer_UpdateableChange.AffectedAreaFromBounds(target, layerGuid, frame)));
             }
             }
         }
         }
@@ -143,8 +143,8 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
             }
             }
             else if (layer is VectorLayerNode transformableObject)
             else if (layer is VectorLayerNode transformableObject)
             {
             {
-                (transformableObject.ShapeData as PathVectorData)?.Path.Dispose();
-                transformableObject.ShapeData = originalShapes[layerGuid];
+                (transformableObject.EmbeddedShapeData as PathVectorData)?.Path.Dispose();
+                transformableObject.EmbeddedShapeData = originalShapes[layerGuid];
             }
             }
         }
         }
 
 

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

@@ -85,7 +85,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
 
 
         if (memberData.Count == 1 && firstLayer is VectorLayerNode vectorLayer)
         if (memberData.Count == 1 && firstLayer is VectorLayerNode vectorLayer)
         {
         {
-            tightBounds = vectorLayer.ShapeData?.GeometryAABB ?? default;
+            tightBounds = vectorLayer.EmbeddedShapeData?.GeometryAABB ?? default;
         }
         }
 
 
         for (var i = 1; i < memberData.Count; i++)
         for (var i = 1; i < memberData.Count; i++)

+ 49 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/EvaluateGraph_Change.cs

@@ -0,0 +1,49 @@
+using Drawie.Backend.Core;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class EvaluateGraph_Change : Change
+{
+    private readonly Guid endNodeGuid;
+    private readonly KeyFrameTime frameTime;
+
+    [GenerateMakeChangeAction]
+    public EvaluateGraph_Change(Guid endNodeGuid, KeyFrameTime frameTime)
+    {
+        this.endNodeGuid = endNodeGuid;
+        this.frameTime = frameTime;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        return target.HasNode(endNodeGuid);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        ignoreInUndo = true;
+
+        var node = target.FindNode(endNodeGuid);
+        var queue = GraphUtils.CalculateExecutionQueue(node);
+
+        using Texture renderTexture = Texture.ForProcessing(target.Size, target.ProcessingColorSpace);
+        RenderContext context =
+            new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, target.Size, target.Size,
+                target.ProcessingColorSpace) { FullRerender = true };
+        foreach (var nodeToEvaluate in queue)
+        {
+            nodeToEvaluate.Execute(context);
+        }
+
+        return new None();
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        return new None();
+    }
+}

+ 80 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/GetComputedPropertyValue_Change.cs

@@ -0,0 +1,80 @@
+using Drawie.Backend.Core.Shaders.Generation;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class GetComputedPropertyValue_Change : Change
+{
+    private readonly Guid nodeId;
+    private readonly string propertyName;
+    private readonly bool isInput;
+
+    [GenerateMakeChangeAction]
+    public GetComputedPropertyValue_Change(Guid nodeId, string propertyName, bool isInput)
+    {
+        this.nodeId = nodeId;
+        this.propertyName = propertyName;
+        this.isInput = isInput;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        var foundNode = target.FindNode(nodeId);
+        if (foundNode == null)
+        {
+            return false;
+        }
+
+        if (isInput)
+        {
+            return foundNode.InputProperties.Any(x => x.InternalPropertyName == propertyName);
+        }
+
+        return foundNode.OutputProperties.Any(x => x.InternalPropertyName == propertyName);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        var node = target.FindNode(nodeId);
+        ignoreInUndo = true;
+
+        if (node == null)
+        {
+            return new None();
+        }
+
+        object value;
+        if (isInput)
+        {
+            value = node.GetInputProperty(propertyName).Value;
+        }
+        else
+        {
+            value = node.GetOutputProperty(propertyName).Value;
+        }
+        if (value is Delegate del)
+        {
+            try
+            {
+                value = del.DynamicInvoke(FuncContext.NoContext);
+            }
+            catch (Exception e)
+            {
+                return new None();
+            }
+        }
+        if(value is ShaderExpressionVariable variable)
+        {
+            value = variable.GetConstant();
+        }
+
+        return new ComputedPropertyValue_ChangeInfo(nodeId, propertyName, isInput, value);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        return new None();
+    }
+}

+ 2 - 4
src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs

@@ -31,8 +31,7 @@ internal class DeleteStructureMember_Change : Change
 
 
         originalConnections = NodeOperations.CreateConnectionsData(member);
         originalConnections = NodeOperations.CreateConnectionsData(member);
 
 
-        savedCopy = (StructureNode)member.Clone();
-        savedCopy.Id = memberGuid;
+        savedCopy = (StructureNode)member.Clone(true);
 
 
         savedKeyFrameGroup = DeleteNode_Change.CloneGroupKeyFrame(document, memberGuid);
         savedKeyFrameGroup = DeleteNode_Change.CloneGroupKeyFrame(document, memberGuid);
 
 
@@ -78,8 +77,7 @@ internal class DeleteStructureMember_Change : Change
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document doc)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document doc)
     {
     {
-        var copy = (StructureNode)savedCopy!.Clone();
-        copy.Id = memberGuid;
+        var copy = (StructureNode)savedCopy!.Clone(true);
 
 
         doc.NodeGraph.AddNode(copy);
         doc.NodeGraph.AddNode(copy);
 
 

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changes/Vectors/ConvertToCurve_Change.cs

@@ -24,7 +24,7 @@ internal class ConvertToCurve_Change : Change
     {
     {
         if (target.TryFindNode(memberId, out VectorLayerNode? node))
         if (target.TryFindNode(memberId, out VectorLayerNode? node))
         {
         {
-            return node.ShapeData != null && node.ShapeData is not PathVectorData;
+            return node.EmbeddedShapeData != null && node.EmbeddedShapeData is not PathVectorData;
         }
         }
 
 
         return false;
         return false;
@@ -34,10 +34,10 @@ internal class ConvertToCurve_Change : Change
         out bool ignoreInUndo)
         out bool ignoreInUndo)
     {
     {
         VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
         VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
-        originalData = node.ShapeData;
+        originalData = node.EmbeddedShapeData;
 
 
         // TODO: Stroke Line cap and join is missing? Validate
         // TODO: Stroke Line cap and join is missing? Validate
-        node.ShapeData = new PathVectorData(originalData.ToPath())
+        node.EmbeddedShapeData = new PathVectorData(originalData.ToPath())
         {
         {
             Fill = originalData.Fill,
             Fill = originalData.Fill,
             FillPaintable = originalData.FillPaintable,
             FillPaintable = originalData.FillPaintable,
@@ -51,7 +51,7 @@ internal class ConvertToCurve_Change : Change
 
 
         ignoreInUndo = false;
         ignoreInUndo = false;
 
 
-        var aabb = node.ShapeData.TransformedVisualAABB;
+        var aabb = node.EmbeddedShapeData.TransformedVisualAABB;
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
             (RectI)aabb, ChunkyImage.FullChunkSize));
             (RectI)aabb, ChunkyImage.FullChunkSize));
 
 
@@ -61,11 +61,11 @@ internal class ConvertToCurve_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
         VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
-        node.ShapeData = originalData;
+        node.EmbeddedShapeData = originalData;
 
 
         node.AllowHighDpiRendering = originalHighDpiRendering;
         node.AllowHighDpiRendering = originalHighDpiRendering;
 
 
-        var aabb = node.ShapeData.TransformedVisualAABB;
+        var aabb = node.EmbeddedShapeData.TransformedVisualAABB;
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
             (RectI)aabb, ChunkyImage.FullChunkSize));
             (RectI)aabb, ChunkyImage.FullChunkSize));
 
 

+ 9 - 9
src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs

@@ -28,12 +28,12 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
     {
     {
         if (target.TryFindNode<VectorLayerNode>(TargetId, out var node))
         if (target.TryFindNode<VectorLayerNode>(TargetId, out var node))
         {
         {
-            if (IsIdentical(node.ShapeData, Data))
+            if (IsIdentical(node.EmbeddedShapeData, Data))
             {
             {
                 return false;
                 return false;
             }
             }
 
 
-            originalData = (ShapeVectorData?)node.ShapeData?.Clone();
+            originalData = (ShapeVectorData?)node.EmbeddedShapeData?.Clone();
             return true;
             return true;
         }
         }
 
 
@@ -50,9 +50,9 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
     {
     {
         var node = target.FindNode<VectorLayerNode>(TargetId);
         var node = target.FindNode<VectorLayerNode>(TargetId);
 
 
-        node.ShapeData = Data;
+        node.EmbeddedShapeData = Data;
 
 
-        RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
+        RectD aabb = node.EmbeddedShapeData.TransformedAABB.RoundOutwards();
         aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
         aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
 
 
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
@@ -80,9 +80,9 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
             return new None();
             return new None();
         }
         }
 
 
-        node.ShapeData = Data;
+        node.EmbeddedShapeData = Data;
 
 
-        RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
+        RectD aabb = node.EmbeddedShapeData.TransformedAABB.RoundOutwards();
         aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
         aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
 
 
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
         var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
@@ -94,13 +94,13 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var node = target.FindNode<VectorLayerNode>(TargetId);
         var node = target.FindNode<VectorLayerNode>(TargetId);
-        node.ShapeData = originalData;
+        node.EmbeddedShapeData = originalData;
 
 
         AffectedArea affected = new AffectedArea();
         AffectedArea affected = new AffectedArea();
 
 
-        if (node.ShapeData != null)
+        if (node.EmbeddedShapeData != null)
         {
         {
-            RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
+            RectD aabb = node.EmbeddedShapeData.TransformedAABB.RoundOutwards();
             aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
             aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
 
 
             affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
             affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(

+ 48 - 7
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -5,11 +5,14 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Text;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
 
 
 namespace PixiEditor.ChangeableDocument.Rendering;
 namespace PixiEditor.ChangeableDocument.Rendering;
 
 
@@ -63,7 +66,7 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
 
 
-        RenderContext context = new(renderTexture.DrawingSurface, frame, resolution, Document.Size,
+        RenderContext context = new(renderTexture.DrawingSurface, frame, resolution, Document.Size, Document.Size,
             Document.ProcessingColorSpace);
             Document.ProcessingColorSpace);
         context.FullRerender = true;
         context.FullRerender = true;
         IReadOnlyNodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
         IReadOnlyNodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
@@ -109,7 +112,7 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
 
 
-        RenderContext context = new(renderTexture.DrawingSurface, frameTime, resolution, Document.Size,
+        RenderContext context = new(renderTexture.DrawingSurface, frameTime, resolution, Document.Size, Document.Size,
             Document.ProcessingColorSpace);
             Document.ProcessingColorSpace);
         context.FullRerender = true;
         context.FullRerender = true;
 
 
@@ -223,9 +226,6 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.Save();
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
         toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
 
 
-        RenderContext context =
-            new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, Document.Size,
-                Document.ProcessingColorSpace) { FullRerender = true };
 
 
         bool hasCustomOutput = !string.IsNullOrEmpty(customOutput) && customOutput != "DEFAULT";
         bool hasCustomOutput = !string.IsNullOrEmpty(customOutput) && customOutput != "DEFAULT";
 
 
@@ -233,14 +233,31 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
             ? RenderingUtils.SolveFinalNodeGraph(customOutput, Document)
             ? RenderingUtils.SolveFinalNodeGraph(customOutput, Document)
             : Document.NodeGraph;
             : Document.NodeGraph;
 
 
+        RenderContext context =
+            new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, SolveRenderOutputSize(customOutput, graph, Document.Size),
+                Document.Size, Document.ProcessingColorSpace) { FullRerender = true };
+
         if (hasCustomOutput)
         if (hasCustomOutput)
         {
         {
             context.TargetOutput = customOutput;
             context.TargetOutput = customOutput;
         }
         }
 
 
-        graph.Execute(context);
+        try
+        {
+            graph.Execute(context);
+            toRenderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
+        }
+        catch (Exception e)
+        {
+            renderTexture.DrawingSurface.Canvas.Clear();
+            using Paint paint = new Paint { Color = Colors.White, IsAntiAliased = true };
+
+            using Font defaultSizedFont = Font.CreateDefault();
+            defaultSizedFont.Size = 24;
 
 
-        toRenderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
+            renderTexture.DrawingSurface.Canvas.DrawText("Graph Setup produced an error. Fix it the node graph",
+                renderTexture.Size / 2f, TextAlign.Center, defaultSizedFont, paint);
+        }
 
 
         renderTexture.DrawingSurface.Canvas.Restore();
         renderTexture.DrawingSurface.Canvas.Restore();
         toRenderOn.Canvas.Restore();
         toRenderOn.Canvas.Restore();
@@ -319,6 +336,30 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         return found ?? (membersOnlyGraph.OutputNode as IRenderInput)?.Background;
         return found ?? (membersOnlyGraph.OutputNode as IRenderInput)?.Background;
     }
     }
 
 
+    private static VecI SolveRenderOutputSize(string? targetOutput, IReadOnlyNodeGraph finalGraph, VecI documentSize)
+    {
+        VecI finalSize = documentSize;
+        if (targetOutput != null)
+        {
+            var outputNode = finalGraph.AllNodes.FirstOrDefault(n =>
+                n is CustomOutputNode outputNode && outputNode.OutputName.Value == targetOutput);
+
+            if (outputNode is CustomOutputNode customOutputNode)
+            {
+                if (customOutputNode.Size.Value.ShortestAxis > 0)
+                {
+                    finalSize = customOutputNode.Size.Value;
+                }
+            }
+            else
+            {
+                finalSize = documentSize;
+            }
+        }
+
+        return finalSize;
+    }
+
     public void Dispose()
     public void Dispose()
     {
     {
         renderTexture?.Dispose();
         renderTexture?.Dispose();

+ 5 - 3
src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs

@@ -13,8 +13,9 @@ public class RenderContext
 
 
     public KeyFrameTime FrameTime { get; }
     public KeyFrameTime FrameTime { get; }
     public ChunkResolution ChunkResolution { get; }
     public ChunkResolution ChunkResolution { get; }
+    public VecI RenderOutputSize { get; set; }
+
     public VecI DocumentSize { get; set; }
     public VecI DocumentSize { get; set; }
-    
     public DrawingSurface RenderSurface { get; set; }
     public DrawingSurface RenderSurface { get; set; }
     public bool FullRerender { get; set; } = false;
     public bool FullRerender { get; set; } = false;
     
     
@@ -23,14 +24,15 @@ public class RenderContext
 
 
 
 
     public RenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution,
     public RenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution,
-        VecI docSize, ColorSpace processingColorSpace, double opacity = 1) 
+        VecI renderOutputSize, VecI documentSize, ColorSpace processingColorSpace, double opacity = 1)
     {
     {
         RenderSurface = renderSurface;
         RenderSurface = renderSurface;
         FrameTime = frameTime;
         FrameTime = frameTime;
         ChunkResolution = chunkResolution;
         ChunkResolution = chunkResolution;
-        DocumentSize = docSize;
+        RenderOutputSize = renderOutputSize;
         Opacity = opacity;
         Opacity = opacity;
         ProcessingColorSpace = processingColorSpace;
         ProcessingColorSpace = processingColorSpace;
+        DocumentSize = documentSize;
     }
     }
 
 
     public static DrawingApiBlendMode GetDrawingBlendMode(BlendMode blendMode)
     public static DrawingApiBlendMode GetDrawingBlendMode(BlendMode blendMode)

+ 2 - 1
src/PixiEditor.ChangeableDocument/Rendering/RenderingUtils.cs

@@ -1,6 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 
 namespace PixiEditor.ChangeableDocument.Rendering;
 namespace PixiEditor.ChangeableDocument.Rendering;
@@ -9,7 +10,7 @@ public static class RenderingUtils
 {
 {
     public static IReadOnlyNodeGraph SolveFinalNodeGraph(string? targetOutput, IReadOnlyDocument document)
     public static IReadOnlyNodeGraph SolveFinalNodeGraph(string? targetOutput, IReadOnlyDocument document)
     {
     {
-        if (targetOutput == null || targetOutput == "DEFAULT")
+        if (targetOutput is null or "DEFAULT")
         {
         {
             return document.NodeGraph;
             return document.NodeGraph;
         }
         }

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

@@ -1,6 +1,7 @@
 using System;
 using System;
 using Avalonia;
 using Avalonia;
 using Avalonia.Logging;
 using Avalonia.Logging;
+using Drawie.Interop.Avalonia;
 using Drawie.Interop.VulkanAvalonia;
 using Drawie.Interop.VulkanAvalonia;
 
 
 namespace PixiEditor.Desktop;
 namespace PixiEditor.Desktop;

+ 14 - 0
src/PixiEditor.Extensions.CommonApi/Commands/CommandMetadata.Impl.cs

@@ -0,0 +1,14 @@
+namespace PixiEditor.Extensions.CommonApi.Commands;
+
+public partial class CommandMetadata
+{
+    public CommandMetadata()
+    {
+
+    }
+
+    public CommandMetadata(string uniqueName)
+    {
+        UniqueName = uniqueName;
+    }
+}

+ 9 - 0
src/PixiEditor.Extensions.CommonApi/Commands/ICommandProvider.cs

@@ -0,0 +1,9 @@
+namespace PixiEditor.Extensions.CommonApi.Commands;
+
+public interface ICommandProvider
+{
+    public void RegisterCommand(CommandMetadata command, Action execute, Func<bool>? canExecute = null);
+    public void InvokeCommand(string commandName);
+    public void InvokeCommand(string commandName, object? parameter);
+    public bool CommandExists(string commandName);
+}

+ 1144 - 0
src/PixiEditor.Extensions.CommonApi/Commands/Shortcut.Impl.cs

@@ -0,0 +1,1144 @@
+namespace PixiEditor.Extensions.CommonApi.Commands;
+
+public partial class Shortcut
+{
+    public Shortcut()
+    {
+    }
+
+    public Shortcut(Key key, KeyModifiers modifiers)
+    {
+        Key = (int)key;
+        Modifiers = (int)modifiers;
+    }
+}
+
+[Flags]
+public enum KeyModifiers
+{
+    None = 0,
+    Alt = 1,
+    Control = 2,
+    Shift = 4,
+    System = 8,
+}
+
+// Avalonia Key.cs
+public enum Key
+{
+    /// <summary>
+    /// No key pressed.
+    /// </summary>
+    None = 0,
+
+    /// <summary>
+    /// The Cancel key.
+    /// </summary>
+    Cancel = 1,
+
+    /// <summary>
+    /// The Back key.
+    /// </summary>
+    Back = 2,
+
+    /// <summary>
+    /// The Tab key.
+    /// </summary>
+    Tab = 3,
+
+    /// <summary>
+    /// The Linefeed key.
+    /// </summary>
+    LineFeed = 4,
+
+    /// <summary>
+    /// The Clear key.
+    /// </summary>
+    Clear = 5,
+
+    /// <summary>
+    /// The Return key.
+    /// </summary>
+    Return = 6,
+
+    /// <summary>
+    /// The Enter key.
+    /// </summary>
+    Enter = 6,
+
+    /// <summary>
+    /// The Pause key.
+    /// </summary>
+    Pause = 7,
+
+    /// <summary>
+    /// The Caps Lock key.
+    /// </summary>
+    CapsLock = 8,
+
+    /// <summary>
+    /// The Caps Lock key.
+    /// </summary>
+    Capital = 8,
+
+    /// <summary>
+    /// The IME Hangul mode key.
+    /// </summary>
+    HangulMode = 9,
+
+    /// <summary>
+    /// The IME Kana mode key.
+    /// </summary>
+    KanaMode = 9,
+
+    /// <summary>
+    /// The IME Junja mode key.
+    /// </summary>
+    JunjaMode = 10,
+
+    /// <summary>
+    /// The IME Final mode key.
+    /// </summary>
+    FinalMode = 11,
+
+    /// <summary>
+    /// The IME Kanji mode key.
+    /// </summary>
+    KanjiMode = 12,
+
+    /// <summary>
+    /// The IME Hanja mode key.
+    /// </summary>
+    HanjaMode = 12,
+
+    /// <summary>
+    /// The Escape key.
+    /// </summary>
+    Escape = 13,
+
+    /// <summary>
+    /// The IME Convert key.
+    /// </summary>
+    ImeConvert = 14,
+
+    /// <summary>
+    /// The IME NonConvert key.
+    /// </summary>
+    ImeNonConvert = 15,
+
+    /// <summary>
+    /// The IME Accept key.
+    /// </summary>
+    ImeAccept = 16,
+
+    /// <summary>
+    /// The IME Mode change key.
+    /// </summary>
+    ImeModeChange = 17,
+
+    /// <summary>
+    /// The space bar.
+    /// </summary>
+    Space = 18,
+
+    /// <summary>
+    /// The Page Up key.
+    /// </summary>
+    PageUp = 19,
+
+    /// <summary>
+    /// The Page Up key.
+    /// </summary>
+    Prior = 19,
+
+    /// <summary>
+    /// The Page Down key.
+    /// </summary>
+    PageDown = 20,
+
+    /// <summary>
+    /// The Page Down key.
+    /// </summary>
+    Next = 20,
+
+    /// <summary>
+    /// The End key.
+    /// </summary>
+    End = 21,
+
+    /// <summary>
+    /// The Home key.
+    /// </summary>
+    Home = 22,
+
+    /// <summary>
+    /// The Left arrow key.
+    /// </summary>
+    Left = 23,
+
+    /// <summary>
+    /// The Up arrow key.
+    /// </summary>
+    Up = 24,
+
+    /// <summary>
+    /// The Right arrow key.
+    /// </summary>
+    Right = 25,
+
+    /// <summary>
+    /// The Down arrow key.
+    /// </summary>
+    Down = 26,
+
+    /// <summary>
+    /// The Select key.
+    /// </summary>
+    Select = 27,
+
+    /// <summary>
+    /// The Print key.
+    /// </summary>
+    Print = 28,
+
+    /// <summary>
+    /// The Execute key.
+    /// </summary>
+    Execute = 29,
+
+    /// <summary>
+    /// The Print Screen key.
+    /// </summary>
+    Snapshot = 30,
+
+    /// <summary>
+    /// The Print Screen key.
+    /// </summary>
+    PrintScreen = 30,
+
+    /// <summary>
+    /// The Insert key.
+    /// </summary>
+    Insert = 31,
+
+    /// <summary>
+    /// The Delete key.
+    /// </summary>
+    Delete = 32,
+
+    /// <summary>
+    /// The Help key.
+    /// </summary>
+    Help = 33,
+
+    /// <summary>
+    /// The 0 key.
+    /// </summary>
+    D0 = 34,
+
+    /// <summary>
+    /// The 1 key.
+    /// </summary>
+    D1 = 35,
+
+    /// <summary>
+    /// The 2 key.
+    /// </summary>
+    D2 = 36,
+
+    /// <summary>
+    /// The 3 key.
+    /// </summary>
+    D3 = 37,
+
+    /// <summary>
+    /// The 4 key.
+    /// </summary>
+    D4 = 38,
+
+    /// <summary>
+    /// The 5 key.
+    /// </summary>
+    D5 = 39,
+
+    /// <summary>
+    /// The 6 key.
+    /// </summary>
+    D6 = 40,
+
+    /// <summary>
+    /// The 7 key.
+    /// </summary>
+    D7 = 41,
+
+    /// <summary>
+    /// The 8 key.
+    /// </summary>
+    D8 = 42,
+
+    /// <summary>
+    /// The 9 key.
+    /// </summary>
+    D9 = 43,
+
+    /// <summary>
+    /// The A key.
+    /// </summary>
+    A = 44,
+
+    /// <summary>
+    /// The B key.
+    /// </summary>
+    B = 45,
+
+    /// <summary>
+    /// The C key.
+    /// </summary>
+    C = 46,
+
+    /// <summary>
+    /// The D key.
+    /// </summary>
+    D = 47,
+
+    /// <summary>
+    /// The E key.
+    /// </summary>
+    E = 48,
+
+    /// <summary>
+    /// The F key.
+    /// </summary>
+    F = 49,
+
+    /// <summary>
+    /// The G key.
+    /// </summary>
+    G = 50,
+
+    /// <summary>
+    /// The H key.
+    /// </summary>
+    H = 51,
+
+    /// <summary>
+    /// The I key.
+    /// </summary>
+    I = 52,
+
+    /// <summary>
+    /// The J key.
+    /// </summary>
+    J = 53,
+
+    /// <summary>
+    /// The K key.
+    /// </summary>
+    K = 54,
+
+    /// <summary>
+    /// The L key.
+    /// </summary>
+    L = 55,
+
+    /// <summary>
+    /// The M key.
+    /// </summary>
+    M = 56,
+
+    /// <summary>
+    /// The N key.
+    /// </summary>
+    N = 57,
+
+    /// <summary>
+    /// The O key.
+    /// </summary>
+    O = 58,
+
+    /// <summary>
+    /// The P key.
+    /// </summary>
+    P = 59,
+
+    /// <summary>
+    /// The Q key.
+    /// </summary>
+    Q = 60,
+
+    /// <summary>
+    /// The R key.
+    /// </summary>
+    R = 61,
+
+    /// <summary>
+    /// The S key.
+    /// </summary>
+    S = 62,
+
+    /// <summary>
+    /// The T key.
+    /// </summary>
+    T = 63,
+
+    /// <summary>
+    /// The U key.
+    /// </summary>
+    U = 64,
+
+    /// <summary>
+    /// The V key.
+    /// </summary>
+    V = 65,
+
+    /// <summary>
+    /// The W key.
+    /// </summary>
+    W = 66,
+
+    /// <summary>
+    /// The X key.
+    /// </summary>
+    X = 67,
+
+    /// <summary>
+    /// The Y key.
+    /// </summary>
+    Y = 68,
+
+    /// <summary>
+    /// The Z key.
+    /// </summary>
+    Z = 69,
+
+    /// <summary>
+    /// The left Windows key.
+    /// </summary>
+    LWin = 70,
+
+    /// <summary>
+    /// The right Windows key.
+    /// </summary>
+    RWin = 71,
+
+    /// <summary>
+    /// The Application key.
+    /// </summary>
+    Apps = 72,
+
+    /// <summary>
+    /// The Sleep key.
+    /// </summary>
+    Sleep = 73,
+
+    /// <summary>
+    /// The 0 key on the numeric keypad.
+    /// </summary>
+    NumPad0 = 74,
+
+    /// <summary>
+    /// The 1 key on the numeric keypad.
+    /// </summary>
+    NumPad1 = 75,
+
+    /// <summary>
+    /// The 2 key on the numeric keypad.
+    /// </summary>
+    NumPad2 = 76,
+
+    /// <summary>
+    /// The 3 key on the numeric keypad.
+    /// </summary>
+    NumPad3 = 77,
+
+    /// <summary>
+    /// The 4 key on the numeric keypad.
+    /// </summary>
+    NumPad4 = 78,
+
+    /// <summary>
+    /// The 5 key on the numeric keypad.
+    /// </summary>
+    NumPad5 = 79,
+
+    /// <summary>
+    /// The 6 key on the numeric keypad.
+    /// </summary>
+    NumPad6 = 80,
+
+    /// <summary>
+    /// The 7 key on the numeric keypad.
+    /// </summary>
+    NumPad7 = 81,
+
+    /// <summary>
+    /// The 8 key on the numeric keypad.
+    /// </summary>
+    NumPad8 = 82,
+
+    /// <summary>
+    /// The 9 key on the numeric keypad.
+    /// </summary>
+    NumPad9 = 83,
+
+    /// <summary>
+    /// The Multiply key.
+    /// </summary>
+    Multiply = 84,
+
+    /// <summary>
+    /// The Add key.
+    /// </summary>
+    Add = 85,
+
+    /// <summary>
+    /// The Separator key.
+    /// </summary>
+    Separator = 86,
+
+    /// <summary>
+    /// The Subtract key.
+    /// </summary>
+    Subtract = 87,
+
+    /// <summary>
+    /// The Decimal key.
+    /// </summary>
+    Decimal = 88,
+
+    /// <summary>
+    /// The Divide key.
+    /// </summary>
+    Divide = 89,
+
+    /// <summary>
+    /// The F1 key.
+    /// </summary>
+    F1 = 90,
+
+    /// <summary>
+    /// The F2 key.
+    /// </summary>
+    F2 = 91,
+
+    /// <summary>
+    /// The F3 key.
+    /// </summary>
+    F3 = 92,
+
+    /// <summary>
+    /// The F4 key.
+    /// </summary>
+    F4 = 93,
+
+    /// <summary>
+    /// The F5 key.
+    /// </summary>
+    F5 = 94,
+
+    /// <summary>
+    /// The F6 key.
+    /// </summary>
+    F6 = 95,
+
+    /// <summary>
+    /// The F7 key.
+    /// </summary>
+    F7 = 96,
+
+    /// <summary>
+    /// The F8 key.
+    /// </summary>
+    F8 = 97,
+
+    /// <summary>
+    /// The F9 key.
+    /// </summary>
+    F9 = 98,
+
+    /// <summary>
+    /// The F10 key.
+    /// </summary>
+    F10 = 99,
+
+    /// <summary>
+    /// The F11 key.
+    /// </summary>
+    F11 = 100,
+
+    /// <summary>
+    /// The F12 key.
+    /// </summary>
+    F12 = 101,
+
+    /// <summary>
+    /// The F13 key.
+    /// </summary>
+    F13 = 102,
+
+    /// <summary>
+    /// The F14 key.
+    /// </summary>
+    F14 = 103,
+
+    /// <summary>
+    /// The F15 key.
+    /// </summary>
+    F15 = 104,
+
+    /// <summary>
+    /// The F16 key.
+    /// </summary>
+    F16 = 105,
+
+    /// <summary>
+    /// The F17 key.
+    /// </summary>
+    F17 = 106,
+
+    /// <summary>
+    /// The F18 key.
+    /// </summary>
+    F18 = 107,
+
+    /// <summary>
+    /// The F19 key.
+    /// </summary>
+    F19 = 108,
+
+    /// <summary>
+    /// The F20 key.
+    /// </summary>
+    F20 = 109,
+
+    /// <summary>
+    /// The F21 key.
+    /// </summary>
+    F21 = 110,
+
+    /// <summary>
+    /// The F22 key.
+    /// </summary>
+    F22 = 111,
+
+    /// <summary>
+    /// The F23 key.
+    /// </summary>
+    F23 = 112,
+
+    /// <summary>
+    /// The F24 key.
+    /// </summary>
+    F24 = 113,
+
+    /// <summary>
+    /// The Numlock key.
+    /// </summary>
+    NumLock = 114,
+
+    /// <summary>
+    /// The Scroll key.
+    /// </summary>
+    Scroll = 115,
+
+    /// <summary>
+    /// The left Shift key.
+    /// </summary>
+    LeftShift = 116,
+
+    /// <summary>
+    /// The right Shift key.
+    /// </summary>
+    RightShift = 117,
+
+    /// <summary>
+    /// The left Ctrl key.
+    /// </summary>
+    LeftCtrl = 118,
+
+    /// <summary>
+    /// The right Ctrl key.
+    /// </summary>
+    RightCtrl = 119,
+
+    /// <summary>
+    /// The left Alt key.
+    /// </summary>
+    LeftAlt = 120,
+
+    /// <summary>
+    /// The right Alt key.
+    /// </summary>
+    RightAlt = 121,
+
+    /// <summary>
+    /// The browser Back key.
+    /// </summary>
+    BrowserBack = 122,
+
+    /// <summary>
+    /// The browser Forward key.
+    /// </summary>
+    BrowserForward = 123,
+
+    /// <summary>
+    /// The browser Refresh key.
+    /// </summary>
+    BrowserRefresh = 124,
+
+    /// <summary>
+    /// The browser Stop key.
+    /// </summary>
+    BrowserStop = 125,
+
+    /// <summary>
+    /// The browser Search key.
+    /// </summary>
+    BrowserSearch = 126,
+
+    /// <summary>
+    /// The browser Favorites key.
+    /// </summary>
+    BrowserFavorites = 127,
+
+    /// <summary>
+    /// The browser Home key.
+    /// </summary>
+    BrowserHome = 128,
+
+    /// <summary>
+    /// The Volume Mute key.
+    /// </summary>
+    VolumeMute = 129,
+
+    /// <summary>
+    /// The Volume Down key.
+    /// </summary>
+    VolumeDown = 130,
+
+    /// <summary>
+    /// The Volume Up key.
+    /// </summary>
+    VolumeUp = 131,
+
+    /// <summary>
+    /// The media Next Track key.
+    /// </summary>
+    MediaNextTrack = 132,
+
+    /// <summary>
+    /// The media Previous Track key.
+    /// </summary>
+    MediaPreviousTrack = 133,
+
+    /// <summary>
+    /// The media Stop key.
+    /// </summary>
+    MediaStop = 134,
+
+    /// <summary>
+    /// The media Play/Pause key.
+    /// </summary>
+    MediaPlayPause = 135,
+
+    /// <summary>
+    /// The Launch Mail key.
+    /// </summary>
+    LaunchMail = 136,
+
+    /// <summary>
+    /// The Select Media key.
+    /// </summary>
+    SelectMedia = 137,
+
+    /// <summary>
+    /// The Launch Application 1 key.
+    /// </summary>
+    LaunchApplication1 = 138,
+
+    /// <summary>
+    /// The Launch Application 2 key.
+    /// </summary>
+    LaunchApplication2 = 139,
+
+    /// <summary>
+    /// The OEM Semicolon key.
+    /// </summary>
+    OemSemicolon = 140,
+
+    /// <summary>
+    /// The OEM 1 key.
+    /// </summary>
+    Oem1 = 140,
+
+    /// <summary>
+    /// The OEM Plus key.
+    /// </summary>
+    OemPlus = 141,
+
+    /// <summary>
+    /// The OEM Comma key.
+    /// </summary>
+    OemComma = 142,
+
+    /// <summary>
+    /// The OEM Minus key.
+    /// </summary>
+    OemMinus = 143,
+
+    /// <summary>
+    /// The OEM Period key.
+    /// </summary>
+    OemPeriod = 144,
+
+    /// <summary>
+    /// The OEM Question Mark key.
+    /// </summary>
+    OemQuestion = 145,
+
+    /// <summary>
+    /// The OEM 2 key.
+    /// </summary>
+    Oem2 = 145,
+
+    /// <summary>
+    /// The OEM Tilde key.
+    /// </summary>
+    OemTilde = 146,
+
+    /// <summary>
+    /// The OEM 3 key.
+    /// </summary>
+    Oem3 = 146,
+
+    /// <summary>
+    /// The ABNT_C1 (Brazilian) key.
+    /// </summary>
+    AbntC1 = 147,
+
+    /// <summary>
+    /// The ABNT_C2 (Brazilian) key.
+    /// </summary>
+    AbntC2 = 148,
+
+    /// <summary>
+    /// The OEM Open Brackets key.
+    /// </summary>
+    OemOpenBrackets = 149,
+
+    /// <summary>
+    /// The OEM 4 key.
+    /// </summary>
+    Oem4 = 149,
+
+    /// <summary>
+    /// The OEM Pipe key.
+    /// </summary>
+    OemPipe = 150,
+
+    /// <summary>
+    /// The OEM 5 key.
+    /// </summary>
+    Oem5 = 150,
+
+    /// <summary>
+    /// The OEM Close Brackets key.
+    /// </summary>
+    OemCloseBrackets = 151,
+
+    /// <summary>
+    /// The OEM 6 key.
+    /// </summary>
+    Oem6 = 151,
+
+    /// <summary>
+    /// The OEM Quotes key.
+    /// </summary>
+    OemQuotes = 152,
+
+    /// <summary>
+    /// The OEM 7 key.
+    /// </summary>
+    Oem7 = 152,
+
+    /// <summary>
+    /// The OEM 8 key.
+    /// </summary>
+    Oem8 = 153,
+
+    /// <summary>
+    /// The OEM Backslash key.
+    /// </summary>
+    OemBackslash = 154,
+
+    /// <summary>
+    /// The OEM 3 key.
+    /// </summary>
+    Oem102 = 154,
+
+    /// <summary>
+    /// A special key masking the real key being processed by an IME.
+    /// </summary>
+    ImeProcessed = 155,
+
+    /// <summary>
+    /// A special key masking the real key being processed as a system key.
+    /// </summary>
+    System = 156,
+
+    /// <summary>
+    /// The OEM ATTN key.
+    /// </summary>
+    OemAttn = 157,
+
+    /// <summary>
+    /// The DBE_ALPHANUMERIC key.
+    /// </summary>
+    DbeAlphanumeric = 157,
+
+    /// <summary>
+    /// The OEM Finish key.
+    /// </summary>
+    OemFinish = 158,
+
+    /// <summary>
+    /// The DBE_KATAKANA key.
+    /// </summary>
+    DbeKatakana = 158,
+
+    /// <summary>
+    /// The DBE_HIRAGANA key.
+    /// </summary>
+    DbeHiragana = 159,
+
+    /// <summary>
+    /// The OEM Copy key.
+    /// </summary>
+    OemCopy = 159,
+
+    /// <summary>
+    /// The DBE_SBCSCHAR key.
+    /// </summary>
+    DbeSbcsChar = 160,
+
+    /// <summary>
+    /// The OEM Auto key.
+    /// </summary>
+    OemAuto = 160,
+
+    /// <summary>
+    /// The DBE_DBCSCHAR key.
+    /// </summary>
+    DbeDbcsChar = 161,
+
+    /// <summary>
+    /// The OEM ENLW key.
+    /// </summary>
+    OemEnlw = 161,
+
+    /// <summary>
+    /// The OEM BackTab key.
+    /// </summary>
+    OemBackTab = 162,
+
+    /// <summary>
+    /// The DBE_ROMAN key.
+    /// </summary>
+    DbeRoman = 162,
+
+    /// <summary>
+    /// The DBE_NOROMAN key.
+    /// </summary>
+    DbeNoRoman = 163,
+
+    /// <summary>
+    /// The ATTN key.
+    /// </summary>
+    Attn = 163,
+
+    /// <summary>
+    /// The CRSEL key.
+    /// </summary>
+    CrSel = 164,
+
+    /// <summary>
+    /// The DBE_ENTERWORDREGISTERMODE key.
+    /// </summary>
+    DbeEnterWordRegisterMode = 164,
+
+    /// <summary>
+    /// The EXSEL key.
+    /// </summary>
+    ExSel = 165,
+
+    /// <summary>
+    /// The DBE_ENTERIMECONFIGMODE key.
+    /// </summary>
+    DbeEnterImeConfigureMode = 165,
+
+    /// <summary>
+    /// The ERASE EOF Key.
+    /// </summary>
+    EraseEof = 166,
+
+    /// <summary>
+    /// The DBE_FLUSHSTRING key.
+    /// </summary>
+    DbeFlushString = 166,
+
+    /// <summary>
+    /// The Play key.
+    /// </summary>
+    Play = 167,
+
+    /// <summary>
+    /// The DBE_CODEINPUT key.
+    /// </summary>
+    DbeCodeInput = 167,
+
+    /// <summary>
+    /// The DBE_NOCODEINPUT key.
+    /// </summary>
+    DbeNoCodeInput = 168,
+
+    /// <summary>
+    /// The Zoom key.
+    /// </summary>
+    Zoom = 168,
+
+    /// <summary>
+    /// Reserved for future use.
+    /// </summary>
+    NoName = 169,
+
+    /// <summary>
+    /// The DBE_DETERMINESTRING key.
+    /// </summary>
+    DbeDetermineString = 169,
+
+    /// <summary>
+    /// The DBE_ENTERDLGCONVERSIONMODE key.
+    /// </summary>
+    DbeEnterDialogConversionMode = 170,
+
+    /// <summary>
+    /// The PA1 key.
+    /// </summary>
+    Pa1 = 170,
+
+    /// <summary>
+    /// The OEM Clear key.
+    /// </summary>
+    OemClear = 171,
+
+    /// <summary>
+    /// The key is used with another key to create a single combined character.
+    /// </summary>
+    DeadCharProcessed = 172,
+
+
+    /// <summary>
+    /// OSX Platform-specific Fn+Left key
+    /// </summary>
+    FnLeftArrow = 10001,
+
+    /// <summary>
+    /// OSX Platform-specific Fn+Right key
+    /// </summary>
+    FnRightArrow = 10002,
+
+    /// <summary>
+    /// OSX Platform-specific Fn+Up key
+    /// </summary>
+    FnUpArrow = 10003,
+
+    /// <summary>
+    /// OSX Platform-specific Fn+Down key
+    /// </summary>
+    FnDownArrow = 10004,
+
+    /// <summary>
+    /// Remove control home button
+    /// </summary>
+    MediaHome = 100000,
+
+    /// <summary>
+    /// TV Channel up
+    /// </summary>
+    MediaChannelList = 100001,
+
+    /// <summary>
+    /// TV Channel up
+    /// </summary>
+    MediaChannelRaise = 100002,
+
+    /// <summary>
+    /// TV Channel down
+    /// </summary>
+    MediaChannelLower = 100003,
+
+    /// <summary>
+    /// TV Channel down
+    /// </summary>
+    MediaRecord = 100005,
+
+    /// <summary>
+    /// Remote control Red button
+    /// </summary>
+    MediaRed = 100010,
+
+    /// <summary>
+    /// Remote control Green button
+    /// </summary>
+    MediaGreen = 100011,
+
+    /// <summary>
+    /// Remote control Yellow button
+    /// </summary>
+    MediaYellow = 100012,
+
+    /// <summary>
+    /// Remote control Blue button
+    /// </summary>
+    MediaBlue = 100013,
+
+    /// <summary>
+    /// Remote control Menu button
+    /// </summary>
+    MediaMenu = 100020,
+
+    /// <summary>
+    /// Remote control dots button
+    /// </summary>
+    MediaMore = 100021,
+
+    /// <summary>
+    /// Remote control option button
+    /// </summary>
+    MediaOption = 100022,
+
+    /// <summary>
+    /// Remote control channel info button
+    /// </summary>
+    MediaInfo = 100023,
+
+    /// <summary>
+    /// Remote control search button
+    /// </summary>
+    MediaSearch = 100024,
+
+    /// <summary>
+    /// Remote control subtitle/caption button
+    /// </summary>
+    MediaSubtitle = 100025,
+
+    /// <summary>
+    /// Remote control Tv guide detail button
+    /// </summary>
+    MediaTvGuide = 100026,
+
+    /// <summary>
+    /// Remote control Previous Channel
+    /// </summary>
+    MediaPreviousChannel = 100027,
+}

+ 43 - 0
src/PixiEditor.Extensions.CommonApi/DataContracts/CommandMetadata.proto

@@ -0,0 +1,43 @@
+syntax = "proto3";
+package PixiEditor.Commands;
+
+import "Shortcut.proto";
+option csharp_namespace = "PixiEditor.Extensions.CommonApi.Commands";
+
+message CommandMetadata
+{
+  string UniqueName = 1;
+  string DisplayName = 2;
+  string Description = 3;
+  Shortcut Shortcut = 4;
+  string Icon = 5;
+  string MenuItemPath = 6;
+  int32 Order = 7;
+  InvokePermissions InvokePermissions = 8;
+  string ExplicitlyAllowedExtensions = 9;
+}
+
+enum InvokePermissions
+{
+
+  /// <summary>
+  ///     Only the registering extension can use this command.
+  /// </summary>
+    Owner = 0;
+
+  /// <summary>
+  ///     Only extensions explicitly whitelisted by the registering extension can use this command.
+  /// </summary>
+    Explicit = 1;
+
+  /// <summary>
+  ///     Only extensions that are part of the same family can use this command. A family is a group under the same
+  ///     unique name prefix.
+  /// </summary>
+    Family = 2;
+
+  /// <summary>
+  ///     Any extension can use this command.
+  /// </summary>
+    Public = 3;
+}

+ 10 - 0
src/PixiEditor.Extensions.CommonApi/DataContracts/Shortcut.proto

@@ -0,0 +1,10 @@
+syntax = "proto3";
+package PixiEditor.Commands;
+
+option csharp_namespace = "PixiEditor.Extensions.CommonApi.Commands";
+
+message Shortcut
+{
+   int32 Key = 1;
+   int32 Modifiers = 2;
+}

+ 7 - 0
src/PixiEditor.Extensions.CommonApi/Documents/IDocument.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Extensions.CommonApi.Documents;
+
+public interface IDocument
+{
+    public Guid Id { get; }
+    public void Resize(int width, int height);
+}

+ 6 - 3
src/PixiEditor.Extensions.CommonApi/FlyUI/ByteMap.cs

@@ -1,11 +1,13 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
-
-namespace PixiEditor.Extensions.CommonApi.FlyUI;
+namespace PixiEditor.Extensions.CommonApi.FlyUI;
 
 
 public static class ByteMap
 public static class ByteMap
 {
 {
     public static byte GetTypeByteId(Type type)
     public static byte GetTypeByteId(Type type)
     {
     {
+        if (type == null)
+        {
+            return 255;
+        }
         if (type == typeof(int))
         if (type == typeof(int))
         {
         {
             return 0;
             return 0;
@@ -64,6 +66,7 @@ public static class ByteMap
             7 => typeof(char),
             7 => typeof(char),
             8 => typeof(string),
             8 => typeof(string),
             9 => typeof(byte[]),
             9 => typeof(byte[]),
+            255 => null,
             _ => throw new Exception($"Unknown unmanaged type id: {id}")
             _ => throw new Exception($"Unknown unmanaged type id: {id}")
         };
         };
     }
     }

+ 72 - 0
src/PixiEditor.Extensions.CommonApi/FlyUI/Cursor.cs

@@ -0,0 +1,72 @@
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+
+namespace PixiEditor.Extensions.CommonApi.FlyUI;
+
+public struct Cursor : IStructProperty
+{
+    private const int Version = 1; // Serialization version, increment when changing the struct
+    public BuiltInCursor? BuiltInCursor { get; set; }
+    public bool IsCustom => BuiltInCursor == null;
+
+    public Cursor(BuiltInCursor builtInCursor)
+    {
+        BuiltInCursor = builtInCursor;
+    }
+
+    byte[] IStructProperty.Serialize()
+    {
+        var data = new List<byte>();
+        data.Add(Version);
+        data.Add(BuiltInCursor != null ? (byte)1 : (byte)0);
+        if (BuiltInCursor != null)
+        {
+            data.Add((byte)BuiltInCursor.Value);
+        }
+
+        return data.ToArray();
+    }
+
+    void IStructProperty.Deserialize(byte[] data)
+    {
+        int version = data[0];
+
+        int index = 1;
+        if (data[index] == 1)
+        {
+            index++;
+            BuiltInCursor = (BuiltInCursor)data[index];
+        }
+        else
+        {
+            BuiltInCursor = null;
+        }
+    }
+}
+
+public enum BuiltInCursor
+{
+    Arrow,
+    IBeam,
+    Wait,
+    Cross,
+    UpArrow,
+    SizeWestEast,
+    SizeNorthSouth,
+    SizeAll,
+    No,
+    Hand,
+    AppStarting,
+    Help,
+    TopSide,
+    BottomSide,
+    LeftSide,
+    RightSide,
+    TopLeftCorner,
+    TopRightCorner,
+    BottomLeftCorner,
+    BottomRightCorner,
+    DragMove,
+    DragCopy,
+    DragLink,
+    None,
+}

+ 37 - 4
src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ElementEventArgs.cs

@@ -1,12 +1,45 @@
-namespace PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using System.Collections;
+using PixiEditor.Extensions.CommonApi.Utilities;
+
+namespace PixiEditor.Extensions.CommonApi.FlyUI.Events;
 
 
 public class ElementEventArgs
 public class ElementEventArgs
 {
 {
-    public object Sender { get; set; } 
-    public static ElementEventArgs Empty { get; } = new ElementEventArgs();
+    public object Sender { get; set; }
+
+    public static ElementEventArgs Deserialize(byte[] data)
+    {
+        if (data == null) return new ElementEventArgs();
+
+        ByteReader reader = new ByteReader(data);
+        string eventType = reader.ReadString();
+        ElementEventArgs eventArgs = eventType switch // TODO: more generic implementation
+        {
+            nameof(ToggleEventArgs) => new ToggleEventArgs(reader.ReadBool()),
+            nameof(TextEventArgs) => new TextEventArgs(reader.ReadString()),
+            nameof(NumberEventArgs) => new NumberEventArgs(reader.ReadDouble()),
+            nameof(ElementEventArgs) => new ElementEventArgs(),
+            _ => throw new NotSupportedException($"Event type '{eventType}' is not supported.")
+        };
+
+        return eventArgs;
+    }
+
+    public byte[] Serialize()
+    {
+        ByteWriter writer = new ByteWriter();
+        writer.WriteString(GetType().Name);
+        SerializeArgs(writer);
+
+        return writer.ToArray();
+    }
+
+    protected virtual void SerializeArgs(ByteWriter writer)
+    {
+        // Default implementation does nothing. Override in derived classes to serialize specific properties.
+    }
 }
 }
 
 
 public class ElementEventArgs<TEventArgs> : ElementEventArgs where TEventArgs : ElementEventArgs
 public class ElementEventArgs<TEventArgs> : ElementEventArgs where TEventArgs : ElementEventArgs
 {
 {
-    public static new ElementEventArgs<TEventArgs> Empty { get; } = new ElementEventArgs<TEventArgs>();
 }
 }

+ 3 - 1
src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ElementEventHandler.cs

@@ -1,4 +1,6 @@
 namespace PixiEditor.Extensions.CommonApi.FlyUI.Events;
 namespace PixiEditor.Extensions.CommonApi.FlyUI.Events;
 
 
 public delegate void ElementEventHandler(ElementEventArgs args);
 public delegate void ElementEventHandler(ElementEventArgs args);
-public delegate void ElementEventHandler<in TEventArgs>(TEventArgs args) where TEventArgs : ElementEventArgs<TEventArgs>;
+
+public delegate void ElementEventHandler<in TEventArgs>(TEventArgs args)
+    where TEventArgs : ElementEventArgs<TEventArgs>;

+ 19 - 0
src/PixiEditor.Extensions.CommonApi/FlyUI/Events/NumberEventArgs.cs

@@ -0,0 +1,19 @@
+using System.Numerics;
+using PixiEditor.Extensions.CommonApi.Utilities;
+
+namespace PixiEditor.Extensions.CommonApi.FlyUI.Events;
+
+public class NumberEventArgs : ElementEventArgs<NumberEventArgs>
+{
+    public double Value { get; }
+
+    public NumberEventArgs(double value)
+    {
+        Value = value;
+    }
+
+    protected override void SerializeArgs(ByteWriter writer)
+    {
+        writer.WriteDouble(Value);
+    }
+}

+ 19 - 0
src/PixiEditor.Extensions.CommonApi/FlyUI/Events/TextEventArgs.cs

@@ -0,0 +1,19 @@
+using System.Collections;
+using PixiEditor.Extensions.CommonApi.Utilities;
+
+namespace PixiEditor.Extensions.CommonApi.FlyUI.Events;
+
+public class TextEventArgs : ElementEventArgs<TextEventArgs>
+{
+    public string Text { get; set; }
+
+    public TextEventArgs(string newText)
+    {
+        Text = newText;
+    }
+
+    protected override void SerializeArgs(ByteWriter writer)
+    {
+        writer.WriteString(Text);
+    }
+}

+ 9 - 1
src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ToggleEventArgs.cs

@@ -1,4 +1,7 @@
-namespace PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using System.Collections;
+using PixiEditor.Extensions.CommonApi.Utilities;
+
+namespace PixiEditor.Extensions.CommonApi.FlyUI.Events;
 
 
 public class ToggleEventArgs : ElementEventArgs<ToggleEventArgs>
 public class ToggleEventArgs : ElementEventArgs<ToggleEventArgs>
 {
 {
@@ -8,4 +11,9 @@ public class ToggleEventArgs : ElementEventArgs<ToggleEventArgs>
     {
     {
         IsToggled = isToggled;
         IsToggled = isToggled;
     }
     }
+
+    protected override void SerializeArgs(ByteWriter writer)
+    {
+        writer.WriteBool(IsToggled);
+    }
 }
 }

+ 12 - 1
src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/Color.cs

@@ -22,7 +22,7 @@ public struct Color : IStructProperty
 
 
     byte[] IStructProperty.Serialize()
     byte[] IStructProperty.Serialize()
     {
     {
-        return new byte[] { R, G, B, A };
+        return [R, G, B, A];
     }
     }
 
 
     void IStructProperty.Deserialize(byte[] data)
     void IStructProperty.Deserialize(byte[] data)
@@ -32,4 +32,15 @@ public struct Color : IStructProperty
         B = data[2];
         B = data[2];
         A = data[3];
         A = data[3];
     }
     }
+
+    public static Color FromBytes(byte[] data)
+    {
+        if (data.Length < 4)
+        {
+            throw new ArgumentException("Data array must contain at least 4 bytes.");
+        }
+
+
+        return new Color(data[0], data[1], data[2], data[3]);
+    }
 }
 }

+ 0 - 8
src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/FontStyle.cs

@@ -1,8 +0,0 @@
-namespace PixiEditor.Extensions.CommonApi.FlyUI.Properties;
-
-public enum FontStyle
-{
-    Normal,
-    Italic,
-    Oblique
-}

+ 148 - 0
src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/TextStyle.cs

@@ -0,0 +1,148 @@
+using System.Text;
+
+namespace PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+
+public struct TextStyle : IStructProperty
+{
+    // IMPORTANT: If you change this struct, you must also change the version below and handle
+    // deserialization in the FlyUI deserializer.
+    public const int Version = 1;
+    public string? FontFamily { get; set; }
+    public double? FontSize { get; set; }
+    public FontStyle? FontStyle { get; set; }
+    public FontWeight? FontWeight { get; set; }
+    public Color? Color { get; set; }
+
+    public static TextStyle Default => new TextStyle(null, null, null, null, null);
+
+    public TextStyle(string? fontFamily = null, double? fontSize = null, FontStyle? fontStyle = null, FontWeight? fontWeight = null, Color? color = null)
+    {
+        FontFamily = fontFamily;
+        FontSize = fontSize;
+        FontStyle = fontStyle;
+        FontWeight = fontWeight;
+        Color = color;
+    }
+
+    public byte[] Serialize()
+    {
+        var data = new List<byte>();
+        data.AddRange(BitConverter.GetBytes(Version));
+        data.AddRange(BitConverter.GetBytes(FontFamily?.Length ?? 0));
+        if (FontFamily != null)
+        {
+            data.AddRange(Encoding.UTF8.GetBytes(FontFamily));
+        }
+
+        data.Add(FontSize != null ? (byte)1 : (byte)0);
+        if (FontSize != null)
+        {
+            data.AddRange(BitConverter.GetBytes(FontSize.Value));
+        }
+
+        data.Add(FontStyle != null ? (byte)1 : (byte)0);
+        if (FontStyle != null)
+        {
+            data.Add((byte)FontStyle.Value);
+        }
+
+        data.Add(FontWeight != null ? (byte)1 : (byte)0);
+        if (FontWeight != null)
+        {
+            data.AddRange(BitConverter.GetBytes((int)FontWeight.Value));
+        }
+
+        data.Add(Color != null ? (byte)1 : (byte)0);
+        if (Color != null)
+        {
+            data.AddRange(((IStructProperty)Color).Serialize());
+        }
+
+        return data.ToArray();
+    }
+
+    public void Deserialize(byte[] data)
+    {
+        int index = 0;
+        int version = BitConverter.ToInt32(data, index);
+        index += 4;
+        int fontFamilyLength = BitConverter.ToInt32(data, index);
+        index += 4;
+        if (fontFamilyLength > 0)
+        {
+            FontFamily = Encoding.UTF8.GetString(data, index, fontFamilyLength);
+            index += fontFamilyLength;
+        }
+        else
+        {
+            FontFamily = null;
+        }
+
+        bool hasFontSize = data[index] == 1;
+        index++;
+        if (hasFontSize)
+        {
+            FontSize = BitConverter.ToDouble(data, index);
+            index += 8;
+        }
+        else
+        {
+            FontSize = null;
+        }
+
+        bool hasFontStyle = data[index] == 1;
+        index++;
+        if (hasFontStyle)
+        {
+            FontStyle = (FontStyle)data[index];
+            index++;
+        }
+        else
+        {
+            FontStyle = null;
+        }
+
+        bool hasFontWeight = data[index] == 1;
+        index++;
+        if (hasFontWeight)
+        {
+            FontWeight = (FontWeight)BitConverter.ToInt32(data, index);
+            index += 4;
+        }
+        else
+        {
+            FontWeight = null;
+        }
+
+        bool hasColor = data[index] == 1;
+        index++;
+        if (hasColor)
+        {
+            Color = Properties.Color.FromBytes(data[index..]);
+        }
+        else
+        {
+            Color = null;
+        }
+    }
+}
+
+public enum FontStyle
+{
+    Normal,
+    Italic,
+    Oblique
+}
+
+public enum FontWeight
+{
+    Thin = 100,
+    ExtraLight = 200,
+    Light = 300,
+    Normal = 400,
+    Medium = 500,
+    SemiBold = 600,
+    Bold = 700,
+    ExtraBold = 800,
+    Black = 900
+}

+ 10 - 0
src/PixiEditor.Extensions.CommonApi/IO/IDocumentProvider.cs

@@ -0,0 +1,10 @@
+using PixiEditor.Extensions.CommonApi.Documents;
+
+namespace PixiEditor.Extensions.CommonApi.IO;
+
+public interface IDocumentProvider
+{
+   public IDocument? ActiveDocument { get; }
+   public IDocument? ImportFile(string path, bool associatePath = true);
+   public IDocument? GetDocument(Guid id);
+}

+ 3 - 3
src/PixiEditor.Extensions.CommonApi/Palettes/FilteringSettings.Impl.cs

@@ -4,10 +4,10 @@ public partial class FilteringSettings
 {
 {
     public FilteringSettings()
     public FilteringSettings()
     {
     {
-    }
 
 
-    public FilteringSettings(ColorsNumberMode colorsNumberMode, int colorsCount, string name, bool showOnlyFavourites,
-        List<string> favourites)
+    }
+    
+    public FilteringSettings(ColorsNumberMode colorsNumberMode, int colorsCount, string name, bool showOnlyFavourites, List<string> favourites)
     {
     {
         ColorsNumberMode = colorsNumberMode;
         ColorsNumberMode = colorsNumberMode;
         ColorsCount = colorsCount;
         ColorsCount = colorsCount;

+ 66 - 0
src/PixiEditor.Extensions.CommonApi/ProtoAutogen/CommandMetadata.cs

@@ -0,0 +1,66 @@
+// <auto-generated>
+//   This file was generated by a tool; you should avoid making direct changes.
+//   Consider using 'partial classes' to extend these types
+//   Input: CommandMetadata.proto
+// </auto-generated>
+
+#region Designer generated code
+#pragma warning disable CS0612, CS0618, CS1591, CS3021, CS8981, IDE0079, IDE1006, RCS1036, RCS1057, RCS1085, RCS1192
+namespace PixiEditor.Extensions.CommonApi.Commands
+{
+
+    [global::ProtoBuf.ProtoContract()]
+    public partial class CommandMetadata : global::ProtoBuf.IExtensible
+    {
+        private global::ProtoBuf.IExtension __pbn__extensionData;
+        global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+            => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+        [global::ProtoBuf.ProtoMember(1)]
+        [global::System.ComponentModel.DefaultValue("")]
+        public string UniqueName { get; set; } = "";
+
+        [global::ProtoBuf.ProtoMember(2)]
+        [global::System.ComponentModel.DefaultValue("")]
+        public string DisplayName { get; set; } = "";
+
+        [global::ProtoBuf.ProtoMember(3)]
+        [global::System.ComponentModel.DefaultValue("")]
+        public string Description { get; set; } = "";
+
+        [global::ProtoBuf.ProtoMember(4)]
+        public Shortcut Shortcut { get; set; }
+
+        [global::ProtoBuf.ProtoMember(5)]
+        [global::System.ComponentModel.DefaultValue("")]
+        public string Icon { get; set; } = "";
+
+        [global::ProtoBuf.ProtoMember(6)]
+        [global::System.ComponentModel.DefaultValue("")]
+        public string MenuItemPath { get; set; } = "";
+
+        [global::ProtoBuf.ProtoMember(7)]
+        public int Order { get; set; }
+
+        [global::ProtoBuf.ProtoMember(8)]
+        public InvokePermissions InvokePermissions { get; set; }
+
+        [global::ProtoBuf.ProtoMember(9)]
+        [global::System.ComponentModel.DefaultValue("")]
+        public string ExplicitlyAllowedExtensions { get; set; } = "";
+
+    }
+
+    [global::ProtoBuf.ProtoContract()]
+    public enum InvokePermissions
+    {
+        Owner = 0,
+        Explicit = 1,
+        Family = 2,
+        Public = 3,
+    }
+
+}
+
+#pragma warning restore CS0612, CS0618, CS1591, CS3021, CS8981, IDE0079, IDE1006, RCS1036, RCS1057, RCS1085, RCS1192
+#endregion

+ 30 - 0
src/PixiEditor.Extensions.CommonApi/ProtoAutogen/Shortcut.cs

@@ -0,0 +1,30 @@
+// <auto-generated>
+//   This file was generated by a tool; you should avoid making direct changes.
+//   Consider using 'partial classes' to extend these types
+//   Input: Shortcut.proto
+// </auto-generated>
+
+#region Designer generated code
+#pragma warning disable CS0612, CS0618, CS1591, CS3021, CS8981, IDE0079, IDE1006, RCS1036, RCS1057, RCS1085, RCS1192
+namespace PixiEditor.Extensions.CommonApi.Commands
+{
+
+    [global::ProtoBuf.ProtoContract()]
+    public partial class Shortcut : global::ProtoBuf.IExtensible
+    {
+        private global::ProtoBuf.IExtension __pbn__extensionData;
+        global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+            => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+        [global::ProtoBuf.ProtoMember(1)]
+        public int Key { get; set; }
+
+        [global::ProtoBuf.ProtoMember(2)]
+        public int Modifiers { get; set; }
+
+    }
+
+}
+
+#pragma warning restore CS0612, CS0618, CS1591, CS3021, CS8981, IDE0079, IDE1006, RCS1036, RCS1057, RCS1085, RCS1192
+#endregion

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