Переглянути джерело

Merge pull request #645 from PixiEditor/highres-toolset

Highres toolset and .NET 9
Krzysztof Krysiński 8 місяців тому
батько
коміт
898f556714
100 змінених файлів з 1097 додано та 237 видалено
  1. 2 2
      samples/Sample1_HelloWorld/Sample1_HelloWorld.csproj
  2. 2 2
      samples/Sample2_LocalizationSample/Sample2_LocalizationSample.csproj
  3. 2 2
      samples/Sample3_Preferences/Sample3_Preferences.csproj
  4. 2 2
      samples/Sample4_CreatePopup/Sample4_CreatePopup.csproj
  5. 2 2
      samples/Sample5_Resources/Sample5_Resources.csproj
  6. 2 2
      samples/Sample6_Palettes/Sample6_Palettes.csproj
  7. 2 2
      samples/Sample7_FlyUI/Sample7_FlyUI.csproj
  8. 12 2
      src/ChunkyImageLib/ChunkyImage.cs
  9. 1 1
      src/ChunkyImageLib/ChunkyImageLib.csproj
  10. 17 2
      src/ChunkyImageLib/DataHolders/ColorBounds.cs
  11. 3 0
      src/ChunkyImageLib/DataHolders/ShapeData.cs
  12. 10 1
      src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs
  13. 45 3
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  14. 76 24
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  15. 1 1
      src/ChunkyImageLibVis/ChunkyImageLibVis.csproj
  16. 1 1
      src/Drawie
  17. 1 1
      src/PixiDocks
  18. 1 1
      src/PixiEditor.AnimationRenderer.Core/PixiEditor.AnimationRenderer.Core.csproj
  19. 1 1
      src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj
  20. 1 1
      src/PixiEditor.Browser/PixiEditor.Browser.csproj
  21. 1 1
      src/PixiEditor.Builder/build/PixiEditor.Builder.csproj
  22. 5 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  23. 17 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  24. 4 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterEllipse_UpdateableChange.cs
  25. 18 3
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterLine_UpdateableChange.cs
  26. 2 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  27. 4 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  28. 72 43
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  29. 4 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs
  30. 4 2
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWand_Change.cs
  31. 8 8
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  32. 1 1
      src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj
  33. 11 10
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  34. 2 2
      src/PixiEditor.ClosedBeta/PixiEditor.ClosedBeta.csproj
  35. 1 1
      src/PixiEditor.Common/PixiEditor.Common.csproj
  36. 1 1
      src/PixiEditor.Desktop/PixiEditor.Desktop.csproj
  37. 1 1
      src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj
  38. 1 1
      src/PixiEditor.Extensions.Runtime/PixiEditor.Extensions.Runtime.csproj
  39. 1 1
      src/PixiEditor.Extensions.Sdk/PixiEditor.Extensions.Sdk.csproj
  40. 1 1
      src/PixiEditor.Extensions.WasmRuntime/PixiEditor.Extensions.WasmRuntime.csproj
  41. 1 1
      src/PixiEditor.Extensions/PixiEditor.Extensions.csproj
  42. 1 1
      src/PixiEditor.Linux/PixiEditor.Linux.csproj
  43. 1 1
      src/PixiEditor.MacOs/PixiEditor.MacOs.csproj
  44. 1 1
      src/PixiEditor.OperatingSystem/PixiEditor.OperatingSystem.csproj
  45. 1 1
      src/PixiEditor.Platform.MSStore/PixiEditor.Platform.MSStore.csproj
  46. 1 1
      src/PixiEditor.Platform.Standalone/PixiEditor.Platform.Standalone.csproj
  47. 1 1
      src/PixiEditor.Platform.Steam/PixiEditor.Platform.Steam.csproj
  48. 1 1
      src/PixiEditor.Platform/PixiEditor.Platform.csproj
  49. 1 1
      src/PixiEditor.SVG/PixiEditor.SVG.csproj
  50. 1 1
      src/PixiEditor.UI.Common/PixiEditor.UI.Common.csproj
  51. 1 1
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.Desktop/PixiEditor.UpdateInstaller.Desktop.csproj
  52. 1 1
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj
  53. 1 1
      src/PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj
  54. 1 1
      src/PixiEditor.Windows/PixiEditor.Windows.csproj
  55. 1 1
      src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj
  56. 75 2
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  57. 7 1
      src/PixiEditor/Data/Localization/Languages/en.json
  58. 46 0
      src/PixiEditor/Helpers/Converters/IntPercentConverter.cs
  59. 6 5
      src/PixiEditor/Models/AnalyticsAPI/AnalyticsClient.cs
  60. 1 1
      src/PixiEditor/Models/Commands/CommandController.cs
  61. 53 3
      src/PixiEditor/Models/Config/ConfigManager.cs
  62. 68 1
      src/PixiEditor/Models/Config/ToolSetConfig.cs
  63. 15 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ComplexShapeToolExecutor.cs
  64. 12 5
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs
  65. 4 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/FloodFillToolExecutor.cs
  66. 9 5
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  67. 3 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs
  68. 12 8
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs
  69. 3 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs
  70. 6 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterLineToolExecutor.cs
  71. 8 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs
  72. 6 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  73. 3 1
      src/PixiEditor/Models/Handlers/IToolHandler.cs
  74. 2 0
      src/PixiEditor/Models/Handlers/IToolSetHandler.cs
  75. 1 0
      src/PixiEditor/Models/Handlers/Toolbars/IBasicShapeToolbar.cs
  76. 2 0
      src/PixiEditor/Models/Handlers/Toolbars/ILineToolbar.cs
  77. 8 0
      src/PixiEditor/Models/Handlers/Toolbars/IPenToolbar.cs
  78. 1 0
      src/PixiEditor/Models/Handlers/Tools/IFloodFillToolHandler.cs
  79. 1 0
      src/PixiEditor/Models/Handlers/Tools/IMagicWandToolHandler.cs
  80. 3 0
      src/PixiEditor/Models/IO/Paths.cs
  81. 1 1
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  82. 8 1
      src/PixiEditor/PixiEditor.csproj
  83. 4 4
      src/PixiEditor/Properties/AssemblyInfo.cs
  84. 1 1
      src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs
  85. 18 5
      src/PixiEditor/ViewModels/SubViewModels/ToolSetViewModel.cs
  86. 15 7
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  87. 44 0
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/PercentSettingViewModel.cs
  88. 47 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/Setting.cs
  89. 14 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/BasicShapeToolbar.cs
  90. 23 0
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/LineToolbar.cs
  91. 32 0
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/PenToolbar.cs
  92. 23 0
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/SettingAttributes.cs
  93. 2 2
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/Toolbar.cs
  94. 10 3
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/ToolbarFactory.cs
  95. 131 6
      src/PixiEditor/ViewModels/Tools/ToolViewModel.cs
  96. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs
  97. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/ColorPickerToolViewModel.cs
  98. 2 2
      src/PixiEditor/ViewModels/Tools/Tools/EraserToolViewModel.cs
  99. 6 1
      src/PixiEditor/ViewModels/Tools/Tools/FloodFillToolViewModel.cs
  100. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/LassoToolViewModel.cs

+ 2 - 2
samples/Sample1_HelloWorld/Sample1_HelloWorld.csproj

@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
         <OutputType>Exe</OutputType>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net9.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <RootNamespace>HelloWorld</RootNamespace>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>

+ 2 - 2
samples/Sample2_LocalizationSample/Sample2_LocalizationSample.csproj

@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
         <OutputType>Exe</OutputType>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net9.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <RootNamespace>LocalizationSample</RootNamespace>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>

+ 2 - 2
samples/Sample3_Preferences/Sample3_Preferences.csproj

@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
         <OutputType>Exe</OutputType>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net9.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <RootNamespace>Preferences</RootNamespace>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>

+ 2 - 2
samples/Sample4_CreatePopup/Sample4_CreatePopup.csproj

@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
         <OutputType>Exe</OutputType>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net9.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <RootNamespace>CreatePopupSample</RootNamespace>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>

+ 2 - 2
samples/Sample5_Resources/Sample5_Resources.csproj

@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
         <OutputType>Exe</OutputType>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net9.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <RootNamespace>ResourcesSample</RootNamespace>

+ 2 - 2
samples/Sample6_Palettes/Sample6_Palettes.csproj

@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
         <OutputType>Exe</OutputType>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net9.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <RootNamespace>PalettesSample</RootNamespace>

+ 2 - 2
samples/Sample7_FlyUI/Sample7_FlyUI.csproj

@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
         <OutputType>Exe</OutputType>
         <PublishTrimmed>true</PublishTrimmed>
         <WasmSingleFileBundle>true</WasmSingleFileBundle>
         <GenerateExtensionPackage>true</GenerateExtensionPackage>
-        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <PixiExtOutputPath>..\..\src\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net9.0\win-x64\Extensions</PixiExtOutputPath>
         <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
         <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
         <RootNamespace>FlyUISample</RootNamespace>

+ 12 - 2
src/ChunkyImageLib/ChunkyImage.cs

@@ -591,13 +591,13 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawEllipse(RectI location, Color strokeColor, Color fillColor, int strokeWidth,
-        double rotationRad = 0,
+        double rotationRad = 0, bool antiAliased = false,
         Paint? paint = null)
     {
         lock (lockObject)
         {
             ThrowIfDisposed();
-            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, paint);
+            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, antiAliased, paint);
             EnqueueOperation(operation);
         }
     }
@@ -738,6 +738,16 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         }
     }
 
+    public void EnqueueDrawSkiaLine(VecI from, VecI to, Paint paint)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            DrawingSurfaceLineOperation operation = new(from, to, paint);
+            EnqueueOperation(operation);
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawPixels(IEnumerable<VecI> pixels, Color color, BlendMode blendMode)
     {

+ 1 - 1
src/ChunkyImageLib/ChunkyImageLib.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <AllowUnsafeBlocks>True</AllowUnsafeBlocks>

+ 17 - 2
src/ChunkyImageLib/DataHolders/ColorBounds.cs

@@ -21,13 +21,17 @@ public struct ColorBounds
 
     public float UpperA { get; set; }
 
-    public ColorBounds(Color color)
+    public ColorBounds(Color color, double tolerance = 0)
     {
         static (float lower, float upper) FindInclusiveBoundaryPremul(byte channel, float alpha)
         {
             float subHalf = channel > 0 ? channel - .5f : channel;
             float addHalf = channel < 255 ? channel + .5f : channel;
-            return (subHalf * alpha / 255f, addHalf * alpha / 255f);
+            
+            var lower = subHalf * alpha / 255f;
+            var upper = addHalf * alpha / 255f;
+            
+            return (lower, upper);
         }
 
         static (float lower, float upper) FindInclusiveBoundary(byte channel)
@@ -40,9 +44,20 @@ public struct ColorBounds
         float a = color.A / 255f;
 
         (LowerR, UpperR) = FindInclusiveBoundaryPremul(color.R, a);
+        LowerR -= (float)tolerance;
+        UpperR += (float)tolerance;
+        
         (LowerG, UpperG) = FindInclusiveBoundaryPremul(color.G, a);
+        LowerG -= (float)tolerance;
+        UpperG += (float)tolerance;
+        
         (LowerB, UpperB) = FindInclusiveBoundaryPremul(color.B, a);
+        LowerB -= (float)tolerance;
+        UpperB += (float)tolerance;
+        
         (LowerA, UpperA) = FindInclusiveBoundary(color.A);
+        LowerA -= (float)tolerance;
+        UpperA += (float)tolerance;
     }
 
     [MethodImpl(MethodImplOptions.AggressiveInlining)]

+ 3 - 0
src/ChunkyImageLib/DataHolders/ShapeData.cs

@@ -27,6 +27,9 @@ public record struct ShapeData
     public double Angle { get; }
     public int StrokeWidth { get; }
 
+    public bool AntiAliasing { get; set; } = false;
+    
+
     public ShapeData AsMirroredAcrossHorAxis(double horAxisY)
         => new ShapeData(Center.ReflectY(horAxisY), new(Size.X, -Size.Y), -Angle, StrokeWidth, StrokeColor, FillColor, BlendMode);
     public ShapeData AsMirroredAcrossVerAxis(double verAxisX)

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

@@ -13,6 +13,7 @@ internal class DrawingSurfaceLineOperation : IMirroredDrawOperation
     private Paint paint;
     private readonly VecI from;
     private readonly VecI to;
+    private bool isAntiAliased;
 
     public DrawingSurfaceLineOperation(VecI from, VecI to, StrokeCap strokeCap, float strokeWidth, Color color, BlendMode blendMode)
     {
@@ -27,10 +28,18 @@ internal class DrawingSurfaceLineOperation : IMirroredDrawOperation
         this.from = from;
         this.to = to;
     }
+    
+    public DrawingSurfaceLineOperation(VecI from, VecI to, Paint paint)
+    {
+        this.paint = paint.Clone();
+        this.from = from;
+        this.to = to;
+        isAntiAliased = paint.IsAntiAliased;
+    }
 
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
-        paint.IsAntiAliased = targetChunk.Resolution != ChunkResolution.Full;
+        paint.IsAntiAliased = isAntiAliased || targetChunk.Resolution != ChunkResolution.Full;
         var surf = targetChunk.Surface.DrawingSurface;
         surf.Canvas.Save();
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());

