Browse Source

Merge branch 'master' into toolsets

flabbet 11 months ago
parent
commit
b5635e2ee5
100 changed files with 1361 additions and 615 deletions
  1. 6 0
      .gitmodules
  2. 0 46
      azure-pipelines.yml
  3. 74 0
      pipelines/Linux/tests-ubuntu.yml
  4. 74 0
      pipelines/MacOS/tests-macos.yml
  5. 74 0
      pipelines/Windows/tests-windows.yml
  6. 1 0
      samples/Sample1_HelloWorld/Sample1_HelloWorld.csproj
  7. 5 4
      samples/Sample2_LocalizationSample/Sample2_LocalizationSample.csproj
  8. 1 0
      samples/Sample3_Preferences/Sample3_Preferences.csproj
  9. 1 0
      samples/Sample4_CreatePopup/Sample4_CreatePopup.csproj
  10. 1 0
      samples/Sample5_Resources/Sample5_Resources.csproj
  11. 1 0
      samples/Sample6_Palettes/Sample6_Palettes.csproj
  12. 1 0
      samples/Sample7_FlyUI/Sample7_FlyUI.csproj
  13. 9 9
      src/Directory.Build.props
  14. 1 0
      src/PixiDocks
  15. 18 5
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs
  16. 42 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodeMetadata.cs
  17. 5 3
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs
  18. 5 3
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs
  19. 3 2
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs
  20. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs
  21. 69 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  22. 11 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs
  23. 48 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  24. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPairNode.cs
  25. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  26. 1 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeInfoAttribute.cs
  27. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  29. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs
  30. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecDNode.cs
  31. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecINode.cs
  32. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  33. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs
  34. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs
  35. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs
  36. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  37. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreatingNode.md
  38. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs
  39. 1 79
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EllipseNode.cs
  40. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  41. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs
  42. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs
  43. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/KernelFilterNode.cs
  44. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  45. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  46. 0 31
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs
  47. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LerpColorNode.cs
  48. 35 16
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  49. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  50. 7 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  51. 17 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  52. 15 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  53. 41 22
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  54. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  55. 0 51
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/DistributePointsNode.cs
  56. 0 25
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/PointList.cs
  57. 0 50
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RasterizePointsNode.cs
  58. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  59. 55 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseData.cs
  60. 49 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsData.cs
  61. 24 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeData.cs
  62. 44 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs
  63. 35 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs
  64. 43 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  65. 23 22
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs
  66. 39 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs
  67. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/OutputProperty.cs
  68. 68 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/PropertyValidator.cs
  69. 92 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectProperties_Change.cs
  70. 18 16
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodePair_Change.cs
  71. 10 12
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs
  72. 16 5
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeleteNode_Change.cs
  73. 24 11
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/UpdateProperty_Change.cs
  74. 1 4
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  75. 15 1
      src/PixiEditor.ChangeableDocument/Enums/MathNodeMode.cs
  76. 2 27
      src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj
  77. 4 1
      src/PixiEditor.ChangeableDocument/Rendering/RenderingContext.cs
  78. 1 0
      src/PixiEditor.ClosedBeta/PixiEditor.ClosedBeta.csproj
  79. 8 4
      src/PixiEditor.Desktop/PixiEditor.Desktop.csproj
  80. 2 0
      src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IShaderImplementation.cs
  81. 12 2
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Float2.cs
  82. 15 2
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half4.cs
  83. 13 2
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Int2.cs
  84. 8 0
      src/PixiEditor.DrawingApi.Core/Shaders/Generation/IMultiValueVariable.cs
  85. 6 0
      src/PixiEditor.DrawingApi.Core/Shaders/Shader.cs
  86. 6 1
      src/PixiEditor.DrawingApi.Core/Texture.cs
  87. 11 0
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaShaderImplementation.cs
  88. 0 43
      src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj
  89. 1 0
      src/PixiEditor.Extensions.Sdk/PixiEditor.Extensions.Sdk.csproj
  90. 2 2
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.deps.json
  91. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  92. 2 2
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.deps.json
  93. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  94. 1 1
      src/PixiEditor.MSIX/Package.appxmanifest
  95. 1 2
      src/PixiEditor.MSIX/PixiEditor.MSIX.wapproj
  96. 46 3
      src/PixiEditor.Numerics/VecD.cs
  97. 14 1
      src/PixiEditor.Numerics/VecI.cs
  98. 45 4
      src/PixiEditor.UI.Common/Accents/Base.axaml
  99. 3 2
      src/PixiEditor.UI.Common/Controls/ListBoxItem.axaml
  100. 1 1
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml

+ 6 - 0
.gitmodules

@@ -0,0 +1,6 @@
+[submodule "src/PixiDocks"]
+	path = src/PixiDocks
+	url = https://github.com/PixiEditor/PixiDocks.git
+[submodule "src/PixiParser"]
+	path = src/PixiParser
+	url = https://github.com/PixiEditor/PixiParser.git

+ 0 - 46
azure-pipelines.yml

@@ -1,46 +0,0 @@
-trigger:
-- development
-- master
-
-pool:
-  vmImage: 'windows-latest'
-
-variables:
-  solution: '**/*.sln'
-  buildPlatform: 'Any CPU'
-  buildConfiguration: 'Release'
-
-steps:
-- task: NuGetToolInstaller@1
-
-- task: NuGetCommand@2
-  inputs:
-    restoreSolution: '$(solution)'
-
-- task: DotNetCoreCLI@2
-  displayName: Build
-  inputs:
-    command: 'build'
-    projects: '**/*.csproj'
-    arguments: '--configuration Release'
-
-- task: DotNetCoreCLI@2
-  displayName: Tests
-  inputs:
-    command: test
-    projects: '**/*Tests/*.csproj'
-    arguments: '--configuration $(buildConfiguration)'
-
-#- task: PowerShell@2
-#  inputs:
-#    targetType: 'inline'
-#    script: '& "$env:userprofile\.nuget\packages\opencover\4.7.1221\tools\OpenCover.Console.exe" -register -target:"$env:programfiles/dotnet/dotnet.exe" -targetargs:test -filter:"+[*]*" -output:".\PixiEditor_coverage.xml" -oldstyle'
-#    workingDirectory: 'PixiEditorTests\'
-#  displayName: Collect code coverage
-
-  # Disiabled, because there is a problem with .NET 6 and OpenCover.Console.exe
-#- task: CmdLine@2
-#  continueOnError: true
-#  inputs:
-#    script: codecov -f .\PixiEditorTests\PixiEditor_coverage.xml -t $(CODECOV_TOKEN)
-#  displayName: Upload to Codecov.io

+ 74 - 0
pipelines/Linux/tests-ubuntu.yml

@@ -0,0 +1,74 @@
+trigger:
+- development
+- master
+- 2.0-cicd
+
+pool:
+  vmImage: 'ubuntu-latest'
+
+variables:
+  solution: '**/*.sln'
+  buildPlatform: 'linux-$(arch)'
+  buildConfiguration: 'Release'
+  wasiVer: 'wasi-sdk-24.0-$(wasi-arch)-linux'
+  wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/$(wasiVer).tar.gz'
+
+steps:
+- task: CmdLine@2
+  displayName: 'Download WASI SDK'
+  inputs:
+    script: |
+      curl -L -o $(wasiVer).tar.gz $(wasiUrl)
+
+- 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: '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: DotNetCoreCLI@2
+  displayName: Install wasi-wasm
+  inputs:
+    command: 'custom'
+    custom: 'workload'
+    arguments: 'install wasi-experimental'
+
+
+- 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: Tests
+  inputs:
+    command: test
+    projects: '**/*Tests/*.csproj'
+    arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'

+ 74 - 0
pipelines/MacOS/tests-macos.yml

@@ -0,0 +1,74 @@
+trigger:
+- development
+- master
+- 2.0-cicd
+
+pool:
+  vmImage: 'macos-latest'
+
+variables:
+  solution: '**/*.sln'
+  buildPlatform: 'osx-$(arch)'
+  buildConfiguration: 'Release'
+  wasiVer: 'wasi-sdk-24.0-$(wasi-arch)-macos'
+  wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/$(wasiVer).tar.gz'
+
+steps:
+- task: CmdLine@2
+  displayName: 'Download WASI SDK'
+  inputs:
+    script: |
+      curl -L -o $(wasiVer).tar.gz $(wasiUrl)
+
+- 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: '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: DotNetCoreCLI@2
+  displayName: Install wasi-wasm
+  inputs:
+    command: 'custom'
+    custom: 'workload'
+    arguments: 'install wasi-experimental'
+
+
+- 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: Tests
+  inputs:
+    command: test
+    projects: '**/*Tests/*.csproj'
+    arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'

+ 74 - 0
pipelines/Windows/tests-windows.yml

@@ -0,0 +1,74 @@
+trigger:
+- development
+- master
+- 2.0-cicd
+
+pool:
+  vmImage: 'windows-latest'
+
+variables:
+  solution: '**/*.sln'
+  buildPlatform: 'win-$(arch)'
+  buildConfiguration: 'Release'
+  wasiVer: 'wasi-sdk-24.0-$(wasi-arch)-windows'
+  wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/$(wasiVer).tar.gz'
+
+steps:
+- task: CmdLine@2
+  displayName: 'Download WASI SDK'
+  inputs:
+    script: |
+      curl -L -o $(wasiVer).tar.gz $(wasiUrl)
+
+- 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: '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: DotNetCoreCLI@2
+  displayName: Install wasi-wasm
+  inputs:
+    command: 'custom'
+    custom: 'workload'
+    arguments: 'install wasi-experimental'
+
+
+- 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: Tests
+  inputs:
+    command: test
+    projects: '**/*Tests/*.csproj'
+    arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'

+ 1 - 0
samples/Sample1_HelloWorld/Sample1_HelloWorld.csproj

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

+ 5 - 4
samples/Sample2_LocalizationSample/Sample2_LocalizationSample.csproj

@@ -9,15 +9,16 @@
         <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <RootNamespace>LocalizationSample</RootNamespace>
+        <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
     </PropertyGroup>
 
     <ItemGroup>
-        <None Remove="extension.json" />
+        <None Remove="extension.json"/>
         <Content Include="extension.json">
             <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </Content>
     </ItemGroup>
-    
+
     <ItemGroup>
         <Content Include="Localization\*">
             <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -26,11 +27,11 @@
 
     <!--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" />
+        <ProjectReference Include="..\..\src\PixiEditor.Extensions.Sdk\PixiEditor.Extensions.Sdk.csproj"/>
     </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" />
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.targets"/>
 
 </Project>

+ 1 - 0
samples/Sample3_Preferences/Sample3_Preferences.csproj

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

+ 1 - 0
samples/Sample4_CreatePopup/Sample4_CreatePopup.csproj

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

+ 1 - 0
samples/Sample5_Resources/Sample5_Resources.csproj

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

+ 1 - 0
samples/Sample6_Palettes/Sample6_Palettes.csproj

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

+ 1 - 0
samples/Sample7_FlyUI/Sample7_FlyUI.csproj

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

+ 9 - 9
src/Directory.Build.props

@@ -7,33 +7,33 @@
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
     </ItemGroup>
     <ItemGroup>
-        <AdditionalFiles Include="../stylecop.json" />
+        <AdditionalFiles Include="$(SolutionDir)/stylecop.json" />
     </ItemGroup>
 
-  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">
     <RuntimeIdentifier>win-x64</RuntimeIdentifier>
   </PropertyGroup>
-  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'ARM64'">
     <RuntimeIdentifier>win-arm64</RuntimeIdentifier>
   </PropertyGroup>
-  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Linux')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Linux')) AND '$(Platform)' == 'x64'">
     <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
   </PropertyGroup>
-  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Linux')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Linux')) AND '$(Platform)' == 'ARM64'">
     <RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
   </PropertyGroup>
-  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('OSX')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('OSX')) AND '$(Platform)' == 'x64'">
     <RuntimeIdentifier>osx-x64</RuntimeIdentifier>
   </PropertyGroup>
-  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('OSX')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('OSX')) AND '$(Platform)' == 'ARM64'">
     <RuntimeIdentifier>osx-arm64</RuntimeIdentifier>
   </PropertyGroup>
 
-  <PropertyGroup Condition="'$(Platform)'=='x64'">
+  <PropertyGroup Condition="'$(Platform)'=='X64'">
     <PlatformTarget>x64</PlatformTarget>
   </PropertyGroup>
 
-  <PropertyGroup Condition="'$(Platform)'=='arm64'">
+  <PropertyGroup Condition="'$(Platform)'=='ARM64'">
     <PlatformTarget>arm64</PlatformTarget>
   </PropertyGroup>
 

+ 1 - 0
src/PixiDocks

@@ -0,0 +1 @@
+Subproject commit d55a7e27bbed73d19cc59d96b4b20ffdfee35634

+ 18 - 5
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs

@@ -16,15 +16,18 @@ public record CreateNode_ChangeInfo(
     VecD Position,
     Guid Id,
     ImmutableArray<NodePropertyInfo> Inputs,
-    ImmutableArray<NodePropertyInfo> Outputs) : IChangeInfo
+    ImmutableArray<NodePropertyInfo> Outputs,
+    NodeMetadata? Metadata) : IChangeInfo
 {
+
     public static ImmutableArray<NodePropertyInfo> CreatePropertyInfos(IEnumerable<INodeProperty> properties,
         bool isInput, Guid guid)
     {
-        return properties.Select(p => new NodePropertyInfo(p.InternalPropertyName, p.DisplayName, p.ValueType, isInput, GetNonOverridenValue(p), guid))
+        return properties.Select(p => new NodePropertyInfo(p.InternalPropertyName, p.DisplayName, p.ValueType, isInput,
+                GetNonOverridenValue(p), guid))
             .ToImmutableArray();
     }
-    
+
     public static CreateNode_ChangeInfo CreateFromNode(IReadOnlyNode node)
     {
         if (node is IReadOnlyStructureNode structureNode)
@@ -42,12 +45,22 @@ public record CreateNode_ChangeInfo(
 
         if (string.IsNullOrEmpty(internalName))
         {
-            throw new ArgumentException("Node does not have a unique name attribute. Please add [NodeInfo(\"UNIQUE_NAME\")] to the node class.");
+            throw new ArgumentException(
+                "Node does not have a unique name attribute. Please add [NodeInfo(\"UNIQUE_NAME\")] to the node class.");
+        }
+
+        Guid? pairNodeGuid = null;
+        if (node is IPairNode pairNode)
+        {
+            pairNodeGuid = pairNode.OtherNode;
         }
         
+        NodeMetadata metadata = new NodeMetadata(node) { PairNodeGuid = pairNodeGuid };
+
         return new CreateNode_ChangeInfo(internalName, node.DisplayName, node.Position,
             node.Id,
-            CreatePropertyInfos(node.InputProperties, true, node.Id), CreatePropertyInfos(node.OutputProperties, false, node.Id));
+            CreatePropertyInfos(node.InputProperties, true, node.Id),
+            CreatePropertyInfos(node.OutputProperties, false, node.Id), metadata);
     }
 
     private static object? GetNonOverridenValue(INodeProperty property) => property switch

+ 42 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodeMetadata.cs

@@ -0,0 +1,42 @@
+using System.Reflection;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public class NodeMetadata
+{
+    public bool IsPairNode { get; private set; }
+    public bool IsPairNodeStart { get; private set; }
+    public bool IsPairNodeEnd => IsPairNode && !IsPairNodeStart;
+
+    public Guid? PairNodeGuid { get; set; }
+    public string? ZoneUniqueName { get; private set; }
+    
+    public Type NodeType { get; private set; }
+
+    public NodeMetadata(Type type)
+    {
+        NodeType = type;
+        AddAttributes(type);
+    }
+    
+    public NodeMetadata(IReadOnlyNode node) : this(node.GetType()) { }
+
+    private void AddAttributes(Type type)
+    {
+        AddPairAttributes(type);
+    }
+
+    private void AddPairAttributes(Type type)
+    {
+        var attribute = type.GetCustomAttribute<PairNodeAttribute>();
+
+        if (attribute == null)
+            return;
+
+        ZoneUniqueName = attribute.ZoneUniqueName;
+        IsPairNode = true;
+        IsPairNodeStart = attribute.IsStartingType;
+    }
+}

+ 5 - 3
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs

@@ -18,9 +18,10 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
         bool hasMask,
         bool maskIsVisible,
         ImmutableArray<NodePropertyInfo> Inputs,
-        ImmutableArray<NodePropertyInfo> Outputs
+        ImmutableArray<NodePropertyInfo> Outputs,
+        NodeMetadata metadata
     ) : base(internalName, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
-        maskIsVisible, Inputs, Outputs)
+        maskIsVisible, Inputs, Outputs, metadata)
     {
     }
 
@@ -36,6 +37,7 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
             folder.Id,
             folder.Mask.Value is not null,
             folder.MaskIsVisible.Value, CreatePropertyInfos(folder.InputProperties, true, folder.Id),
-            CreatePropertyInfos(folder.OutputProperties, false, folder.Id));
+            CreatePropertyInfos(folder.OutputProperties, false, folder.Id),
+            new NodeMetadata(folder));
     }
 }

+ 5 - 3
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs

@@ -21,9 +21,10 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
         bool maskIsVisible,
         bool lockTransparency,
         ImmutableArray<NodePropertyInfo> inputs,
-        ImmutableArray<NodePropertyInfo> outputs) :
+        ImmutableArray<NodePropertyInfo> outputs,
+        NodeMetadata metadata) :
         base(internalName, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
-            maskIsVisible, inputs, outputs)
+            maskIsVisible, inputs, outputs, metadata)
     {
         LockTransparency = lockTransparency;
     }
@@ -44,7 +45,8 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
             layer.MaskIsVisible.Value,
             layer is ITransparencyLockable { LockTransparency: true },
             CreatePropertyInfos(layer.InputProperties, true, layer.Id),
-            CreatePropertyInfos(layer.OutputProperties, false, layer.Id)
+            CreatePropertyInfos(layer.OutputProperties, false, layer.Id),
+            new NodeMetadata(layer.GetType())
         );
     }
 }

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

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

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs

@@ -8,7 +8,7 @@ internal class AnimationData : IReadOnlyAnimationData
     public int FrameRate { get; set; } = 24;
     public int OnionFrames { get; set; } = 1;
     public IReadOnlyList<IReadOnlyKeyFrame> KeyFrames => keyFrames;
-    public double OnionOpacity { get; set; }
+    public double OnionOpacity { get; set; } = 50;
 
     private List<KeyFrame> keyFrames = new List<KeyFrame>();
     private readonly Document document;

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

@@ -58,38 +58,80 @@ public class FuncContext
 
     public Float2 NewFloat2(Expression x, Expression y)
     {
+        if (!HasContext && x is Float1 firstFloat && y is Float1 secondFloat)
+        {
+            Float2 constantFloat = new Float2("");
+            constantFloat.ConstantValue = new VecD(firstFloat.ConstantValue, secondFloat.ConstantValue);
+            return constantFloat;
+        }
+        
         return Builder.ConstructFloat2(x, y);
     }
 
     public Float1 NewFloat1(Expression result)
     {
+        if (!HasContext && result is Float1 float1)
+        {
+            Float1 constantFloat = new Float1("");
+            constantFloat.ConstantValue = float1.ConstantValue;
+            return constantFloat;
+        }
+        
         return Builder.ConstructFloat1(result);
     }
 
 
     public Int2 NewInt2(Expression first, Expression second)
     {
+        if (!HasContext && first is Int1 firstInt && second is Int1 secondInt)
+        {
+            Int2 constantInt = new Int2("");
+            constantInt.ConstantValue = new VecI(firstInt.ConstantValue, secondInt.ConstantValue);
+            return constantInt;
+        }
+        
         return Builder.ConstructInt2(first, second);
     }
 
     public Half4 NewHalf4(Expression r, Expression g, Expression b, Expression a)
     {
+        if (!HasContext && r is Float1 firstFloat && g is Float1 secondFloat && b is Float1 thirdFloat && a is Float1 fourthFloat)
+        {
+            Half4 constantHalf4 = new Half4("");
+            byte rByte = (byte)firstFloat.ConstantValue;
+            byte gByte = (byte)secondFloat.ConstantValue;
+            byte bByte = (byte)thirdFloat.ConstantValue;
+            byte aByte = (byte)fourthFloat.ConstantValue;
+            constantHalf4.ConstantValue = new Color(rByte, gByte, bByte, aByte);
+            return constantHalf4;
+        }
+        
         return Builder.ConstructHalf4(r, g, b, a);
     }
 
 
     public Half4 NewHalf4(Expression assignment)
     {
+        if (!HasContext && assignment is Half4 half4)
+        {
+            Half4 constantHalf4 = new Half4("");
+            constantHalf4.ConstantValue = half4.ConstantValue;
+            return constantHalf4;
+        }
+        
         return Builder.AssignNewHalf4(assignment);
     }
 
     public Float1 GetValue(FuncInputProperty<Float1> getFrom)
     {
-        if (getFrom.Connection == null || !IsFuncType(getFrom))
+        if (HasContext)
         {
-            string uniformName = $"float_{Builder.GetUniqueNameNumber()}";
-            Builder.AddUniform(uniformName, (float)getFrom.Value(this).ConstantValue);
-            return new Float1(uniformName);
+            if (getFrom.Connection == null || !IsFuncType(getFrom))
+            {
+                string uniformName = $"float_{Builder.GetUniqueNameNumber()}";
+                Builder.AddUniform(uniformName, (float)getFrom.Value(this).ConstantValue);
+                return new Float1(uniformName);
+            }
         }
 
         return getFrom.Value(this);
@@ -97,11 +139,14 @@ public class FuncContext
 
     public Expression GetValue(FuncInputProperty<Int1> getFrom)
     {
-        if (getFrom.Connection == null || !IsFuncType(getFrom))
+        if (HasContext)
         {
-            string uniformName = $"int_{Builder.GetUniqueNameNumber()}";
-            Builder.AddUniform(uniformName, (int)getFrom.Value(this).ConstantValue);
-            return new Expression(uniformName);
+            if (getFrom.Connection == null || !IsFuncType(getFrom))
+            {
+                string uniformName = $"int_{Builder.GetUniqueNameNumber()}";
+                Builder.AddUniform(uniformName, (int)getFrom.Value(this).ConstantValue);
+                return new Expression(uniformName);
+            }
         }
 
         return getFrom.Value(this);
@@ -114,12 +159,15 @@ public class FuncContext
 
     public ShaderExpressionVariable GetValue(FuncInputProperty<Half4> getFrom)
     {
-        if (getFrom.Connection == null || !IsFuncType(getFrom))
+        if (HasContext)
         {
-            Half4 color = getFrom.Value(this);
-            color.VariableName = $"color_{Builder.GetUniqueNameNumber()}";
-            Builder.AddUniform(color.VariableName, color.ConstantValue);
-            return color;
+            if (getFrom.Connection == null || !IsFuncType(getFrom))
+            {
+                Half4 color = getFrom.Value(this);
+                color.VariableName = $"color_{Builder.GetUniqueNameNumber()}";
+                Builder.AddUniform(color.VariableName, color.ConstantValue);
+                return color;
+            }
         }
 
         return getFrom.Value(this);
@@ -127,12 +175,15 @@ public class FuncContext
 
     public Float2 GetValue(FuncInputProperty<Float2> getFrom)
     {
-        if (getFrom.Connection == null || !IsFuncType(getFrom))
+        if (HasContext)
         {
-            Float2 value = getFrom.Value(this);
-            value.VariableName = $"float2_{Builder.GetUniqueNameNumber()}";
-            Builder.AddUniform(value.VariableName, value.ConstantValue);
-            return value;
+            if (getFrom.Connection == null || !IsFuncType(getFrom))
+            {
+                Float2 value = getFrom.Value(this);
+                value.VariableName = $"float2_{Builder.GetUniqueNameNumber()}";
+                Builder.AddUniform(value.VariableName, value.ConstantValue);
+                return value;
+            }
         }
 
         return getFrom.Value(this);

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

@@ -57,7 +57,7 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
                 }
                 else if (sourceObj is Expression expression)
                 {
-                    ((ShaderExpressionVariable)toReturn).OverrideExpression = expression;
+                    ((ShaderExpressionVariable)toReturn).OverrideExpression = Adjust(expression, toReturn);
                 }
 
                 return (T)toReturn;
@@ -67,6 +67,16 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
         };
         return func;
     }
+    
+    private Expression Adjust(Expression expression, object toReturn)
+    {
+        if (expression is IMultiValueVariable multiVal && toReturn is not IMultiValueVariable)
+        {
+            return multiVal.GetValueAt(0);
+        }
+
+        return expression;
+    }
 
     object? IFuncInputProperty.GetFuncConstantValue() => constantNonOverrideValue;
 

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

@@ -3,6 +3,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.Common;
+using PixiEditor.DrawingApi.Core.Shaders.Generation;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
@@ -10,6 +11,8 @@ public class InputProperty : IInputProperty
 {
     private object _internalValue;
     private int _lastExecuteHash = -1;
+    private PropertyValidator? validator;
+    
     public string InternalPropertyName { get; }
     public string DisplayName { get; }
 
@@ -46,6 +49,19 @@ public class InputProperty : IInputProperty
             _internalValue = value;
         }
     }
+    
+    public PropertyValidator Validator
+    {
+        get
+        {
+            if (validator is null)
+            {
+                validator = new PropertyValidator();
+            }
+
+            return validator;
+        }
+    }
 
     protected virtual object FuncFactory(object toReturn)
     {
@@ -126,7 +142,7 @@ public class InputProperty : IInputProperty
             return new InputProperty(forNode, InternalPropertyName, DisplayName, enumVal, ValueType);
         }
 
-        if (NonOverridenValue is null)
+        if (NonOverridenValue is null || (!NonOverridenValue.GetType().IsValueType && NonOverridenValue.GetType() != typeof(string)))
         {
             object? nullValue = null;
             if (ValueType.IsValueType)
@@ -137,8 +153,13 @@ public class InputProperty : IInputProperty
             return new InputProperty(forNode, InternalPropertyName, DisplayName, nullValue, ValueType);
         }
         
-        if(!NonOverridenValue.GetType().IsValueType && NonOverridenValue.GetType() != typeof(string))
-            throw new InvalidOperationException("Value is not cloneable and not a primitive type");
+        /*if(!NonOverridenValue.GetType().IsValueType && NonOverridenValue.GetType() != typeof(string))
+            throw new InvalidOperationException($"Value of type {NonOverridenValue.GetType()} is not cloneable and not a primitive type");*/
+
+        if (!NonOverridenValue.GetType().IsValueType && NonOverridenValue.GetType() != typeof(string))
+        {
+            
+        }
         
         return new InputProperty(forNode, InternalPropertyName, DisplayName, NonOverridenValue, ValueType);
     }
@@ -153,16 +174,27 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
         {
             object value = base.Value;
             if (value is null) return default(T);
-            
-            if(value is T tValue)
+
+            if (value is T tValue)
                 return tValue;
 
             if (value is Delegate func && typeof(T).IsAssignableTo(typeof(Delegate)))
             {
-                return (T)FuncFactoryDelegate(func); 
+                return (T)FuncFactoryDelegate(func);
+            }
+            
+            object target = value;
+            if(value is ShaderExpressionVariable shaderExpression)
+            {
+                target = shaderExpression.GetConstant();
             }
 
-            return ConversionTable.TryConvert(value, typeof(T), out object result) ? (T)result : default;
+            if (!ConversionTable.TryConvert(target, typeof(T), out object result))
+            {
+                return default;
+            }
+
+            return (T)Validator.GetClosestValidValue(result);
         }
     }
 
@@ -171,8 +203,15 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
         get => (T)(base.NonOverridenValue ?? default(T));
         set => base.NonOverridenValue = value;
     }