+ 45 - 3
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -25,8 +25,10 @@ internal class EllipseOperation : IMirroredDrawOperation
     private VecF[]? ellipse;
     private VecF[]? ellipseFill;
     private RectI? ellipseFillRect;
+    private bool antialiased;
 
-    public EllipseOperation(RectI location, Color strokeColor, Color fillColor, int strokeWidth, double rotationRad, Paint? paint = null)
+    public EllipseOperation(RectI location, Color strokeColor, Color fillColor, int strokeWidth, double rotationRad,
+        bool antiAliased, Paint? paint = null)
     {
         this.location = location;
         this.strokeColor = strokeColor;
@@ -34,6 +36,7 @@ internal class EllipseOperation : IMirroredDrawOperation
         this.strokeWidth = strokeWidth;
         this.rotation = rotationRad;
         this.paint = paint?.Clone() ?? new Paint();
+        antialiased = antiAliased;
     }
 
     private void Init()
@@ -76,8 +79,23 @@ internal class EllipseOperation : IMirroredDrawOperation
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
         surf.Canvas.Translate(-chunkPos * ChunkyImage.FullChunkSize);
 
-        paint.IsAntiAliased = targetChunk.Resolution != ChunkResolution.Full;
+        paint.IsAntiAliased = antialiased || targetChunk.Resolution != ChunkResolution.Full;
 
+        if (antialiased)
+        {
+            DrawAntiAliased(surf);   
+        }
+        else
+        {
+            DrawAliased(surf);
+        }
+
+        surf.Canvas.Restore();
+    }
+
+    private void DrawAliased(DrawingSurface surf)
+    {
+        paint.IsAntiAliased = false;
         if (strokeWidth == 1)
         {
             if (Math.Abs(rotation) < 0.001)
@@ -131,6 +149,30 @@ internal class EllipseOperation : IMirroredDrawOperation
             surf.Canvas.DrawColor(strokeColor, paint.BlendMode);
             surf.Canvas.Restore();
         }
+    }
+
+    private void DrawAntiAliased(DrawingSurface surf)
+    {
+        surf.Canvas.Save();
+        surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
+        
+        paint.IsAntiAliased = false;
+        paint.Color = fillColor;
+        paint.Style = PaintStyle.Fill;
+        
+        RectD fillRect = ((RectD)location).Inflate(-strokeWidth / 2f);
+        
+        surf.Canvas.DrawOval(fillRect.Center, fillRect.Size / 2f, paint);
+        
+        paint.IsAntiAliased = true;
+        paint.Color = strokeColor;
+        paint.Style = PaintStyle.Stroke;
+        paint.StrokeWidth = strokeWidth;
+        
+        RectD strokeRect = ((RectD)location).Inflate((-strokeWidth / 2f));
+        
+        surf.Canvas.DrawOval(strokeRect.Center, strokeRect.Size / 2f, paint);
+        
         surf.Canvas.Restore();
     }
 
@@ -157,7 +199,7 @@ internal class EllipseOperation : IMirroredDrawOperation
             newLocation = (RectI)newLocation.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
             newLocation = (RectI)newLocation.ReflectY((double)horAxisY).Round();
-        return new EllipseOperation(newLocation, strokeColor, fillColor, strokeWidth, rotation, paint);
+        return new EllipseOperation(newLocation, strokeColor, fillColor, strokeWidth, rotation, antialiased, paint);
     }
 
     public void Dispose()

+ 76 - 24
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -1,25 +1,30 @@
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
 namespace ChunkyImageLib.Operations;
 
 internal class RectangleOperation : IMirroredDrawOperation
 {
+    public ShapeData Data { get; }
+
+    public bool IgnoreEmptyChunks => false;
+
+    private Paint paint = new();
+
     public RectangleOperation(ShapeData rect)
     {
         Data = rect;
+        paint.StrokeWidth = Data.StrokeWidth;
+        paint.IsAntiAliased = Data.AntiAliasing;
+        paint.BlendMode = Data.BlendMode;
     }
 
-    public ShapeData Data { get; }
-
-    public bool IgnoreEmptyChunks => false;
 
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
-        var skiaSurf = targetChunk.Surface.DrawingSurface;
-
         var surf = targetChunk.Surface.DrawingSurface;
 
         var rect = RectD.FromCenterAndSize(Data.Center, Data.Size.Abs());
@@ -27,56 +32,103 @@ internal class RectangleOperation : IMirroredDrawOperation
         if (innerRect.IsZeroOrNegativeArea)
             innerRect = RectD.Empty;
 
-        surf.Canvas.Save();
+        int initial = surf.Canvas.Save();
+
+
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
         surf.Canvas.Translate(-chunkPos * ChunkyImage.FullChunkSize);
-        skiaSurf.Canvas.RotateRadians((float)Data.Angle, (float)rect.Center.X, (float)rect.Center.Y);
+        surf.Canvas.RotateRadians((float)Data.Angle, (float)rect.Center.X, (float)rect.Center.Y);
+
+        if (Data.AntiAliasing)
+        {
+            DrawAntiAliased(surf, rect);
+        }
+        else
+        {
+            DrawPixelPerfect(surf, rect, innerRect);
+        }
 
+        surf.Canvas.RestoreToCount(initial);
+    }
+
+    private void DrawPixelPerfect(DrawingSurface surf, RectD rect, RectD innerRect)
+    {
         // draw fill
         if (Data.FillColor.A > 0)
         {
-            skiaSurf.Canvas.Save();
-            skiaSurf.Canvas.ClipRect(innerRect);
-            skiaSurf.Canvas.DrawColor(Data.FillColor, Data.BlendMode);
-            skiaSurf.Canvas.Restore();
+            int saved = surf.Canvas.Save();
+            surf.Canvas.ClipRect(innerRect);
+            surf.Canvas.DrawColor(Data.FillColor, Data.BlendMode);
+            surf.Canvas.RestoreToCount(saved);
         }
 
         // draw stroke
-        skiaSurf.Canvas.Save();
-        skiaSurf.Canvas.ClipRect(rect);
-        skiaSurf.Canvas.ClipRect(innerRect, ClipOperation.Difference);
-        skiaSurf.Canvas.DrawColor(Data.StrokeColor, Data.BlendMode);
-        skiaSurf.Canvas.Restore();
+        surf.Canvas.Save();
+        surf.Canvas.ClipRect(rect);
+        surf.Canvas.ClipRect(innerRect, ClipOperation.Difference);
+        surf.Canvas.DrawColor(Data.StrokeColor, Data.BlendMode);
+    }
 
-        surf.Canvas.Restore();
+    private void DrawAntiAliased(DrawingSurface surf, RectD rect)
+    {
+        // draw fill
+        if (Data.FillColor.A > 0)
+        {
+            int saved = surf.Canvas.Save();
+
+            paint.StrokeWidth = 0;
+            paint.Color = Data.FillColor;
+            paint.Style = PaintStyle.Fill;
+            surf.Canvas.DrawRect((float)rect.Left, (float)rect.Top, (float)rect.Width, (float)rect.Height, paint);
+
+            surf.Canvas.RestoreToCount(saved);
+        }
+
+        // draw stroke
+        surf.Canvas.Save();
+        paint.StrokeWidth = (float)Data.StrokeWidth;
+        paint.Color = Data.StrokeColor;
+        paint.Style = PaintStyle.Stroke;
+        RectD innerRect = rect.Inflate(-Data.StrokeWidth / 2f);
+        surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width, (float)innerRect.Height, paint);
     }
 
     public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        if (Math.Abs(Data.Size.X) < 1 || Math.Abs(Data.Size.Y) < 1 || (Data.StrokeColor.A == 0 && Data.FillColor.A == 0))
+        if (Math.Abs(Data.Size.X) < 1 || Math.Abs(Data.Size.Y) < 1 ||
+            (Data.StrokeColor.A == 0 && Data.FillColor.A == 0))
             return new();
 
-        RectI affRect = (RectI)new ShapeCorners(Data.Center, Data.Size).AsRotated(Data.Angle, Data.Center).AABBBounds.RoundOutwards();
+        RectI affRect = (RectI)new ShapeCorners(Data.Center, Data.Size).AsRotated(Data.Angle, Data.Center).AABBBounds
+            .RoundOutwards();
 
         if (Data.FillColor.A != 0 || Math.Abs(Data.Size.X) == 1 || Math.Abs(Data.Size.Y) == 1)
-            return new (OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle, ChunkPool.FullChunkSize), affRect);
+            return new(
+                OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle,
+                    ChunkPool.FullChunkSize), affRect);
 
-        var chunks = OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle, ChunkPool.FullChunkSize);
+        var chunks =
+            OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle,
+                ChunkPool.FullChunkSize);
         chunks.ExceptWith(
             OperationHelper.FindChunksFullyInsideRectangle(
                 Data.Center,
                 Data.Size.Abs() - new VecD(Data.StrokeWidth * 2, Data.StrokeWidth * 2),
                 Data.Angle,
                 ChunkPool.FullChunkSize));
-        return new (chunks, affRect);
+        return new(chunks, affRect);
     }
 
-    public void Dispose() { }
+    public void Dispose()
+    {
+        paint.Dispose();
+    }
 
     public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         if (verAxisX is not null && horAxisY is not null)
-            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((double)horAxisY).AsMirroredAcrossVerAxis((double)verAxisX));
+            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((double)horAxisY)
+                .AsMirroredAcrossVerAxis((double)verAxisX));
         else if (verAxisX is not null)
             return new RectangleOperation(Data.AsMirroredAcrossVerAxis((double)verAxisX));
         else if (horAxisY is not null)

+ 1 - 1
src/ChunkyImageLibVis/ChunkyImageLibVis.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>WinExe</OutputType>
-    <TargetFramework>net8.0-windows</TargetFramework>
+    <TargetFramework>net9.0-windows</TargetFramework>
     <Nullable>enable</Nullable>
     <UseWPF>true</UseWPF>
     <Configurations>Debug;Release;Steam;DevRelease</Configurations>

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit f1b0d435bb5a916def8a7eed99fd71bf2749cc30
+Subproject commit 479031ffc550a71821b673c03014affdfa40d570

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit cf516edcce55f6cf0028c6446a4ecf2475606ce1
+Subproject commit e270d65878607f16f10c27c604b6e054d54e2fb5

+ 1 - 1
src/PixiEditor.AnimationRenderer.Core/PixiEditor.AnimationRenderer.Core.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

+ 1 - 1
src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <Configurations>Release;Debug</Configurations>

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

@@ -1,6 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFramework>net8.0-browser</TargetFramework>
+    <TargetFramework>net9.0-browser</TargetFramework>
     <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
     <WasmMainJSPath>wwwroot\main.js</WasmMainJSPath>
     <OutputType>Exe</OutputType>

+ 1 - 1
src/PixiEditor.Builder/build/PixiEditor.Builder.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
         <OutputType>Exe</OutputType>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
         <AssemblyName>PixiEditor.Builder</AssemblyName>
         <RootNamespace>PixiEditor.Builder</RootNamespace>

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -119,6 +119,11 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
     public void RenderForOutput(RenderContext context, DrawingSurface renderTarget, RenderOutputProperty output)
     {
+        if(IsDisposed)
+        {
+            return;
+        }
+        
         var renderObjectContext = CreateSceneContext(context, renderTarget, output);
 
         int renderSaved = renderTarget.Canvas.Save();

+ 17 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -87,7 +87,9 @@ internal class CombineStructureMembersOnto_Change : Change
         var chunksToCombine = new HashSet<VecI>();
         List<IChangeInfo> changes = new();
 
-        foreach (var guid in layersToCombine)
+        var ordererd = OrderLayers(layersToCombine, target);
+
+        foreach (var guid in ordererd)
         {
             var layer = target.FindMemberOrThrow<LayerNode>(guid);
 
@@ -150,6 +152,20 @@ internal class CombineStructureMembersOnto_Change : Change
         changes.Add(new LayerImageArea_ChangeInfo(targetLayerGuid, affArea));
         return changes;
     }
+    
+    private HashSet<Guid> OrderLayers(HashSet<Guid> layersToCombine, Document document)
+    {
+        HashSet<Guid> ordered = new();
+        document.NodeGraph.TryTraverse(node =>
+        {
+            if (node is LayerNode layer && layersToCombine.Contains(layer.Id))
+            {
+                ordered.Add(layer.Id);
+            }
+        });
+
+        return ordered.Reverse().ToHashSet();
+    }
 
     private void AddMissingKeyFrame(LayerNode targetLayer, int frame, LayerNode layer, List<IChangeInfo> changes,
         Document target)

+ 4 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterEllipse_UpdateableChange.cs

@@ -13,11 +13,12 @@ internal class DrawRasterEllipse_UpdateableChange : UpdateableChange
     private int strokeWidth;
     private readonly bool drawOnMask;
     private int frame;
+    private bool antialiased;
 
     private CommittedChunkStorage? storedChunks;
 
     [GenerateUpdateableChangeActions]
-    public DrawRasterEllipse_UpdateableChange(Guid memberGuid, RectI location, double rotationRad, Color strokeColor, Color fillColor, int strokeWidth, bool drawOnMask, int frame)
+    public DrawRasterEllipse_UpdateableChange(Guid memberGuid, RectI location, double rotationRad, Color strokeColor, Color fillColor, int strokeWidth, bool antialiased, bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.location = location;
@@ -27,6 +28,7 @@ internal class DrawRasterEllipse_UpdateableChange : UpdateableChange
         this.strokeWidth = strokeWidth;
         this.drawOnMask = drawOnMask;
         this.frame = frame;
+        this.antialiased = antialiased;
     }
 
     [UpdateChangeMethod]
@@ -53,7 +55,7 @@ internal class DrawRasterEllipse_UpdateableChange : UpdateableChange
         if (!location.IsZeroOrNegativeArea)
         {
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, targetImage, memberGuid, drawOnMask);
-            targetImage.EnqueueDrawEllipse(location, strokeColor, fillColor, strokeWidth, rotation);
+            targetImage.EnqueueDrawEllipse(location, strokeColor, fillColor, strokeWidth, rotation, antialiased);
         }
 
         var affectedArea = targetImage.FindAffectedArea();

+ 18 - 3
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterLine_UpdateableChange.cs

@@ -16,10 +16,12 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
     private readonly bool drawOnMask;
     private CommittedChunkStorage? savedChunks;
     private int frame;
+    private bool antiAliasing;
+    private Paint paint;
 
     [GenerateUpdateableChangeActions]
     public DrawRasterLine_UpdateableChange
-        (Guid memberGuid, VecI from, VecI to, int strokeWidth, Color color, StrokeCap caps, bool drawOnMask, int frame)
+        (Guid memberGuid, VecI from, VecI to, int strokeWidth, Color color, StrokeCap caps, bool antiAliasing, bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.from = from;
@@ -29,6 +31,10 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         this.caps = caps;
         this.drawOnMask = drawOnMask;
         this.frame = frame;
+        this.antiAliasing = antiAliasing;
+
+        paint = new Paint() { Color = color, 
+            StrokeWidth = strokeWidth, StrokeCap = caps, IsAntiAliased = antiAliasing, BlendMode = BlendMode.SrcOver };
     }
 
     [UpdateChangeMethod]
@@ -39,6 +45,10 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         this.color = color;
         this.caps = caps;
         this.strokeWidth = strokeWidth;
+        
+        paint.Color = color;
+        paint.StrokeWidth = strokeWidth;
+        paint.StrokeCap = caps;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -54,10 +64,14 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
         if (from != to)
         {
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
-            if (strokeWidth == 1)
+            if (strokeWidth == 1 && !antiAliasing)
+            {
                 image.EnqueueDrawBresenhamLine(from, to, color, BlendMode.SrcOver);
+            }
             else
-                image.EnqueueDrawSkiaLine(from, to, caps, strokeWidth, color, BlendMode.SrcOver);
+            {
+                image.EnqueueDrawSkiaLine(from, to, paint);
+            }
         }
         var totalAffected = image.FindAffectedArea();
         totalAffected.UnionWith(oldAffected);
@@ -98,5 +112,6 @@ internal class DrawRasterLine_UpdateableChange : UpdateableChange
     public override void Dispose()
     {
         savedChunks?.Dispose();
+        paint?.Dispose();
     }
 }

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -42,6 +42,7 @@ public static class FloodFillHelper
         VectorPath? selection,
         VecI startingPos,
         Color drawingColor,
+        float tolerance,
         int frame)
     {
         if (selection is not null && !selection.Contains(startingPos.X + 0.5f, startingPos.Y + 0.5f))
@@ -66,7 +67,7 @@ public static class FloodFillHelper
 
         // Pre-multiplies the color and convert it to floats. Since floats are imprecise, a range is used.
         // Used for faster pixel checking
-        ColorBounds colorRange = new(colorToReplace);
+        ColorBounds colorRange = new(colorToReplace, tolerance);
         ulong uLongColor = drawingColor.ToULong();
 
         Dictionary<VecI, Chunk> drawingChunks = new();

+ 4 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs

@@ -14,9 +14,10 @@ internal class FloodFill_Change : Change
     private readonly bool drawOnMask;
     private CommittedChunkStorage? chunkStorage = null;
     private int frame;
+    private float tolerance;
 
     [GenerateMakeChangeAction]
-    public FloodFill_Change(Guid memberGuid, VecI pos, Color color, bool referenceAll, bool drawOnMask, int frame)
+    public FloodFill_Change(Guid memberGuid, VecI pos, Color color, bool referenceAll, float tolerance, bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.pos = pos;
@@ -24,6 +25,7 @@ internal class FloodFill_Change : Change
         this.referenceAll = referenceAll;
         this.drawOnMask = drawOnMask;
         this.frame = frame;
+        this.tolerance = tolerance;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -44,7 +46,7 @@ internal class FloodFill_Change : Change
             target.ForEveryReadonlyMember(member => membersToReference.Add(member.Id));
         else
             membersToReference.Add(memberGuid);
-        var floodFilledChunks = FloodFillHelper.FloodFill(membersToReference, target, selection, pos, color, frame);
+        var floodFilledChunks = FloodFillHelper.FloodFill(membersToReference, target, selection, pos, color, tolerance, frame);
         if (floodFilledChunks.Count == 0)
         {
             ignoreInUndo = true;

+ 72 - 43
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -1,36 +1,56 @@
 using ChunkyImageLib.Operations;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
 internal class LineBasedPen_UpdateableChange : UpdateableChange
 {
     private readonly Guid memberGuid;
     private readonly Color color;
     private int strokeWidth;
-    private readonly bool replacing;
+    private readonly bool erasing;
     private readonly bool drawOnMask;
+    private readonly bool antiAliasing;
+    private float hardness;
+    private float spacing = 1;
     private readonly Paint srcPaint = new Paint() { BlendMode = BlendMode.Src };
 
     private CommittedChunkStorage? storedChunks;
     private readonly List<VecI> points = new();
     private int frame;
+    private VecF lastPos;
 
     [GenerateUpdateableChangeActions]
-    public LineBasedPen_UpdateableChange(Guid memberGuid, Color color, VecI pos, int strokeWidth, bool replacing,
+    public LineBasedPen_UpdateableChange(Guid memberGuid, Color color, VecI pos, int strokeWidth, bool erasing,
+        bool antiAliasing,
+        float hardness,
+        float spacing,
         bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.color = color;
         this.strokeWidth = strokeWidth;
-        this.replacing = replacing;
+        this.erasing = erasing;
+        this.antiAliasing = antiAliasing;
         this.drawOnMask = drawOnMask;
+        this.hardness = hardness;
+        this.spacing = spacing;
         points.Add(pos);
         this.frame = frame;
-}
+        if (this.antiAliasing && !erasing)
+        {
+            srcPaint.BlendMode = BlendMode.SrcOver;
+        }
+        else if (erasing)
+        {
+            srcPaint.BlendMode = BlendMode.DstOut;
+        }
+    }
 
     [UpdateChangeMethod]
     public void Update(VecI pos, int strokeWidth)
@@ -46,9 +66,10 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         if (strokeWidth < 1)
             return false;
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
-        if (!replacing)
+        if (!erasing)
             image.SetBlendMode(BlendMode.SrcOver);
         DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
+        srcPaint.IsAntiAliased = antiAliasing;
         return true;
     }
 
@@ -60,25 +81,25 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
         int opCount = image.QueueLength;
 
-        if (strokeWidth == 1)
-        {
-            image.EnqueueDrawBresenhamLine(from, to, color, BlendMode.Src);
-        }
-        else if (strokeWidth <= 10)
+        var bresenham = BresenhamLineHelper.GetBresenhamLine(from, to);
+        
+        float spacingPixels = strokeWidth * spacing;
+
+        foreach (var point in bresenham)
         {
-            var bresenham = BresenhamLineHelper.GetBresenhamLine(from, to);
-            foreach (var point in bresenham)
+            if (points.Count > 1 && VecF.Distance(lastPos, point) < spacingPixels)
+                continue;
+
+            lastPos = point;
+            var rect = new RectI(point - new VecI(strokeWidth / 2), new VecI(strokeWidth));
+            if (antiAliasing)
             {
-                var rect = new RectI(point - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-                image.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
+                ApplySoftnessGradient((VecD)point);
             }
+
+            image.EnqueueDrawEllipse(rect, color, color, 1, 0, antiAliasing, srcPaint);
         }
-        else
-        {
-            var rect = new RectI(to - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-            image.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
-            image.EnqueueDrawSkiaLine(from, to, StrokeCap.Butt, strokeWidth, color, BlendMode.Src);
-        }
+
         var affChunks = image.FindAffectedArea(opCount);
 
         return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affChunks, drawOnMask);
@@ -88,37 +109,43 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     {
         if (points.Count == 1)
         {
-            if (strokeWidth == 1)
-            {
-                targetImage.EnqueueDrawBresenhamLine(points[0], points[0], color, BlendMode.Src);
-            }
-            else
-            {
-                var rect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-                targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
-            }
+            var rect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
+            targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, antiAliasing, srcPaint);
             return;
         }
 
-        var firstRect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-        targetImage.EnqueueDrawEllipse(firstRect, color, color, 1, 0, srcPaint);
+        VecF lastPos = points[0];
+        
+        float spacingInPixels = strokeWidth * this.spacing;
 
-        for (int i = 1; i < points.Count; i++)
+        for (int i = 0; i < points.Count; i++)
         {
-            if (strokeWidth == 1)
-            {
-                targetImage.EnqueueDrawBresenhamLine(points[i - 1], points[i], color, BlendMode.Src);
-            }
-            else
+            if (i > 0 && VecF.Distance(lastPos, points[i]) < spacingInPixels)
+                continue;
+
+            lastPos = points[i];
+            var rect = new RectI(points[i] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
+            if (antiAliasing)
             {
-                var rect = new RectI(points[i] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-                targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
-                targetImage.EnqueueDrawSkiaLine(points[i - 1], points[i], StrokeCap.Butt, strokeWidth, color, BlendMode.Src);
+                ApplySoftnessGradient(points[i]);
             }
+
+            targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, antiAliasing, srcPaint);
         }
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    private void ApplySoftnessGradient(VecD pos)
+    {
+        srcPaint.Shader?.Dispose();
+        float radius = strokeWidth / 2f;
+        radius = MathF.Max(1, radius);
+        srcPaint.Shader = Shader.CreateRadialGradient(
+            pos, radius, new Color[] { color, color.WithAlpha(0) }, 
+            new float[] { hardness, 1 }, ShaderTileMode.Clamp);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         if (storedChunks is not null)
             throw new InvalidOperationException("Trying to save chunks while there are saved chunks already");
@@ -135,7 +162,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         }
         else
         {
-            if (!replacing)
+            if (!erasing)
                 image.SetBlendMode(BlendMode.SrcOver);
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
 
@@ -150,7 +177,9 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, frame, ref storedChunks);
+        var affected =
+            DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, frame,
+                ref storedChunks);
         return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affected, drawOnMask);
     }
 

+ 4 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs

@@ -108,10 +108,13 @@ internal class MagicWandHelper
     }
 
     public static VectorPath DoMagicWandFloodFill(VecI startingPos, HashSet<Guid> membersToFloodFill,
+        double tolerance,
         IReadOnlyDocument document, int frame)
     {
         if (startingPos.X < 0 || startingPos.Y < 0 || startingPos.X >= document.Size.X || startingPos.Y >= document.Size.Y)
             return new VectorPath();
+        
+        tolerance = Math.Clamp(tolerance, 0, 1);
 
         int chunkSize = ChunkResolution.Full.PixelSize();
 
@@ -127,7 +130,7 @@ internal class MagicWandHelper
             static (EmptyChunk _) => Colors.Transparent
         );
 
-        ColorBounds colorRange = new(colorToReplace);
+        ColorBounds colorRange = new(colorToReplace, tolerance);
 
         HashSet<VecI> processedEmptyChunks = new();
 

+ 4 - 2
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWand_Change.cs

@@ -14,15 +14,17 @@ internal class MagicWand_Change : Change
     private readonly List<Guid> memberGuids;
     private readonly SelectionMode mode;
     private int frame;
+    private double tolerance;
 
     [GenerateMakeChangeAction]
-    public MagicWand_Change(List<Guid> memberGuids, VecI point, SelectionMode mode, int frame)
+    public MagicWand_Change(List<Guid> memberGuids, VecI point, SelectionMode mode, double tolerance, int frame)
     {
         path.MoveTo(point);
         this.mode = mode;
         this.memberGuids = memberGuids;
         this.point = point;
         this.frame = frame;
+        this.tolerance = tolerance;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -41,7 +43,7 @@ internal class MagicWand_Change : Change
                 membersToReference.Add(member.Id);
         });
 