-    
-    internal InputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node, internalName, displayName, defaultValue, typeof(T))
+
+    internal InputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node,
+        internalName, displayName, defaultValue, typeof(T))
+    {
+    }
+
+    public InputProperty<T> WithRules(Action<PropertyValidator> rules)
     {
+        rules(Validator);
+        return this;
     }
 }

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPairNodeEnd.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPairNode.cs

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
-public interface IPairNodeEnd
+public interface IPairNode
 {
-    public Node StartNode { get; set; }
+    public Guid OtherNode { get; set; }
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -45,6 +45,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         while (queueNodes.Count > 0)
         {
             var node = queueNodes.Dequeue();
+            
             if (finalQueue.Contains(node))
             {
                 continue;

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

@@ -5,11 +5,7 @@ public class NodeInfoAttribute : Attribute
 {
     public string UniqueName { get; }
     
-    public string DisplayName { get; }
-    
-    public string? PickerName { get; set; }
-
-    public NodeInfoAttribute(string uniqueName, string displayName)
+    public NodeInfoAttribute(string uniqueName)
     {
         if (!uniqueName.StartsWith("PixiEditor"))
         {
@@ -17,6 +13,5 @@ public class NodeInfoAttribute : Attribute
         }
         
         UniqueName = uniqueName;
-        DisplayName = displayName;
     }
 }

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

@@ -3,7 +3,7 @@ using PixiEditor.DrawingApi.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
 
-[NodeInfo("Time", "TIME_NODE")]
+[NodeInfo("Time")]
 public class TimeNode : Node
 {
     public OutputProperty<int> ActiveFrame { get; set; }

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

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineChannels", "COMBINE_CHANNELS_NODE")]
+[NodeInfo("CombineChannels")]
 public class CombineChannelsNode : Node
 {
     private readonly Paint _screenPaint = new() { BlendMode = BlendMode.Screen };

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

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineColor", "COMBINE_COLOR_NODE")]
+[NodeInfo("CombineColor")]
 public class CombineColorNode : Node
 {
     public FuncOutputProperty<Half4> Color { get; }

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecD.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecDNode.cs

@@ -7,8 +7,8 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineVecD", "COMBINE_VECD_NODE")]
-public class CombineVecD : Node
+[NodeInfo("CombineVecD")]
+public class CombineVecDNode : Node
 {
     public FuncOutputProperty<Float2> Vector { get; }
     
@@ -16,7 +16,7 @@ public class CombineVecD : Node
     
     public FuncInputProperty<Float1> Y { get; }
     
-    public CombineVecD()
+    public CombineVecDNode()
     {
         Vector = CreateFuncOutput(nameof(Vector), "VECTOR", GetVector);
 
@@ -38,5 +38,5 @@ public class CombineVecD : Node
     }
 
 
-    public override Node CreateCopy() => new CombineVecD();
+    public override Node CreateCopy() => new CombineVecDNode();
 }

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecI.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecINode.cs

@@ -6,8 +6,8 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineVecI", "COMBINE_VECI_NODE")]
-public class CombineVecI : Node
+[NodeInfo("CombineVecI")]
+public class CombineVecINode : Node
 {
     public FuncOutputProperty<Int2> Vector { get; }
     
@@ -15,7 +15,7 @@ public class CombineVecI : Node
     
     public FuncInputProperty<Int1> Y { get; }
 
-    public CombineVecI()
+    public CombineVecINode()
     {
         Vector = CreateFuncOutput(nameof(Vector), "VECTOR", GetVector);
 
@@ -38,5 +38,5 @@ public class CombineVecI : Node
     }
 
 
-    public override Node CreateCopy() => new CombineVecI();
+    public override Node CreateCopy() => new CombineVecINode();
 }

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

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateChannels", "SEPARATE_CHANNELS_NODE")]
+[NodeInfo("SeparateChannels")]
 public class SeparateChannelsNode : Node
 {
     private readonly Paint _paint = new();

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

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateColor", "SEPARATE_COLOR_NODE")]
+[NodeInfo("SeparateColor")]
 public class SeparateColorNode : Node
 {
     public FuncInputProperty<Half4> Color { get; }

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

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateVecD", "SEPARATE_VECD_NODE")]
+[NodeInfo("SeparateVecD")]
 public class SeparateVecDNode : Node
 {
     public FuncInputProperty<Float2> Vector { get; }

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

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateVecI", "SEPARATE_VECI_NODE")]
+[NodeInfo("SeparateVecI")]
 public class SeparateVecINode : Node
 {
     public FuncInputProperty<Int2> Vector { get; }

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

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("CreateImage", "CREATE_IMAGE_NODE")]
+[NodeInfo("CreateImage")]
 public class CreateImageNode : Node
 {
     private Paint _paint = new();
@@ -21,7 +21,7 @@ public class CreateImageNode : Node
     public CreateImageNode()
     {
         Output = CreateOutput<Texture>(nameof(Output), "EMPTY_IMAGE", null);
-        Size = CreateInput(nameof(Size), "SIZE", new VecI(32, 32));
+        Size = CreateInput(nameof(Size), "SIZE", new VecI(32, 32)).WithRules(v => v.Min(VecI.One));
         Fill = CreateInput(nameof(Fill), "FILL", new Color(0, 0, 0, 255));
     }
 

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreatingNode.md

@@ -8,6 +8,7 @@ with solving common problems that may occur during the process.
 1. Each node needs to inherit from `Node` class
 2. Each node class needs `NodeInfoAttribute` with Unique Name and Display name, display name should be localized, unique name should be unique across all nodes.
 3. Node inputs are serialized and therefore any input should have a proper SerializationFactory class, otherwise saving fails.
+4. Run Tests to make sure node doesn't have any obvious issues.
 
 # Important notes
 

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

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 // TODO: Add based on debug mode, not debug build.
-[NodeInfo("DebugBlendMode", "Debug Blend Mode")]
+[NodeInfo("DebugBlendMode")]
 public class DebugBlendModeNode : Node
 {
     private Paint _paint = new();

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

@@ -1,79 +1 @@
-using PixiEditor.ChangeableDocument.Rendering;
-using PixiEditor.DrawingApi.Core;
-using PixiEditor.DrawingApi.Core.ColorsImpl;
-using PixiEditor.DrawingApi.Core.Surfaces;
-using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-
-[NodeInfo("Ellipse", "ELLIPSE_NODE")]
-public class EllipseNode : Node
-{
-    public InputProperty<VecI> Radius { get; }
-    public InputProperty<Color> StrokeColor { get; }
-    public InputProperty<Color> FillColor { get; }
-    public InputProperty<int> StrokeWidth { get; }
-    public OutputProperty<Texture> Output { get; }
-
-    private ChunkyImage? workingImage;
-    private Texture? targetSurface;
-
-    private VecI _lastRadius = new VecI(-1, -1);
-    private Color _lastStrokeColor = new Color(0, 0, 0, 0);
-    private Color _lastFillColor = new Color(0, 0, 0, 0);
-    private int _lastStrokeWidth = -1;
-    private Paint replacingPaint = new Paint() { BlendMode = BlendMode.Src };
-
-    public EllipseNode()
-    {
-        Radius = CreateInput<VecI>("Radius", "RADIUS", new VecI(32, 32));
-        StrokeColor = CreateInput<Color>("StrokeColor", "STROKE_COLOR", new Color(0, 0, 0, 255));
-        FillColor = CreateInput<Color>("FillColor", "FILL_COLOR", new Color(0, 0, 0, 255));
-        StrokeWidth = CreateInput<int>("StrokeWidth", "STROKE_WIDTH", 1);
-        Output = CreateOutput<Texture?>("Output", "OUTPUT", null);
-    }
-
-    protected override Texture? OnExecute(RenderingContext context)
-    {
-        var radius = Radius.Value;
-        VecI targetDimensions = radius * 2;
-
-        if (workingImage is null || workingImage.LatestSize.X != targetDimensions.X ||
-            workingImage.LatestSize.Y != targetDimensions.Y)
-        {
-            workingImage?.Dispose();
-
-            if (Radius.Value.LongestAxis <= 0)
-            {
-                Output.Value = null;
-                return null;
-            }
-            
-            workingImage = new ChunkyImage(targetDimensions);
-
-            targetSurface = RequestTexture(0, targetDimensions);
-        }
-
-        if (radius != _lastRadius || StrokeColor.Value != _lastStrokeColor || FillColor.Value != _lastFillColor ||
-            StrokeWidth.Value != _lastStrokeWidth)
-        {
-            _lastRadius = radius;
-            _lastStrokeColor = StrokeColor.Value;
-            _lastFillColor = FillColor.Value;
-            _lastStrokeWidth = StrokeWidth.Value;
-
-            RectI location = new RectI(VecI.Zero, targetDimensions);
-            workingImage.EnqueueDrawEllipse(location, StrokeColor.Value, FillColor.Value, StrokeWidth.Value);
-            workingImage.CommitChanges();
-        }
-
-        workingImage.DrawMostUpToDateChunkOn(context.ChunkToUpdate, context.ChunkResolution, targetSurface.DrawingSurface, VecI.Zero,
-            replacingPaint);
-
-        Output.Value = targetSurface;
-        return targetSurface;
-    }
-
-    public override Node CreateCopy() => new EllipseNode();
-}
+

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

@@ -5,7 +5,7 @@ using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("ApplyFilter", "APPLY_FILTER_NODE")]
+[NodeInfo("ApplyFilter")]
 public class ApplyFilterNode : Node
 {
     private Paint _paint = new();

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

@@ -3,7 +3,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("ColorMatrixFilter", "COLOR_MATRIX_TRANSFORM_FILTER_NODE")]
+[NodeInfo("ColorMatrixFilter")]
 public class ColorMatrixFilterNode : FilterNode
 {
     public InputProperty<ColorMatrix> Matrix { get; }

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

@@ -3,7 +3,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("GrayscaleFilter", "GRAYSCALE_FILTER_NODE")]
+[NodeInfo("GrayscaleFilter")]
 public class GrayscaleNode : FilterNode
 {
     private static readonly ColorMatrix WeightedMatrix = ColorMatrix.WeightedWavelengthGrayscale + ColorMatrix.UseAlpha;

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

@@ -4,7 +4,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("KernelFilter", "KERNEL_FILTER_NODE")]
+[NodeInfo("KernelFilter")]
 public class KernelFilterNode : FilterNode
 {
     private readonly Paint _paint = new();

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

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Folder", "FOLDER_NODE")]
+[NodeInfo("Folder")]
 public class FolderNode : StructureNode, IReadOnlyFolderNode
 {
     public InputProperty<Texture?> Content { get; }

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

@@ -10,7 +10,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ImageLayer", "IMAGE_LAYER_NODE")]
+[NodeInfo("ImageLayer")]
 public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 {
     public const string ImageFramesKey = "Frames";

+ 0 - 31
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs

@@ -1,31 +0,0 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.ChangeableDocument.Rendering;
-using PixiEditor.DrawingApi.Core;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-
-[NodeInfo("ImageSpace", "IMAGE_SPACE_NODE")]
-public class ImageSpaceNode : Node
-{
-    public FuncOutputProperty<VecD> SpacePosition { get; }
-    
-    public FuncOutputProperty<VecI> Size { get; }
-
-    public ImageSpaceNode()
-    {
-        // TODO: Implement this
-        //SpacePosition = CreateFuncOutput(nameof(SpacePosition), "UV", ctx => ctx.Position);
-        Size = CreateFuncOutput(nameof(Size), "SIZE", ctx => ctx.Size);
-    }
-
-
-    protected override Texture? OnExecute(RenderingContext context)
-    {
-        return null;
-    }
-
-
-    public override Node CreateCopy() => new ImageSpaceNode();
-}

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

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Lerp", "LERP_NODE")]
+[NodeInfo("Lerp")]
 public class LerpColorNode : Node // TODO: ILerpable as inputs? 
 {
     public FuncOutputProperty<Half4> Result { get; } 

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

@@ -8,7 +8,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Math", "MATH_NODE")]
+[NodeInfo("Math")]
 public class MathNode : Node
 {
     public FuncOutputProperty<Float1> Result { get; }
@@ -34,26 +34,45 @@ public class MathNode : Node
     {
         var (x, y) = GetValues(context);
 
-        var result = Mode.Value switch
+        if (context.HasContext)
         {
-            MathNodeMode.Add => ShaderMath.Add(x, y),
-            MathNodeMode.Subtract => ShaderMath.Subtract(x, y),
-            MathNodeMode.Multiply => ShaderMath.Multiply(x, y),
-            MathNodeMode.Divide => ShaderMath.Divide(x, y),
-            MathNodeMode.Sin => ShaderMath.Sin(x),
-            MathNodeMode.Cos => ShaderMath.Cos(x),
-            MathNodeMode.Tan => ShaderMath.Tan(x),
-        };
+            var result = Mode.Value switch
+            {
+                MathNodeMode.Add => ShaderMath.Add(x, y),
+                MathNodeMode.Subtract => ShaderMath.Subtract(x, y),
+                MathNodeMode.Multiply => ShaderMath.Multiply(x, y),
+                MathNodeMode.Divide => ShaderMath.Divide(x, y),
+                MathNodeMode.Sin => ShaderMath.Sin(x),
+                MathNodeMode.Cos => ShaderMath.Cos(x),
+                MathNodeMode.Tan => ShaderMath.Tan(x),
+            };
 
-        if (Clamp.Value)
-        {
-            result = ShaderMath.Clamp(result, (Float1)0, (Float1)1);
+            if (Clamp.Value)
+            {
+                result = ShaderMath.Clamp(result, (Float1)0, (Float1)1);
+            }
+
+            return context.NewFloat1(result);
         }
-        
-        return context.NewFloat1(result);
+
+        var xConst = x.ConstantValue;
+        var yConst = y.ConstantValue;
+            
+        var constValue = Mode.Value switch
+        {
+            MathNodeMode.Add => xConst + yConst,
+            MathNodeMode.Subtract => xConst - yConst,
+            MathNodeMode.Multiply => xConst * yConst,
+            MathNodeMode.Divide => xConst / yConst,
+            MathNodeMode.Sin => Math.Sin(xConst),
+            MathNodeMode.Cos => Math.Cos(xConst),
+            MathNodeMode.Tan => Math.Tan(xConst),
+        };
+            
+        return new Float1(string.Empty) { ConstantValue = constValue };
     }
 
-    private (Float1 x, Float1 y) GetValues(FuncContext context)
+    private (Float1 xConst, Float1 y) GetValues(FuncContext context)
     {
         return (context.GetValue(X), context.GetValue(Y));
     }

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

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Merge", "MERGE_NODE")]
+[NodeInfo("Merge")]
 public class MergeNode : Node, IBackgroundInput
 {
     private Paint _paint = new();

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

@@ -13,16 +13,18 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ModifyImageLeft", "MODIFY_IMAGE_LEFT_NODE", PickerName = "MODIFY_IMAGE_PAIR_NODE")]
+[NodeInfo("ModifyImageLeft")]
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
-public class ModifyImageLeftNode : Node
+public class ModifyImageLeftNode : Node, IPairNode
 {
     public InputProperty<Texture?> Image { get; }
-    
+
     public FuncOutputProperty<Float2> Coordinate { get; }
-    
+
     public FuncOutputProperty<Half4> Color { get; }
 
+    public Guid OtherNode { get; set; }
+
     private ConcurrentDictionary<RenderingContext, Pixmap> pixmapCache = new();
 
     public ModifyImageLeftNode()
@@ -53,7 +55,7 @@ public class ModifyImageLeftNode : Node
     {
         pixmapCache[forContext] = Image.Value?.PeekReadOnlyPixels();
     }
-    
+
     internal void DisposePixmap(RenderingContext forContext)
     {
         if (pixmapCache.TryRemove(forContext, out var targetPixmap))

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

@@ -14,11 +14,11 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ModifyImageRight", "MODIFY_IMAGE_RIGHT_NODE", PickerName = "")]
+[NodeInfo("ModifyImageRight")]
 [PairNode(typeof(ModifyImageLeftNode), "ModifyImageZone")]
-public class ModifyImageRightNode : Node, IPairNodeEnd, ICustomShaderNode
+public class ModifyImageRightNode : Node, IPairNode, ICustomShaderNode
 {
-    public Node StartNode { get; set; }
+    public Guid OtherNode { get; set; }
 
     private Paint drawingPaint = new Paint() { BlendMode = BlendMode.Src };
 
@@ -40,16 +40,21 @@ public class ModifyImageRightNode : Node, IPairNodeEnd, ICustomShaderNode
 
     protected override Texture? OnExecute(RenderingContext renderingContext)
     {
-        if (StartNode == null)
+        if (OtherNode == null)
         {
             FindStartNode();
-            if (StartNode == null)
+            if (OtherNode == null)
             {
                 return null;
             }
         }
 
-        var startNode = StartNode as ModifyImageLeftNode;
+        var startNode = FindStartNode(); 
+        if (startNode == null)
+        {
+            return null;
+        }
+        
         if (startNode.Image.Value is not { Size: var size })
         {
             return null;
@@ -171,18 +176,22 @@ public class ModifyImageRightNode : Node, IPairNodeEnd, ICustomShaderNode
         drawingPaint?.Dispose();
     }
 
-    private void FindStartNode()
+    private ModifyImageLeftNode FindStartNode()
     {
+        ModifyImageLeftNode startNode = null;
         TraverseBackwards(node =>
         {
             if (node is ModifyImageLeftNode leftNode)
             {
-                StartNode = leftNode;
+                startNode = leftNode;
+                OtherNode = leftNode.Id;
                 return false;
             }
 
             return true;
         });
+        
+        return startNode;
     }
 
     public override Node CreateCopy() => new ModifyImageRightNode();

+ 15 - 18
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -5,6 +5,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.Common;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Shaders;
@@ -44,11 +45,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected virtual bool AffectedByChunkToUpdate { get; }
 
-    protected Node()
-    {
-        displayName = GetType().GetCustomAttribute<NodeInfoAttribute>().DisplayName;
-    }
-
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
     IReadOnlyList<IReadOnlyKeyFrameData> IReadOnlyNode.KeyFrames => keyFrames;
@@ -66,13 +62,13 @@ public abstract class Node : IReadOnlyNode, IDisposable
     private bool _keyFramesDirty;
     private Texture? _lastCachedResult;
     private bool _isDisposed;
-    
+
     private Dictionary<int, Texture> _managedTextures = new();
 
     public Texture? Execute(RenderingContext context)
     {
         var result = ExecuteInternal(context);
-        
+
         if (result is null)
         {
             return null;
@@ -84,8 +80,8 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     internal Texture? ExecuteInternal(RenderingContext context)
     {
-        if(_isDisposed) throw new ObjectDisposedException("Node was disposed before execution.");
-        
+        if (_isDisposed) throw new ObjectDisposedException("Node was disposed before execution.");
+
         if (!CacheChanged(context)) return CachedResult;
 
         CachedResult = OnExecute(context);
@@ -93,7 +89,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         {
             throw new ObjectDisposedException("Texture was disposed after execution.");
         }
-        
+
         UpdateCache(context);
         return CachedResult;
     }
@@ -133,15 +129,15 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 _managedTextures[id] = texture;
                 return texture;
             }
-            
+
             if (clear)
             {
                 texture.DrawingSurface.Canvas.Clear(Colors.Transparent);
             }
-            
+
             return texture;
         }
-        
+
         _managedTextures[id] = new Texture(size);
         return _managedTextures[id];
     }
@@ -315,7 +311,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 keyFrame.Dispose();
             }
         }
-        
+
         foreach (var texture in _managedTextures)
         {
             texture.Value.Dispose();
@@ -370,18 +366,19 @@ public abstract class Node : IReadOnlyNode, IDisposable
         {
             var cloneOutput = outputs[i];
             var newOutput = cloneOutput.Clone(clone);
-            clone.outputs[i].Value = newOutput.Value; 
+            clone.outputs[i].Value = newOutput.Value;
         }
-        
+
         foreach (var keyFrame in keyFrames)
         {
             KeyFrameData newKeyFrame = new KeyFrameData(keyFrame.KeyFrameGuid, keyFrame.StartFrame, keyFrame.Duration,
                 keyFrame.AffectedElement)
             {
-                IsVisible = keyFrame.IsVisible, Duration = keyFrame.Duration,
+                IsVisible = keyFrame.IsVisible,
+                Duration = keyFrame.Duration,
                 Data = keyFrame.Data is ICloneable cloneable ? cloneable.Clone() : keyFrame.Data
             };
-            
+
             clone.keyFrames.Add(newKeyFrame);
         }
 

+ 41 - 22
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs

@@ -2,43 +2,54 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Shaders;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Noise", "NOISE_NODE")]
+[NodeInfo("Noise")]
 public class NoiseNode : Node
 {
     private double previousScale = double.NaN;
     private double previousSeed = double.NaN;
     private NoiseType previousNoiseType = Nodes.NoiseType.FractalPerlin;
     private int previousOctaves = -1;
-    
+    private VecD previousOffset = new VecD(0d, 0d);
+
     private Paint paint = new();
-    
+
     private static readonly ColorFilter grayscaleFilter = ColorFilter.CreateColorMatrix(
         ColorMatrix.MapAlphaToRedGreenBlue + ColorMatrix.OpaqueAlphaOffset);
-    
+
     public OutputProperty<Texture> Noise { get; }
-    
+
     public InputProperty<NoiseType> NoiseType { get; }
     public InputProperty<VecI> Size { get; }
+
+    public InputProperty<VecD> Offset { get; }
     
     public InputProperty<double> Scale { get; }
-    
+
     public InputProperty<int> Octaves { get; }
-    
+
     public InputProperty<double> Seed { get; }
 
     public NoiseNode()
     {
         Noise = CreateOutput<Texture>(nameof(Noise), "NOISE", null);
         NoiseType = CreateInput(nameof(NoiseType), "NOISE_TYPE", Nodes.NoiseType.FractalPerlin);
-        Size = CreateInput(nameof(Size), "SIZE", new VecI(64, 64));
-        Scale = CreateInput(nameof(Scale), "SCALE", 10d);
-        Octaves = CreateInput(nameof(Octaves), "OCTAVES", 1);
+        Size = CreateInput(nameof(Size), "SIZE", new VecI(64, 64))
+            .WithRules(v => v.Min(VecI.One)
+            );
+
+        Offset = CreateInput(nameof(Offset), "OFFSET", new VecD(0d, 0d));
+        
+        Scale = CreateInput(nameof(Scale), "SCALE", 10d).WithRules(v => v.Min(0.1));
+        Octaves = CreateInput(nameof(Octaves), "OCTAVES", 1)
+            .WithRules(validator => validator.Min(1));
+
         Seed = CreateInput(nameof(Seed), "SEED", 0d);
     }
 
@@ -48,59 +59,67 @@ public class NoiseNode : Node
             || previousSeed != Seed.Value
             || previousOctaves != Octaves.Value
             || previousNoiseType != NoiseType.Value
+            || previousOffset != Offset.Value
             || double.IsNaN(previousScale))
         {
-            if(Scale.Value < 0.000001)
+            if (Scale.Value < 0.000001)
             {
                 Noise.Value = null;
                 return null;
             }
-            
+
             var shader = SelectShader();
             if (shader == null)
             {
                 Noise.Value = null;
                 return null;
             }
-            
+
             paint.Shader = shader;
-            
+
             // Define a grayscale color filter to apply to the image
-            paint.ColorFilter = grayscaleFilter; 
+            paint.ColorFilter = grayscaleFilter;
             
             previousScale = Scale.Value;
             previousSeed = Seed.Value;
             previousOctaves = Octaves.Value;
             previousNoiseType = NoiseType.Value;
         }
-        
+
         var size = Size.Value;
-        
+
         if (size.X < 1 || size.Y < 1)
         {
             Noise.Value = null;
             return null;
         }
+
+        var workingSurface = RequestTexture(0, size);
+
+        workingSurface.DrawingSurface.Canvas.Save();
+        workingSurface.DrawingSurface.Canvas.Translate(-(float)Offset.Value.X, -(float)Offset.Value.Y);
         
-        var workingSurface = RequestTexture(0, size); 
-       
         workingSurface.DrawingSurface.Canvas.DrawPaint(paint);
+        
+        workingSurface.DrawingSurface.Canvas.Restore();
 
         Noise.Value = workingSurface;
-        
+
         return Noise.Value;
     }
 
     private Shader SelectShader()
     {
+        int octaves = Math.Max(1, Octaves.Value);
         Shader shader = NoiseType.Value switch
         {
             Nodes.NoiseType.TurbulencePerlin => Shader.CreatePerlinNoiseTurbulence(
                 (float)(1d / Scale.Value),
-                (float)(1d / Scale.Value), Octaves.Value, (float)Seed.Value),
+                (float)(1d / Scale.Value), octaves, (float)Seed.Value),
             Nodes.NoiseType.FractalPerlin => Shader.CreatePerlinFractalNoise(
                 (float)(1d / Scale.Value),
-                (float)(1d / Scale.Value), Octaves.Value, (float)Seed.Value),
+                (float)(1d / Scale.Value),
+                octaves, (float)Seed.Value),
             _ => null
         };
 

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

@@ -5,7 +5,7 @@ using PixiEditor.DrawingApi.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Output", "OUTPUT_NODE", PickerName = "")]
+[NodeInfo("Output")]
 public class OutputNode : Node, IBackgroundInput
 {
     public const string InputPropertyName = "Background";

+ 0 - 51
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/DistributePointsNode.cs

@@ -1,51 +0,0 @@
-using PixiEditor.ChangeableDocument.Rendering;
-using PixiEditor.DrawingApi.Core;
-using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
-
-[NodeInfo("DistributePoints", "DISTRIBUTE_POINTS")]
-public class DistributePointsNode : Node
-{
-    public OutputProperty<PointList> Points { get; }
-
-    public InputProperty<int> MaxPointCount { get; }
-
-    public InputProperty<int> Seed { get; }
-
-    public DistributePointsNode()
-    {
-        Points = CreateOutput(nameof(Points), "POINTS", PointList.Empty);
-
-        MaxPointCount = CreateInput("MaxPointCount", "MAX_POINTS", 10);
-        Seed = CreateInput("Seed", "SEED", 0);
-    }
-
-    protected override Texture? OnExecute(RenderingContext context)
-    {
-        Points.Value = GetPointsRandomly();
-        
-        return null;
-    }
-
-    private PointList GetPointsRandomly()
-    {
-        var seed = Seed.Value;
-        var random = new Random(seed);
-        var pointCount = MaxPointCount.Value;
-        var finalPoints = new PointList(pointCount)
-        {
-            HashValue = HashCode.Combine(pointCount, seed)
-        };
-
-        for (int i = 0; i < pointCount; i++)
-        {
-            finalPoints.Add(new VecD(random.NextDouble(), random.NextDouble()));
-        }
-        
-        return finalPoints;
-    }
-
-    public override Node CreateCopy() => new DistributePointsNode();
-}

+ 0 - 25
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/PointList.cs

@@ -1,25 +0,0 @@
-using PixiEditor.Common;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
-
-public class PointList : List<VecD>, ICacheable
-{
-    public required int HashValue { get; set; }
-
-    public PointList()
-    {
-    }
-
-    public PointList(IEnumerable<VecD> collection) : base(collection)
-    {
-    }
-
-    public PointList(int capacity) : base(capacity)
-    {
-    }
-
-    public static PointList Empty { get; } = new(0) { HashValue = 0 };
-
-    public int GetCacheHash() => HashValue;
-}

+ 0 - 50
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RasterizePointsNode.cs

@@ -1,50 +0,0 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
-using PixiEditor.ChangeableDocument.Rendering;
-using PixiEditor.DrawingApi.Core;
-using PixiEditor.DrawingApi.Core.ColorsImpl;
-using PixiEditor.DrawingApi.Core.Shaders.Generation;
-using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
-using PixiEditor.DrawingApi.Core.Surfaces;
-using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
-
-[NodeInfo("RasterizePoints", "RASTERIZE_POINTS")]
-public class RasterizePointsNode : Node
-{
-    private Paint _paint = new() { Color = Colors.White };
-
-    public OutputProperty<Texture> Image { get; }
-
-    public InputProperty<PointList> Points { get; }
-
-
-    public RasterizePointsNode()
-    {
-        Image = CreateOutput<Texture>("Image", "IMAGE", null);
-        Points = CreateInput("Points", "POINTS", PointList.Empty);
-    }
-
-    protected override Texture? OnExecute(RenderingContext context)
-    {
-        var points = Points.Value;
-
-        if (points.Count == 0)
-            return null;
-
-        var size = context.DocumentSize;
-        var image = RequestTexture(0, size);
-
-        image.DrawingSurface.Canvas.DrawPoints(
-            PointMode.Points, 
-            points.Select(x => new Point((float)x.X * size.X, (float)x.Y * size.Y)).ToArray(),
-            _paint);
-
-        Image.Value = image;
-        
-        return image;
-    }
-
-    public override Node CreateCopy() => new RasterizePointsNode();
-}

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

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("SampleImage", "SAMPLE_IMAGE")]
+[NodeInfo("SampleImage")]
 public class SampleImageNode : Node
 {
     public InputProperty<Texture?> Image { get; }

+ 55 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseData.cs

@@ -0,0 +1,55 @@
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public class EllipseData : ShapeData
+{
+    public VecD Center { get; set; }
+    public VecD Radius { get; set; }
+
+    public EllipseData(VecD center, VecD radius)
+    {
+        Center = center;
+        Radius = radius;
+    }
+    
+    public override void Rasterize(DrawingSurface drawingSurface)
+    {
+        var imageSize = new VecI((int)Radius.X * 2, (int)Radius.Y * 2);
+
+        using ChunkyImage img = new ChunkyImage(imageSize);
+        RectI rect = new RectI(0, 0, (int)Radius.X * 2, (int)Radius.Y * 2);
+        
+        img.EnqueueDrawEllipse(rect, StrokeColor, FillColor, StrokeWidth);
+        img.CommitChanges();
+        
+        VecI pos = new VecI((int)(Center.X - Radius.X), (int)(Center.Y - Radius.Y));
+        img.DrawMostUpToDateRegionOn(rect, ChunkResolution.Full, drawingSurface, pos);
+    }
+
+    public override bool IsValid()
+    {
+        return Radius is { X: > 0, Y: > 0 };
+    }
+
+    public override int CalculateHash()
+    {
+        return HashCode.Combine(Center, Radius);
+    }
+
+    public override int GetCacheHash()
+    {
+        return CalculateHash();
+    }
+
+    public override object Clone()
+    {
+        return new EllipseData(Center, Radius)
+        {
+            StrokeColor = StrokeColor,
+            FillColor = FillColor,
+            StrokeWidth = StrokeWidth
+        };
+    }
+}

+ 49 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsData.cs

@@ -0,0 +1,49 @@
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public class PointsData : ShapeData
+{
+    public List<VecD> Points { get; set; } = new();
+    
+    public PointsData(IEnumerable<VecD> points)
+    {
+        Points = new List<VecD>(points);
+    }
+    
+    public override void Rasterize(DrawingSurface drawingSurface)
+    {
+        using Paint paint = new Paint();
+        paint.Color = FillColor;
+        paint.StrokeWidth = StrokeWidth;
+        
+        drawingSurface.Canvas.DrawPoints(PointMode.Points, Points.Select(p => new Point((int)p.X, (int)p.Y)).ToArray(), paint);
+    }
+
+    public override bool IsValid()
+    {
+        return Points.Count > 0;
+    }
+
+    public override int GetCacheHash()
+    {
+        return CalculateHash();
+    }
+
+    public override int CalculateHash()
+    {
+        return Points.GetHashCode();
+    }
+
+    public override object Clone()
+    {
+        return new PointsData(Points)
+        {
+            StrokeColor = StrokeColor,
+            FillColor = FillColor,
+            StrokeWidth = StrokeWidth
+        };
+    }
+}

+ 24 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeData.cs

@@ -0,0 +1,24 @@
+using PixiEditor.Common;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public abstract class ShapeData : ICacheable, ICloneable
+{
+    public Color StrokeColor { get; set; } = Colors.White;
+    public Color FillColor { get; set; } = Colors.White;
+    public int StrokeWidth { get; set; } = 1;
+
+    public abstract void Rasterize(DrawingSurface drawingSurface);
+    public abstract bool IsValid();
+
+    public abstract int GetCacheHash();
+    public abstract int CalculateHash();
+    public abstract object Clone();
+
+    public override int GetHashCode()
+    {
+        return CalculateHash();
+    }
+}

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

@@ -0,0 +1,44 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.Numerics;
+using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+
+[NodeInfo("DistributePoints")]
+public class DistributePointsNode : ShapeNode<PointsData>
+{
+    public InputProperty<int> MaxPointCount { get; }
+
+    public InputProperty<int> Seed { get; }
+
+    public DistributePointsNode()
+    {
+        MaxPointCount = CreateInput("MaxPointCount", "MAX_POINTS", 10).
+            WithRules(v => v.Min(1));
+        Seed = CreateInput("Seed", "SEED", 0);
+    }
+
+    protected override PointsData? GetShapeData(RenderingContext context)
+    {
+        return GetPointsRandomly(context.DocumentSize);
+    }
+
+    private PointsData GetPointsRandomly(VecI size)
+    {
+        var seed = Seed.Value;
+        var random = new Random(seed);
+        var pointCount = MaxPointCount.Value;
+
+        List<VecD> finalPoints = new List<VecD>(pointCount);
+        for (int i = 0; i < pointCount; i++)
+        {
+            finalPoints.Add(new VecD(random.NextDouble() * size.X, random.NextDouble() * size.Y));
+        }
+        
+        var shapeData = new PointsData(finalPoints);
+        return shapeData;
+    }
+
+    public override Node CreateCopy() => new DistributePointsNode();
+}

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

@@ -0,0 +1,35 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Numerics;
+using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+
+[NodeInfo("Ellipse")]
+public class EllipseNode : ShapeNode<EllipseData>
+{
+    public InputProperty<VecD> Position { get; }
+    public InputProperty<VecD> Radius { get; }
+    public InputProperty<Color> StrokeColor { get; }
+    public InputProperty<Color> FillColor { get; }
+    public InputProperty<int> StrokeWidth { get; }
+
+    public EllipseNode()
+    {
+        Position = CreateInput<VecD>("Position", "POSITION", VecI.Zero);
+        Radius = CreateInput<VecD>("Radius", "RADIUS", new VecD(32, 32)).WithRules(
+            v => v.Min(new VecD(1)));
+        StrokeColor = CreateInput<Color>("StrokeColor", "STROKE_COLOR", new Color(0, 0, 0, 255));
+        FillColor = CreateInput<Color>("FillColor", "FILL_COLOR", new Color(0, 0, 0, 255));
+        StrokeWidth = CreateInput<int>("StrokeWidth", "STROKE_WIDTH", 1);
+    }
+
+    protected override EllipseData? GetShapeData(RenderingContext context)
+    {
+        return new EllipseData(Position.Value, Radius.Value)
+            { StrokeColor = StrokeColor.Value, FillColor = FillColor.Value, StrokeWidth = StrokeWidth.Value };
+    }
+
+    public override Node CreateCopy() => new EllipseNode();
+}

+ 43 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs

@@ -0,0 +1,43 @@
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+
+[NodeInfo("RasterizeShape")]
+public class RasterizeShapeNode : Node
+{
+    public OutputProperty<Texture> Image { get; }
+
+    public InputProperty<ShapeData> Data { get; }
+
+
+    public RasterizeShapeNode()
+    {
+        Image = CreateOutput<Texture>("Image", "IMAGE", null);
+        Data = CreateInput<ShapeData>("Points", "SHAPE", null);
+    }
+
+    protected override Texture? OnExecute(RenderingContext context)
+    {
+        var shape = Data.Value;
+
+        if (shape == null || !shape.IsValid())
+            return null;
+
+        var size = context.DocumentSize;
+        var image = RequestTexture(0, size);
+        
+        shape.Rasterize(image.DrawingSurface);
+
+        Image.Value = image;
+        
+        return image;
+    }
+
+    public override Node CreateCopy() => new RasterizeShapeNode();
+}

+ 23 - 22
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RemoveClosePointsNode.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs

@@ -1,41 +1,41 @@
-using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Numerics;
+using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
-[NodeInfo("RemoveClosePoints", "REMOVE_CLOSE_POINTS")]
-public class RemoveClosePointsNode : Node
+[NodeInfo("RemoveClosePoints")]
+public class RemoveClosePointsNode : ShapeNode<PointsData>
 {
-    public OutputProperty<PointList> Output { get; }
-    
-    public InputProperty<PointList> Input { get; }
-    
+    public InputProperty<PointsData> Input { get; }
+
     public InputProperty<double> MinDistance { get; }
 
     public InputProperty<int> Seed { get; }
 
     public RemoveClosePointsNode()
     {
-        Output = CreateOutput("Output", "POINTS", PointList.Empty);
-        Input = CreateInput("Input", "POINTS", PointList.Empty);
+        Input = CreateInput<PointsData>("Input", "POINTS", null);
         MinDistance = CreateInput("MinDistance", "MIN_DISTANCE", 0d);
         Seed = CreateInput("Seed", "SEED", 0);
     }
-    
-    protected override Texture? OnExecute(RenderingContext context)
+
+    protected override PointsData? GetShapeData(RenderingContext context)
     {
+        var data = Input.Value;
+
         var distance = MinDistance.Value;
 
-        if (distance == 0)
+        if (distance == 0 || data == null || data.Points == null)
         {
-            Output.Value = Input.Value;
             return null;
         }
 
-        var availablePoints = Input.Value.Distinct().ToList();
-        var newPoints = new PointList(availablePoints.Count) { HashValue = HashCode.Combine(Input.Value.HashValue, MinDistance.Value, Seed.Value) };
-
+        var availablePoints = data.Points.Distinct().ToList();
+        List<VecD> newPoints = new List<VecD>();
+        
         var minDistance = MinDistance.Value;
         var documentSize = context.DocumentSize;
 
@@ -54,18 +54,19 @@ public class RemoveClosePointsNode : Node
             }
 
             continue;
-            bool InRange(VecD other) => (other.Multiply(documentSize) - point.Multiply(documentSize)).Length <= minDistance;
+
+            bool InRange(VecD other) =>
+                (other - point).Length <= minDistance;
         }
 
         if (availablePoints.Count == 1)
         {
             newPoints.Add(availablePoints[0]);
         }
-        
-        Output.Value = newPoints;
-        
-        return null;
 
+        var finalData = new PointsData(newPoints);
+
+        return finalData;
     }
 
     public override Node CreateCopy() => new RemoveClosePointsNode();

+ 39 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs

@@ -0,0 +1,39 @@
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.Numerics;
+using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+
+public abstract class ShapeNode<T> : Node where T : ShapeData
+{
+    public OutputProperty<T> Output { get; }
+    
+    public ShapeNode()
+    {
+        Output = CreateOutput<T>("Output", "OUTPUT", null);
+    }
+
+    protected override Texture? OnExecute(RenderingContext context)
+    {
+        var data = GetShapeData(context);
+
+        Output.Value = data;
+        
+        if (data == null || !data.IsValid())
+            return null;
+
+        return RasterizePreview(data, context.DocumentSize);
+    }
+    
+    protected abstract T? GetShapeData(RenderingContext context);
+
+    public Texture RasterizePreview(ShapeData data, VecI size)
+    {
+        Texture texture = RequestTexture(0, size);
+        
+        data.Rasterize(texture.DrawingSurface);
+        
+        return texture;
+    }
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/OutputProperty.cs

@@ -68,7 +68,7 @@ public class OutputProperty : IOutputProperty
 
     public OutputProperty Clone(Node clone)
     {
-        if (Value is null)
+        if (Value is null || (Value is not ICloneable && !Value.GetType().IsPrimitive && Value.GetType() != typeof(string)))
         {
             object defaultValue = null;
             if(ValueType.IsValueType)

+ 68 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/PropertyValidator.cs

@@ -0,0 +1,68 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph;
+
+public delegate (bool validationResult, object? closestValidValue) ValidateProperty(object? value);
+
+public class PropertyValidator
+{
+    public List<ValidateProperty> Rules { get; } = new();
+
+    public PropertyValidator Min(VecI min)
+    {
+       return Min(min, v => new VecI(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y))); 
+    }
+    
+    public PropertyValidator Min(VecD min)
+    {
+        return Min(min, v => new VecD(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y))); 
+    }
+
+    public PropertyValidator Min<T>(T min, Func<T, T>? adjust = null) where T : IComparable<T>
+    {
+        Rules.Add((value) =>
+        {
+            if (value is T val)
+            {
+                bool isValid = val.CompareTo(min) >= 0;
+                return (isValid, isValid ? val : GetReturnValue(val, min, adjust));
+            }
+
+            return (false, GetReturnValue(min, min, adjust));
+        });
+
+        return this;
+    }
+
+    private object? GetReturnValue<T>(T original, T min, Func<T, T>? fallback) where T : IComparable<T>
+    {
+        if (fallback != null)
+        {
+            return fallback(original);
+        }
+
+        return min;
+    }
+
+    public bool Validate(object? value)
+    {
+        object lastValue = value;
+
+        foreach (var rule in Rules)
+        {
+            var (isValid, toPass) = rule(lastValue);
+            lastValue = toPass;
+            if (!isValid)
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public object? GetClosestValidValue(object? o)
+    {
+        return Rules.Aggregate(o, (current, rule) => rule(current).closestValidValue);
+    }
+}

+ 92 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectProperties_Change.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
@@ -41,6 +42,11 @@ internal class ConnectProperties_Change : Change
         {
             return false;
         }
+        
+        if(IsLoop(inputProp, outputProp))
+        {
+            return false;
+        }
 
         bool canConnect = CheckTypeCompatibility(inputProp, outputProp);
 
@@ -53,6 +59,20 @@ internal class ConnectProperties_Change : Change
 
         return true;
     }
+    
+    private bool IsLoop(InputProperty input, OutputProperty output)
+    {
+        if (input.Node == output.Node)
+        {
+            return true;
+        }
+        if(input.Node.OutputProperties.Any(x => x.Connections.Any(y => y.Node == output.Node)))
+        {
+            return true;
+        }
+
+        return false;
+    }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
@@ -105,8 +125,30 @@ internal class ConnectProperties_Change : Change
             {
                 return true;
             }
+            
+            var outputValue = output.Value;
+            
+            if(IsExpressionToConstant(output, input, out var result))
+            {
+                outputValue = result;
+            }
+            
+            if(IsConstantToExpression(input, outputValue, out result))
+            {
+                return ConversionTable.TryConvert(result, output.ValueType, out _);
+            }
+
+            if (outputValue == null)
+            {
+                return true;
+            }
+            
+            if (outputValue.GetType().IsAssignableTo(input.ValueType))
+            {
+                return true;
+            }
 
-            if (ConversionTable.TryConvert(output.Value, input.ValueType, out _))
+            if (ConversionTable.TryConvert(outputValue, input.ValueType, out _))
             {
                 return true;
             }
@@ -116,6 +158,55 @@ internal class ConnectProperties_Change : Change
 
         return true;
     }
+    
+    private static bool IsConstantToExpression(InputProperty input, object objValue, out object result)
+    {
+        if (input.Value is Delegate func && func.Method.ReturnType.IsAssignableTo(typeof(ShaderExpressionVariable)))
+        {
+            try
+            {
+                var actualArg = func.DynamicInvoke(FuncContext.NoContext);
+                if(actualArg is ShaderExpressionVariable variable)
+                {
+                    result = variable.GetConstant();
+                    return true;
+                }
+            }
+            catch
+            {
+                result = null;
+                return false;
+            }
+        }
+
+        result = null;
+        return false;
+    }
+
+    private static bool IsExpressionToConstant(OutputProperty output, InputProperty input, out object o)
+    {
+        if (output.Value is Delegate func && func.Method.ReturnType.IsAssignableTo(typeof(ShaderExpressionVariable)))
+        {
+            try
+            {
+                o = func.DynamicInvoke(FuncContext.NoContext);
+                if(o is ShaderExpressionVariable variable)
+                {
+                    o = variable.GetConstant();
+                }
+                
+                return true;
+            }
+            catch
+            {
+                o = null;
+                return false;
+            }
+        }
+
+        o = null;
+        return false;
+    }
 
     private static bool IsCrossExpression(object first, Type secondType)
     {

+ 18 - 16
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodePair_Change.cs

@@ -11,54 +11,57 @@ internal class CreateNodePair_Change : Change
 {
     private Guid startId;
     private Guid endId;
-    private Guid zoneId;
     private Type nodeType;
-    
+
     [GenerateMakeChangeAction]
-    public CreateNodePair_Change(Guid startId, Guid endId, Guid zoneId, Type nodeType)
+    public CreateNodePair_Change(Guid startId, Guid endId, Type nodeType)
     {
         this.startId = startId;
         this.endId = endId;
-        this.zoneId = zoneId;
         this.nodeType = nodeType;
     }
 
     public override bool InitializeAndValidate(Document target)
     {
-        return nodeType.GetCustomAttribute<PairNodeAttribute>() != null && nodeType is { IsAbstract: false, IsInterface: false };
+        return nodeType.GetCustomAttribute<PairNodeAttribute>() != null &&
+               nodeType is { IsAbstract: false, IsInterface: false };
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         if (startId == Guid.Empty)
             startId = Guid.NewGuid();
         if (endId == Guid.Empty)
             endId = Guid.NewGuid();
-        
+
         PairNodeAttribute attribute = nodeType.GetCustomAttribute<PairNodeAttribute>();
         Type startingType = attribute.IsStartingType ? nodeType : attribute.OtherType;
         Type endingType = attribute.IsStartingType ? attribute.OtherType : nodeType;
-        
+
         var start = NodeOperations.CreateNode(startingType, target);
         var end = NodeOperations.CreateNode(endingType, target);
-        
-        if(end is IPairNodeEnd pairEnd)
-            pairEnd.StartNode = start;
 
         start.Id = startId;
         end.Id = endId;
+
+        if (start is IPairNode pairStart)
+            pairStart.OtherNode = end.Id;
+
+        if (end is IPairNode pairEnd)
+            pairEnd.OtherNode = start.Id;
+
         end.Position = new VecD(100, 0);
-        
+
         target.NodeGraph.AddNode(start);
         target.NodeGraph.AddNode(end);
-        
+
         ignoreInUndo = false;
 
         return new List<IChangeInfo>
         {
             CreateNode_ChangeInfo.CreateFromNode(start),
             CreateNode_ChangeInfo.CreateFromNode(end),
-            new CreateNodeZone_ChangeInfo(zoneId, $"PixiEditor.{attribute.ZoneUniqueName}", startId, endId)
         };
     }
 
@@ -66,9 +69,8 @@ internal class CreateNodePair_Change : Change
     {
         var startChange = RemoveNode(target, startId);
         var endChange = RemoveNode(target, endId);
-        var zoneChange = new DeleteNodeFrame_ChangeInfo(zoneId);
 
-        return new List<IChangeInfo> { startChange, endChange, zoneChange };
+        return new List<IChangeInfo> { startChange, endChange };
     }
 
     private static DeleteNode_ChangeInfo RemoveNode(Document target, Guid id)

+ 10 - 12
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs

@@ -15,44 +15,42 @@ internal class CreateNode_Change : Change
 {
     private Type nodeType;
     private Guid id;
-    
+
     [GenerateMakeChangeAction]
     public CreateNode_Change(Type nodeType, Guid id)
     {
         this.id = id;
         this.nodeType = nodeType;
     }
-    
+
     public override bool InitializeAndValidate(Document target)
     {
         bool canCreate = nodeType.IsSubclassOf(typeof(Node)) && nodeType is { IsAbstract: false, IsInterface: false };
         return canCreate && (!nodeType.IsAssignableTo(typeof(OutputNode)) || target.NodeGraph.OutputNode is null);
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
-        if(id == Guid.Empty)
+        if (id == Guid.Empty)
             id = Guid.NewGuid();
 
         Node node = NodeOperations.CreateNode(nodeType, target);
-        
+
         node.Position = new VecD(0, 0);
         node.Id = id;
-        
+
         target.NodeGraph.AddNode(node);
         ignoreInUndo = false;
-       
-        using RenderingContext context = new RenderingContext(new KeyFrameTime(0, 0), VecI.Zero, ChunkResolution.Full, target.Size);
-        node.ExecuteInternal(context);
-        
-        return CreateNode_ChangeInfo.CreateFromNode(node); 
+
+        return CreateNode_ChangeInfo.CreateFromNode(node);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         Node node = target.FindNodeOrThrow<Node>(id);
         target.NodeGraph.RemoveNode(node);
-        
+
         return new DeleteNode_ChangeInfo(id);
     }
 }

+ 16 - 5
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeleteNode_Change.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
@@ -13,7 +14,7 @@ internal class DeleteNode_Change : Change
     private ConnectionsData originalConnections;
 
     private Node savedCopy;
-    
+
     private GroupKeyFrame? savedKeyFrameGroup;
 
     [GenerateMakeChangeAction]
@@ -32,6 +33,10 @@ internal class DeleteNode_Change : Change
         originalConnections = NodeOperations.CreateConnectionsData(node);
 
         savedCopy = node.Clone();
+        if (savedCopy is IPairNode pairNode)
+        {
+            pairNode.OtherNode = (node as IPairNode).OtherNode;
+        }
 
         savedKeyFrameGroup = CloneGroupKeyFrame(target, NodeId);
 
@@ -71,6 +76,11 @@ internal class DeleteNode_Change : Change
     {
         var copy = savedCopy!.Clone();
         copy.Id = NodeId;
+        
+        if (copy is IPairNode pairNode)
+        {
+            pairNode.OtherNode = (savedCopy as IPairNode).OtherNode;
+        }
 
         doc.NodeGraph.AddNode(copy);
 
@@ -81,7 +91,7 @@ internal class DeleteNode_Change : Change
         changes.Add(createChange);
 
         changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, copy, doc.NodeGraph));
-        
+
         RevertKeyFrames(doc, savedKeyFrameGroup, changes);
 
         return changes;
@@ -95,10 +105,11 @@ internal class DeleteNode_Change : Change
             doc.AnimationData.AddKeyFrame(cloned);
             foreach (var keyFrame in savedKeyFrameGroup.Children)
             {
-                changes.Add(new CreateRasterKeyFrame_ChangeInfo(keyFrame.NodeId, keyFrame.StartFrame, keyFrame.Id, false));
+                changes.Add(new CreateRasterKeyFrame_ChangeInfo(keyFrame.NodeId, keyFrame.StartFrame, keyFrame.Id,
+                    false));
                 changes.Add(new KeyFrameLength_ChangeInfo(keyFrame.Id, keyFrame.StartFrame, keyFrame.Duration));
-            } 
-            
+            }
+
             changes.Add(new KeyFrameVisibility_ChangeInfo(savedKeyFrameGroup.Id, savedKeyFrameGroup.IsVisible));
         }
     }

+ 24 - 11
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/UpdateProperty_Change.cs

@@ -10,7 +10,7 @@ internal class UpdatePropertyValue_Change : Change
     private readonly string _propertyName;
     private object? _value;
     private object? previousValue;
-    
+
     [GenerateMakeChangeAction]
     public UpdatePropertyValue_Change(Guid nodeId, string property, object? value)
     {
@@ -21,24 +21,37 @@ internal class UpdatePropertyValue_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        if(target.TryFindNode<Node>(_nodeId, out var node))
+        if (target.TryFindNode<Node>(_nodeId, out var node))
         {
             return node.HasInputProperty(_propertyName);
         }
-        
+
         return false;
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         var node = target.NodeGraph.Nodes.First(x => x.Id == _nodeId);
         var property = node.GetInputProperty(_propertyName);
 
         previousValue = GetValue(property);
-        _value = SetValue(property, _value);
+        if (!property.Validator.Validate(_value))
+        {
+            _value = property.Validator.GetClosestValidValue(_value);
+            if (_value == previousValue)
+            {
+                ignoreInUndo = true;
+            }
+            
+            ignoreInUndo = false;
+        }
+        else
+        {
+            _value = SetValue(property, _value);
+            ignoreInUndo = false;
+        }
 
-        ignoreInUndo = false;
-        
         return new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, _value);
     }
 
@@ -47,7 +60,7 @@ internal class UpdatePropertyValue_Change : Change
         var node = target.NodeGraph.Nodes.First(x => x.Id == _nodeId);
         var property = node.GetInputProperty(_propertyName);
         SetValue(property, previousValue);
-        
+
         return new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, previousValue);
     }
 
@@ -63,13 +76,13 @@ internal class UpdatePropertyValue_Change : Change
             {
                 value = Enum.ToObject(property.ValueType, value);
             }
-            
+
             property.NonOverridenValue = value;
         }
-        
+
         return value;
     }
-    
+
 
     private static object? GetValue(InputProperty property)
     {

+ 1 - 4
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -47,10 +47,7 @@ internal class CreateStructureMember_Change : Change
         List<IChangeInfo> changes = new() { CreateChangeInfo(member) };
         
         InputProperty<Texture> targetInput = parentNode.InputProperties.FirstOrDefault(x => 
-            x.ValueType == typeof(Texture) &&
-            x.Connection != null &&
-            x.Connection.Node is StructureNode) as InputProperty<Texture>;
-        
+            x.ValueType == typeof(Texture)) as InputProperty<Texture>;
         
         
         if (member is FolderNode folder)

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

@@ -1,12 +1,26 @@
-namespace PixiEditor.ChangeableDocument.Enums;
+using System.ComponentModel;
+
+namespace PixiEditor.ChangeableDocument.Enums;
 
 public enum MathNodeMode
 {
+    [Description("MATH_ADD")]
     Add,
+    [Description("MATH_SUBTRACT")]
     Subtract,
+    [Description("MULTIPLY")]
     Multiply,
+    [Description("DIVIDE")]
     Divide,
+    [Description("SIN")]
     Sin,
+    [Description("COS")]
     Cos,
+    [Description("TAN")]
     Tan,
 }
+
+public static class MathNodeModeExtensions
+{
+    public static bool UsesYValue(this MathNodeMode mode) => !(mode is >= MathNodeMode.Sin and <= MathNodeMode.Tan);
+}

+ 2 - 27
src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj

@@ -5,8 +5,9 @@
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
     <Configurations>Debug;Release;Steam;DevRelease;DevSteam</Configurations>
-    <Platforms>AnyCPU;x86;x64</Platforms>
+    <Platforms>AnyCPU;x64</Platforms>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">
@@ -18,49 +19,23 @@
     <PlatformTarget>AnyCPU</PlatformTarget>
   </PropertyGroup>
 
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|x86'">
-    <Optimize>True</Optimize>
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
     <PlatformTarget>AnyCPU</PlatformTarget>
   </PropertyGroup>
 
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|x64' ">
     <PlatformTarget>AnyCPU</PlatformTarget>
   </PropertyGroup>
 
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|x86' ">
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
     <PlatformTarget>AnyCPU</PlatformTarget>
   </PropertyGroup>
 
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevSteam|x86' ">
-    <Optimize>True</Optimize>
-    <PlatformTarget>AnyCPU</PlatformTarget>
-  </PropertyGroup>
-
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevSteam|x64' ">
     <Optimize>True</Optimize>
     <PlatformTarget>AnyCPU</PlatformTarget>
   </PropertyGroup>
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'DevSteam' ">
-    <Optimize>True</Optimize>
-  </PropertyGroup>
-
   <ItemGroup>
     <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj" />
     <ProjectReference Include="..\PixiEditor.ChangeableDocument.Gen\PixiEditor.ChangeableDocument.Gen.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

+ 4 - 1
src/PixiEditor.ChangeableDocument/Rendering/RenderingContext.cs

@@ -1,4 +1,6 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
+using System;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
@@ -19,6 +21,7 @@ public class RenderingContext : IDisposable
     public VecI ChunkToUpdate { get; }
     public ChunkResolution ChunkResolution { get; }
     public VecI DocumentSize { get; set; }
+    
 
     public bool IsDisposed { get; private set; }
     

+ 1 - 0
src/PixiEditor.ClosedBeta/PixiEditor.ClosedBeta.csproj

@@ -10,6 +10,7 @@
     <WasmSingleFileBundle>true</WasmSingleFileBundle>
     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
     <PixiExtOutputPath>..\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+    <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
   </PropertyGroup>
 
   <ItemGroup>

+ 8 - 4
src/PixiEditor.Desktop/PixiEditor.Desktop.csproj

@@ -15,6 +15,10 @@
     <ApplicationManifest>app.manifest</ApplicationManifest>
   </PropertyGroup>
 
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+  </PropertyGroup>
+
   <ItemGroup>
     <PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)"/>
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
@@ -25,16 +29,16 @@
     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2"/>
   </ItemGroup>
 
-  <ItemGroup>
-    <ProjectReference Include="..\PixiEditor\PixiEditor.csproj"/>
-  </ItemGroup>
-
   <ItemGroup>
     <None Include="..\PixiEditor\Images\favicon.ico">
       <Link>favicon.ico</Link>
     </None>
   </ItemGroup>
 
+  <ItemGroup>
+    <ProjectReference Include="..\PixiEditor\PixiEditor.csproj" />
+  </ItemGroup>
+
   <Target Name="Rename" AfterTargets="AfterBuild">
     <Move SourceFiles="$(OutDir)PixiEditor.Desktop.exe" DestinationFiles="$(OutDir)PixiEditor.exe" />
     <Message Text="Renamed build executable file." Importance="high" />

+ 2 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IShaderImplementation.cs

@@ -1,5 +1,6 @@
 using System;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Shaders;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Numerics;
@@ -17,4 +18,5 @@ public interface IShaderImplementation
     public Shader CreatePerlinFractalNoise(float baseFrequencyX, float baseFrequencyY, int numOctaves, float seed);
     public object GetNativeShader(IntPtr objectPointer);
     public Shader WithUpdatedUniforms(IntPtr objectPointer, Uniforms uniforms);
+    public void SetLocalMatrix(IntPtr objectPointer, Matrix3X3 matrix);
 }

+ 12 - 2
src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Float2.cs

@@ -1,8 +1,9 @@
-using PixiEditor.Numerics;
+using System;
+using PixiEditor.Numerics;
 
 namespace PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
-public class Float2(string name) : ShaderExpressionVariable<VecD>(name)
+public class Float2(string name) : ShaderExpressionVariable<VecD>(name), IMultiValueVariable
 {
     private Expression? _overrideExpression;
     public override string ConstantValueString
@@ -44,4 +45,13 @@ public class Float2(string name) : ShaderExpressionVariable<VecD>(name)
     
     public static implicit operator Float2(VecD value) => new Float2("") { ConstantValue = value };
     public static explicit operator VecD(Float2 value) => value.ConstantValue;
+    public ShaderExpressionVariable GetValueAt(int index)
+    {
+        return index switch
+        {
+            0 => X,
+            1 => Y,
+            _ => throw new IndexOutOfRangeException()
+        };
+    }
 }

+ 15 - 2
src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Half4.cs

@@ -1,8 +1,9 @@
-using PixiEditor.DrawingApi.Core.ColorsImpl;
+using System;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 
 namespace PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
-public class Half4(string name) : ShaderExpressionVariable<Color>(name)
+public class Half4(string name) : ShaderExpressionVariable<Color>(name), IMultiValueVariable
 {
     private Expression? _overrideExpression;
     public override string ConstantValueString => $"half4({ConstantValue.R}, {ConstantValue.G}, {ConstantValue.B}, {ConstantValue.A})";
@@ -23,4 +24,16 @@ public class Half4(string name) : ShaderExpressionVariable<Color>(name)
             _overrideExpression = value;
         }
     }
+
+    public ShaderExpressionVariable GetValueAt(int index)
+    {
+        return index switch
+        {
+            0 => R,
+            1 => G,
+            2 => B,
+            3 => A,
+            _ => throw new IndexOutOfRangeException()
+        };
+    }
 }

+ 13 - 2
src/PixiEditor.DrawingApi.Core/Shaders/Generation/Expressions/Int2.cs

@@ -1,8 +1,9 @@
-using PixiEditor.Numerics;
+using System;
+using PixiEditor.Numerics;
 
 namespace PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
-public class Int2(string name) : ShaderExpressionVariable<VecI>(name)
+public class Int2(string name) : ShaderExpressionVariable<VecI>(name), IMultiValueVariable
 {
     private Expression? _overrideExpression;
     public override string ConstantValueString => $"int2({ConstantValue.X}, {ConstantValue.Y})";
@@ -30,4 +31,14 @@ public class Int2(string name) : ShaderExpressionVariable<VecI>(name)
             Y.OverrideExpression = value;
         }
     }
+
+    public ShaderExpressionVariable GetValueAt(int index)
+    {
+        return index switch
+        {
+            0 => X,
+            1 => Y,
+            _ => throw new IndexOutOfRangeException()
+        };
+    }
 }

+ 8 - 0
src/PixiEditor.DrawingApi.Core/Shaders/Generation/IMultiValueVariable.cs

@@ -0,0 +1,8 @@
+using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
+
+namespace PixiEditor.DrawingApi.Core.Shaders.Generation;
+
+public interface IMultiValueVariable
+{
+    public ShaderExpressionVariable GetValueAt(int index);
+}

+ 6 - 0
src/PixiEditor.DrawingApi.Core/Shaders/Shader.cs

@@ -2,6 +2,7 @@
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Exceptions;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.Numerics;
 
@@ -57,4 +58,9 @@ public class Shader : NativeObject
     {
         return DrawingBackendApi.Current.ShaderImplementation.CreatePerlinFractalNoise(baseFrequencyX, baseFrequencyY, numOctaves, seed);
     }
+
+    public void SetLocalMatrix(Matrix3X3 matrix)
+    {
+        DrawingBackendApi.Current.ShaderImplementation.SetLocalMatrix(ObjectPointer, matrix);
+    }
 }

+ 6 - 1
src/PixiEditor.DrawingApi.Core/Texture.cs

@@ -9,7 +9,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.DrawingApi.Core;
 
-public class Texture : IDisposable
+public class Texture : IDisposable, ICloneable
 {
     public VecI Size { get; }
     public DrawingSurface DrawingSurface { get; private set; }
@@ -54,6 +54,11 @@ public class Texture : IDisposable
         DrawingSurface.Changed -= DrawingSurfaceOnChanged;
     }
 
+    public object Clone()
+    {
+        return new Texture(this);
+    }
+
     private void DrawingSurfaceOnChanged(RectD? changedRect)
     {
         Changed?.Invoke(changedRect);

+ 11 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaShaderImplementation.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Shaders;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Numerics;
@@ -122,6 +123,16 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             return new Shader(newShader.Handle);
         }
 
+        public void SetLocalMatrix(IntPtr objectPointer, Matrix3X3 matrix)
+        {
+            if (!ManagedInstances.TryGetValue(objectPointer, out var shader))
+            {
+                throw new InvalidOperationException("Shader does not exist");
+            }
+            
+            shader.WithLocalMatrix(matrix.ToSkMatrix());
+        }
+
         public void Dispose(IntPtr shaderObjPointer)
         {
             if (!ManagedInstances.TryGetValue(shaderObjPointer, out var shader)) return;

+ 0 - 43
src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj

@@ -56,47 +56,4 @@
     <ItemGroup>
       <ProjectReference Include="..\PixiEditor.Extensions.CommonApi.Diagnostics\PixiEditor.Extensions.CommonApi.Diagnostics.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
     </ItemGroup>
-  
-    <ItemGroup>
-      <PackageReference Include="protobuf-net" Version="3.2.30" />
-    </ItemGroup>
-  
-  <PropertyGroup>
-    <ProtogenVersion>3.2.42</ProtogenVersion>
-  </PropertyGroup>
-  
-  <Target Name="ProtogenCheck" BeforeTargets="GenerateProtoContracts">
-    <PropertyGroup>
-      <ProtogenExists>false</ProtogenExists>
-    </PropertyGroup>
-    <Exec Command="dotnet tool run protogen --version" IgnoreExitCode="true">
-      <Output TaskParameter="ExitCode" PropertyName="ProtogenExitCode"/>
-    </Exec>
-    <PropertyGroup>
-      <ProtogenExists Condition="'$(ProtogenExitCode)' == '0'">true</ProtogenExists>
-    </PropertyGroup>
-  </Target>
-  
-  <Target Name="InstallProtogen" BeforeTargets="GenerateProtoContracts"
-          Condition="'$(ProtogenExists)' != 'true'">
-    <Message Text="Downloading protogen v$(ProtogenVersion)..." Importance="high"/>
-    <Exec Command="dotnet tool install --local protobuf-net.Protogen --version $(ProtogenVersion)"/>
-    <PropertyGroup>
-      <ProtogenExists>true</ProtogenExists>
-    </PropertyGroup>
-    
-    <Message Text="protogen installed successfully." Importance="high"/>
-  </Target>
-    
-  
-  <Target Name="GenerateProtoContracts" BeforeTargets="BeforeCompile"
-          Inputs="$(MSBuildProjectDirectory)\DataContracts\*.proto"
-          Outputs="$(MSBuildProjectDirectory)\ProtoAutogen\*.cs">
-    <Exec Command="dotnet tool run protogen --csharp_out=ProtoAutogen --proto_path=DataContracts +listset=yes *.proto"/>
-
-    <ItemGroup>
-      <Compile Include="ProtoAutogen\*.cs" KeepDuplicates="false"/>
-    </ItemGroup>
-
-  </Target>
 </Project>

+ 1 - 0
src/PixiEditor.Extensions.Sdk/PixiEditor.Extensions.Sdk.csproj

@@ -15,6 +15,7 @@
     <EventSourceSupport>false</EventSourceSupport>
     <UseSystemResourceKeys>true</UseSystemResourceKeys>
     <NativeDebugSymbols>false</NativeDebugSymbols>
+    <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
   </PropertyGroup>
 
   <ItemGroup>

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

@@ -1,12 +1,12 @@
 {
   "runtimeTarget": {
-    "name": ".NETStandard,Version=v2.0/win-x64",
+    "name": ".NETStandard,Version=v2.0/",
     "signature": ""
   },
   "compilationOptions": {},
   "targets": {
     ".NETStandard,Version=v2.0": {},
-    ".NETStandard,Version=v2.0/win-x64": {
+    ".NETStandard,Version=v2.0/": {
       "PixiEditor.Api.CGlueMSBuild/1.0.0": {
         "dependencies": {
           "Microsoft.Build.Utilities.Core": "17.9.5",

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


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

@@ -1,12 +1,12 @@
 {
   "runtimeTarget": {
-    "name": ".NETStandard,Version=v2.0/win-x64",
+    "name": ".NETStandard,Version=v2.0/",
     "signature": ""
   },
   "compilationOptions": {},
   "targets": {
     ".NETStandard,Version=v2.0": {},
-    ".NETStandard,Version=v2.0/win-x64": {
+    ".NETStandard,Version=v2.0/": {
       "PixiEditor.Extensions.MSPackageBuilder/1.0.0": {
         "dependencies": {
           "Microsoft.Build.Utilities.Core": "17.9.5",

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


+ 1 - 1
src/PixiEditor.MSIX/Package.appxmanifest

@@ -9,7 +9,7 @@
   <Identity
     Name="56069PixiEditorOrganizati.PixiEditor"
     Publisher="CN=0AFA75AD-56A3-481D-B5E4-D3C6274DD38A"
-    Version="1.0.3.0" />
+    Version="1.2.5.0" />
 
   <Properties>
     <DisplayName>PixiEditor</DisplayName>

+ 1 - 2
src/PixiEditor.MSIX/PixiEditor.MSIX.wapproj

@@ -54,7 +54,7 @@
     <TargetPlatformVersion>10.0.22621.0</TargetPlatformVersion>
     <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
     <DefaultLanguage>en-US</DefaultLanguage>
-    <AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
+    <AppxPackageSigningEnabled>False</AppxPackageSigningEnabled>
     <EntryPointProjectUniqueName>..\PixiEditor\PixiEditor.csproj</EntryPointProjectUniqueName>
     <GenerateAppInstallerFile>False</GenerateAppInstallerFile>
     <AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
@@ -63,7 +63,6 @@
     <AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
     <GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
     <HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
-    <PackageCertificateThumbprint>22A93FB097222C16145701D346AE19000A37094E</PackageCertificateThumbprint>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
     <OutputPath>bin\Debug\</OutputPath>

+ 46 - 3
src/PixiEditor.Numerics/VecD.cs

@@ -1,6 +1,6 @@
 namespace PixiEditor.Numerics;
 
-public struct VecD : IEquatable<VecD>
+public struct VecD : IEquatable<VecD>, IComparable<VecD>
 {
     public double X { set; get; }
     public double Y { set; get; }
@@ -19,27 +19,33 @@ public struct VecD : IEquatable<VecD>
         X = x;
         Y = y;
     }
+
     public VecD(double bothAxesValue)
     {
         X = bothAxesValue;
         Y = bothAxesValue;
     }
+
     public static VecD FromAngleAndLength(double angle, double length)
     {
         return new VecD(Math.Cos(angle) * length, Math.Sin(angle) * length);
     }
+
     public VecD Round()
     {
         return new(Math.Round(X), Math.Round(Y));
     }
+
     public VecD Ceiling()
     {
         return new(Math.Ceiling(X), Math.Ceiling(Y));
     }
+
     public VecD Floor()
     {
         return new(Math.Floor(X), Math.Floor(Y));
     }
+
     /// <summary>
     ///     Rotates the vector by the specified angle in radians
     /// </summary>
@@ -52,10 +58,12 @@ public struct VecD : IEquatable<VecD>
         result.Y = X * Math.Sin(angleRad) + Y * Math.Cos(angleRad);
         return result;
     }
+
     public VecD Rotate(double angleRad, VecD around)
     {
         return (this - around).Rotate(angleRad) + around;
     }
+
     public double DistanceToLineSegment(VecD pos1, VecD pos2)
     {
         VecD segment = pos2 - pos1;
@@ -65,6 +73,7 @@ public struct VecD : IEquatable<VecD>
             return (this - pos2).Length;
         return DistanceToLine(pos1, pos2);
     }
+
     public double DistanceToLine(VecD pos1, VecD pos2)
     {
         double a = (pos1 - pos2).Length;
@@ -76,12 +85,14 @@ public struct VecD : IEquatable<VecD>
 
         return triangleArea / a * 2;
     }
+
     public VecD ProjectOntoLine(VecD pos1, VecD pos2)
     {
         VecD line = (pos2 - pos1).Normalize();
         VecD point = this - pos1;
         return (line * point) * line + pos1;
     }
+
     /// <summary>
     /// Reflects the vector across a vertical line with the specified position
     /// </summary>
@@ -89,6 +100,7 @@ public struct VecD : IEquatable<VecD>
     {
         return new(2 * lineX - X, Y);
     }
+
     /// <summary>
     /// Reflects the vector along a horizontal line with the specified position
     /// </summary>
@@ -96,11 +108,13 @@ public struct VecD : IEquatable<VecD>
     {
         return new(X, 2 * lineY - Y);
     }
+
     public VecD ReflectAcrossLine(VecD pos1, VecD pos2)
     {
         var onLine = ProjectOntoLine(pos1, pos2);
         return onLine - (this - onLine);
     }
+
     public double AngleTo(VecD other)
     {
         return Math.Acos((this * other) / Length / other.Length);
@@ -114,22 +128,27 @@ public struct VecD : IEquatable<VecD>
         var rot = other.Rotate(-Angle);
         return rot.Angle;
     }
+
     public VecD Lerp(VecD other, double factor)
     {
         return (other - this) * factor + this;
     }
+
     public VecD Normalize()
     {
         return new VecD(X / Length, Y / Length);
     }
+
     public VecD Abs()
     {
         return new VecD(Math.Abs(X), Math.Abs(Y));
     }
+
     public VecD Signs()
     {
         return new VecD(X >= 0 ? 1 : -1, Y >= 0 ? 1 : -1);
     }
+
     /// <summary>
     /// Returns the signed magnitude (Z coordinate) of the vector resulting from the cross product
     /// </summary>
@@ -139,51 +158,62 @@ public struct VecD : IEquatable<VecD>
     }
 
     public double Dot(VecD other) => (X * other.X) + (Y * other.Y);
-    
+
     public VecD Multiply(VecD other)
     {
         return new VecD(X * other.X, Y * other.Y);
     }
+
     public VecD Divide(VecD other)
     {
         return new VecD(X / other.X, Y / other.Y);
     }
+
     public static VecD operator +(VecD a, VecD b)
     {
         return new VecD(a.X + b.X, a.Y + b.Y);
     }
+
     public static VecD operator -(VecD a, VecD b)
     {
         return new VecD(a.X - b.X, a.Y - b.Y);
     }
+
     public static VecD operator -(VecD a)
     {
         return new VecD(-a.X, -a.Y);
     }
+
     public static VecD operator *(double b, VecD a)
     {
         return new VecD(a.X * b, a.Y * b);
     }
+
     public static double operator *(VecD a, VecD b)
     {
         return a.X * b.X + a.Y * b.Y;
     }
+
     public static VecD operator *(VecD a, double b)
     {
         return new VecD(a.X * b, a.Y * b);
     }
+
     public static VecD operator /(VecD a, double b)
     {
         return new VecD(a.X / b, a.Y / b);
     }
+
     public static VecD operator %(VecD a, double b)
     {
         return new(a.X % b, a.Y % b);
     }
+
     public static bool operator ==(VecD a, VecD b)
     {
         return a.X == b.X && a.Y == b.Y;
     }
+
     public static bool operator !=(VecD a, VecD b)
     {
         return !(a.X == b.X && a.Y == b.Y);
@@ -193,16 +223,18 @@ public struct VecD : IEquatable<VecD>
     {
         return new VecI((int)vec.X, (int)vec.Y);
     }
-    
+
     public static implicit operator VecD((double, double) tuple)
     {
         return new VecD(tuple.Item1, tuple.Item2);
     }
+
     public void Deconstruct(out double x, out double y)
     {
         x = X;
         y = Y;
     }
+
     public bool IsNaNOrInfinity()
     {
         return double.IsNaN(X) || double.IsNaN(Y) || double.IsInfinity(X) || double.IsInfinity(Y);
@@ -213,6 +245,17 @@ public struct VecD : IEquatable<VecD>
         return $"({X}; {Y})";
     }
 
+    public int CompareTo(VecD other)
+    {
+        int xComparison = X.CompareTo(other.X);
+        int yComparison = Y.CompareTo(other.Y);
+        if (xComparison == 0 && yComparison == 0)
+            return 0;
+
+        bool anyNegative = xComparison < 0 || yComparison < 0;
+        return anyNegative ? -1 : 1;
+    }
+
     public override bool Equals(object? obj)
     {
         var item = obj as VecD?;

+ 14 - 1
src/PixiEditor.Numerics/VecI.cs

@@ -1,6 +1,6 @@
 namespace PixiEditor.Numerics;
 
-public struct VecI : IEquatable<VecI>
+public struct VecI : IEquatable<VecI>, IComparable<VecI>
 {
     public int X { set; get; }
     public int Y { set; get; }
@@ -86,6 +86,8 @@ public struct VecI : IEquatable<VecI>
         return new VecI(Math.Clamp(X, rect.Left, rect.Right), Math.Clamp(Y, rect.Top, rect.Bottom));
     }
 
+    public bool HasNegativeComponent() => X < 0 || Y < 0;
+
     public byte[] ToByteArray()
     {
         var data = new byte[sizeof(int) * 2];
@@ -191,6 +193,17 @@ public struct VecI : IEquatable<VecI>
         return $"({X}; {Y})";
     }
 
+    public int CompareTo(VecI other)
+    {
+        int xComparison = X.CompareTo(other.X);
+        int yComparison = Y.CompareTo(other.Y);
+        if (xComparison == 0 && yComparison == 0)
+            return 0;
+        
+        bool anyNegative = xComparison < 0 || yComparison < 0;
+        return anyNegative ? -1 : 1;
+    }
+
     public override bool Equals(object? obj)
     {
         var item = obj as VecI?;

+ 45 - 4
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -38,17 +38,31 @@
 
             <Color x:Key="NotificationCardBackgroundColor">#303030</Color>
             
-            <Color x:Key="DefaultSocketColor">#c0334e</Color>
+            <!--                          -->
+            <!--     <| Node Colors |>    -->
+            <!--                          -->
+            
+            <!-- Sockets -->
+            <Color x:Key="DefaultSocketColor">#FF00FF</Color>
             <Color x:Key="ImageSocketColor">#99c47a</Color>
+            <Color x:Key="FilterSocketColor">#CC5C5C</Color>
             <Color x:Key="BoolSocketColor">#68abdf</Color>
             <Color x:Key="FloatSocketColor">#ffc66d</Color>
             <Color x:Key="DoubleSocketColor">#efb66d</Color>
-            <Color x:Key="ColorSocketColor">#99e4aa</Color>
+            <Color x:Key="ColorSocketColor">#8cf2dd</Color>
             <Color x:Key="VecDSocketColor">#c984ca</Color>
             <Color x:Key="VecISocketColor">#c9b4ca</Color>
             <Color x:Key="IntSocketColor">#4C64B1</Color>
-            <Color x:Key="PointListSocketColor">#7f5280</Color>
+            <Color x:Key="EllipseDataSocketColor">#a473a5</Color>
+            <Color x:Key="PointsDataSocketColor">#e1d0e1</Color>
+            <GradientStops x:Key="ShapeDataSocketGradient">
+                <GradientStop Offset="0" Color="{StaticResource EllipseDataSocketColor}"/>
+                <GradientStop Offset="0.5" Color="{StaticResource EllipseDataSocketColor}"/>
+                <GradientStop Offset="0.5" Color="{StaticResource PointsDataSocketColor}"/>
+                <GradientStop Offset="1" Color="{StaticResource PointsDataSocketColor}"/>
+            </GradientStops>
             
+            <!-- Zones & Frames -->
             <Color x:Key="PixiEditorModifyImageBorderColor">#68abdf</Color>
             <Color x:Key="PixiEditorModifyImageNodeBackgroundColor">#4068abdf</Color>
 
@@ -57,6 +71,15 @@
 
             <Color x:Key="NodeFrameBorderColor">#101010</Color>
             <Color x:Key="NodeFrameBackgroundColor">#40101010</Color>
+            
+            <!-- Categories -->
+            <Color x:Key="ImageCategoryBackgroundColor">#5B7348</Color>
+            <Color x:Key="StructureCategoryBackgroundColor">#735C39</Color>
+            <Color x:Key="FiltersCategoryBackgroundColor">#733535</Color>
+            <Color x:Key="ShapeCategoryBackgroundColor">#654266</Color>
+            <Color x:Key="NumbersCategoryBackgroundColor">#666666</Color>
+            <Color x:Key="ColorCategoryBackgroundColor">#3B665D</Color>
+            <Color x:Key="AnimationCategoryBackgroundColor">#4D4466</Color>
 
             <system:Double x:Key="ThemeDisabledOpacity">0.4</system:Double>
 
@@ -92,8 +115,14 @@
             
             <SolidColorBrush x:Key="SelectionFillBrush" Color="{StaticResource SelectionFillColor}"/>
             
+            <!--                           -->
+            <!--     <| Node Brushes |>    -->
+            <!--                           -->
+            
+            <!-- Sockets -->
             <SolidColorBrush x:Key="DefaultSocketBrush" Color="{StaticResource DefaultSocketColor}"/>
             <SolidColorBrush x:Key="TextureSocketBrush" Color="{StaticResource ImageSocketColor}"/>
+            <SolidColorBrush x:Key="FilterSocketBrush" Color="{StaticResource FilterSocketColor}"/>
             <SolidColorBrush x:Key="TextureSamplerSocketBrush" Color="{StaticResource ImageSocketColor}"/>
             <SolidColorBrush x:Key="BooleanSocketBrush" Color="{StaticResource BoolSocketColor}"/>
             <SolidColorBrush x:Key="SingleSocketBrush" Color="{StaticResource FloatSocketColor}"/>
@@ -107,8 +136,11 @@
             <SolidColorBrush x:Key="Int2SocketBrush" Color="{StaticResource VecISocketColor}"/>
             <SolidColorBrush x:Key="Int32SocketBrush" Color="{StaticResource IntSocketColor}"/>
             <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}"/>
-            <SolidColorBrush x:Key="PointListSocketBrush" Color="{StaticResource PointListSocketColor}"/>
+            <ConicGradientBrush x:Key="ShapeDataSocketBrush" GradientStops="{StaticResource ShapeDataSocketGradient}"/>
+            <SolidColorBrush x:Key="EllipseDataSocketBrush" Color="{StaticResource EllipseDataSocketColor}"/>
+            <SolidColorBrush x:Key="PointsDataSocketBrush" Color="{StaticResource PointsDataSocketColor}"/>
             
+            <!-- Zones & Frames -->
             <SolidColorBrush x:Key="PixiEditorModifyImageLeftBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
             <SolidColorBrush x:Key="PixiEditorModifyImageRightBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
             <SolidColorBrush x:Key="PixiEditorModifyImageZoneBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
@@ -121,6 +153,15 @@
 
             <SolidColorBrush x:Key="NodeFrameBorderBrush" Color="{StaticResource NodeFrameBorderColor}"/>
             <SolidColorBrush x:Key="NodeFrameBackgroundBrush" Color="{StaticResource NodeFrameBackgroundColor}"/>
+            
+            <!-- Categories -->
+            <SolidColorBrush x:Key="ImageCategoryBackgroundBrush" Color="{StaticResource ImageCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="StructureCategoryBackgroundBrush" Color="{StaticResource StructureCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="FiltersCategoryBackgroundBrush" Color="{StaticResource FiltersCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="ShapeCategoryBackgroundBrush" Color="{StaticResource ShapeCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="NumbersCategoryBackgroundBrush" Color="{StaticResource NumbersCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="ColorCategoryBackgroundBrush" Color="{StaticResource ColorCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="AnimationCategoryBackgroundBrush" Color="{StaticResource AnimationCategoryBackgroundColor}" />
 
             <CornerRadius x:Key="ControlCornerRadius">5</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadiusTop">5, 5, 0, 0</CornerRadius>

+ 3 - 2
src/PixiEditor.UI.Common/Controls/ListBoxItem.axaml

@@ -5,9 +5,10 @@
         <Setter Property="Background" Value="Transparent" />
         <Setter Property="BorderBrush" Value="Transparent" />
         <Setter Property="BorderThickness" Value="0" />
+        <Setter Property="Height" Value="30"/>
         <Setter Property="Template">
             <ControlTemplate>
-                <Border Height="40" 
+                <Border MinHeight="20" 
                         BorderBrush="{TemplateBinding BorderBrush}"
                         BorderThickness="{TemplateBinding BorderThickness}"
                         Background="{TemplateBinding Background}"
@@ -19,7 +20,7 @@
                     </Border.Styles>
                     
                     <Grid>
-                        <Rectangle RadiusX="8" RadiusY="8" Width="16" Height="32" IsVisible="False" Fill="{DynamicResource ThemeAccentBrush}" HorizontalAlignment="Left">
+                        <Rectangle RadiusX="8" RadiusY="8" Width="16" MinHeight="20" IsVisible="False" Fill="{DynamicResource ThemeAccentBrush}" HorizontalAlignment="Left">
                             <Rectangle.Styles>
                                 <Style Selector=":selected /template/ Rectangle">
                                     <Setter Property="IsVisible" Value="True"></Setter>

+ 1 - 1
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/App.axaml

@@ -12,7 +12,7 @@
     <Application.Resources>
         <ResourceDictionary>
             <ResourceDictionary.MergedDictionaries>
-                <ResourceInclude Source="avares://PixiEditor.UI.Common/Styles/Colors.axaml"/>
+                <ResourceInclude Source="avares://PixiEditor.UI.Common/Accents/Base.axaml"/>
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Application.Resources>

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