-        path = MagicWandHelper.DoMagicWandFloodFill(point, membersToReference, target, frame);
+        path = MagicWandHelper.DoMagicWandFloodFill(point, membersToReference, tolerance, target, frame);
 
         ignoreInUndo = false;
         return CommonApply(target);

+ 8 - 8
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -13,6 +13,8 @@ internal class CreateStructureMember_Change : Change
 
     private Guid parentGuid;
     private Type structureMemberOfType;
+    
+    private ConnectionsData? connectionsData;
 
     [GenerateMakeChangeAction]
     public CreateStructureMember_Change(Guid parent, Guid newGuid,
@@ -44,7 +46,6 @@ internal class CreateStructureMember_Change : Change
         InputProperty<Painter> targetInput = parentNode.InputProperties.FirstOrDefault(x => 
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
         
-        
         if (member is FolderNode folder)
         {
             document.NodeGraph.AddNode(member);
@@ -77,10 +78,9 @@ internal class CreateStructureMember_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document document)
     {
         var container = document.FindNodeOrThrow<Node>(parentGuid);
-        if (container is not IRenderInput backgroundInput)
-        {
-            throw new InvalidOperationException("Parent folder is not a valid container.");
-        }
+
+       InputProperty<Painter> backgroundInput = container.InputProperties.FirstOrDefault(x => 
+            x.ValueType == typeof(Painter)) as InputProperty<Painter>;
 
         StructureNode child = document.FindMemberOrThrow(newMemberGuid);
         var childBackgroundConnection = child.Background.Connection;
@@ -92,10 +92,10 @@ internal class CreateStructureMember_Change : Change
 
         if (childBackgroundConnection != null)
         {
-            childBackgroundConnection?.ConnectTo(backgroundInput.Background);
+            childBackgroundConnection?.ConnectTo(backgroundInput);
             ConnectProperty_ChangeInfo change = new(childBackgroundConnection.Node.Id,
-                backgroundInput.Background.Node.Id, childBackgroundConnection.InternalPropertyName,
-                backgroundInput.Background.InternalPropertyName);
+                backgroundInput.Node.Id, childBackgroundConnection.InternalPropertyName,
+                backgroundInput.InternalPropertyName);
             changes.Add(change);
         }
 

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

+ 11 - 10
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -24,6 +24,7 @@ public class DocumentRenderer : IPreviewRenderable
     }
 
     private IReadOnlyDocument Document { get; }
+    public bool IsBusy { get; private set; }
 
     public void UpdateChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime)
     {
@@ -42,19 +43,10 @@ public class DocumentRenderer : IPreviewRenderable
         }
     }
 
-    private static RectI? TransformClipRect(RectI? globalClippingRect, ChunkResolution resolution, VecI chunkPos)
-    {
-        if (globalClippingRect is not RectI rect)
-            return null;
-
-        double multiplier = resolution.Multiplier();
-        VecI pixelChunkPos = chunkPos * (int)(ChunkyImage.FullChunkSize * multiplier);
-        return (RectI?)rect.Scale(multiplier).Translate(-pixelChunkPos).RoundOutwards();
-    }
-
     public void RenderLayers(DrawingSurface toDrawOn, HashSet<Guid> layersToCombine, int frame,
         ChunkResolution resolution)
     {
+        IsBusy = true;
         RenderContext context = new(toDrawOn, frame, resolution, Document.Size);
         context.FullRerender = true;
         IReadOnlyNodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
@@ -65,6 +57,10 @@ public class DocumentRenderer : IPreviewRenderable
         catch (ObjectDisposedException)
         {
         }
+        finally
+        {
+            IsBusy = false;
+        }
     }
 
 
@@ -76,11 +72,14 @@ public class DocumentRenderer : IPreviewRenderable
         {
             return;
         }
+        
+        IsBusy = true;
 
         RenderContext context = new(renderOn, frameTime, resolution, Document.Size);
         context.FullRerender = true;
         
         node.RenderForOutput(context, renderOn, null);
+        IsBusy = false;
     }
 
     public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(IReadOnlyNodeGraph fullGraph)
@@ -135,7 +134,9 @@ public class DocumentRenderer : IPreviewRenderable
 
     public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime)
     {
+        IsBusy = true;
         RenderContext context = new(toRenderOn, frameTime, ChunkResolution.Full, Document.Size) { FullRerender = true };
         Document.NodeGraph.Execute(context);
+        IsBusy = false;
     }
 }

+ 2 - 2
src/PixiEditor.ClosedBeta/PixiEditor.ClosedBeta.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
     <OutputType>Exe</OutputType>
     <ImplicitUsings>enable</ImplicitUsings>
@@ -9,7 +9,7 @@
     <PublishTrimmed>true</PublishTrimmed>
     <WasmSingleFileBundle>true</WasmSingleFileBundle>
     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
-    <PixiExtOutputPath>..\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+    <PixiExtOutputPath>..\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net9.0\win-x64\Extensions</PixiExtOutputPath>
     <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
   </PropertyGroup>
 

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <RootNamespace>PixiEditor.Common</RootNamespace>

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

@@ -3,7 +3,7 @@
     <OutputType>WinExe</OutputType>
     <!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
     One for Windows with net7.0-windows TFM, one for MacOS with net7.0-macos and one with net7.0 TFM for Linux.-->
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <Nullable>enable</Nullable>
     <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
     <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>

+ 1 - 1
src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>disable</Nullable>
     </PropertyGroup>

+ 1 - 1
src/PixiEditor.Extensions.Runtime/PixiEditor.Extensions.Runtime.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>disable</Nullable>

+ 1 - 1
src/PixiEditor.Extensions.WasmRuntime/PixiEditor.Extensions.WasmRuntime.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-      <TargetFramework>net8.0</TargetFramework>
+      <TargetFramework>net9.0</TargetFramework>
       <ImplicitUsings>enable</ImplicitUsings>
       <Nullable>enable</Nullable>
       <GeneratePackageOnBuild>true</GeneratePackageOnBuild>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

+ 1 - 1
src/PixiEditor.Platform.MSStore/PixiEditor.Platform.MSStore.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

+ 1 - 1
src/PixiEditor.Platform.Standalone/PixiEditor.Platform.Standalone.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

+ 1 - 1
src/PixiEditor.Platform.Steam/PixiEditor.Platform.Steam.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
   </PropertyGroup>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

+ 1 - 1
src/PixiEditor.UI.Common/PixiEditor.UI.Common.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

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

@@ -3,7 +3,7 @@
         <OutputType>WinExe</OutputType>
         <!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
         One for Windows with net7.0-windows TFM, one for MacOS with net7.0-macos and one with net7.0 TFM for Linux.-->
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <Nullable>enable</Nullable>
         <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
     </PropertyGroup>

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

@@ -1,6 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <Nullable>enable</Nullable>
         <LangVersion>latest</LangVersion>
         <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
   </PropertyGroup>
 
 </Project>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <Nullable>enable</Nullable>
     <WarningsAsErrors>Nullable</WarningsAsErrors>
   </PropertyGroup>

+ 75 - 2
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -5,7 +5,13 @@
       "MoveViewport",
       "RotateViewport",
       "Move",
-      "Pen",
+      {
+        "ToolName": "Pen",
+        "Settings": {
+          "ExposePixelPerfectEnabled": true,
+          "Spacing": 0
+        }
+      },
       "Select",
       "MagicWand",
       "Lasso",
@@ -13,7 +19,74 @@
       "RasterLine",
       "RasterEllipse",
       "RasterRectangle",
-      "Eraser",
+      {
+        "ToolName": "Eraser",
+        "Settings": {
+          "Spacing": 0
+        }
+      },
+      "ColorPicker",
+      "Brightness",
+      "Zoom"
+    ]
+  },
+  {
+    "Name": "PAINT_TOOLSET",
+    "Tools": [
+      "MoveViewport",
+      "RotateViewport",
+      "Move",
+      {
+        "ToolName": "Pen",
+        "Settings": {
+          "AntiAliasing": true,
+          "ExposeHardness": true,
+          "ExposeSpacing": true
+        }
+      },
+      "Select",
+      {
+        "ToolName": "MagicWand",
+        "Settings": {
+          "ExposeTolerance": true
+        }
+      },
+      "Lasso",
+      {
+        "ToolName": "FloodFill",
+        "Settings": {
+          "ExposeTolerance": true
+        }
+      },
+      {
+        "ToolName": "RasterLine",
+        "Settings": {
+          "AntiAliasing": true
+        },
+        "Icon": "\ue93a"
+      },
+      {
+        "ToolName": "RasterEllipse",
+        "Settings": {
+          "AntiAliasing": true
+        },
+        "Icon": "\ue910"
+      },
+      {
+        "ToolName": "RasterRectangle",
+        "Settings": {
+          "AntiAliasing": true
+        },
+        "Icon": "\uE953" 
+      },
+      {
+        "ToolName": "Eraser",
+        "Settings": {
+          "AntiAliasing": true,
+          "ExposeHardness": true,
+          "ExposeSpacing": true
+        }
+      },
       "ColorPicker",
       "Brightness",
       "Zoom"

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

@@ -65,6 +65,7 @@
   "UPDATE_CHECK_FAILED": "Update check failed",
   "COULD_NOT_CHECK_FOR_UPDATES": "Could not check if there is an update available.",
   "VERSION": "Version {0}",
+  "BUILD_ID": "Build ID: {0}",
   "OPEN_TEMP_DIR": "Open temp directory",
   "OPEN_LOCAL_APPDATA_DIR": "Open Local AppData directory",
   "OPEN_ROAMING_APPDATA_DIR": "Open Roaming AppData directory",
@@ -756,5 +757,10 @@
   "NEW_ELLIPSE_LAYER_NAME": "Ellipse",
   "NEW_RECTANGLE_LAYER_NAME": "Rectangle",
   "NEW_LINE_LAYER_NAME": "Line",
-  "RENDER_OUTPUT": "Render Output"
+  "RENDER_OUTPUT": "Render Output",
+  "PAINT_TOOLSET": "Painting",
+  "HARDNESS_SETTING": "Hardness",
+  "SPACING_SETTING": "Spacing",
+  "ANTI_ALIASING_SETTING": "Anti-aliasing",
+  "TOLERANCE_LABEL": "Tolerance"
 }

+ 46 - 0
src/PixiEditor/Helpers/Converters/IntPercentConverter.cs

@@ -0,0 +1,46 @@
+using System.Globalization;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class IntPercentConverter : SingleInstanceConverter<IntPercentConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is double doubleValue)
+        {
+            return (int)Math.Round(doubleValue * 100d);
+        }
+        
+        if (value is float floatValue)
+        {
+            return (int)Math.Round(floatValue * 100f);
+        }
+        
+        if (value is int intValue)
+        {
+            return intValue * 100;
+        }
+
+        return 0;
+    }
+    
+    public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is double doubleValue)
+        {
+            return doubleValue / 100d;
+        }
+        
+        if (value is float floatValue)
+        {
+            return floatValue / 100f;
+        }
+        
+        if (value is int intValue)
+        {
+            return intValue / 100f;
+        }
+
+        return 0;
+    }
+}

+ 6 - 5
src/PixiEditor/Models/AnalyticsAPI/AnalyticsClient.cs

@@ -1,6 +1,7 @@
 using System.Globalization;
 using System.Net;
 using System.Net.Http.Json;
+using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 using PixiEditor.Helpers;
@@ -44,10 +45,10 @@ public class AnalyticsClient
         {
             return await response.Content.ReadFromJsonAsync<Guid?>(_options, cancellationToken);
         }
-
+        
         if (response.StatusCode is not HttpStatusCode.ServiceUnavailable)
         {
-            await ReportInvalidStatusCodeAsync(response.StatusCode);
+            await ReportInvalidStatusCodeAsync(response.StatusCode, await response.Content.ReadAsStringAsync(cancellationToken));
         }
             
         return null;
@@ -66,7 +67,7 @@ public class AnalyticsClient
 
         if (response.StatusCode is not (HttpStatusCode.NotFound or HttpStatusCode.ServiceUnavailable))
         {
-            await ReportInvalidStatusCodeAsync(response.StatusCode);
+            await ReportInvalidStatusCodeAsync(response.StatusCode, await response.Content.ReadAsStringAsync(cancellationToken));
         }
             
         return false;
@@ -84,9 +85,9 @@ public class AnalyticsClient
         await _client.DeleteAsync($"sessions/{sessionId}", cancellationToken);
     }
 
-    private static async Task ReportInvalidStatusCodeAsync(HttpStatusCode statusCode)
+    private static async Task ReportInvalidStatusCodeAsync(HttpStatusCode statusCode, string message)
     {
-        await CrashHelper.SendExceptionInfoToWebhookAsync(new InvalidOperationException($"Invalid status code from analytics API '{statusCode}'"));
+        await CrashHelper.SendExceptionInfoToWebhookAsync(new InvalidOperationException($"Invalid status code from analytics API '{statusCode}', message: {message}"));
     }
 
     class KeyCombinationConverter : JsonConverter<KeyCombination>

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

@@ -203,7 +203,7 @@ internal class CommandController
                 InternalName = internalName,
                 DisplayName = displayName,
                 Description = displayName,
-                Icon = toolInstance.Icon,
+                Icon = toolInstance.DefaultIcon,
                 IconEvaluator = IconEvaluator.Default,
                 TransientKey = toolAttr.Transient,
                 DefaultShortcut = toolAttr.GetShortcut(),

+ 53 - 3
src/PixiEditor/Models/Config/ConfigManager.cs

@@ -1,21 +1,71 @@
 using System.Reflection;
 using Avalonia.Platform;
 using Newtonsoft.Json;
+using PixiEditor.Models.IO;
 using PixiEditor.Views;
 
 namespace PixiEditor.Models.Config;
 
 public class ConfigManager
 {
-    // TODO: Copy config to local folder so it can be modified
     public T GetConfig<T>(string configName)
     {
-        string path = $"avares://{Assembly.GetExecutingAssembly().GetName().Name}/Data/Configs/{configName}.json";
+        // TODO: Local configs require a mechanism that will allow to update them when the embedded config changes
+        // but merges the changes with the local config or something like that, leaving as is for now
+        /*if (LocalConfigExists(configName))
+        {
+            try
+            {
+                return GetLocalConfig<T>(configName);
+            }
+            catch(JsonReaderException)
+            {
+                // If the local config is corrupted, delete it and load the embedded one
+                File.Delete(Path.Combine(Paths.UserConfigPath, $"Configs/{configName}.json"));
+            }
+        }*/
+
+        var embeddedConfig = GetEmbeddedConfig<T>(configName);
+        //SaveConfig(embeddedConfig, configName);
+        return embeddedConfig;
+    }
+
+    private T GetLocalConfig<T>(string configName)
+    {
+        string path = $"Configs/{configName}.json";
+        using FileStream file = File.Open(Path.Combine(Paths.UserConfigPath, path), FileMode.Open);
+        using StreamReader reader = new(file);
+
+        string json = reader.ReadToEnd();
+        return JsonConvert.DeserializeObject<T>(json);
+    }
+
+    private T GetEmbeddedConfig<T>(string configName)
+    {
+        string path = Path.Combine(Paths.InternalResourceDataPath, $"Configs/{configName}.json");
 
         using Stream config = AssetLoader.Open(new Uri(path));
         using StreamReader reader = new(config);
-        
+
         string json = reader.ReadToEnd();
         return JsonConvert.DeserializeObject<T>(json);
     }
+    
+    private void SaveConfig<T>(T config, string configName)
+    {
+        string path = Path.Combine(Paths.UserConfigPath, $"Configs/{configName}.json");
+        string json = JsonConvert.SerializeObject(config, Formatting.Indented);
+
+        Directory.CreateDirectory(Path.GetDirectoryName(path));
+        using FileStream file = File.Open(path, FileMode.Create);
+        using StreamWriter writer = new(file);
+
+        writer.Write(json);
+    }
+    
+    private bool LocalConfigExists(string configName)
+    {
+        string path = Path.Combine(Paths.UserConfigPath, $"Configs/{configName}.json");
+        return File.Exists(path);
+    }
 }

+ 68 - 1
src/PixiEditor/Models/Config/ToolSetConfig.cs

@@ -1,4 +1,5 @@
 using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
 
 namespace PixiEditor.Models.Config;
 
@@ -9,5 +10,71 @@ public class ToolSetsConfig : List<ToolSetConfig>
 public class ToolSetConfig
 {
     public string Name { get; set; }
-    public List<string> Tools { get; set; }
+    
+    [JsonConverter(typeof(ToolConverter))]
+    public List<ToolConfig> Tools { get; set; }
+}
+
+public class ToolConfig
+{
+    public string ToolName { get; set; }
+    public Dictionary<string, object>? Settings { get; set; }
+    public bool IsSimpleTool => Settings == null || Settings.Count == 0;
+    public string? Icon { get; set; }
+}
+
+public class ToolConverter : JsonConverter
+{
+    public override bool CanConvert(Type objectType) => objectType == typeof(List<ToolConfig>);
+
+    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+    {
+        var token = JToken.Load(reader);
+
+        if (token.Type == JTokenType.Array)
+        {
+            var tools = new List<ToolConfig>();
+
+            foreach (var item in token)
+            {
+                if (item.Type == JTokenType.String)
+                {
+                    tools.Add(new ToolConfig { ToolName = item.ToString() });
+                }
+                else if (item.Type == JTokenType.Object)
+                {
+                    tools.Add(item.ToObject<ToolConfig>(serializer));
+                }
+                else
+                {
+                    throw new JsonSerializationException("Unexpected token type in Tools array");
+                }
+            }
+
+            return tools;
+        }
+
+        throw new JsonSerializationException("Expected array for Tools");
+    }
+
+    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+    {
+        var tools = (List<ToolConfig>)value;
+
+        writer.WriteStartArray();
+
+        foreach (var tool in tools)
+        {
+            if (tool.IsSimpleTool)
+            {
+                writer.WriteValue(tool.ToolName);
+            }
+            else
+            {
+                serializer.Serialize(writer, tool);
+            }
+        }
+
+        writer.WriteEndArray();
+    }
 }

+ 15 - 4
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ComplexShapeToolExecutor.cs

@@ -29,7 +29,7 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
     protected double lastRadians;
 
     private bool noMovement = true;
-    private IBasicShapeToolbar toolbar;
+    protected IBasicShapeToolbar toolbar;
     private IColorsHandler? colorsVM;
 
     public override bool CanUndo => document.TransformHandler.HasUndo;
@@ -54,6 +54,12 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
 
         if (ActiveMode == ShapeToolMode.Drawing)
         {
+            if (toolbar.SyncWithPrimaryColor)
+            {
+                toolbar.FillColor = colorsVM.PrimaryColor.ToColor();
+                toolbar.StrokeColor = colorsVM.PrimaryColor.ToColor();
+            }
+
             return ExecutionState.Success;
         }
 
@@ -61,7 +67,12 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
         {
             var node = (VectorLayerNode)internals.Tracker.Document.FindMember(member.Id);
 
-            if (!InitShapeData(node.ShapeData))
+            if (node == null)
+            {
+                return ExecutionState.Error;
+            }
+
+            if (node.ShapeData == null || !InitShapeData(node.ShapeData))
             {
                 ActiveMode = ShapeToolMode.Preview;
                 return ExecutionState.Success;
@@ -141,7 +152,7 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
 
         var rect = RectD.FromCenterAndSize(corners.RectCenter, corners.RectSize);
         ShapeData shapeData = new ShapeData(rect.Center, rect.Size, corners.RectRotation, StrokeWidth, StrokeColor,
-            FillColor);
+            FillColor) { AntiAliasing = toolbar.AntiAliasing };
         IAction drawAction = TransformMovedAction(shapeData, corners);
 
         internals!.ActionAccumulator.AddActions(drawAction);
@@ -163,7 +174,7 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
 
     public override void OnColorChanged(Color color, bool primary)
     {
-        if (primary && toolbar.SyncWithPrimaryColor)
+        if (primary && toolbar.SyncWithPrimaryColor && ActiveMode == ShapeToolMode.Transform)
         {
             toolbar.StrokeColor = color.ToColor();
             toolbar.FillColor = color.ToColor();

+ 12 - 5
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs

@@ -17,18 +17,22 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
     private Guid guidValue;
     private Color color;
     private int toolSize;
+    private bool antiAliasing;
+    private float hardness;
+    private float spacing;
+    
     private bool drawOnMask;
 
     public override ExecutionState Start()
     {
         IStructureMemberHandler? member = document!.SelectedStructureMember;
         IEraserToolHandler? eraserTool = GetHandler<IEraserToolHandler>();
-        IBasicToolbar? toolbar = eraserTool?.Toolbar as IBasicToolbar;
+        IPenToolbar? toolbar = eraserTool?.Toolbar as IPenToolbar;
         IColorsHandler? colorsHandler = GetHandler<IColorsHandler>();
 
         if (colorsHandler is null || eraserTool is null || member is null || toolbar is null)
             return ExecutionState.Error;
-        drawOnMask = member is ILayerHandler layer ? layer.ShouldDrawOnMask : true;
+        drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
         if (drawOnMask && !member.HasMaskBindable)
             return ExecutionState.Error;
         if (!drawOnMask && member is not ILayerHandler)
@@ -38,10 +42,13 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
         guidValue = member.Id;
         color = GetHandler<IColorsHandler>().PrimaryColor;
         toolSize = toolbar.ToolSize;
+        antiAliasing = toolbar.AntiAliasing;
+        hardness = toolbar.Hardness;
+        spacing = toolbar.Spacing;
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
-        IAction? action = new LineBasedPen_Action(guidValue, Colors.Transparent, controller!.LastPixelPosition, toolSize, true,
-            drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, toolSize, true,
+            antiAliasing, hardness, spacing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
 
         return ExecutionState.Success;
@@ -49,7 +56,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        IAction? action = new LineBasedPen_Action(guidValue, Colors.Transparent, pos, toolSize, true, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, pos, toolSize, true, antiAliasing, hardness, spacing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
     }
 

+ 4 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/FloodFillToolExecutor.cs

@@ -16,6 +16,7 @@ internal class FloodFillToolExecutor : UpdateableChangeExecutor
     private bool drawOnMask;
     private Guid memberGuid;
     private Color color;
+    private float tolerance;
 
     public override ExecutionState Start()
     {
@@ -37,15 +38,16 @@ internal class FloodFillToolExecutor : UpdateableChangeExecutor
         considerAllLayers = fillTool.ConsiderAllLayers;
         color = colorsVM.PrimaryColor;
         var pos = controller!.LastPixelPosition;
+        tolerance = fillTool.Tolerance;
 
-        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
+        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, tolerance, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
 
         return ExecutionState.Success;
     }
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
+        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, tolerance, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     public override void OnLeftMouseButtonUp()

+ 9 - 5
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -24,11 +24,11 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     private bool startedDrawing = false;
     private T? toolViewModel;
     private IColorsHandler? colorsVM;
-    private ILineToolbar? toolbar;
+    protected ILineToolbar? toolbar;
 
-    public override bool CanUndo => document.LineToolOverlayHandler.HasUndo; 
+    public override bool CanUndo => document.LineToolOverlayHandler.HasUndo;
     public override bool CanRedo => document.LineToolOverlayHandler.HasRedo;
-    
+
     public override ExecutionState Start()
     {
         if (base.Start() == ExecutionState.Error)
@@ -49,6 +49,11 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
 
         if (ActiveMode == ShapeToolMode.Drawing)
         {
+            if (toolbar.SyncWithPrimaryColor)
+            {
+                toolbar.StrokeColor = colorsVM.PrimaryColor.ToColor();
+            }
+
             return ExecutionState.Success;
         }
 
@@ -62,7 +67,6 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
                 return ExecutionState.Success;
             }
 
-            toolbar.StrokeColor = data.StrokeColor.ToColor();
 
             if (!InitShapeData(data))
             {
@@ -133,7 +137,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
 
     public override void OnColorChanged(Color color, bool primary)
     {
-        if (!primary)
+        if (!primary || !toolbar!.SyncWithPrimaryColor || ActiveMode != ShapeToolMode.Transform)
             return;
 
         toolbar!.StrokeColor = color.ToColor();

+ 3 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs

@@ -14,6 +14,7 @@ internal class MagicWandToolExecutor : UpdateableChangeExecutor
     private bool drawOnMask;
     private List<Guid> memberGuids;
     private SelectionMode mode;
+    private float tolerance;
 
     public override ExecutionState Start()
     {
@@ -29,8 +30,9 @@ internal class MagicWandToolExecutor : UpdateableChangeExecutor
         if (considerAllLayers)
             memberGuids = document!.StructureHelper.GetAllLayers().Select(x => x.Id).ToList();
         var pos = controller!.LastPixelPosition;
+        tolerance = (float)magicWand.Tolerance;
 
-        internals!.ActionAccumulator.AddActions(new MagicWand_Action(memberGuids, pos, mode, document!.AnimationHandler.ActiveFrameBindable));
+        internals!.ActionAccumulator.AddActions(new MagicWand_Action(memberGuids, pos, mode, tolerance, document!.AnimationHandler.ActiveFrameBindable));
 
         return ExecutionState.Success;
     }

+ 12 - 8
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -1,14 +1,12 @@
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using Drawie.Backend.Core.ColorsImpl;
-using Drawie.Backend.Core.Numerics;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
-using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
@@ -16,11 +14,14 @@ internal class PenToolExecutor : UpdateableChangeExecutor
 {
     private Guid guidValue;
     private Color color;
-    public int ToolSize => basicToolbar.ToolSize;
+    public int ToolSize => penToolbar.ToolSize;
     private bool drawOnMask;
     private bool pixelPerfect;
+    private bool antiAliasing;
+    private float hardness;
+    private float spacing = 1;
 
-    private IBasicToolbar basicToolbar;
+    private IBasicToolbar penToolbar;
 
     public override ExecutionState Start()
     {
@@ -28,7 +29,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         IColorsHandler? colorsHandler = GetHandler<IColorsHandler>();
 
         IPenToolHandler? penTool = GetHandler<IPenToolHandler>();
-        if (colorsHandler is null || penTool is null || member is null || penTool?.Toolbar is not IBasicToolbar toolbar)
+        if (colorsHandler is null || penTool is null || member is null || penTool?.Toolbar is not IPenToolbar toolbar)
             return ExecutionState.Error;
         drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
         if (drawOnMask && !member.HasMaskBindable)
@@ -36,15 +37,18 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         if (!drawOnMask && member is not ILayerHandler)
             return ExecutionState.Error;
 
-        basicToolbar = toolbar;
+        penToolbar = toolbar;
         guidValue = member.Id;
         color = colorsHandler.PrimaryColor;
         pixelPerfect = penTool.PixelPerfectEnabled;
+        antiAliasing = toolbar.AntiAliasing;
+        hardness = toolbar.Hardness;
+        spacing = toolbar.Spacing;
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, ToolSize, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, ToolSize, false, antiAliasing, hardness, spacing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);
@@ -56,7 +60,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     {
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, pos, ToolSize, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, pos, ToolSize, false, antiAliasing, hardness, spacing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);

+ 3 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs

@@ -25,7 +25,7 @@ internal class RasterEllipseToolExecutor : ComplexShapeToolExecutor<IRasterEllip
         lastRect = rect;
         lastRadians = rotationRad;
 
-        internals!.ActionAccumulator.AddActions(new DrawRasterEllipse_Action(memberId, rect, rotationRad, StrokeColor, FillColor, StrokeWidth, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
+        internals!.ActionAccumulator.AddActions(new DrawRasterEllipse_Action(memberId, rect, rotationRad, StrokeColor, FillColor, StrokeWidth, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     public override ExecutorType Type => ExecutorType.ToolLinked;
@@ -33,7 +33,7 @@ internal class RasterEllipseToolExecutor : ComplexShapeToolExecutor<IRasterEllip
     protected override void DrawShape(VecI currentPos, double rotationRad, bool firstDraw) => DrawEllipseOrCircle(currentPos, rotationRad, firstDraw);
     protected override IAction SettingsChangedAction()
     {
-        return new DrawRasterEllipse_Action(memberId, lastRect, lastRadians, StrokeColor, FillColor, StrokeWidth, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+        return new DrawRasterEllipse_Action(memberId, lastRect, lastRadians, StrokeColor, FillColor, StrokeWidth, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
     protected override IAction TransformMovedAction(ShapeData data, ShapeCorners corners)
@@ -45,7 +45,7 @@ internal class RasterEllipseToolExecutor : ComplexShapeToolExecutor<IRasterEllip
         lastRadians = radians;
         
         return new DrawRasterEllipse_Action(memberId, lastRect, lastRadians, StrokeColor,
-            FillColor, StrokeWidth, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+            FillColor, StrokeWidth, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
     protected override IAction EndDrawAction() => new EndDrawRasterEllipse_Action();

+ 6 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterLineToolExecutor.cs

@@ -19,7 +19,7 @@ internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
         VecD dir = GetSignedDirection(startDrawingPos, pos);
         VecD oppositeDir = new VecD(-dir.X, -dir.Y);
         return new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos, oppositeDir), ToPixelPos(pos, dir), StrokeWidth,
-            StrokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+            StrokeColor, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
     protected override IAction TransformOverlayMoved(VecD start, VecD end)
@@ -27,7 +27,7 @@ internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
         VecD dir = GetSignedDirection(start, end);
         VecD oppositeDir = new VecD(-dir.X, -dir.Y);
         return new DrawRasterLine_Action(memberId, ToPixelPos(start, oppositeDir), ToPixelPos(end, dir), 
-            StrokeWidth, StrokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+            StrokeWidth, StrokeColor, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
     protected override IAction SettingsChange()
@@ -35,15 +35,18 @@ internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
         VecD dir = GetSignedDirection(startDrawingPos, curPos);
         VecD oppositeDir = new VecD(-dir.X, -dir.Y);
         return new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos, oppositeDir), ToPixelPos(curPos, dir), StrokeWidth,
-            StrokeColor, StrokeCap.Butt, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+            StrokeColor, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
     private VecI ToPixelPos(VecD pos, VecD dir)
     {
+        if (StrokeWidth > 1) return (VecI)pos.Round();
+        
         double xAdjustment = dir.X > 0 ? 0.5 : -0.5;
         double yAdjustment = dir.Y > 0 ? 0.5 : -0.5;
         
         VecD adjustment = new VecD(xAdjustment, yAdjustment);
+
         
         VecI finalPos = (VecI)(pos - adjustment);
 

+ 8 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs

@@ -25,7 +25,10 @@ internal class RasterRectangleToolExecutor : ComplexShapeToolExecutor<IRasterRec
         lastRect = rect;
         lastRadians = rotationRad;
         
-        lastData = new ShapeData(rect.Center, rect.Size, rotationRad, StrokeWidth, StrokeColor, FillColor);
+        lastData = new ShapeData(rect.Center, rect.Size, rotationRad, StrokeWidth, StrokeColor, FillColor)
+        {
+            AntiAliasing = toolbar.AntiAliasing
+        };
 
         internals!.ActionAccumulator.AddActions(new DrawRasterRectangle_Action(memberId, lastData, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
     }
@@ -33,7 +36,10 @@ internal class RasterRectangleToolExecutor : ComplexShapeToolExecutor<IRasterRec
     protected override void DrawShape(VecI currentPos, double rotationRad, bool first) => DrawRectangle(currentPos, rotationRad, first);
     protected override IAction SettingsChangedAction()
     {
-        lastData = new ShapeData(lastData.Center, lastData.Size, lastRadians, StrokeWidth, StrokeColor, FillColor);
+        lastData = new ShapeData(lastData.Center, lastData.Size, lastRadians, StrokeWidth, StrokeColor, FillColor)
+        {
+            AntiAliasing = toolbar.AntiAliasing
+        };
         return new DrawRasterRectangle_Action(memberId, lastData, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);   
     }
 

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

@@ -22,6 +22,8 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
     public override ExecutorType Type { get; }
 
     public override bool BlocksOtherActions => false; 
+    
+    private List<Guid> selectedMembers = new();
 
     public TransformSelectedExecutor(bool toolLinked)
     {
@@ -85,6 +87,8 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             document.SnappingHandler.Remove(structureMemberHandler.Id.ToString());
         }
         
+        selectedMembers = members.Select(m => m.Id).ToList();
+        
         document.TransformHandler.ShowTransform(mode, true, masterCorners, Type == ExecutorType.Regular);
         internals!.ActionAccumulator.AddActions(
             new TransformSelected_Action(masterCorners, tool.KeepOriginalImage, memberCorners, false,
@@ -100,8 +104,9 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         {
             internals.ActionAccumulator.AddActions(new EndTransformSelected_Action());
             document!.TransformHandler.HideTransform();
-            AddSnappingForMembers(memberGuids);
+            AddSnappingForMembers(selectedMembers);
             
+            selectedMembers.Clear();
             memberCorners.Clear();
             isInProgress = false;
         }

+ 3 - 1
src/PixiEditor/Models/Handlers/IToolHandler.cs

@@ -12,7 +12,7 @@ internal interface IToolHandler : IHandler
     public LocalizedString DisplayName => new LocalizedString(ToolNameLocalizationKey);
     public string ToolName => GetType().Name.Replace("Tool", string.Empty).Replace("ViewModel", string.Empty);
     public string ToolNameLocalizationKey { get; }
-    public string Icon => $"icon-{ToolName.ToLower()}";
+    public string DefaultIcon => $"icon-{ToolName.ToLower()}";
     public Type[]? SupportedLayerTypes { get; }
 
     public bool HideHighlight { get; }
@@ -60,5 +60,7 @@ internal interface IToolHandler : IHandler
     public void UseTool(VecD pos);
     public void OnSelected(bool restoring);
 
+    public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings);
+    public void ApplyToolSetSettings(IToolSetHandler toolset);
     public void OnDeselecting(bool transient);
 }

+ 2 - 0
src/PixiEditor/Models/Handlers/IToolSetHandler.cs

@@ -4,4 +4,6 @@ internal interface IToolSetHandler : IHandler
 {
     public string Name { get; }
     public ICollection<IToolHandler> Tools { get; }
+    public void ApplyToolSetSettings();
+    public IReadOnlyDictionary<IToolHandler, string> IconOverwrites { get; }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/Toolbars/IBasicShapeToolbar.cs

@@ -8,4 +8,5 @@ internal interface IBasicShapeToolbar : IBasicToolbar
     public bool Fill { get; set; }
     public Color FillColor { get; set; }
     public bool SyncWithPrimaryColor { get; set; }
+    public bool AntiAliasing { get; set; }
 }

+ 2 - 0
src/PixiEditor/Models/Handlers/Toolbars/ILineToolbar.cs

@@ -5,4 +5,6 @@ namespace PixiEditor.Models.Handlers.Toolbars;
 internal interface ILineToolbar : IBasicToolbar
 {
     public Color StrokeColor { get; set; }
+    public bool AntiAliasing { get; set; }
+    public bool SyncWithPrimaryColor { get; }
 }

+ 8 - 0
src/PixiEditor/Models/Handlers/Toolbars/IPenToolbar.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Models.Handlers.Toolbars;
+
+internal interface IPenToolbar : IBasicToolbar
+{
+    public bool AntiAliasing { get; set; }
+    public float Hardness { get; set; }
+    public float Spacing { get; set; }
+}

+ 1 - 0
src/PixiEditor/Models/Handlers/Tools/IFloodFillToolHandler.cs

@@ -3,4 +3,5 @@
 internal interface IFloodFillToolHandler : IToolHandler
 {
     public bool ConsiderAllLayers { get; }
+    public float Tolerance { get; }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/Tools/IMagicWandToolHandler.cs

@@ -7,4 +7,5 @@ internal interface IMagicWandToolHandler : IToolHandler
 {
     public SelectionMode SelectMode { get; }
     public DocumentScope DocumentScope { get; }
+    public float Tolerance { get; }
 }

+ 3 - 0
src/PixiEditor/Models/IO/Paths.cs

@@ -7,6 +7,9 @@ public static class Paths
     public static string DataResourceUri { get; } = $"avares://{typeof(Paths).Assembly.GetName().Name}/Data/";
     public static string DataFullPath { get; } = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Data");
     public static string ExtensionPackagesPath { get; } = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Extensions");
+    public static string UserConfigPath { get; } = Path.Combine(
+        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 
+        "PixiEditor", "Configs");
     public static string UserExtensionsPath { get; } = Path.Combine(
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 
         "PixiEditor", "Extensions");

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

@@ -15,7 +15,6 @@ namespace PixiEditor.Models.Rendering;
 
 internal class SceneRenderer
 {
-    
     public IReadOnlyDocument Document { get; }
     public IDocument DocumentViewModel { get; }
     public bool HighResRendering { get; set; } = true;
@@ -28,6 +27,7 @@ internal class SceneRenderer
 
     public void RenderScene(DrawingSurface target, ChunkResolution resolution)
     {
+        if(Document.Renderer.IsBusy) return;
         RenderOnionSkin(target, resolution);
         RenderGraph(target, resolution);
     }

+ 8 - 1
src/PixiEditor/PixiEditor.csproj

@@ -1,6 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <Nullable>enable</Nullable>
     <LangVersion>latest</LangVersion>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -133,4 +133,11 @@
     </None>
   </ItemGroup>
 
+  <ItemGroup>
+    <Compile Update="Views\Tools\ToolSettings\Settings\PercentSettingView.axaml.cs">
+      <DependentUpon>PercentSettingView.axaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+  </ItemGroup>
+
 </Project>

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

@@ -6,11 +6,11 @@ using System.Windows;
 // set of attributes. Change these attribute values to modify the information
 // associated with an assembly.
 [assembly: AssemblyTitle("PixiEditor")]
-[assembly: AssemblyDescription("A fast, nice looking universal graphics editor.")]
+[assembly: AssemblyDescription("Fast, universal graphics editor.")]
 [assembly: AssemblyConfiguration("")]
 [assembly: AssemblyCompany("PixiEditor")]
 [assembly: AssemblyProduct("PixiEditor")]
-[assembly: AssemblyCopyright("Copyright PixiEditor © 2017 - 2024")]
+[assembly: AssemblyCopyright("Copyright PixiEditor © 2017 - 2025")]
 [assembly: AssemblyTrademark("")]
 [assembly: AssemblyCulture("")]
 
@@ -42,5 +42,5 @@ using System.Windows;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.19")]
-[assembly: AssemblyFileVersion("2.0.0.19")]
+[assembly: AssemblyVersion("2.0.0.20")]
+[assembly: AssemblyFileVersion("2.0.0.20")]

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

@@ -193,8 +193,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
             return;
 
         drawingWithRight = args.Button == MouseButton.Right;
-        Owner.ToolsSubViewModel.UseToolEventInlet(args.PositionOnCanvas, args.Button);
         activeDocument.EventInlet.OnCanvasLeftMouseButtonDown(args.PositionOnCanvas);
+        Owner.ToolsSubViewModel.UseToolEventInlet(args.PositionOnCanvas, args.Button);
 
         Analytics.SendUseTool(Owner.ToolsSubViewModel.ActiveTool, args.PositionOnCanvas, activeDocument.SizeBindable);
     }

+ 18 - 5
src/PixiEditor/ViewModels/SubViewModels/ToolSetViewModel.cs

@@ -9,14 +9,27 @@ internal class ToolSetViewModel : PixiObservableObject, IToolSetHandler
 {
     public string Name { get; }
     ICollection<IToolHandler> IToolSetHandler.Tools => Tools;
+    IReadOnlyDictionary<IToolHandler, string> IToolSetHandler.IconOverwrites => IconOverwrites;
+
     public ObservableCollection<IToolHandler> Tools { get; } = new();
-    
-    public ToolSetViewModel(string setName, List<IToolHandler> tools)
+    public Dictionary<IToolHandler, string> IconOverwrites { get; set; } = new Dictionary<IToolHandler, string>();
+
+    public ToolSetViewModel(string setName)
     {
         Name = setName;
-        foreach (var tool in tools)
+    }
+
+    public void AddTool(IToolHandler tool)
+    {
+        Tools.Add(tool);
+    }
+
+    public void ApplyToolSetSettings()
+    {
+        foreach (IToolHandler tool in Tools)
         {
-            Tools.Add(tool);
-        }    
+            tool.ApplyToolSetSettings(this);
+        }
     }
+
 }

+ 15 - 7
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -131,6 +131,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     public void SetActiveToolSet(IToolSetHandler toolSetHandler)
     {
         ActiveToolSet = toolSetHandler;
+        ActiveToolSet.ApplyToolSetSettings();
         UpdateEnabledState();
     }
 
@@ -426,24 +427,31 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     {
         foreach (ToolSetConfig toolSet in toolSetConfig)
         {
-            List<IToolHandler> tools = new List<IToolHandler>();
-
-            foreach (string toolName in toolSet.Tools)
+            var toolSetViewModel = new ToolSetViewModel(toolSet.Name);
+            
+            foreach (var toolFromToolset in toolSet.Tools)
             {
-                IToolHandler? tool = allTools.FirstOrDefault(tool => tool.ToolName == toolName);
+                IToolHandler? tool = allTools.FirstOrDefault(tool => tool.ToolName == toolFromToolset.ToolName);
+                tool.SetToolSetSettings(toolSetViewModel, toolFromToolset.Settings);
+
+                if (!string.IsNullOrEmpty(toolFromToolset.Icon))
+                {
+                    toolSetViewModel.IconOverwrites[tool] = toolFromToolset.Icon;
+                }
+                
                 if (tool is null)
                 {
 #if DEBUG
-                    throw new InvalidOperationException($"Tool '{toolName}' not found.");
+                    throw new InvalidOperationException($"Tool '{tool}' not found.");
 #endif
 
                     continue;
                 }
 
-                tools.Add(tool);
+                toolSetViewModel.AddTool(tool);
             }
 
-            AllToolSets.Add(new ToolSetViewModel(toolSet.Name, tools));
+            AllToolSets.Add(toolSetViewModel);
         }
     }
 

+ 44 - 0
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/PercentSettingViewModel.cs

@@ -0,0 +1,44 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using PixiEditor.Views.Input;
+
+namespace PixiEditor.ViewModels.Tools.ToolSettings.Settings;
+
+internal sealed class PercentSettingViewModel : Setting<float>
+{
+    private float min = 0;
+    private float max = 1;
+    
+    public PercentSettingViewModel(
+        string name,
+        float initialValue,
+        string label = "",
+        float min = 0,
+        float max = 1)
+        : base(name)
+    {
+        Label = label;
+        Value = initialValue;
+        Min = min;
+        Max = max;
+    }
+
+    public float Min
+    {
+        get => min;
+        set
+        {
+            SetProperty(ref min, value);
+        }
+    }
+
+    public float Max
+    {
+        get => max;
+        set
+        {
+            SetProperty(ref max, value);
+        }
+    }
+}

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

@@ -47,6 +47,13 @@ internal abstract class Setting<T> : Setting
 internal abstract class Setting : ObservableObject
 {
     private object _value;
+    private bool isExposed = true;
+    
+    private bool overwrittenExposed;
+    private object overwrittenValue;
+
+    private bool hasOverwrittenValue;
+    private bool hasOverwrittenExposed;
     
     protected Setting(string name)
     {
@@ -57,7 +64,7 @@ internal abstract class Setting : ObservableObject
 
     public object Value
     {
-        get => _value;
+        get => hasOverwrittenValue ? overwrittenValue : _value;
         set
         {
             var old = _value;
@@ -68,11 +75,50 @@ internal abstract class Setting : ObservableObject
         }
     }
 
+    public bool IsExposed
+    {
+        get => hasOverwrittenExposed ? overwrittenExposed : isExposed;
+        set => SetProperty(ref isExposed, value);
+    }
+
     public string Name { get; }
 
     public LocalizedString Label { get; set; }
 
     public bool HasLabel => !string.IsNullOrEmpty(Label);
 
+    public object UserValue
+    {
+        get => _value;
+        set => _value = value;
+    }
+
     public abstract Type GetSettingType();
+    
+    public void SetOverwriteValue(object value)
+    {
+        overwrittenValue = value;
+        hasOverwrittenValue = true;
+        
+        OnPropertyChanged(nameof(Value));
+    }
+    
+    public void SetOverwriteExposed(bool value)
+    {
+        overwrittenExposed = value;
+        hasOverwrittenExposed = true;
+        
+        OnPropertyChanged(nameof(IsExposed));
+    }
+    
+    public void ResetOverwrite()
+    {
+        overwrittenValue = null;
+        overwrittenExposed = false;
+        hasOverwrittenValue = false;
+        hasOverwrittenExposed = false;
+        
+        OnPropertyChanged(nameof(Value));
+        OnPropertyChanged(nameof(IsExposed));
+    }
 }

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

@@ -52,7 +52,19 @@ internal class BasicShapeToolbar : BasicToolbar, IBasicShapeToolbar
         {
             GetSetting<BoolSettingViewModel>(nameof(SyncWithPrimaryColor)).Value = value;
         }
-    } 
+    }
+
+    public bool AntiAliasing
+    {
+        get
+        {
+            return GetSetting<BoolSettingViewModel>(nameof(AntiAliasing)).Value;
+        }
+        set
+        {
+            GetSetting<BoolSettingViewModel>(nameof(AntiAliasing)).Value = value;
+        }
+    }
 
     public BasicShapeToolbar()
     {
@@ -60,5 +72,6 @@ internal class BasicShapeToolbar : BasicToolbar, IBasicShapeToolbar
         AddSetting(new BoolSettingViewModel(nameof(Fill), "FILL_SHAPE_LABEL") { Value = true });
         AddSetting(new ColorSettingViewModel(nameof(FillColor), "FILL_COLOR_LABEL"));
         AddSetting(new BoolSettingViewModel(nameof(SyncWithPrimaryColor), "SYNC_WITH_PRIMARY_COLOR_LABEL") { Value = true });
+        AddSetting(new BoolSettingViewModel(nameof(AntiAliasing), "ANTI_ALIASING_LABEL") { Value = false, IsExposed = false});
     }
 }

+ 23 - 0
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/LineToolbar.cs

@@ -19,8 +19,31 @@ internal class LineToolbar : BasicToolbar, ILineToolbar
         }
     }
 
+    public bool AntiAliasing
+    {
+        get
+        {
+            return GetSetting<BoolSettingViewModel>(nameof(AntiAliasing)).Value;
+        }
+        set
+        {
+            GetSetting<BoolSettingViewModel>(nameof(AntiAliasing)).Value = value;
+        }
+    }
+
+    public bool SyncWithPrimaryColor
+    {
+        get => GetSetting<BoolSettingViewModel>(nameof(SyncWithPrimaryColor)).Value;
+        set
+        {
+            GetSetting<BoolSettingViewModel>(nameof(SyncWithPrimaryColor)).Value = value;
+        }
+    }
+
     public LineToolbar()
     {
         AddSetting(new ColorSettingViewModel(nameof(StrokeColor), "STROKE_COLOR_LABEL"));
+        AddSetting(new BoolSettingViewModel(nameof(AntiAliasing), "ANTI_ALIASING_LABEL") { IsExposed = false, Value = false });
+        AddSetting(new BoolSettingViewModel(nameof(SyncWithPrimaryColor), "SYNC_WITH_PRIMARY_COLOR_LABEL") { Value = true });
     }
 }

+ 32 - 0
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/PenToolbar.cs

@@ -0,0 +1,32 @@
+using PixiEditor.Models.Handlers.Toolbars;
+using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
+
+namespace PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
+
+internal class PenToolbar : BasicToolbar, IPenToolbar
+{
+    public bool AntiAliasing
+    {
+        get => GetSetting<BoolSettingViewModel>(nameof(AntiAliasing)).Value;
+        set => GetSetting<BoolSettingViewModel>(nameof(AntiAliasing)).Value = value;
+    }
+
+    public float Hardness
+    {
+        get => GetSetting<PercentSettingViewModel>(nameof(Hardness)).Value;
+        set => GetSetting<PercentSettingViewModel>(nameof(Hardness)).Value = value;
+    }
+
+    public float Spacing
+    {
+        get => GetSetting<PercentSettingViewModel>(nameof(Spacing)).Value;
+        set => GetSetting<PercentSettingViewModel>(nameof(Spacing)).Value = value;
+    }
+
+    public PenToolbar()
+    {
+        AddSetting(new BoolSettingViewModel(nameof(AntiAliasing), "ANTI_ALIASING_SETTING") { IsExposed = false });
+        AddSetting(new PercentSettingViewModel(nameof(Hardness), 0.8f, "HARDNESS_SETTING") { IsExposed = false });
+        AddSetting(new PercentSettingViewModel(nameof(Spacing), 0.15f, "SPACING_SETTING") { IsExposed = false });
+    }
+}

+ 23 - 0
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/SettingAttributes.cs

@@ -4,6 +4,27 @@ namespace PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 public static class Settings
 {
+    public class PercentAttribute : SettingsAttribute
+    {
+        public float Min { get; set; } = 0;
+
+        public float Max { get; set; } = 1;
+        
+        public PercentAttribute(string labelKey) : base(labelKey) { }
+
+        public PercentAttribute(string labelKey, float defaultValue) : base(labelKey, defaultValue)
+        {
+            
+        }
+        
+        public PercentAttribute(string labelKey, float defaultValue, float min, float max) : base(labelKey, defaultValue)
+        {
+            Min = min;
+            Max = max;
+        }
+        
+    }
+    
     /// <summary>
     /// A toolbar setting of type <see cref="bool"/>
     /// </summary>
@@ -73,6 +94,8 @@ public static class Settings
         public string Name { get; set; }
         
         public string Notify { get; set; }
+        
+        public bool ExposedByDefault { get; set; } = true;
 
         public SettingsAttribute() { }
         

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

@@ -61,7 +61,7 @@ internal abstract class Toolbar : ObservableObject, IToolbar
         {
             if (SharedSettings.Any(x => x.Name == Settings[i].Name))
             {
-                SharedSettings.First(x => x.Name == Settings[i].Name).Value = Settings[i].Value;
+                SharedSettings.First(x => x.Name == Settings[i].Name).UserValue = Settings[i].UserValue;
             }
             else
             {
@@ -79,7 +79,7 @@ internal abstract class Toolbar : ObservableObject, IToolbar
         {
             if (Settings.Any(x => x.Name == SharedSettings[i].Name))
             {
-                Settings.First(x => x.Name == SharedSettings[i].Name).Value = SharedSettings[i].Value;
+                Settings.First(x => x.Name == SharedSettings[i].Name).UserValue = SharedSettings[i].UserValue;
             }
         }
 

+ 10 - 3
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/ToolbarFactory.cs

@@ -21,6 +21,7 @@ internal static class ToolbarFactory
 
             var name = attribute.Name ?? property.Name;
             var label = attribute.LabelKey ?? name;
+            bool exposedByDefault = attribute.ExposedByDefault;
 
             if (attribute is Settings.InheritedAttribute)
             {
@@ -28,7 +29,7 @@ internal static class ToolbarFactory
             }
             else
             {
-                var setting = CreateSetting(property.PropertyType, name, attribute, label);
+                var setting = CreateSetting(property.PropertyType, name, attribute, label, exposedByDefault);
                 AddValueChangedHandlerIfRequired(toolType, tool, setting, attribute);
                 toolbar.AddSetting(setting);
             }
@@ -51,20 +52,26 @@ internal static class ToolbarFactory
     }
 
     private static Setting CreateSetting(Type propertyType, string name, Settings.SettingsAttribute attribute,
-        string label)
+        string label, bool exposedByDefault)
     {
-        return attribute switch
+        var attr = attribute switch
         {
             Settings.BoolAttribute => new BoolSettingViewModel(name, (bool)(attribute.DefaultValue ?? false), label),
             Settings.ColorAttribute => new ColorSettingViewModel(name,
                 ((Color)(attribute.DefaultValue ?? Colors.White)).ToColor(), label),
             Settings.EnumAttribute => GetEnumSetting(propertyType, name, attribute),
+            Settings.PercentAttribute percentAttribute => new PercentSettingViewModel(name, (float)(attribute.DefaultValue ?? 0f), label,
+                percentAttribute.Min, percentAttribute.Max),
             Settings.FloatAttribute floatAttribute => new FloatSettingViewModel(name, (float)(attribute.DefaultValue ?? 0f), label,
                 floatAttribute.Min, floatAttribute.Max),
             Settings.SizeAttribute => new SizeSettingViewModel(name, label),
             _ => throw new NotImplementedException(
                 $"SettingsAttribute of type '{attribute.GetType().FullName}' has not been implemented")
         };
+        
+        attr.IsExposed = exposedByDefault;
+        
+        return attr;
     }
 
     private static void AddValueChangedHandlerIfRequired(Type toolType, ToolViewModel tool, Setting setting,

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

@@ -1,4 +1,5 @@
-using System.Reflection;
+using System.Diagnostics;
+using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Input;
 using CommunityToolkit.Mvvm.ComponentModel;
@@ -8,6 +9,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Input;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
@@ -24,7 +26,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
     public abstract string ToolNameLocalizationKey { get; }
     public virtual LocalizedString DisplayName => new LocalizedString(ToolNameLocalizationKey);
 
-    public virtual string Icon => $"\u25a1";
+    public virtual string DefaultIcon => $"\u25a1";
 
     public virtual BrushShape BrushShape => BrushShape.Square;
 
@@ -75,6 +77,10 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
             OnPropertyChanged(nameof(ActionDisplay));
         }
     }
+    
+    public string IconOverwrite { get; set; }
+    
+    public string IconToUse => IconOverwrite ?? DefaultIcon;
 
     private bool isActive;
 
@@ -92,6 +98,8 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
 
     public IToolbar Toolbar { get; set; } = new EmptyToolbar();
 
+    public Dictionary<IToolSetHandler, Dictionary<string, object>> ToolSetSettings { get; } = new();
+
     internal ToolViewModel()
     {
         ILocalizationProvider.Current.OnLanguageChanged += OnLanguageChanged;
@@ -104,7 +112,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
             CanBeUsedOnActiveLayer = SupportedLayerTypes == null;
             return;
         }
-        
+
         var layer = layers[0];
 
         if (IsActive)
@@ -129,7 +137,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
 
         CanBeUsedOnActiveLayer = false;
     }
-    
+
     private bool IsFolderAndRasterSupported(IStructureMemberHandler layer)
     {
         return SupportedLayerTypes.Contains(typeof(IRasterLayerHandler)) && layer is IFolderHandler;
@@ -144,13 +152,97 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
 
     public virtual void UseTool(VecD pos) { }
     public virtual void OnSelected(bool restoring) { }
-    
+
     protected virtual void OnSelectedLayersChanged(IStructureMemberHandler[] layers) { }
 
     public virtual void OnDeselecting(bool transient)
     {
     }
 
+    public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings)
+    {
+        if (settings == null || settings.Count == 0 || toolset == null)
+        {
+            return;
+        }
+
+        foreach (var valueSetting in settings)
+        {
+            if (valueSetting.Value is long)
+            {
+                settings[valueSetting.Key] = Convert.ToSingle(valueSetting.Value);
+            }
+        }
+
+        ToolSetSettings[toolset] = settings;
+    }
+
+    public void ApplyToolSetSettings(IToolSetHandler toolset)
+    {
+        IconOverwrite = null;
+        foreach (var toolbarSetting in Toolbar.Settings)
+        {
+            toolbarSetting.ResetOverwrite();
+        }
+        
+        if(toolset.IconOverwrites.TryGetValue(this, out var icon))
+        {
+            IconOverwrite = icon;
+        }
+
+        if (!ToolSetSettings.TryGetValue(toolset, out var settings))
+        {
+            return;
+        }
+
+        foreach (var setting in settings)
+        {
+            if (IsExposeSetting(setting, out bool expose))
+            {
+                string settingName = setting.Key.Replace("Expose", string.Empty);
+                var foundSetting = TryGetSettingByName(settingName, setting);
+                if (foundSetting is null)
+                {
+                    continue;
+                }
+
+                foundSetting.SetOverwriteExposed(expose);
+            }
+            else
+            {
+                try
+                {
+                    var foundSetting = TryGetSettingByName(setting.Key, setting);
+                    if (foundSetting is null)
+                    {
+                        continue;
+                    }
+
+                    foundSetting.SetOverwriteValue(setting.Value);
+                }
+                catch (InvalidCastException)
+                {
+#if DEBUG
+                    throw;
+#endif
+                }
+            }
+        }
+    }
+
+    private Setting? TryGetSettingByName(string settingName, KeyValuePair<string, object> setting)
+    {
+        var foundSetting = Toolbar.GetSetting(settingName);
+        if (foundSetting is null)
+        {
+#if DEBUG
+            Debug.WriteLine($"Setting {settingName} not found in toolbar {Toolbar.GetType().Name}");
+#endif
+        }
+
+        return foundSetting;
+    }
+
     protected T GetValue<T>([CallerMemberName] string name = null)
     {
         var setting = Toolbar.GetSetting(name);
@@ -162,6 +254,39 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
             return (T)property!.GetValue(setting);
         }
 
-        return (T)setting.Value;
+        try
+        {
+            return (T)setting.Value;
+        }
+        catch (InvalidCastException)
+        {
+            if (typeof(T) == typeof(float) || typeof(T) == typeof(double) || typeof(T) == typeof(int))
+            {
+                return (T)(object)Convert.ToSingle(setting.Value);
+            }
+
+            throw;
+        }
+    }
+
+    private bool IsExposeSetting(KeyValuePair<string, object> settingConfig, out bool expose)
+    {
+        bool isExpose = settingConfig.Key.StartsWith("Expose", StringComparison.InvariantCultureIgnoreCase);
+        if (!isExpose)
+        {
+            expose = false;
+            return false;
+        }
+
+        var settingName = settingConfig.Key.Replace("Expose", string.Empty);
+
+        if (settingConfig.Value is bool value)
+        {
+            expose = value;
+            return true;
+        }
+
+        expose = false;
+        return false;
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs

@@ -31,7 +31,7 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
 
     public override BrushShape BrushShape => BrushShape.Circle;
 
-    public override string Icon => PixiPerfectIcons.Sun;
+    public override string DefaultIcon => PixiPerfectIcons.Sun;
 
     public override Type[]? SupportedLayerTypes { get; } =
     {

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/ColorPickerToolViewModel.cs

@@ -30,7 +30,7 @@ internal class ColorPickerToolViewModel : ToolViewModel, IColorPickerHandler
     public override string ToolNameLocalizationKey => "COLOR_PICKER_TOOL";
     public override BrushShape BrushShape => BrushShape.Pixel;
 
-    public override string Icon => PixiPerfectIcons.Picker;
+    public override string DefaultIcon => PixiPerfectIcons.Picker;
 
     public override Type[]? SupportedLayerTypes { get; } = null;  // all layer types are supported
 

+ 2 - 2
src/PixiEditor/ViewModels/Tools/Tools/EraserToolViewModel.cs

@@ -18,7 +18,7 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
     public EraserToolViewModel()
     {
         ActionDisplay = "ERASER_TOOL_ACTION_DISPLAY";
-        Toolbar = ToolbarFactory.Create<EraserToolViewModel, BasicToolbar>(this);
+        Toolbar = ToolbarFactory.Create<EraserToolViewModel, PenToolbar>(this);
     }
 
     [Settings.Inherited]
@@ -33,7 +33,7 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
         typeof(IRasterLayerHandler)
     };
 
-    public override string Icon => PixiPerfectIcons.Eraser;
+    public override string DefaultIcon => PixiPerfectIcons.Eraser;
 
     public override LocalizedString Tooltip => new LocalizedString("ERASER_TOOL_TOOLTIP", Shortcut);
 

+ 6 - 1
src/PixiEditor/ViewModels/Tools/Tools/FloodFillToolViewModel.cs

@@ -7,6 +7,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using Drawie.Numerics;
 using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
@@ -28,10 +29,14 @@ internal class FloodFillToolViewModel : ToolViewModel, IFloodFillToolHandler
 
     public bool ConsiderAllLayers { get; private set; }
 
-    public override string Icon => PixiPerfectIcons.Bucket;
+    [Settings.Percent("TOLERANCE_LABEL", ExposedByDefault = false)]
+    public float Tolerance => GetValue<float>();
+
+    public override string DefaultIcon => PixiPerfectIcons.Bucket;
 
     public FloodFillToolViewModel()
     {
+        Toolbar = ToolbarFactory.Create(this);
         ActionDisplay = defaultActionDisplay;
     }
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/LassoToolViewModel.cs

@@ -49,7 +49,7 @@ internal class LassoToolViewModel : ToolViewModel, ILassoToolHandler
     public override LocalizedString Tooltip => new LocalizedString("LASSO_TOOL_TOOLTIP", Shortcut);
 
     public override string ToolNameLocalizationKey => "LASSO_TOOL";
-    public string Icon => PixiPerfectIcons.Lasso;
+    public override string DefaultIcon => PixiPerfectIcons.Lasso;
     public override BrushShape BrushShape => BrushShape.Pixel;
     
     public override Type[]? SupportedLayerTypes { get; } = null; // all layer types are supported

Деякі файли не було показано, через те що забагато файлів було змінено