瀏覽代碼

Merge pull request #211 from PixiEditor/quickfill

Skia Rework
Krzysztof Krysiński 3 年之前
父節點
當前提交
4b7c05a0c2
共有 100 個文件被更改,包括 5185 次插入2134 次删除
  1. 5 1
      Custom.ruleset
  2. 14 14
      Installer/installer-setup-x64-light.iss
  3. 14 14
      Installer/installer-setup-x86-light.iss
  4. 1 1
      PixiEditor.MSIX/PixiEditor.MSIX.wapproj
  5. 2 2
      PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj
  6. 1 1
      PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj
  7. 26 0
      PixiEditor.UpdateModule/UpdateChannel.cs
  8. 10 10
      PixiEditor.UpdateModule/UpdateChecker.cs
  9. 38 2
      PixiEditor.sln
  10. 0 15
      PixiEditor/Helpers/AssemblyHelper.cs
  11. 3 3
      PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs
  12. 5 1
      PixiEditor/Helpers/Behaviours/MouseBehavior.cs
  13. 28 50
      PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs
  14. 45 0
      PixiEditor/Helpers/ClipboardHelper.cs
  15. 19 0
      PixiEditor/Helpers/Converters/DebugConverter.cs
  16. 1 1
      PixiEditor/Helpers/Converters/FinalIsVisibleToVisiblityConverter.cs
  17. 8 7
      PixiEditor/Helpers/Converters/IndexOfConverter.cs
  18. 2 1
      PixiEditor/Helpers/Converters/LayerStructureToGroupsConverter.cs
  19. 30 12
      PixiEditor/Helpers/Converters/LayersToStructuredLayersConverter.cs
  20. 23 0
      PixiEditor/Helpers/Converters/SKColorToMediaColorConverter.cs
  21. 6 7
      PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
  22. 23 0
      PixiEditor/Helpers/Converters/ViewboxInverseTransformConverter.cs
  23. 85 0
      PixiEditor/Helpers/CoordinatesHelper.cs
  24. 168 0
      PixiEditor/Helpers/EllipseGenerator.cs
  25. 54 0
      PixiEditor/Helpers/Extensions/Int32RectEx.cs
  26. 0 16
      PixiEditor/Helpers/Extensions/ObservableCollectionEx.cs
  27. 105 91
      PixiEditor/Helpers/Extensions/ParserHelpers.cs
  28. 73 0
      PixiEditor/Helpers/Extensions/PixelFormatHelper.cs
  29. 11 0
      PixiEditor/Helpers/Extensions/PixiParserHelper.cs
  30. 63 0
      PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs
  31. 7 4
      PixiEditor/Helpers/GlobalMouseHook.cs
  32. 1 1
      PixiEditor/Helpers/RelayCommand.cs
  33. 3 1
      PixiEditor/Helpers/Validators/SizeValidationRule.cs
  34. 48 0
      PixiEditor/Helpers/VersionHelpers.cs
  35. 10 10
      PixiEditor/Models/Colors/ExColor.cs
  36. 179 190
      PixiEditor/Models/Controllers/BitmapManager.cs
  37. 50 250
      PixiEditor/Models/Controllers/BitmapOperationsUtility.cs
  38. 265 51
      PixiEditor/Models/Controllers/ClipboardController.cs
  39. 15 0
      PixiEditor/Models/Controllers/ICanvasInputTarget.cs
  40. 173 0
      PixiEditor/Models/Controllers/LayerStackRenderer.cs
  41. 53 0
      PixiEditor/Models/Controllers/MouseInputFilter.cs
  42. 0 83
      PixiEditor/Models/Controllers/MouseMovementController.cs
  43. 0 105
      PixiEditor/Models/Controllers/PixelChangesController.cs
  44. 0 14
      PixiEditor/Models/Controllers/ReadonlyToolUtility.cs
  45. 2 3
      PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs
  46. 101 0
      PixiEditor/Models/Controllers/SingleLayerRenderer.cs
  47. 39 0
      PixiEditor/Models/Controllers/SurfaceRenderer.cs
  48. 121 0
      PixiEditor/Models/Controllers/ToolSession.cs
  49. 140 0
      PixiEditor/Models/Controllers/ToolSessionController.cs
  50. 28 7
      PixiEditor/Models/Controllers/UndoManager.cs
  51. 15 19
      PixiEditor/Models/DataHolders/BitmapPixelChanges.cs
  52. 12 3
      PixiEditor/Models/DataHolders/Document/Document.Constructors.cs
  53. 2 1
      PixiEditor/Models/DataHolders/Document/Document.Discord.cs
  54. 121 89
      PixiEditor/Models/DataHolders/Document/Document.Layers.cs
  55. 138 3
      PixiEditor/Models/DataHolders/Document/Document.Operations.cs
  56. 12 2
      PixiEditor/Models/DataHolders/Document/Document.Preview.cs
  57. 35 33
      PixiEditor/Models/DataHolders/Document/Document.cs
  58. 0 24
      PixiEditor/Models/DataHolders/LayerChange.cs
  59. 666 0
      PixiEditor/Models/DataHolders/RangeObservableCollection.cs
  60. 16 24
      PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs
  61. 55 14
      PixiEditor/Models/DataHolders/Selection.cs
  62. 287 0
      PixiEditor/Models/DataHolders/Surface.cs
  63. 104 0
      PixiEditor/Models/DataHolders/WpfObservableRangeCollection.cs
  64. 14 25
      PixiEditor/Models/Dialogs/NewFileDialog.cs
  65. 8 0
      PixiEditor/Models/Enums/FlipType.cs
  66. 37 7
      PixiEditor/Models/IO/Exporter.cs
  67. 53 11
      PixiEditor/Models/IO/Importer.cs
  68. 78 98
      PixiEditor/Models/ImageManipulation/BitmapUtils.cs
  69. 257 0
      PixiEditor/Models/ImageManipulation/ToolCalculator.cs
  70. 5 5
      PixiEditor/Models/Layers/BasicLayer.cs
  71. 3 1
      PixiEditor/Models/Layers/GuidStructureItem.cs
  72. 9 0
      PixiEditor/Models/Layers/IHasGuid.cs
  73. 226 122
      PixiEditor/Models/Layers/Layer.cs
  74. 21 16
      PixiEditor/Models/Layers/LayerGroup.cs
  75. 11 19
      PixiEditor/Models/Layers/LayerHelper.cs
  76. 24 24
      PixiEditor/Models/Layers/LayerStructure.cs
  77. 19 21
      PixiEditor/Models/Layers/StructuredLayerTree.cs
  78. 31 6
      PixiEditor/Models/Layers/Utils/LayerStructureUtils.cs
  79. 55 7
      PixiEditor/Models/Position/Coordinates.cs
  80. 40 34
      PixiEditor/Models/Position/CoordinatesCalculator.cs
  81. 82 0
      PixiEditor/Models/Position/CropData.cs
  82. 14 14
      PixiEditor/Models/Position/DoubleCoords.cs
  83. 62 0
      PixiEditor/Models/Services/DocumentProvider.cs
  84. 55 14
      PixiEditor/Models/Tools/BitmapOperationTool.cs
  85. 13 0
      PixiEditor/Models/Tools/Brushes/Brush.cs
  86. 22 0
      PixiEditor/Models/Tools/Brushes/CircleBrush.cs
  87. 14 0
      PixiEditor/Models/Tools/Brushes/CircleBrushOverrides/InterestingShapeBrush.cs
  88. 25 0
      PixiEditor/Models/Tools/FloodFillRange.cs
  89. 94 0
      PixiEditor/Models/Tools/MatrixBrush.cs
  90. 4 4
      PixiEditor/Models/Tools/ReadonlyTool.cs
  91. 14 18
      PixiEditor/Models/Tools/ShapeTool.cs
  92. 46 67
      PixiEditor/Models/Tools/Tool.cs
  93. 0 71
      PixiEditor/Models/Tools/ToolBuilder.cs
  94. 3 5
      PixiEditor/Models/Tools/ToolSettings/Settings/BoolSetting.cs
  95. 7 9
      PixiEditor/Models/Tools/ToolSettings/Settings/SizeSetting.cs
  96. 56 55
      PixiEditor/Models/Tools/Tools/BrightnessTool.cs
  97. 71 162
      PixiEditor/Models/Tools/Tools/CircleTool.cs
  98. 119 23
      PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
  99. 32 36
      PixiEditor/Models/Tools/Tools/EraserTool.cs
  100. 0 80
      PixiEditor/Models/Tools/Tools/FloodFill.cs

+ 5 - 1
Custom.ruleset

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<RuleSet Name="Name" Description="Description" ToolsVersion="16.0">
+<RuleSet Name="Name" Description="Description" ToolsVersion="17.0">
   <Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">
     <Rule Id="AD0001" Action="None" />
   </Rules>
@@ -16,8 +16,10 @@
   <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
     <Rule Id="SA0001" Action="None" />
     <Rule Id="SA1000" Action="None" />
+    <Rule Id="SA1005" Action="None" />
     <Rule Id="SA1008" Action="None" />
     <Rule Id="SA1009" Action="None" />
+    <Rule Id="SA1028" Action="None" />
     <Rule Id="SA1101" Action="None" />
     <Rule Id="SA1110" Action="None" />
     <Rule Id="SA1111" Action="None" />
@@ -34,6 +36,8 @@
     <Rule Id="SA1139" Action="None" />
     <Rule Id="SA1200" Action="None" />
     <Rule Id="SA1201" Action="None" />
+    <Rule Id="SA1202" Action="None" />
+    <Rule Id="SA1204" Action="None" />
     <Rule Id="SA1207" Action="None" />
     <Rule Id="SA1208" Action="None" />
     <Rule Id="SA1209" Action="None" />

+ 14 - 14
Installer/installer-setup-x64-light.iss

@@ -4,8 +4,8 @@
 // requires netcorecheck.exe and netcorecheck_x64.exe (see download link below)
 #define UseNetCoreCheck
 #ifdef UseNetCoreCheck
-  ;#define UseDotNet50
-  #define UseDotNet50Desktop
+  ;#define UseDotNet60
+  #define UseDotNet60Desktop
 #endif
 
 // custom setup info
@@ -396,24 +396,24 @@ var
   Version: String;
 begin
 
-#ifdef UseDotNet50
-  // https://dotnet.microsoft.com/download/dotnet/5.0
-  if not IsNetCoreInstalled('Microsoft.NETCore.App 5.0.0') then begin
-    AddDependency('dotnet50' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet60
+  // https://dotnet.microsoft.com/download/dotnet/6.0
+  if not IsNetCoreInstalled('Microsoft.NETCore.App 6.0.0') then begin
+    AddDependency('dotnet60' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Runtime 5.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/a7e15da3-7a15-43c2-a481-cf50bf305214/c69b951e8b47101e90b1289c387bb01a/dotnet-runtime-5.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/36a9dc4e-1745-4f17-8a9c-f547a12e3764/ae25e38f20a4854d5e015a88659a22f9/dotnet-runtime-5.0.0-win-x64.exe'),
+      '.NET Runtime 6.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/34df41d5-c813-4e30-8aa3-3603ce6600c0/976e801af82c7108abbcb736a8bc5c14/dotnet-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b9cfdb9e-d5cd-4024-b318-00390b729d2f/65690f2440f40654898020cdfffa1050/dotnet-runtime-6.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif
 
-#ifdef UseDotNet50Desktop
-  // https://dotnet.microsoft.com/download/dotnet/5.0
-  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 5.0.0') then begin
-    AddDependency('dotnet50desktop' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet60Desktop
+  // https://dotnet.microsoft.com/download/dotnet/6.0
+  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 6.0.0') then begin
+    AddDependency('dotnet60desktop' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Desktop Runtime 5.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/b2780d75-e54a-448a-95fc-da9721b2b4c2/62310a9e9f0ba7b18741944cbae9f592/windowsdesktop-runtime-5.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/1b3a8899-127a-4465-a3c2-7ce5e4feb07b/1e153ad470768baa40ed3f57e6e7a9d8/windowsdesktop-runtime-5.0.0-win-x64.exe'),
+      '.NET Desktop Runtime 6.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/a1ca7d0d-ce01-4878-b952-3fa1e6d9a7c6/e386db367490b631b8c013a9fb0f3794/windowsdesktop-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a865ccae-2219-4184-bcd6-0178dc580589/ba452d37e8396b7a49a9adc0e1a07e87/windowsdesktop-runtime-6.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif

+ 14 - 14
Installer/installer-setup-x86-light.iss

@@ -4,8 +4,8 @@
 // requires netcorecheck.exe and netcorecheck_x64.exe (see download link below)
 #define UseNetCoreCheck
 #ifdef UseNetCoreCheck
-  ;#define UseDotNet50
-  #define UseDotNet50Desktop
+  ;#define UseDotNet60
+  #define UseDotNet60Desktop
 #endif
 
 // custom setup info
@@ -395,24 +395,24 @@ var
   Version: String;
 begin
 
-#ifdef UseDotNet50
-  // https://dotnet.microsoft.com/download/dotnet/5.0
-  if not IsNetCoreInstalled('Microsoft.NETCore.App 5.0.0') then begin
-    AddDependency('dotnet50' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet60
+  // https://dotnet.microsoft.com/download/dotnet/6.0
+  if not IsNetCoreInstalled('Microsoft.NETCore.App 6.0.0') then begin
+    AddDependency('dotnet60' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Runtime 5.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/a7e15da3-7a15-43c2-a481-cf50bf305214/c69b951e8b47101e90b1289c387bb01a/dotnet-runtime-5.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/36a9dc4e-1745-4f17-8a9c-f547a12e3764/ae25e38f20a4854d5e015a88659a22f9/dotnet-runtime-5.0.0-win-x64.exe'),
+      '.NET Runtime 6.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/34df41d5-c813-4e30-8aa3-3603ce6600c0/976e801af82c7108abbcb736a8bc5c14/dotnet-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b9cfdb9e-d5cd-4024-b318-00390b729d2f/65690f2440f40654898020cdfffa1050/dotnet-runtime-6.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif
 
-#ifdef UseDotNet50Desktop
-  // https://dotnet.microsoft.com/download/dotnet/5.0
-  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 5.0.0') then begin
-    AddDependency('dotnet50desktop' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet60Desktop
+  // https://dotnet.microsoft.com/download/dotnet/6.0
+  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 6.0.0') then begin
+    AddDependency('dotnet60desktop' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Desktop Runtime 5.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/b2780d75-e54a-448a-95fc-da9721b2b4c2/62310a9e9f0ba7b18741944cbae9f592/windowsdesktop-runtime-5.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/1b3a8899-127a-4465-a3c2-7ce5e4feb07b/1e153ad470768baa40ed3f57e6e7a9d8/windowsdesktop-runtime-5.0.0-win-x64.exe'),
+      '.NET Desktop Runtime 6.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/a1ca7d0d-ce01-4878-b952-3fa1e6d9a7c6/e386db367490b631b8c013a9fb0f3794/windowsdesktop-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a865ccae-2219-4184-bcd6-0178dc580589/ba452d37e8396b7a49a9adc0e1a07e87/windowsdesktop-runtime-6.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif

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

@@ -51,7 +51,7 @@
   <Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
   <PropertyGroup>
     <ProjectGuid>1f97f972-f9e8-4f35-a8b5-3f71408d2230</ProjectGuid>
-    <TargetPlatformVersion>10.0.19041.0</TargetPlatformVersion>
+    <TargetPlatformVersion>10.0.22000.0</TargetPlatformVersion>
     <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
     <DefaultLanguage>en-US</DefaultLanguage>
     <AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>

+ 2 - 2
PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>WinExe</OutputType>
-    <TargetFramework>net5.0-windows</TargetFramework>
+    <TargetFramework>net6.0-windows</TargetFramework>
     <UseWPF>true</UseWPF>
     <ApplicationManifest>app.manifest</ApplicationManifest>
     <Platforms>AnyCPU;x64;x86</Platforms>
@@ -13,7 +13,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Tools.InnoSetup" Version="6.1.2" />
+    <PackageReference Include="Tools.InnoSetup" Version="6.2.0" />
   </ItemGroup>
 
   <ItemGroup>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <Platforms>AnyCPU;x64;x86</Platforms>
   </PropertyGroup>
 

+ 26 - 0
PixiEditor.UpdateModule/UpdateChannel.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.UpdateModule
+{
+    public class UpdateChannel
+    {
+        public string Name { get; }
+        public string RepositoryOwner { get; }
+        public string RepositoryName { get; }
+        public string ApiUrl { get; } 
+        public string IncompatibleFileApiUrl { get; }
+
+        public UpdateChannel(string name, string repositoryOwner, string repositoryName)
+        {
+            Name = name;
+            RepositoryOwner = repositoryOwner;
+            RepositoryName = repositoryName;
+            ApiUrl = $"https://api.github.com/repos/{repositoryOwner}/{repositoryName}/releases/latest";
+            IncompatibleFileApiUrl = "https://raw.githubusercontent.com/" + $"{repositoryOwner}/{repositoryName}/" + "{0}/incompatible.json";
+        }
+    }
+}

+ 10 - 10
PixiEditor.UpdateModule/UpdateChecker.cs

@@ -10,16 +10,16 @@ namespace PixiEditor.UpdateModule
 {
     public class UpdateChecker
     {
-        private const string ReleaseApiUrl = "https://api.github.com/repos/PixiEditor/PixiEditor/releases/latest";
-        private const string IncompatibleFileApiUrl = "https://raw.githubusercontent.com/PixiEditor/PixiEditor/{0}/incompatible.json";
-
-        public UpdateChecker(string currentVersionTag)
+        public UpdateChecker(string currentVersionTag, UpdateChannel channel)
         {
             CurrentVersionTag = currentVersionTag;
+            Channel = channel;
         }
 
         public ReleaseInfo LatestReleaseInfo { get; private set; }
 
+        public UpdateChannel Channel { get; set; }
+
         public string CurrentVersionTag { get; }
 
         /// <summary>
@@ -45,7 +45,7 @@ namespace PixiEditor.UpdateModule
 
         public async Task<bool> CheckUpdateAvailable()
         {
-            LatestReleaseInfo = await GetLatestReleaseInfoAsync();
+            LatestReleaseInfo = await GetLatestReleaseInfoAsync(Channel.ApiUrl);
             return CheckUpdateAvailable(LatestReleaseInfo);
         }
 
@@ -56,7 +56,7 @@ namespace PixiEditor.UpdateModule
 
         public bool IsUpdateCompatible(string[] incompatibleVersions)
         {
-            return !incompatibleVersions.Select(x => x.Trim()).Contains(CurrentVersionTag.Trim());
+            return !incompatibleVersions.Select(x => x.Trim()).Contains(CurrentVersionTag[..7].Trim());
         }
 
         public async Task<bool> IsUpdateCompatible()
@@ -70,7 +70,7 @@ namespace PixiEditor.UpdateModule
             using (HttpClient client = new HttpClient())
             {
                 client.DefaultRequestHeaders.Add("User-Agent", "PixiEditor");
-                HttpResponseMessage response = await client.GetAsync(string.Format(IncompatibleFileApiUrl, tag));
+                HttpResponseMessage response = await client.GetAsync(string.Format(Channel.IncompatibleFileApiUrl, tag));
                 if (response.StatusCode == HttpStatusCode.OK)
                 {
                     string content = await response.Content.ReadAsStringAsync();
@@ -81,12 +81,12 @@ namespace PixiEditor.UpdateModule
             return Array.Empty<string>();
         }
 
-        private static async Task<ReleaseInfo> GetLatestReleaseInfoAsync()
+        private static async Task<ReleaseInfo> GetLatestReleaseInfoAsync(string apiUrl)
         {
             using (HttpClient client = new HttpClient())
             {
                 client.DefaultRequestHeaders.Add("User-Agent", "PixiEditor");
-                HttpResponseMessage response = await client.GetAsync(ReleaseApiUrl);
+                HttpResponseMessage response = await client.GetAsync(apiUrl);
                 if (response.StatusCode == HttpStatusCode.OK)
                 {
                     string content = await response.Content.ReadAsStringAsync();
@@ -99,7 +99,7 @@ namespace PixiEditor.UpdateModule
 
         private static bool ParseVersionString(string versionString, out float version)
         {
-            return float.TryParse(versionString.Replace(".", string.Empty).Insert(1, "."), NumberStyles.Any, CultureInfo.InvariantCulture, out version);
+            return float.TryParse(versionString[..7].Replace(".", string.Empty).Insert(1, "."), NumberStyles.Any, CultureInfo.InvariantCulture, out version);
         }
     }
 }

+ 38 - 2
PixiEditor.sln

@@ -1,7 +1,7 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.28729.10
+# Visual Studio Version 17
+VisualStudioVersion = 17.1.31911.260
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor", "PixiEditor\PixiEditor.csproj", "{2CCDDE79-06CB-4771-AF85-7B25313EBA30}"
 EndProject
@@ -25,6 +25,9 @@ Global
 		Debug|Any CPU = Debug|Any CPU
 		Debug|x64 = Debug|x64
 		Debug|x86 = Debug|x86
+		Dev Release|Any CPU = Dev Release|Any CPU
+		Dev Release|x64 = Dev Release|x64
+		Dev Release|x86 = Dev Release|x86
 		MSIX Debug|Any CPU = MSIX Debug|Any CPU
 		MSIX Debug|x64 = MSIX Debug|x64
 		MSIX Debug|x86 = MSIX Debug|x86
@@ -42,6 +45,12 @@ Global
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x64.Build.0 = Debug|x64
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x86.ActiveCfg = Debug|x86
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x86.Build.0 = Debug|x86
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|Any CPU.ActiveCfg = Dev Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|Any CPU.Build.0 = Dev Release|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|x64.ActiveCfg = Dev Release|x64
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|x64.Build.0 = Dev Release|x64
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|x86.ActiveCfg = Dev Release|x86
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|x86.Build.0 = Dev Release|x86
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.MSIX Debug|Any CPU.ActiveCfg = MSIX Debug|Any CPU
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.MSIX Debug|Any CPU.Build.0 = MSIX Debug|Any CPU
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.MSIX Debug|x64.ActiveCfg = MSIX Debug|x64
@@ -66,6 +75,12 @@ Global
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x64.Build.0 = Debug|x64
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x86.ActiveCfg = Debug|x86
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x86.Build.0 = Debug|x86
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|Any CPU.Build.0 = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|x64.ActiveCfg = Release|x64
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|x64.Build.0 = Release|x64
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|x86.ActiveCfg = Release|x86
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|x86.Build.0 = Release|x86
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|x64.ActiveCfg = Debug|x64
@@ -90,6 +105,12 @@ Global
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x64.Build.0 = Debug|x64
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x86.ActiveCfg = Debug|x86
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x86.Build.0 = Debug|x86
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|Any CPU.Build.0 = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|x64.ActiveCfg = Release|x64
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|x64.Build.0 = Release|x64
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|x86.ActiveCfg = Release|x86
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|x86.Build.0 = Release|x86
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.MSIX Debug|x64.ActiveCfg = Debug|x64
@@ -114,6 +135,12 @@ Global
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x64.Build.0 = Debug|x64
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x86.ActiveCfg = Debug|x86
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x86.Build.0 = Debug|x86
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|Any CPU.Build.0 = Release|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|x64.ActiveCfg = Release|x64
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|x64.Build.0 = Release|x64
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|x86.ActiveCfg = Release|x86
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|x86.Build.0 = Release|x86
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.MSIX Debug|x64.ActiveCfg = Debug|x64
@@ -141,6 +168,15 @@ Global
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|x86.ActiveCfg = Debug|x86
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|x86.Build.0 = Debug|x86
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|x86.Deploy.0 = Debug|x86
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|Any CPU.Build.0 = Release|Any CPU
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|Any CPU.Deploy.0 = Release|Any CPU
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x64.ActiveCfg = Release|x64
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x64.Build.0 = Release|x64
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x64.Deploy.0 = Release|x64
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x86.ActiveCfg = Release|x86
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x86.Build.0 = Release|x86
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x86.Deploy.0 = Release|x86
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX Debug|Any CPU.Deploy.0 = Debug|Any CPU

+ 0 - 15
PixiEditor/Helpers/AssemblyHelper.cs

@@ -1,15 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Reflection;
-using System.Text;
-
-namespace PixiEditor.Helpers
-{
-    public static class AssemblyHelper
-    {
-        public static Version GetCurrentAssemblyVersion() => Assembly.GetExecutingAssembly().GetName().Version;
-
-        public static string GetCurrentAssemblyVersion(Func<Version, string> toString) => toString(GetCurrentAssemblyVersion());
-    }
-}

+ 3 - 3
PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs

@@ -1,6 +1,6 @@
-using System.Windows;
+using PixiEditor.Models.Controllers.Shortcuts;
+using System.Windows;
 using System.Windows.Interactivity;
-using PixiEditor.Models.Controllers.Shortcuts;
 
 namespace PixiEditor.Helpers.Behaviours
 {
@@ -23,4 +23,4 @@ namespace PixiEditor.Helpers.Behaviours
             ShortcutController.BlockShortcutExecution = false;
         }
     }
-}
+}

+ 5 - 1
PixiEditor/Helpers/Behaviours/MouseBehavior.cs

@@ -60,4 +60,8 @@ namespace PixiEditor.Helpers.Behaviours
             MouseY = pos.Y;
         }
     }
-}
+}
+
+#if PUBLISH
+#error Hi
+#endif

+ 28 - 50
PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs

@@ -3,27 +3,32 @@ using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Interactivity;
-using PixiEditor.Models.Controllers.Shortcuts;
 
 namespace PixiEditor.Helpers.Behaviours
 {
     internal class TextBoxFocusBehavior : Behavior<TextBox>
     {
         // Using a DependencyProperty as the backing store for FillSize.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty FillSizeProperty =
+        public static readonly DependencyProperty SelectOnFocusProperty =
             DependencyProperty.Register(
-                "FillSize",
+                nameof(SelectOnFocus),
                 typeof(bool),
                 typeof(TextBoxFocusBehavior),
-                new PropertyMetadata(false));
+                new PropertyMetadata(true));
 
-        private string oldText; // Value of textbox before editing
-        private bool valueConverted; // This bool is used to avoid double convertion if enter is hitted
+        public static readonly DependencyProperty NextControlProperty =
+            DependencyProperty.Register(nameof(NextControl), typeof(FrameworkElement), typeof(TextBoxFocusBehavior));
 
-        public bool FillSize
+        public FrameworkElement NextControl
         {
-            get => (bool)GetValue(FillSizeProperty);
-            set => SetValue(FillSizeProperty, value);
+            get => (FrameworkElement)GetValue(NextControlProperty);
+            set => SetValue(NextControlProperty, value);
+        }
+
+        public bool SelectOnFocus
+        {
+            get => (bool)GetValue(SelectOnFocusProperty);
+            set => SetValue(SelectOnFocusProperty, value);
         }
 
         protected override void OnAttached()
@@ -32,7 +37,6 @@ namespace PixiEditor.Helpers.Behaviours
             AssociatedObject.GotKeyboardFocus += AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotMouseCapture += AssociatedObjectGotMouseCapture;
             AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObjectPreviewMouseLeftButtonDown;
-            AssociatedObject.LostKeyboardFocus += AssociatedObject_LostKeyboardFocus;
             AssociatedObject.KeyUp += AssociatedObject_KeyUp;
         }
 
@@ -42,7 +46,6 @@ namespace PixiEditor.Helpers.Behaviours
             AssociatedObject.GotKeyboardFocus -= AssociatedObjectGotKeyboardFocus;
             AssociatedObject.GotMouseCapture -= AssociatedObjectGotMouseCapture;
             AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObjectPreviewMouseLeftButtonDown;
-            AssociatedObject.LostKeyboardFocus -= AssociatedObject_LostKeyboardFocus;
             AssociatedObject.KeyUp -= AssociatedObject_KeyUp;
         }
 
@@ -54,19 +57,26 @@ namespace PixiEditor.Helpers.Behaviours
                 return;
             }
 
-            ConvertValue();
             RemoveFocus();
         }
 
         private void RemoveFocus()
         {
+            DependencyObject scope = FocusManager.GetFocusScope(AssociatedObject);
+
+            if (NextControl != null)
+            {
+                FocusManager.SetFocusedElement(scope, NextControl);
+                return;
+            }
+
             FrameworkElement parent = (FrameworkElement)AssociatedObject.Parent;
+
             while (parent != null && parent is IInputElement element && !element.Focusable)
             {
                 parent = (FrameworkElement)parent.Parent;
             }
 
-            DependencyObject scope = FocusManager.GetFocusScope(AssociatedObject);
             FocusManager.SetFocusedElement(scope, parent);
         }
 
@@ -74,19 +84,16 @@ namespace PixiEditor.Helpers.Behaviours
             object sender,
             KeyboardFocusChangedEventArgs e)
         {
-            AssociatedObject.SelectAll();
-            if (FillSize)
-            {
-                valueConverted = false;
-                oldText = AssociatedObject.Text; // Sets old value when keyboard is focused on object
-            }
+            if (SelectOnFocus)
+                AssociatedObject.SelectAll();
         }
 
         private void AssociatedObjectGotMouseCapture(
             object sender,
             MouseEventArgs e)
         {
-            AssociatedObject.SelectAll();
+            if (SelectOnFocus)
+                AssociatedObject.SelectAll();
         }
 
         private void AssociatedObjectPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
@@ -97,34 +104,5 @@ namespace PixiEditor.Helpers.Behaviours
                 e.Handled = true;
             }
         }
-
-        private void AssociatedObject_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
-        {
-            ConvertValue();
-        }
-
-        /// <summary>
-        ///     Converts number from textbox to format "number px" ex. "15 px".
-        /// </summary>
-        private void ConvertValue()
-        {
-            if (valueConverted || FillSize == false || AssociatedObject.Text == oldText)
-            {
-                return;
-            }
-
-            if (int.TryParse(Regex.Replace(AssociatedObject.Text, "\\p{L}", string.Empty).Trim(), out int result) && result > 0)
-            {
-                AssociatedObject.Text = $"{result} px";
-            }
-
-            // If text in textbox isn't number, set it to old value
-            else
-            {
-                AssociatedObject.Text = oldText;
-            }
-
-            valueConverted = true;
-        }
     }
-}
+}

+ 45 - 0
PixiEditor/Helpers/ClipboardHelper.cs

@@ -0,0 +1,45 @@
+using System.Windows;
+
+namespace PixiEditor.Helpers
+{
+    class ClipboardHelper
+    {
+        public static bool TrySetDataObject(DataObject obj, bool copy)
+        {
+            try
+            {
+                Clipboard.SetDataObject(obj, copy);
+                return true;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+        public static DataObject TryGetDataObject()
+        {
+            try
+            {
+                return (DataObject)Clipboard.GetDataObject();
+            }
+            catch
+            {
+                return null;
+            }
+        }
+
+        public static bool TryClear()
+        {
+            try
+            {
+                Clipboard.Clear();
+                return true;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+    }
+}

+ 19 - 0
PixiEditor/Helpers/Converters/DebugConverter.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Globalization;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public class DebugConverter
+        : SingleInstanceConverter<DebugConverter>
+    {
+        public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return value;
+        }
+
+        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return value;
+        }
+    }
+}

+ 1 - 1
PixiEditor/Helpers/Converters/FinalIsVisibleToVisiblityConverter.cs

@@ -24,4 +24,4 @@ namespace PixiEditor.Helpers.Converters
                     : (object)Visibility.Collapsed;
         }
     }
-}
+}

+ 8 - 7
PixiEditor/Helpers/Converters/IndexOfConverter.cs

@@ -1,5 +1,4 @@
-using PixiEditor.Models.Controllers;
-using PixiEditor.Models.Layers;
+using PixiEditor.Models.Layers;
 using PixiEditor.ViewModels;
 using System;
 using System.Globalization;
@@ -12,11 +11,13 @@ namespace PixiEditor.Helpers.Converters
     {
         public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
-            BitmapManager bitmapManager = ViewModelMain.Current.BitmapManager;
+            if (value is Layer layer && ViewModelMain.Current.BitmapManager.ActiveDocument != null)
+            {
+                int index = ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.IndexOf(layer);
+                return index;
+            }
 
-            return value is Layer layer && bitmapManager.ActiveDocument != null
-                   ? bitmapManager.ActiveDocument.Layers.IndexOf(layer)
-                   : Binding.DoNothing;
+            return Binding.DoNothing;
         }
     }
-}
+}

+ 2 - 1
PixiEditor/Helpers/Converters/LayerStructureToGroupsConverter.cs

@@ -6,6 +6,7 @@ using System.Collections.ObjectModel;
 using System.Globalization;
 using System.Linq;
 using System.Windows.Data;
+using PixiEditor.Models.DataHolders;
 
 namespace PixiEditor.Helpers.Converters
 {
@@ -24,7 +25,7 @@ namespace PixiEditor.Helpers.Converters
 
         private ObservableCollection<GuidStructureItem> GetSubGroups(IEnumerable<GuidStructureItem> groups)
         {
-            ObservableCollection<GuidStructureItem> finalGroups = new ObservableCollection<GuidStructureItem>();
+            WpfObservableRangeCollection<GuidStructureItem> finalGroups = new WpfObservableRangeCollection<GuidStructureItem>();
             foreach (var group in groups)
             {
                 finalGroups.AddRange(GetSubGroups(group));

+ 30 - 12
PixiEditor/Helpers/Converters/LayersToStructuredLayersConverter.cs

@@ -1,10 +1,10 @@
-using PixiEditor.Models.Layers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
 using System;
 using System.Collections.Generic;
-using System.Collections.ObjectModel;
 using System.Globalization;
 using System.Linq;
-using System.Windows.Data;
+using System.Windows;
 
 namespace PixiEditor.Helpers.Converters
 {
@@ -13,30 +13,34 @@ namespace PixiEditor.Helpers.Converters
         : MultiValueMarkupConverter
     {
         private static StructuredLayerTree cachedTree;
-        private List<Guid> lastLayers = new List<Guid>();
-        private ObservableCollection<GuidStructureItem> lastStructure = new ObservableCollection<GuidStructureItem>();
+        private List<Guid> lastLayerGuids = new List<Guid>();
+        private IList<Layer> lastLayers = new List<Layer>();
+        private WpfObservableRangeCollection<GuidStructureItem> lastStructure = new WpfObservableRangeCollection<GuidStructureItem>();
 
         public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
         {
-            if (values[0] is ObservableCollection<Layer> layers && values[1] is LayerStructure structure)
+            if (values[0] is WpfObservableRangeCollection<Layer> layers && values[1] is LayerStructure structure)
             {
                 if (cachedTree == null)
                 {
                     cachedTree = new StructuredLayerTree(layers, structure);
                 }
 
-                if (TryFindStructureDifferences(structure) || lastLayers.Count != layers.Count || LayerOrderIsDifferent(layers))
+                if (TryFindStructureDifferences(structure) ||
+                    lastLayerGuids.Count != layers.Count ||
+                    LayerOrderIsDifferent(layers) ||
+                    LayersAreDifferentObjects(layers, lastLayers))
                 {
                     cachedTree = new StructuredLayerTree(layers, structure);
-
-                    lastLayers = layers.Select(x => x.LayerGuid).ToList();
+                    lastLayers = layers;
+                    lastLayerGuids = layers.Select(x => x.GuidValue).ToList();
                     lastStructure = structure.CloneGroups();
                 }
 
                 return cachedTree.RootDirectoryItems;
             }
 
-            return new StructuredLayerTree(null, null);
+            return DependencyProperty.UnsetValue;
         }
 
         public override object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
@@ -46,8 +50,22 @@ namespace PixiEditor.Helpers.Converters
 
         private bool LayerOrderIsDifferent(IList<Layer> layers)
         {
-            var guids = layers.Select(x => x.LayerGuid).ToArray();
-            return !guids.SequenceEqual(lastLayers);
+            var guids = layers.Select(x => x.GuidValue).ToArray();
+            return !guids.SequenceEqual(lastLayerGuids);
+        }
+
+        /// <summary>
+        /// This should trigger if you open and close the same files twice.
+        /// Even though the layers are technically the same, having two different objects screws things up down the line.
+        /// </summary>
+        private bool LayersAreDifferentObjects(IList<Layer> layers, IList<Layer> lastLayers)
+        {
+            for (int i = 0; i < layers.Count; i++)
+            {
+                if (layers[i] != lastLayers[i])
+                    return true;
+            }
+            return false;
         }
 
         private bool TryFindStructureDifferences(LayerStructure structure)

+ 23 - 0
PixiEditor/Helpers/Converters/SKColorToMediaColorConverter.cs

@@ -0,0 +1,23 @@
+using SkiaSharp;
+using System;
+using System.Globalization;
+using System.Windows.Data;
+using System.Windows.Media;
+
+namespace PixiEditor.Helpers.Converters
+{
+    class SKColorToMediaColorConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            var skcolor = (SKColor)value;
+            return Color.FromArgb(skcolor.Alpha, skcolor.Red, skcolor.Green, skcolor.Blue);
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            var color = (Color)value;
+            return new SKColor(color.R, color.G, color.B, color.A);
+        }
+    }
+}

+ 6 - 7
PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Globalization;
-using System.Linq;
 using System.Text.RegularExpressions;
 using System.Windows.Data;
 
@@ -12,24 +11,24 @@ namespace PixiEditor.Helpers.Converters
     {
         public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
-            return string.Format("{0} {1}", value, "px");
+            return value.ToString();
         }
 
         public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
         {
-            if (string.IsNullOrWhiteSpace(value as string))
+            if (value is not string s)
             {
                 return null;
             }
 
-            string slicedString = value.ToString().Split(' ').First();
-            slicedString = Regex.Replace(slicedString, "\\p{L}", string.Empty);
-            if (slicedString == string.Empty)
+            Match match = Regex.Match(s, @"\d+");
+
+            if (!match.Success)
             {
                 return null;
             }
 
-            return int.Parse(slicedString);
+            return int.Parse(match.Groups[0].ValueSpan);
         }
     }
 }

+ 23 - 0
PixiEditor/Helpers/Converters/ViewboxInverseTransformConverter.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Media;
+
+namespace PixiEditor.Helpers.Converters
+{
+    class ViewboxInverseTransformConverter : MultiValueMarkupConverter
+    {
+        public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+        {
+            var transform = ((ContainerVisual)VisualTreeHelper.GetChild((DependencyObject)values[0], 0)).Transform;
+            if (transform == null)
+                return DependencyProperty.UnsetValue;
+            return transform.Inverse;
+        }
+
+        public override object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 85 - 0
PixiEditor/Helpers/CoordinatesHelper.cs

@@ -0,0 +1,85 @@
+using PixiEditor.Models.Position;
+using System;
+using System.Collections.Generic;
+
+namespace PixiEditor.Helpers
+{
+    internal class CoordinatesHelper
+    {
+        public static (Coordinates, Coordinates) GetSquareOrLineCoordinates(IReadOnlyList<Coordinates> coords)
+        {
+            if (DoCoordsFormLine(coords))
+            {
+                return GetLineCoordinates(coords);
+            }
+            return GetSquareCoordiantes(coords);
+        }
+
+        private static bool DoCoordsFormLine(IReadOnlyList<Coordinates> coords)
+        {
+            var p1 = coords[0];
+            var p2 = coords[^1];
+            //find delta and mirror to first quadrant
+            float dX = Math.Abs(p2.X - p1.X);
+            float dY = Math.Abs(p2.Y - p1.Y);
+
+            //normalize
+            float length = (float)Math.Sqrt(dX * dX + dY * dY);
+            if (length == 0)
+                return false;
+            dX = dX / length;
+            dY = dY / length;
+
+            return dX < 0.25f || dY < 0.25f; //angle < 15 deg or angle > 75 deg (sin 15 ~= 0.25)
+        }
+
+        public static (Coordinates, Coordinates) GetLineCoordinates(IReadOnlyList<Coordinates> mouseMoveCords)
+        {
+            int xStart = mouseMoveCords[0].X;
+            int yStart = mouseMoveCords[0].Y;
+
+            int xEnd = mouseMoveCords[^1].X;
+            int yEnd = mouseMoveCords[^1].Y;
+
+
+            if (Math.Abs(xStart - xEnd) > Math.Abs(yStart - yEnd))
+            {
+                yEnd = yStart;
+            }
+            else
+            {
+                xEnd = xStart;
+            }
+            return (new(xStart, yStart), new(xEnd, yEnd));
+        }
+
+        /// <summary>
+        ///     Extracts square from rectangle mouse drag, used to draw symmetric shapes.
+        /// </summary>
+        public static (Coordinates, Coordinates) GetSquareCoordiantes(IReadOnlyList<Coordinates> mouseMoveCords)
+        {
+            var end = mouseMoveCords[^1];
+            var start = mouseMoveCords[0];
+
+            //find delta and mirror to first quadrant
+            var dX = Math.Abs(start.X - end.X);
+            var dY = Math.Abs(start.Y - end.Y);
+
+            float sqrt2 = (float)Math.Sqrt(2);
+            //vector of length 1 at 45 degrees;
+            float diagX, diagY;
+            diagX = diagY = 1 / sqrt2;
+
+            //dot product of delta and diag, returns length of [delta projected onto diag]
+            float projectedLength = diagX * dX + diagY * dY;
+            //project above onto axes
+            float axisLength = projectedLength / sqrt2;
+
+            //final coords
+            float x = -Math.Sign(start.X - end.X) * axisLength;
+            float y = -Math.Sign(start.Y - end.Y) * axisLength;
+            end = new Coordinates((int)x + start.X, (int)y + start.Y);
+            return (start, end);
+        }
+    }
+}

+ 168 - 0
PixiEditor/Helpers/EllipseGenerator.cs

@@ -0,0 +1,168 @@
+using PixiEditor.Models.Position;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers
+{
+    internal static class EllipseGenerator
+    {
+        public static List<DoubleCoords> SplitEllipseIntoLines(List<Coordinates> ellipse)
+        {
+            List<DoubleCoords> lines = new();
+            var sorted = ellipse.OrderBy(
+                a => a,
+                Comparer<Coordinates>.Create((a, b) => a.Y != b.Y ? a.Y - b.Y : a.X - b.X)
+                );
+
+            int minX = int.MaxValue;
+            int maxX = int.MinValue;
+            Coordinates? prev = null;
+            foreach (var point in sorted)
+            {
+                if (prev.HasValue && point.Y != prev.Value.Y)
+                {
+                    int prevY = prev.Value.Y;
+                    lines.Add(new DoubleCoords(new(minX, prevY), new(maxX, prevY)));
+                    minX = int.MaxValue;
+                    maxX = int.MinValue;
+                }
+                minX = Math.Min(point.X, minX);
+                maxX = Math.Max(point.X, maxX);
+                prev = point;
+            }
+            lines.Add(new DoubleCoords(new(minX, prev.Value.Y), new(maxX, prev.Value.Y)));
+            return lines;
+        }
+        public static List<Coordinates> GenerateEllipseFromRect(DoubleCoords rect, List<Coordinates> listToFill = null)
+        {
+            float radiusX = (rect.Coords2.X - rect.Coords1.X) / 2.0f;
+            float radiusY = (rect.Coords2.Y - rect.Coords1.Y) / 2.0f;
+            float centerX = (rect.Coords1.X + rect.Coords2.X + 1) / 2.0f;
+            float centerY = (rect.Coords1.Y + rect.Coords2.Y + 1) / 2.0f;
+            return GenerateMidpointEllipse(radiusX, radiusY, centerX, centerY, listToFill);
+        }
+
+        /// <summary>
+        /// Draws an ellipse using it's center and radii
+        ///
+        /// Here is a usage example:
+        /// Let's say you want an ellipse that's 3 pixels wide and 3 pixels tall located in the top right corner of the canvas
+        /// It's center is at (1.5; 1.5). That's in the middle of a pixel
+        /// The radii are both equal to 1. Notice that it's 1 and not 1.5, since we want the ellipse to land in the middle of the pixel, not outside of it.
+        /// See desmos (note the inverted y axis): https://www.desmos.com/calculator/tq9uqg0hcq
+        ///
+        /// Another example:
+        /// 4x4 ellipse in the top right corner of the canvas
+        /// Center is at (2; 2). It's a place where 4 pixels meet
+        /// Both radii are 1.5. Making them 2 would make the ellipse touch the edges of pixels, whereas we want it to stay in the middle
+        /// </summary>
+        public static List<Coordinates> GenerateMidpointEllipse(
+            double halfWidth,
+            double halfHeight,
+            double centerX,
+            double centerY,
+            List<Coordinates> listToFill = null)
+        {
+            listToFill ??= new List<Coordinates>();
+            if (halfWidth < 1 || halfHeight < 1)
+            {
+                AddFallbackRectangle(halfWidth, halfHeight, centerX, centerY, listToFill);
+                return listToFill;
+            }
+
+            // ellipse formula: halfHeight^2 * x^2 + halfWidth^2 * y^2 - halfHeight^2 * halfWidth^2 = 0
+
+            // Make sure we are always at the center of a pixel
+            double currentX = Math.Ceiling(centerX - 0.5) + 0.5;
+            double currentY = centerY + halfHeight;
+
+
+            double currentSlope;
+
+            // from PI/2 to PI/4
+            do
+            {
+                AddRegionPoints(listToFill, currentX, centerX, currentY, centerY);
+
+                // calculate next pixel coords
+                currentX++;
+
+                if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX, 2)) +
+                    (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY - 0.5, 2)) -
+                    (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) >= 0)
+                {
+                    currentY--;
+                }
+
+                // calculate how far we've advanced
+                double derivativeX = 2 * Math.Pow(halfHeight, 2) * (currentX - centerX);
+                double derivativeY = 2 * Math.Pow(halfWidth, 2) * (currentY - centerY);
+                currentSlope = -(derivativeX / derivativeY);
+            }
+            while (currentSlope > -1 && currentY - centerY > 0.5);
+
+            // from PI/4 to 0
+            while (currentY - centerY >= 0)
+            {
+                AddRegionPoints(listToFill, currentX, centerX, currentY, centerY);
+
+                currentY--;
+                if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX + 0.5, 2)) +
+                    (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY, 2)) -
+                    (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) < 0)
+                {
+                    currentX++;
+                }
+            }
+
+            return listToFill;
+        }
+
+        private static void AddFallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY, List<Coordinates> coordinates)
+        {
+            int left = (int)Math.Floor(centerX - halfWidth);
+            int top = (int)Math.Floor(centerY - halfHeight);
+            int right = (int)Math.Floor(centerX + halfWidth);
+            int bottom = (int)Math.Floor(centerY + halfHeight);
+
+            for (int x = left; x <= right; x++)
+            {
+                coordinates.Add(new Coordinates(x, top));
+                coordinates.Add(new Coordinates(x, bottom));
+            }
+
+            for (int y = top; y <= bottom; y++)
+            {
+                coordinates.Add(new Coordinates(left, y));
+                coordinates.Add(new Coordinates(right, y));
+            }
+        }
+
+        private static void AddRegionPoints(List<Coordinates> coordinates, double x, double xc, double y, double yc)
+        {
+            int xFloor = (int)Math.Floor(x);
+            int yFloor = (int)Math.Floor(y);
+            int xFloorInv = (int)Math.Floor(-x + 2 * xc);
+            int yFloorInv = (int)Math.Floor(-y + 2 * yc);
+
+            //top and bottom or left and right
+            if (xFloor == xFloorInv || yFloor == yFloorInv)
+            {
+                coordinates.Add(new Coordinates(xFloor, yFloor));
+                coordinates.Add(new Coordinates(xFloorInv, yFloorInv));
+            }
+            //part of the arc
+            else
+            {
+                coordinates.Add(new Coordinates(xFloor, yFloor));
+                coordinates.Add(new Coordinates(xFloorInv, yFloorInv));
+                coordinates.Add(new Coordinates(xFloorInv, yFloor));
+                coordinates.Add(new Coordinates(xFloor, yFloorInv));
+            }
+        }
+    }
+}

+ 54 - 0
PixiEditor/Helpers/Extensions/Int32RectEx.cs

@@ -0,0 +1,54 @@
+using System;
+using System.Windows;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    static class Int32RectEx
+    {
+        public static Int32Rect Intersect(this Int32Rect rect, Int32Rect other)
+        {
+            int rectX2 = rect.X + rect.Width;
+            int rectY2 = rect.Y + rect.Height;
+
+            int otherX2 = other.X + other.Width;
+            int otherY2 = other.Y + other.Height;
+
+            int maxX1 = Math.Max(rect.X, other.X);
+            int maxY1 = Math.Max(rect.Y, other.Y);
+
+            int minX2 = Math.Min(rectX2, otherX2);
+            int minY2 = Math.Min(rectY2, otherY2);
+
+            int width = minX2 - maxX1;
+            int height = minY2 - maxY1;
+
+            if (width <= 0 || height <= 0)
+                return Int32Rect.Empty;
+
+            return new Int32Rect(maxX1, maxY1, width, height);
+        }
+
+        public static Int32Rect Expand(this Int32Rect rect, Int32Rect other)
+        {
+            int rectX2 = rect.X + rect.Width;
+            int rectY2 = rect.Y + rect.Height;
+
+            int otherX2 = other.X + other.Width;
+            int otherY2 = other.Y + other.Height;
+
+            int minX1 = Math.Min(rect.X, other.X);
+            int minY1 = Math.Min(rect.Y, other.Y);
+
+            int maxX2 = Math.Max(rectX2, otherX2);
+            int maxY2 = Math.Max(rectY2, otherY2);
+
+            int width = maxX2 - minX1;
+            int height = maxY2 - minY1;
+
+            if (width <= 0 || height <= 0)
+                return Int32Rect.Empty;
+
+            return new Int32Rect(minX1, minY1, width, height);
+        }
+    }
+}

+ 0 - 16
PixiEditor/Helpers/Extensions/ObservableCollectionEx.cs

@@ -1,16 +0,0 @@
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-
-namespace PixiEditor.Helpers.Extensions
-{
-    public static class ObservableCollectionEx
-    {
-        public static void AddRange<T>(this ObservableCollection<T> collection, IEnumerable<T> items)
-        {
-            foreach (var item in items)
-            {
-                collection.Add(item);
-            }
-        }
-    }
-}

+ 105 - 91
PixiEditor/Helpers/Extensions/ParserHelpers.cs

@@ -1,14 +1,12 @@
 using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
-using PixiEditor.Parser;
-using System;
+using PixiEditor.Parser;
+using PixiEditor.Parser.Skia;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
 using System.Collections.ObjectModel;
-using System.Linq;
-using System.Windows;
-using System.Windows.Media;
-using SDColor = System.Drawing.Color;
-
+
 namespace PixiEditor.Helpers.Extensions
 {
     public static class ParserHelpers
@@ -18,127 +16,143 @@ namespace PixiEditor.Helpers.Extensions
             Document document = new Document(serializableDocument.Width, serializableDocument.Height)
             {
                 Layers = serializableDocument.ToLayers(),
-                Swatches = new ObservableCollection<Color>(serializableDocument.Swatches.Select(x =>
-                    Color.FromArgb(x.A, x.R, x.G, x.B)))
+                Swatches = new ObservableCollection<SKColor>(serializableDocument.Swatches.ToSKColors())
             };
 
-            document.LayerStructure.Groups = serializableDocument.ToGroups();
+            document.LayerStructure.Groups = serializableDocument.ToGroups(document);
 
             if (document.Layers.Count > 0)
             {
                 document.SetMainActiveLayer(0);
             }
+            document.Renderer.ForceRerender();
 
             return document;
         }
 
-        public static ObservableCollection<GuidStructureItem> ToGroups(this SerializableDocument serializableDocument)
-        {
-            return ToGroups(serializableDocument.Groups);
+        public static WpfObservableRangeCollection<Layer> ToLayers(this SerializableDocument document)
+        {
+            WpfObservableRangeCollection<Layer> layers = new();
+            foreach (SerializableLayer slayer in document)
+            {
+                layers.Add(slayer.ToLayer());
+            }
+
+            return layers;
         }
 
-        public static ObservableCollection<Layer> ToLayers(this SerializableDocument serializableDocument)
+        public static Layer ToLayer(this SerializableLayer layer)
         {
-            ObservableCollection<Layer> layers = new ObservableCollection<Layer>();
-            for (int i = 0; i < serializableDocument.Layers.Count; i++)
+            return new Layer(layer.Name, new Surface(layer.ToSKImage()))
             {
-                SerializableLayer serLayer = serializableDocument.Layers[i];
-                Layer layer =
-                    new Layer(serLayer.Name, BitmapUtils.BytesToWriteableBitmap(serLayer.Width, serLayer.Height, serLayer.BitmapBytes))
-                    {
-                        IsVisible = serLayer.IsVisible,
-                        Offset = new Thickness(serLayer.OffsetX, serLayer.OffsetY, 0, 0),
-                        Opacity = serLayer.Opacity,
-                        MaxHeight = serializableDocument.Height,
-                        MaxWidth = serializableDocument.Width,
-                    };
-                if (serLayer.LayerGuid != Guid.Empty)
-                {
-                    layer.ChangeGuid(serLayer.LayerGuid);
-                }
-                layers.Add(layer);
+                Opacity = layer.Opacity,
+                IsVisible = layer.IsVisible,
+                Offset = new(layer.OffsetX, layer.OffsetY, 0, 0)
+            };
+        }
+
+        public static WpfObservableRangeCollection<GuidStructureItem> ToGroups(this SerializableDocument sdocument, Document document)
+        {
+            WpfObservableRangeCollection<GuidStructureItem> groups = new();
+
+            if (sdocument.Groups == null)
+            {
+                return groups;
             }
 
-            return layers;
+            foreach (SerializableGroup sgroup in sdocument.Groups)
+            {
+                groups.Add(sgroup.ToGroup(null, document));
+            }
+
+            return groups;
         }
 
-        public static SerializableDocument ToSerializable(this Document document)
+        public static GuidStructureItem ToGroup(this SerializableGroup sgroup, GuidStructureItem parent, Document document)
         {
-            SerializableDocument serializable = new SerializableDocument
+            GuidStructureItem group = new GuidStructureItem(sgroup.Name, Guid.Empty)
             {
-                Width = document.Width,
-                Height = document.Height,
-                Layers = document.Layers.Select(x => x.ToSerializable()).ToList(),
-                Groups = document.LayerStructure.Groups.Select(x => x.ToSerializable()).ToArray(),
-                Swatches = document.Swatches.Select(x => SDColor.FromArgb(x.A, x.R, x.G, x.B)).ToList()
+                Opacity = sgroup.Opacity,
+                IsVisible = sgroup.IsVisible,
+                Parent = parent,
+                StartLayerGuid = document.Layers[sgroup.StartLayer].GuidValue,
+                EndLayerGuid = document.Layers[sgroup.EndLayer].GuidValue
             };
 
-            return serializable;
+            group.Subgroups = new(sgroup.Subgroups.ToGroups(document, group));
+
+            return group;
+        }
+
+        public static SerializableDocument ToSerializable(this Document document)
+        {
+            return new SerializableDocument(document.Width, document.Height,
+                                            document.LayerStructure.Groups.ToSerializable(document),
+                                            document.Layers.ToSerializable()).AddSwatches(document.Swatches);
         }
 
-        public static SerializableGuidStructureItem ToSerializable(this GuidStructureItem group, SerializableGuidStructureItem parent = null)
+        public static IEnumerable<SerializableLayer> ToSerializable(this IEnumerable<Layer> layers)
         {
-            var serializedGroup = new SerializableGuidStructureItem(
-                    group.GroupGuid,
-                    group.Name,
-                    group.StartLayerGuid,
-                    group.EndLayerGuid,
-                    null, group.IsVisible, group.Opacity);
-            serializedGroup.Subgroups = group.Subgroups.Select(x => x.ToSerializable(serializedGroup)).ToArray();
-            return serializedGroup;
+            foreach (Layer layer in layers)
+            {
+                yield return layer.ToSerializable();
+            }
         }
 
         public static SerializableLayer ToSerializable(this Layer layer)
         {
-            SerializableLayer serializable = new SerializableLayer
+            return new SerializableLayer(layer.Width, layer.Height, layer.OffsetX, layer.OffsetY)
             {
-                LayerGuid = layer.LayerGuid,
-                Name = layer.Name,
-                Width = layer.Width,
-                Height = layer.Height,
-                BitmapBytes = layer.ConvertBitmapToBytes(),
                 IsVisible = layer.IsVisible,
-                OffsetX = (int)layer.Offset.Left,
-                OffsetY = (int)layer.Offset.Top,
                 Opacity = layer.Opacity,
-                MaxWidth = layer.MaxWidth,
-                MaxHeight = layer.MaxHeight
+                Name = layer.Name
+            }.FromSKImage(layer.LayerBitmap.SkiaSurface.Snapshot());
+        }
+
+        public static IEnumerable<SerializableGroup> ToSerializable(this IEnumerable<GuidStructureItem> groups, Document document)
+        {
+            foreach (GuidStructureItem group in groups)
+            {
+                yield return group.ToSerializable(document);
+            }
+        }
+
+        public static SerializableGroup ToSerializable(this GuidStructureItem group, Document document)
+        {
+            SerializableGroup serializable = new SerializableGroup(group.Name, group.Subgroups.ToSerializable(document))
+            {
+                Opacity = group.Opacity,
+                IsVisible = group.IsVisible
             };
 
+            for (int i = 0; i < document.Layers.Count; i++)
+            {
+                if (group.StartLayerGuid == document.Layers[i].GuidValue)
+                {
+                    serializable.StartLayer = i;
+                }
+
+                if (group.EndLayerGuid == document.Layers[i].GuidValue)
+                {
+                    serializable.EndLayer = i;
+                }
+            }
+
             return serializable;
-        }
-
-        private static ObservableCollection<GuidStructureItem> ToGroups(SerializableGuidStructureItem[] serializableGroups, GuidStructureItem parent = null)
-        {
-            ObservableCollection<GuidStructureItem> groups = new ObservableCollection<GuidStructureItem>();
-
-            if (serializableGroups == null)
-            {
-                return groups;
-            }
-
-            foreach (var serializableGroup in serializableGroups)
-            {
-                groups.Add(ToGroup(serializableGroup, parent));
-            }
-            return groups;
         }
 
-        private static GuidStructureItem ToGroup(SerializableGuidStructureItem group, GuidStructureItem parent = null)
-        {
-            if (group == null)
-            {
-                return null;
-            }
-            var parsedGroup = new GuidStructureItem(
-                group.Name,
-                group.StartLayerGuid,
-                group.EndLayerGuid,
-                new ObservableCollection<GuidStructureItem>(),
-                parent)
-            { Opacity = group.Opacity, IsVisible = group.IsVisible, GroupGuid = group.GroupGuid, IsExpanded = true };
-            parsedGroup.Subgroups = ToGroups(group.Subgroups, parsedGroup);
-            return parsedGroup;
+        private static IEnumerable<GuidStructureItem> ToGroups(this IEnumerable<SerializableGroup> groups, Document document, GuidStructureItem parent)
+        {
+            foreach (SerializableGroup sgroup in groups)
+            {
+                yield return sgroup.ToGroup(parent, document);
+            }
+        }
+
+        private static SerializableDocument AddSwatches(this SerializableDocument document, IEnumerable<SKColor> colors)
+        {
+            document.Swatches.AddRange(colors);
+            return document;
         }
     }
 }

+ 73 - 0
PixiEditor/Helpers/Extensions/PixelFormatHelper.cs

@@ -0,0 +1,73 @@
+using SkiaSharp;
+using System;
+using System.Windows.Media;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    public static class PixelFormatHelper
+    {
+        public static SKColorType ToSkia(this PixelFormat format, out SKAlphaType alphaType)
+        {
+            if (TryToSkia(format, out SKColorType color, out alphaType))
+            {
+                return color;
+            }
+            else
+            {
+                throw new NotImplementedException($"Skia does not support the '{format}' format");
+            }
+        }
+
+        public static bool TryToSkia(this PixelFormat format, out SKColorType colorType, out SKAlphaType alphaType)
+        {
+            if (format == PixelFormats.Rgba64)
+            {
+                alphaType = SKAlphaType.Unpremul;
+                colorType = SKColorType.Rgba16161616;
+                return true;
+            }
+            else if (format == PixelFormats.Bgra32)
+            {
+                alphaType = SKAlphaType.Unpremul;
+                colorType = SKColorType.Bgra8888;
+                return true;
+            }
+            else if (format == PixelFormats.Default)
+            {
+                alphaType = SKAlphaType.Unpremul;
+                colorType = SKColorType.RgbaF16;
+                return true;
+            }
+            else if (format == PixelFormats.Gray8)
+            {
+                alphaType = SKAlphaType.Opaque;
+                colorType = SKColorType.Gray8;
+                return true;
+            }
+            else if (format == PixelFormats.Pbgra32)
+            {
+                alphaType = SKAlphaType.Premul;
+                colorType = SKColorType.Bgra8888;
+                return true;
+            }
+            else if (format == PixelFormats.Bgr101010 || format == PixelFormats.Bgr24 || format == PixelFormats.Bgr32 || format == PixelFormats.Bgr555 ||
+                     format == PixelFormats.Bgr565 || format == PixelFormats.BlackWhite || format == PixelFormats.Cmyk32 || format == PixelFormats.Gray16 ||
+                     format == PixelFormats.Gray2 || format == PixelFormats.Gray32Float || format == PixelFormats.Gray4 || format == PixelFormats.Indexed1 ||
+                     format == PixelFormats.Indexed2 || format == PixelFormats.Indexed4 || format == PixelFormats.Indexed8 || format == PixelFormats.Prgba128Float ||
+                     format == PixelFormats.Prgba64 || format == PixelFormats.Rgb128Float || format == PixelFormats.Rgb24 || format == PixelFormats.Rgb48 ||
+                     format == PixelFormats.Rgba128Float)
+            {
+                alphaType = SKAlphaType.Unknown;
+                colorType = SKColorType.Unknown;
+                return false;
+            }
+
+            throw new NotImplementedException($"'{format}' has not been implemented by {nameof(PixelFormatHelper)}.{nameof(TryToSkia)}()");
+        }
+
+        public static bool IsSkiaSupported(this PixelFormat format)
+        {
+            return TryToSkia(format, out _, out _);
+        }
+    }
+}

+ 11 - 0
PixiEditor/Helpers/Extensions/PixiParserHelper.cs

@@ -0,0 +1,11 @@
+using PixiEditor.Parser;
+using SkiaSharp;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    public static class PixiParserHelper
+    {
+        public static SKRectI GetRect(this SerializableLayer layer) =>
+            SKRectI.Create(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height);
+    }
+}

+ 63 - 0
PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -0,0 +1,63 @@
+using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Controllers.Shortcuts;
+using PixiEditor.Models.Services;
+using PixiEditor.Models.Tools;
+using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Models.UserPreferences;
+using PixiEditor.ViewModels;
+using PixiEditor.ViewModels.SubViewModels.Main;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers.Extensions
+{
+    public static class ServiceCollectionHelpers
+    {
+        /// <summary>
+        /// Add's all the services required to fully run PixiEditor's MainWindow
+        /// </summary>
+        public static IServiceCollection AddPixiEditor(this IServiceCollection collection) => collection
+                .AddSingleton<ViewModelMain>()
+                .AddSingleton<IPreferences, PreferencesSettings>()
+                // View Models
+                .AddSingleton<StylusViewModel>()
+                .AddSingleton<WindowViewModel>()
+                .AddSingleton<ToolsViewModel>()
+                .AddSingleton<FileViewModel>()
+                .AddSingleton<UpdateViewModel>()
+                .AddSingleton<IoViewModel>()
+                .AddSingleton<LayersViewModel>()
+                .AddSingleton<ClipboardViewModel>()
+                .AddSingleton<UndoViewModel>()
+                .AddSingleton<SelectionViewModel>()
+                .AddSingleton<ViewportViewModel>()
+                .AddSingleton<ColorsViewModel>()
+                .AddSingleton<DocumentViewModel>()
+                .AddSingleton<MiscViewModel>()
+                .AddSingleton(x => new DiscordViewModel(x.GetService<ViewModelMain>(), "764168193685979138"))
+                .AddSingleton<DebugViewModel>()
+                // Controllers
+                .AddSingleton<ShortcutController>()
+                .AddSingleton<BitmapManager>()
+                // Tools
+                .AddSingleton<Tool, MoveViewportTool>()
+                .AddSingleton<Tool, MoveTool>()
+                .AddSingleton<Tool, PenTool>()
+                .AddSingleton<Tool, SelectTool>()
+                .AddSingleton<Tool, MagicWandTool>()
+                .AddSingleton<Tool, FloodFillTool>()
+                .AddSingleton<Tool, LineTool>()
+                .AddSingleton<Tool, CircleTool>()
+                .AddSingleton<Tool, RectangleTool>()
+                .AddSingleton<Tool, EraserTool>()
+                .AddSingleton<Tool, ColorPickerTool>()
+                .AddSingleton<Tool, BrightnessTool>()
+                .AddSingleton<Tool, ZoomTool>()
+                // Other
+                .AddSingleton<DocumentProvider>();
+    }
+}

+ 7 - 4
PixiEditor/Helpers/GlobalMouseHook.cs

@@ -5,7 +5,8 @@ using System.Diagnostics.CodeAnalysis;
 using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows.Input;
-
+using System.Windows.Threading;
+
 namespace PixiEditor.Helpers
 {
     public delegate void MouseUpEventHandler(object sender, Point p, MouseButton button);
@@ -88,9 +89,11 @@ namespace PixiEditor.Helpers
                 {
                     if (MouseUp != null)
                     {
+
                         MouseButton button = wParam == WM_LBUTTONUP ? MouseButton.Left
-                            : wParam == WM_MBUTTONUP ? MouseButton.Middle : MouseButton.Right;
-                        MouseUp.Invoke(null, new Point(mouseHookStruct.Pt.X, mouseHookStruct.Pt.Y), button);
+                            : wParam == WM_MBUTTONUP ? MouseButton.Middle : MouseButton.Right;
+                        Dispatcher.CurrentDispatcher.BeginInvoke(() =>
+                            MouseUp.Invoke(null, new Point(mouseHookStruct.Pt.X, mouseHookStruct.Pt.Y), button));
                     }
                 }
             }
@@ -138,4 +141,4 @@ namespace PixiEditor.Helpers
             public IntPtr DwExtraInfo;
         }
     }
-}
+}

+ 1 - 1
PixiEditor/Helpers/RelayCommand.cs

@@ -40,4 +40,4 @@ namespace PixiEditor.Helpers
             execute(parameter);
         }
     }
-}
+}

+ 3 - 1
PixiEditor/Helpers/Validators/SizeValidationRule.cs

@@ -7,7 +7,9 @@ namespace PixiEditor.Helpers.Validators
     {
         public override ValidationResult Validate(object value, CultureInfo cultureInfo)
         {
-            return new ValidationResult(int.Parse(((string)value).Split(' ')[0]) > 0, null); // Size is greater than 0
+            int i = int.Parse(((string)value).Split(' ')[0]);
+
+            return new ValidationResult(i > 0, null); // Size is greater than 0
         }
     }
 }

+ 48 - 0
PixiEditor/Helpers/VersionHelpers.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Diagnostics;
+using System.Reflection;
+using System.Text;
+
+namespace PixiEditor.Helpers
+{
+    public static class VersionHelpers
+    {
+        public static Version GetCurrentAssemblyVersion() => Assembly.GetExecutingAssembly().GetName().Version;
+
+        public static string GetCurrentAssemblyVersion(Func<Version, string> toString) => toString(GetCurrentAssemblyVersion());
+
+        public static string GetCurrentAssemblyVersionString()
+        {
+            StringBuilder builder = new(GetCurrentAssemblyVersion().ToString());
+
+            bool isDone = false;
+
+            AppendDevBuild(builder, ref isDone);
+
+            if (isDone)
+            {
+                return builder.ToString();
+            }
+
+            AppendDebugBuild(builder, ref isDone);
+
+            return builder.ToString();
+        }
+
+        [Conditional("DEV_RELEASE")]
+        private static void AppendDevBuild(StringBuilder builder, ref bool done)
+        {
+            done = true;
+
+            builder.Append(" Dev Build");
+        }
+
+        [Conditional("DEBUG")]
+        private static void AppendDebugBuild(StringBuilder builder, ref bool done)
+        {
+            done = true;
+
+            builder.Append(" Debug Build");
+        }
+    }
+}

+ 10 - 10
PixiEditor/Models/Colors/ExColor.cs

@@ -1,5 +1,5 @@
-using System;
-using System.Windows.Media;
+using SkiaSharp;
+using System;
 
 namespace PixiEditor.Models.Colors
 {
@@ -14,19 +14,19 @@ namespace PixiEditor.Models.Colors
         ///     Negative values produce darker colors.
         /// </param>
         /// <returns>
-        ///     Corrected <see cref="Color" /> structure.
+        ///     Corrected <see cref="SKColor" /> structure.
         /// </returns>
-        public static Color ChangeColorBrightness(Color color, float correctionFactor)
+        public static SKColor ChangeColorBrightness(SKColor color, float correctionFactor)
         {
-            Tuple<int, float, float> hsl = RgbToHsl(color.R, color.G, color.B);
+            Tuple<int, float, float> hsl = RgbToHsl(color.Red, color.Green, color.Blue);
             int h = hsl.Item1;
             float s = hsl.Item2;
             float l = hsl.Item3;
 
             l = Math.Clamp(l + correctionFactor, 0, 100);
-            Color rgb = HslToRgb(h, s, l);
+            SKColor rgb = HslToRgb(h, s, l);
 
-            return Color.FromArgb(color.A, rgb.R, rgb.G, rgb.B);
+            return new SKColor(rgb.Red, rgb.Green, rgb.Blue, color.Alpha);
         }
 
         /// <summary>
@@ -94,7 +94,7 @@ namespace PixiEditor.Models.Colors
         ///     Converts HSL color format to RGB.
         /// </summary>
         /// <returns>RGB Color.</returns>
-        public static Color HslToRgb(int h, float s, float l)
+        public static SKColor HslToRgb(int h, float s, float l)
         {
             s /= 100;
             l /= 100;
@@ -119,7 +119,7 @@ namespace PixiEditor.Models.Colors
                 b = (byte)(255 * HueToRgb(v1, v2, hue - (1.0f / 3)));
             }
 
-            return Color.FromRgb(r, g, b);
+            return new SKColor(r, g, b);
         }
 
         private static float HueToRgb(float v1, float v2, float hue)
@@ -152,4 +152,4 @@ namespace PixiEditor.Models.Colors
             return v1;
         }
     }
-}
+}

+ 179 - 190
PixiEditor/Models/Controllers/BitmapManager.cs

@@ -1,110 +1,114 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Diagnostics;
-using System.Linq;
-using System.Windows;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using PixiEditor.Helpers;
+using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Events;
-using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
-using PixiEditor.Models.Tools;
+using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
-using PixiEditor.Models.Tools.ToolSettings.Settings;
-
+using PixiEditor.ViewModels.SubViewModels.Main;
+using SkiaSharp;
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Windows;
+
 namespace PixiEditor.Models.Controllers
 {
     [DebuggerDisplay("{Documents.Count} Document(s)")]
     public class BitmapManager : NotifyableObject
     {
-        private Document activeDocument;
-        private Tool selectedTool;
-        private Coordinates? startPosition = null;
-
-        public BitmapManager()
-        {
-            MouseController = new MouseMovementController();
-            MouseController.StartedRecordingChanges += MouseController_StartedRecordingChanges;
-            MouseController.MousePositionChanged += Controller_MousePositionChanged;
-            MouseController.StoppedRecordingChanges += MouseController_StoppedRecordingChanges;
-            MouseController.OnMouseDown += MouseController_OnMouseDown;
-            MouseController.OnMouseUp += MouseController_OnMouseUp;
-            BitmapOperations = new BitmapOperationsUtility(this);
-            ReadonlyToolUtility = new ReadonlyToolUtility();
-        }
+        private ToolSessionController ToolSessionController { get; set; }
+        public ICanvasInputTarget InputTarget => ToolSessionController;
+        public BitmapOperationsUtility BitmapOperations { get; set; }
 
-        public event EventHandler<DocumentChangedEventArgs> DocumentChanged;
-
-        public event EventHandler<SelectedToolEventArgs> SelectedToolChanged;
-
-        public MouseMovementController MouseController { get; set; }
-
-        public Tool SelectedTool
-        {
-            get => selectedTool;
-            private set
-            {
-                Tool previousTool = selectedTool;
-                if (SetProperty(ref selectedTool, value))
-                {
-                    SelectedToolChanged?.Invoke(this, new SelectedToolEventArgs(previousTool, value));
-                }
-            }
-        }
-
-        public Layer ActiveLayer => ActiveDocument.ActiveLayer;
-
-        public Color PrimaryColor { get; set; }
-
-        public int ToolSize
-        {
-            get => SelectedTool.Toolbar.GetSetting<SizeSetting>("ToolSize") != null
-            ? SelectedTool.Toolbar.GetSetting<SizeSetting>("ToolSize").Value
-            : 1;
-            set
-            {
-                if (SelectedTool.Toolbar.GetSetting<SizeSetting>("ToolSize") is SizeSetting toolSize)
-                {
-                    toolSize.Value = value;
-                    HighlightPixels(MousePositionConverter.CurrentCoordinates);
-                }
-            }
-        }
+        public ObservableCollection<Document> Documents { get; set; } = new ObservableCollection<Document>();
 
-        public BitmapOperationsUtility BitmapOperations { get; set; }
-
-        public ReadonlyToolUtility ReadonlyToolUtility { get; set; }
-
-#nullable enable
+        private Document activeDocument;
         public Document ActiveDocument
         {
             get => activeDocument;
             set
             {
+                if (activeDocument == value)
+                    return;
                 activeDocument?.UpdatePreviewImage();
-                Document? oldDoc = activeDocument;
+                Document oldDoc = activeDocument;
                 activeDocument = value;
                 RaisePropertyChanged(nameof(ActiveDocument));
+                ActiveWindow = value;
                 DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(value, oldDoc));
             }
         }
 
-#nullable disable
-        public ObservableCollection<Document> Documents { get; set; } = new ObservableCollection<Document>();
+        private object activeWindow;
+        public object ActiveWindow
+        {
+            get => activeWindow;
+            set
+            {
+                if (activeWindow == value)
+                    return;
+                activeWindow = value;
+                RaisePropertyChanged(nameof(ActiveWindow));
+                if (activeWindow is Document doc)
+                    ActiveDocument = doc;
+            }
+        }
 
-        /// <summary>
-        ///     Returns if tool is BitmapOperationTool.
-        /// </summary>
-        public static bool IsOperationTool(Tool tool)
-        {
-            return tool is BitmapOperationTool;
+        public event EventHandler<DocumentChangedEventArgs> DocumentChanged;
+        public event EventHandler StopUsingTool;
+
+        public Layer ActiveLayer => ActiveDocument.ActiveLayer;
+
+        public SKColor PrimaryColor { get; set; }
+
+        private bool hideReferenceLayer;
+        public bool HideReferenceLayer
+        {
+            get => hideReferenceLayer;
+            set => SetProperty(ref hideReferenceLayer, value);
+        }
+
+        private bool onlyReferenceLayer;
+        public bool OnlyReferenceLayer
+        {
+            get => onlyReferenceLayer;
+            set => SetProperty(ref onlyReferenceLayer, value);
         }
 
+        private readonly ToolsViewModel _tools;
+
+        private int previewLayerSize;
+        private int halfSize;
+        private SKColor _highlightColor;
+        private PenTool _highlightPen;
+
+        private ToolSession activeSession = null;
+
+
+        public BitmapManager(ToolsViewModel tools, UndoViewModel undo)
+        {
+            _tools = tools;
+
+            ToolSessionController = new ToolSessionController();
+            ToolSessionController.SessionStarted += OnSessionStart;
+            ToolSessionController.SessionEnded += OnSessionEnd;
+            ToolSessionController.PixelMousePositionChanged += OnPixelMousePositionChange;
+            ToolSessionController.PreciseMousePositionChanged += OnPreciseMousePositionChange;
+            ToolSessionController.KeyStateChanged += (_, _) => UpdateActionDisplay(_tools.ActiveTool);
+            BitmapOperations = new BitmapOperationsUtility(this, tools);
+
+            undo.UndoRedoCalled += (_, _) => ToolSessionController.ForceStopActiveSessionIfAny();
+
+            DocumentChanged += BitmapManager_DocumentChanged;
+
+            _highlightPen = new PenTool(this)
+            {
+                AutomaticallyResizeCanvas = false
+            };
+            _highlightColor = new SKColor(0, 0, 0, 77);
+        }
+
         public void CloseDocument(Document document)
         {
             int nextIndex = 0;
@@ -116,146 +120,131 @@ namespace PixiEditor.Models.Controllers
 
             Documents.Remove(document);
             ActiveDocument = nextIndex >= 0 ? Documents[nextIndex] : null;
-        }
-
-        public void ExecuteTool(Coordinates newPosition, bool clickedOnCanvas)
-        {
-            if (SelectedTool.CanStartOutsideCanvas || clickedOnCanvas)
-            {
-                if (startPosition == null)
-                {
-                    SelectedTool.OnStart(newPosition);
-                    startPosition = newPosition;
-                }
-
-                if (SelectedTool is BitmapOperationTool operationTool)
-                {
-                    BitmapOperations.ExecuteTool(newPosition, MouseController.LastMouseMoveCoordinates, operationTool);
-                }
-                else if (SelectedTool is ReadonlyTool readonlyTool)
-                {
-                    ReadonlyToolUtility.ExecuteTool(MouseController.LastMouseMoveCoordinates, readonlyTool);
-                }
-                else
-                {
-                    throw new InvalidOperationException($"'{SelectedTool.GetType().Name}' is either not a Tool or can't inherit '{nameof(Tool)}' directly.\nChanges the base type to either '{nameof(BitmapOperationTool)}' or '{nameof(ReadonlyTool)}'");
-                }
-            }
+            document.Dispose();
         }
 
-        public WriteableBitmap GetCombinedLayersBitmap()
-        {
-            return BitmapUtils.CombineLayers(ActiveDocument.Width, ActiveDocument.Height, ActiveDocument.Layers.Where(x => ActiveDocument.GetFinalLayerIsVisible(x)).ToArray(), ActiveDocument.LayerStructure);
-        }
-
-        /// <summary>
-        ///     Returns if selected tool is BitmapOperationTool.
-        /// </summary>
-        public bool IsOperationTool()
-        {
-            return IsOperationTool(SelectedTool);
+        public void UpdateActionDisplay(Tool tool)
+        {
+            tool?.UpdateActionDisplay(ToolSessionController.IsCtrlDown, ToolSessionController.IsShiftDown, ToolSessionController.IsAltDown);
         }
 
-        public void SetActiveTool(Tool tool)
+        private void OnSessionStart(object sender, ToolSession e)
+        {
+            activeSession = e;
+
+            ActiveDocument.PreviewLayer.Reset();
+            ExecuteTool();
+        }
+
+        private void OnSessionEnd(object sender, ToolSession e)
         {
-            if (ActiveDocument != null)
+            activeSession = null;
+
+            if (e.Tool is BitmapOperationTool operationTool && operationTool.RequiresPreviewLayer)
             {
-                ActiveDocument.PreviewLayer = null;
+                BitmapOperations.ApplyPreviewLayer();
             }
 
-            SelectedTool?.Toolbar.SaveToolbarSettings();
-            SelectedTool = tool;
-            SelectedTool.Toolbar.LoadSharedSettings();
+            ActiveDocument.PreviewLayer.Reset();
+            HighlightPixels(ToolSessionController.LastPixelPosition);
+            StopUsingTool?.Invoke(this, EventArgs.Empty);
         }
+
+        private void OnPreciseMousePositionChange(object sender, (double, double) e)
+        {
+            if (activeSession == null || !activeSession.Tool.RequiresPreciseMouseData)
+                return;
+            ExecuteTool();
+        }
 
-        private void Controller_MousePositionChanged(object sender, MouseMovementEventArgs e)
-        {
-            SelectedTool.OnMouseMove(new MouseEventArgs(Mouse.PrimaryDevice, (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
-            if (Mouse.LeftButton == MouseButtonState.Pressed && !IsDraggingViewport() && ActiveDocument != null)
-            {
-                ExecuteTool(e.NewPosition, MouseController.ClickedOnCanvas);
+        private void OnPixelMousePositionChange(object sender, MouseMovementEventArgs e)
+        {
+            if (activeSession != null)
+            {
+                if (activeSession.Tool.RequiresPreciseMouseData)
+                    return;
+                ExecuteTool();
+                return;
             }
-            else if (Mouse.LeftButton == MouseButtonState.Released)
+            else
             {
                 HighlightPixels(e.NewPosition);
-            }
-        }
-
-        private void MouseController_OnMouseDown(object sender, MouseEventArgs e)
-        {
-            SelectedTool.OnMouseDown(e);
-        }
-
-        private void MouseController_OnMouseUp(object sender, MouseEventArgs e)
-        {
-            SelectedTool.OnMouseUp(e);
+            }
         }
 
-        private bool IsDraggingViewport()
+        private void ExecuteTool()
         {
-            return SelectedTool is MoveViewportTool;
+            if (activeSession == null)
+                throw new Exception("Can't execute tool's Use outside a session");
+
+            if (activeSession.Tool is BitmapOperationTool operationTool)
+            {
+                BitmapOperations.UseTool(activeSession.MouseMovement, operationTool, PrimaryColor);
+            }
+            else if (activeSession.Tool is ReadonlyTool readonlyTool)
+            {
+                readonlyTool.Use(activeSession.MouseMovement);
+            }
+            else
+            {
+                throw new InvalidOperationException($"'{activeSession.Tool.GetType().Name}' is either not a Tool or can't inherit '{nameof(Tool)}' directly.\nChanges the base type to either '{nameof(BitmapOperationTool)}' or '{nameof(ReadonlyTool)}'");
+            }
         }
 
-        private void MouseController_StartedRecordingChanges(object sender, EventArgs e)
-        {
-            SelectedTool.OnRecordingLeftMouseDown(new MouseEventArgs(Mouse.PrimaryDevice, (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
-            if (ActiveDocument != null)
-            {
-                ActiveDocument.PreviewLayer = null;
-            }
+        private void BitmapManager_DocumentChanged(object sender, DocumentChangedEventArgs e)
+        {
+            e.NewDocument?.GeneratePreviewLayer();
+            if (e.OldDocument != e.NewDocument)
+                ToolSessionController.ForceStopActiveSessionIfAny();
         }
 
-        private void MouseController_StoppedRecordingChanges(object sender, EventArgs e)
-        {
-            SelectedTool.OnStoppedRecordingMouseUp(new MouseEventArgs(Mouse.PrimaryDevice, (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
-            if (IsOperationTool(SelectedTool) && ((BitmapOperationTool)SelectedTool).RequiresPreviewLayer)
-            {
-                BitmapOperations.ApplyPreviewLayer();
-            }
+        public void UpdateHighlightIfNecessary(bool forceHide = false)
+        {
+            if (activeSession != null)
+                return;
 
-            startPosition = null;
+            HighlightPixels(forceHide ? new(-1, -1) : ToolSessionController.LastPixelPosition);
         }
 
         private void HighlightPixels(Coordinates newPosition)
         {
-            if (ActiveDocument == null || ActiveDocument.Layers.Count == 0 || SelectedTool.HideHighlight)
+            if (ActiveDocument == null || ActiveDocument.Layers.Count == 0)
             {
                 return;
             }
 
-            IEnumerable<Coordinates> highlightArea = CoordinatesCalculator.RectangleToCoordinates(
-                CoordinatesCalculator.CalculateThicknessCenter(newPosition, ToolSize));
-            if (CanChangeHighlightOffset(highlightArea))
-            {
-                Coordinates start = highlightArea.First();
-                ActiveDocument.PreviewLayer.Offset = new Thickness(start.X, start.Y, 0, 0);
-            }
-            else if (!IsInsideBounds(highlightArea))
-            {
-                ActiveDocument.PreviewLayer = null;
-            }
-            else
+            var previewLayer = ActiveDocument.PreviewLayer;
+
+            if (newPosition.X > ActiveDocument.Width
+                || newPosition.Y > ActiveDocument.Height
+                || newPosition.X < 0 || newPosition.Y < 0
+                || _tools.ActiveTool.HideHighlight)
             {
-                ActiveDocument.GeneratePreviewLayer();
-                ActiveDocument.PreviewLayer.SetPixels(
-                    BitmapPixelChanges.FromSingleColoredArray(highlightArea, Color.FromArgb(77, 0, 0, 0)));
-            }
-        }
-
-        private bool CanChangeHighlightOffset(IEnumerable<Coordinates> highlightArea)
-        {
-            int count = highlightArea.Count();
-            return count > 0 && ActiveDocument.PreviewLayer != null &&
-                   IsInsideBounds(highlightArea) && count == ActiveDocument.PreviewLayer.Width * ActiveDocument.PreviewLayer.Height;
+                previewLayer.Reset();
+                previewLayerSize = -1;
+                return;
+            }
+
+            if (_tools.ToolSize != previewLayerSize || previewLayer.IsReset)
+            {
+                previewLayerSize = _tools.ToolSize;
+                halfSize = (int)Math.Floor(_tools.ToolSize / 2f);
+                previewLayer.CreateNewBitmap(_tools.ToolSize, _tools.ToolSize);
+
+                Coordinates cords = new Coordinates(halfSize, halfSize);
+
+                previewLayer.Offset = new Thickness(0, 0, 0, 0);
+                _highlightPen.Draw(previewLayer, cords, cords, _highlightColor, _tools.ToolSize);
+            }
+            AdjustOffset(newPosition, previewLayer);
+
+            previewLayer.InvokeLayerBitmapChange();
         }
 
-        private bool IsInsideBounds(IEnumerable<Coordinates> highlightArea)
-        {
-            Coordinates start = highlightArea.First();
-            Coordinates end = highlightArea.Last();
-            return start.X <= ActiveDocument.Width - 1 &&
-                    start.Y <= ActiveDocument.Height - 1 &&
-                   end.X >= 0 && end.Y >= 0;
-        }
+        private void AdjustOffset(Coordinates newPosition, Layer previewLayer)
+        {
+            Coordinates start = newPosition - halfSize;
+            previewLayer.Offset = new Thickness(start.X, start.Y, 0, 0);
+        }
     }
-}
+}

+ 50 - 250
PixiEditor/Models/Controllers/BitmapOperationsUtility.cs

@@ -1,41 +1,30 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using PixiEditor.Helpers.Extensions;
-using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
-using PixiEditor.Models.Tools.ToolSettings.Settings;
 using PixiEditor.Models.Undo;
-using PixiEditor.ViewModels;
+using PixiEditor.ViewModels.SubViewModels.Main;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Windows;
 
 namespace PixiEditor.Models.Controllers
 {
     public class BitmapOperationsUtility
     {
-        public List<LayerChange> PreviewLayerChanges => previewLayerChanges;
-
-        private List<LayerChange> previewLayerChanges;
+        public event EventHandler<BitmapChangedEventArgs> BitmapChanged;
 
-        private Coordinates lastMousePos;
+        public BitmapManager Manager { get; set; }
 
-        private SizeSetting sizeSetting;
+        public ToolsViewModel Tools { get; set; }
 
-        public BitmapOperationsUtility(BitmapManager manager)
+        public BitmapOperationsUtility(BitmapManager manager, ToolsViewModel tools)
         {
             Manager = manager;
+            Tools = tools;
         }
 
-        public event EventHandler<BitmapChangedEventArgs> BitmapChanged;
-
-        public BitmapManager Manager { get; set; }
-
         public void DeletePixels(Layer[] layers, Coordinates[] pixels)
         {
             if (Manager.ActiveDocument == null)
@@ -43,261 +32,72 @@ namespace PixiEditor.Models.Controllers
                 return;
             }
 
-            BitmapPixelChanges changes = BitmapPixelChanges.FromSingleColoredArray(pixels, Color.FromArgb(0, 0, 0, 0));
-            Dictionary<Guid, Color[]> oldValues = BitmapUtils.GetPixelsForSelection(layers, pixels);
-            LayerChange[] old = new LayerChange[layers.Length];
-            LayerChange[] newChange = new LayerChange[layers.Length];
+            StorageBasedChange change = new StorageBasedChange(Manager.ActiveDocument, layers, true);
+
+            BitmapPixelChanges changes = BitmapPixelChanges.FromSingleColoredArray(pixels, SKColors.Empty);
             for (int i = 0; i < layers.Length; i++)
             {
-                Guid guid = layers[i].LayerGuid;
-                old[i] = new LayerChange(
-                    BitmapPixelChanges.FromArrays(pixels, oldValues[layers[i].LayerGuid]), guid);
-                newChange[i] = new LayerChange(changes, guid);
+                Guid guid = layers[i].GuidValue;
                 layers[i].SetPixels(changes);
             }
 
-            Manager.ActiveDocument.UndoManager.AddUndoChange(new Change("UndoChanges", old, newChange, "Deleted pixels"));
+            var args = new object[] { change.Document };
+            Manager.ActiveDocument.UndoManager.AddUndoChange(change.ToChange(StorageBasedChange.BasicUndoProcess, args, "Delete selected pixels"));
         }
 
-        /// <summary>
-        ///     Executes tool Use() method with given parameters. NOTE: [0] is a start point, [^1] is latest.
-        /// </summary>
-        /// <param name="newPos">Most recent coordinates.</param>
-        /// <param name="mouseMove">Last mouse movement coordinates.</param>
-        /// <param name="tool">Tool to execute.</param>
-        public void ExecuteTool(Coordinates newPos, List<Coordinates> mouseMove, BitmapOperationTool tool)
+        public void UseTool(IReadOnlyList<Coordinates> recordedMouseMovement, BitmapOperationTool tool, SKColor color)
         {
-            if (Manager.ActiveDocument != null && tool != null)
-            {
-                if (Manager.ActiveDocument.Layers.Count == 0 || mouseMove.Count == 0)
-                {
-                    return;
-                }
-
-                UseTool(mouseMove, tool, Manager.PrimaryColor);
-
-                lastMousePos = newPos;
-            }
-        }
-
-        /// <summary>
-        ///     Applies pixels from preview layer to selected layer.
-        /// </summary>
-        public void ApplyPreviewLayer()
-        {
-            if (previewLayerChanges == null)
-            {
+            if (Manager.ActiveDocument.Layers.Count == 0)
                 return;
-            }
-
-            Layer[] layers = new Layer[previewLayerChanges.Count];
-
-            for (int i = 0; i < layers.Length; i++)
-            {
-                layers[i] = Manager.ActiveDocument.Layers.First(x => x.LayerGuid == previewLayerChanges[i].LayerGuid);
-            }
-
-            if (layers.Length > 0)
-            {
-                IEnumerable<LayerChange> oldValues =
-                    ApplyToLayers(layers, previewLayerChanges.ToArray());
-
-                foreach (var oldValue in oldValues)
-                {
-                    var previewChanges = previewLayerChanges.First(x => x.LayerGuid == oldValue.LayerGuid);
-
-                    BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
-                        previewChanges.PixelChanges,
-                        oldValue.PixelChanges,
-                        previewChanges.LayerGuid));
-                }
-
-                Manager.ActiveDocument.GeneratePreviewLayer();
-            }
-
-            previewLayerChanges = null;
-        }
-
-        private void UseTool(List<Coordinates> mouseMoveCords, BitmapOperationTool tool, Color color)
-        {
-            if(sizeSetting == null)
-            {
-                sizeSetting = tool.Toolbar.GetSetting<SizeSetting>("ToolSize");
-            }
-
-            int thickness = sizeSetting != null ? sizeSetting.Value : 1;
-
-            bool shiftDown = Keyboard.IsKeyDown(Key.LeftShift);
-           
-            if (shiftDown && tool.UsesShift)
-            {
-                bool mouseInLine = MouseCordsNotInLine(mouseMoveCords, thickness);
-
-                if (!mouseInLine)
-                {
-                    mouseMoveCords = GetSquareCoordiantes(mouseMoveCords);
-                }
-                else
-                {
-                    mouseMoveCords = GetLineCoordinates(mouseMoveCords, thickness);
-                }
-            }
 
             if (!tool.RequiresPreviewLayer)
             {
-                LayerChange[] modifiedLayers = tool.Use(Manager.ActiveLayer, mouseMoveCords, color);
-                LayerChange[] oldPixelsValues = new LayerChange[modifiedLayers.Length];
-                for (int i = 0; i < modifiedLayers.Length; i++)
-                {
-                    Layer layer = Manager.ActiveDocument.Layers.First(x => x.LayerGuid == modifiedLayers[i].LayerGuid);
-                    oldPixelsValues[i] = ApplyToLayer(layer, modifiedLayers[i]);
-
-                    BitmapChanged?.Invoke(this, new BitmapChangedEventArgs(
-                        modifiedLayers[i].PixelChanges,
-                        oldPixelsValues[i].PixelChanges,
-                        modifiedLayers[i].LayerGuid));
-                }
+                tool.Use(Manager.ActiveLayer, null, Manager.ActiveDocument.Layers, recordedMouseMovement, color);
+                BitmapChanged?.Invoke(this, null);
             }
             else
             {
-                UseToolOnPreviewLayer(mouseMoveCords, tool.ClearPreviewLayerOnEachIteration);
+                UseToolOnPreviewLayer(recordedMouseMovement, tool.ClearPreviewLayerOnEachIteration);
             }
         }
 
-        private LayerChange ApplyToLayer(Layer layer, LayerChange change)
-        {
-            return ApplyToLayers(new Layer[] { layer }, new LayerChange[] { change })[0];
-        }
-
-        private LayerChange[] ApplyToLayers(Layer[] layers, LayerChange[] changes)
+        private void UseToolOnPreviewLayer(IReadOnlyList<Coordinates> recordedMouseMovement, bool clearPreviewLayer)
         {
-            LayerChange[] oldPixelValues = new LayerChange[changes.Length];
-            for (int i = 0; i < layers.Length; i++)
-            {
-                Layer layer = layers[i];
-                LayerChange change = changes.First(x => x.LayerGuid == layer.LayerGuid);
-                layer.DynamicResize(change.PixelChanges);
-
-                oldPixelValues[i] = new LayerChange(
-                GetOldPixelsValues(change.PixelChanges.ChangedPixels.Keys.ToArray()),
-                change.LayerGuid);
-            }
-
-            for (int i = 0; i < layers.Length; i++)
-            {
-                Layer layer = layers[i];
-                LayerChange change = changes.First(x => x.LayerGuid == layer.LayerGuid);
-                layer.SetPixels(change.PixelChanges, false);
-            }
-
-            return oldPixelValues;
-        }
-
-        private bool MouseCordsNotInLine(List<Coordinates> cords, int thickness)
-        {
-            return (cords[0].X > cords[^1].X - thickness && cords[0].X < cords[^1].X + thickness)
-                || (cords[0].Y > cords[^1].Y - thickness && cords[0].Y < cords[^1].Y + thickness);
-        }
-
-        private List<Coordinates> GetLineCoordinates(List<Coordinates> mouseMoveCords, int thickness)
-        {
-            int y = mouseMoveCords[0].Y;
-            int x = mouseMoveCords[0].X;
-
-
-            if (Math.Abs(mouseMoveCords[^1].X - mouseMoveCords[0].X) - thickness > 0)
-            {
-                y = mouseMoveCords[^1].Y;
-            }
-            else
-            {
-                x = mouseMoveCords[^1].X;
-            }
-
-            mouseMoveCords[0] = new Coordinates(x, y);
-            return mouseMoveCords;
-        }
-
-        /// <summary>
-        ///     Extracts square from rectangle mouse drag, used to draw symmetric shapes.
-        /// </summary>
-        private List<Coordinates> GetSquareCoordiantes(List<Coordinates> mouseMoveCords)
-        {
-            int xLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
-            int yLength = mouseMoveCords[0].Y - mouseMoveCords[^1].Y;
-            if (mouseMoveCords[^1].Y > mouseMoveCords[0].Y)
-            {
-                xLength *= -1;
-            }
-
-            if (mouseMoveCords[^1].X > mouseMoveCords[0].X)
-            {
-                xLength *= -1;
-            }
-
-            mouseMoveCords[0] = new Coordinates(mouseMoveCords[^1].X + xLength, mouseMoveCords[^1].Y + yLength);
-            return mouseMoveCords;
-        }
-
-        private BitmapPixelChanges GetOldPixelsValues(Coordinates[] coordinates)
-        {
-            Dictionary<Coordinates, Color> values = new Dictionary<Coordinates, Color>();
-            using (Manager.ActiveLayer.LayerBitmap.GetBitmapContext(ReadWriteMode.ReadOnly))
-            {
-                Coordinates[] relativeCoords = Manager.ActiveLayer.ConvertToRelativeCoordinates(coordinates);
-                for (int i = 0; i < coordinates.Length; i++)
-                {
-                    values.Add(
-                        coordinates[i],
-                        Manager.ActiveLayer.GetPixel(relativeCoords[i].X, relativeCoords[i].Y));
-                }
-            }
-
-            return new BitmapPixelChanges(values);
-        }
-
-        private void UseToolOnPreviewLayer(List<Coordinates> mouseMove, bool clearPreviewLayer = true)
-        {
-            LayerChange[] modifiedLayers;
-            if (mouseMove.Count > 0 && mouseMove[0] != lastMousePos)
+            if (recordedMouseMovement.Count > 0)
             {
-                if (clearPreviewLayer || Manager.ActiveDocument.PreviewLayer == null)
+                if (clearPreviewLayer)
                 {
-                    Manager.ActiveDocument.GeneratePreviewLayer();
+                    Manager.ActiveDocument.PreviewLayer.ClearCanvas();
                 }
 
-                modifiedLayers = ((BitmapOperationTool)Manager.SelectedTool).Use(
-                    Manager.ActiveDocument.ActiveLayer,
-                    mouseMove,
+                ((BitmapOperationTool)Tools.ActiveTool).Use(
+                    Manager.ActiveLayer,
+                    Manager.ActiveDocument.PreviewLayer,
+                    Manager.ActiveDocument.Layers,
+                    recordedMouseMovement,
                     Manager.PrimaryColor);
-
-                BitmapPixelChanges[] changes = modifiedLayers.Select(x => x.PixelChanges).ToArray();
-                if (changes.Length == 0)
-                {
-                    return;
-                }
-
-                Manager.ActiveDocument.PreviewLayer.SetPixels(BitmapPixelChanges.CombineOverride(changes));
-
-                if (clearPreviewLayer || previewLayerChanges == null)
-                {
-                    previewLayerChanges = new List<LayerChange>(modifiedLayers);
-                }
-                else
-                {
-                    InjectPreviewLayerChanges(modifiedLayers);
-                }
             }
         }
 
-        private void InjectPreviewLayerChanges(LayerChange[] modifiedLayers)
+        /// <summary>
+        ///     Applies pixels from preview layer to selected layer.
+        /// </summary>
+        public void ApplyPreviewLayer()
         {
-            for (int i = 0; i < modifiedLayers.Length; i++)
-            {
-                var layer = previewLayerChanges.First(x => x.LayerGuid == modifiedLayers[i].LayerGuid);
-                layer.PixelChanges.ChangedPixels.AddRangeOverride(modifiedLayers[i].PixelChanges.ChangedPixels);
-                layer.PixelChanges = layer.PixelChanges.WithoutTransparentPixels();
-            }
+            var previewLayer = Manager.ActiveDocument.PreviewLayer;
+            var activeLayer = Manager.ActiveLayer;
+
+            Int32Rect dirtyRect = new Int32Rect(previewLayer.OffsetX, previewLayer.OffsetY, previewLayer.Width, previewLayer.Height);
+            activeLayer.DynamicResizeAbsolute(dirtyRect);
+            previewLayer.LayerBitmap.SkiaSurface.Draw(
+                    activeLayer.LayerBitmap.SkiaSurface.Canvas,
+                    previewLayer.OffsetX - activeLayer.OffsetX,
+                    previewLayer.OffsetY - activeLayer.OffsetY,
+                    Surface.BlendingPaint
+                );
+
+            Manager.ActiveLayer.InvokeLayerBitmapChange(dirtyRect);
+            BitmapChanged?.Invoke(this, null);
         }
     }
-}
+}

+ 265 - 51
PixiEditor/Models/Controllers/ClipboardController.cs

@@ -1,39 +1,111 @@
-using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Exceptions;
+using PixiEditor.Helpers;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.IO;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Undo;
+using PixiEditor.Parser;
 using PixiEditor.ViewModels;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.IO;
 using System.Linq;
 using System.Windows;
+using System.Windows.Media;
 using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.Controllers
 {
     public static class ClipboardController
     {
+        public static readonly string TempCopyFilePath = Path.Join(
+                    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+                    "PixiEditor",
+                    "Copied.png");
+
         /// <summary>
-        ///     Copies selection to clipboard in PNG, Bitmap and DIB formats.
+        /// Copies the selection to clipboard in PNG, Bitmap and DIB formats. <para/>
+        /// Also serailizes the <paramref name="document"/> in the PIXI format and copies it to the clipboard.
+        /// </summary>
+        public static void CopyToClipboard(Document document)
+        {
+            CopyToClipboard(
+                document.Layers.Where(x => document.GetFinalLayerIsVisible(x) && x.IsActive).ToArray(),
+                document.ActiveSelection.SelectionLayer,
+                document.LayerStructure,
+                document.Width,
+                document.Height,
+                null/*document.ToSerializable()*/);
+        }
+
+        private static Surface CreateMaskedCombinedSurface(Layer[] layers, LayerStructure structure, Layer selLayer)
+        {
+            if (layers.Length == 0)
+                throw new ArgumentException("Can't combine 0 layers");
+            selLayer.ClipCanvas();
+
+            Surface combined = BitmapUtils.CombineLayers(new Int32Rect(selLayer.OffsetX, selLayer.OffsetY, selLayer.Width, selLayer.Height), layers, structure);
+            using SKImage snapshot = selLayer.LayerBitmap.SkiaSurface.Snapshot();
+            combined.SkiaSurface.Canvas.DrawImage(snapshot, 0, 0, Surface.MaskingPaint);
+            return combined;
+        }
+
+        /// <summary>
+        ///     Copies the selection to clipboard in PNG, Bitmap and DIB formats.
         /// </summary>
         /// <param name="layers">Layers where selection is.</param>
-        public static void CopyToClipboard(Layer[] layers, Coordinates[] selection, int originalImageWidth, int originalImageHeight)
+        public static void CopyToClipboard(Layer[] layers, Layer selLayer, LayerStructure structure, int originalImageWidth, int originalImageHeight, SerializableDocument document = null)
         {
-            Clipboard.Clear();
-            WriteableBitmap combinedBitmaps = BitmapUtils.CombineLayers(originalImageWidth, originalImageHeight, layers);
-            using (MemoryStream pngStream = new MemoryStream())
+            if (!ClipboardHelper.TryClear())
+                return;
+            if (layers.Length == 0)
+                return;
+
+            using Surface surface = CreateMaskedCombinedSurface(layers, structure, selLayer);
+            DataObject data = new DataObject();
+
+
+            //BitmapSource croppedBmp = BitmapSelectionToBmpSource(finalBitmap, selLayer, out int offsetX, out int offsetY, out int width, out int height);
+
+            //Remove for now
+            //data.SetData(typeof(CropData), new CropData(width, height, offsetX, offsetY).ToStream());
+
+            using (SKData pngData = surface.SkiaSurface.Snapshot().Encode())
             {
-                DataObject data = new DataObject();
-                BitmapSource croppedBmp = BitmapSelectionToBmpSource(combinedBitmaps, selection);
-                data.SetData(DataFormats.Bitmap, croppedBmp, true); // Bitmap, no transparency support
+                // Stream should not be disposed
+                MemoryStream pngStream = new MemoryStream();
+                pngData.AsStream().CopyTo(pngStream);
 
-                PngBitmapEncoder encoder = new PngBitmapEncoder();
-                encoder.Frames.Add(BitmapFrame.Create(croppedBmp));
-                encoder.Save(pngStream);
                 data.SetData("PNG", pngStream, false); // PNG, supports transparency
 
-                Clipboard.SetImage(croppedBmp); // DIB format
-                Clipboard.SetDataObject(data, true);
+                pngStream.Position = 0;
+                Directory.CreateDirectory(Path.GetDirectoryName(TempCopyFilePath));
+                using FileStream fileStream = new FileStream(TempCopyFilePath, FileMode.Create, FileAccess.Write);
+                pngStream.CopyTo(fileStream);
+                data.SetFileDropList(new StringCollection() { TempCopyFilePath });
             }
+
+            WriteableBitmap finalBitmap = surface.ToWriteableBitmap();
+            data.SetData(DataFormats.Bitmap, finalBitmap, true); // Bitmap, no transparency
+            data.SetImage(finalBitmap); // DIB format, no transparency
+
+            // Remove pixi copying for now
+            /*
+            if (document != null)
+            {
+                MemoryStream memoryStream = new();
+                PixiParser.Serialize(document, memoryStream);
+                data.SetData("PIXI", memoryStream); // PIXI, supports transparency, layers, groups and swatches
+                ClipboardHelper.TrySetDataObject(data, true);
+            }
+            */
+
+            ClipboardHelper.TrySetDataObject(data, true);
         }
 
         /// <summary>
@@ -41,92 +113,234 @@ namespace PixiEditor.Models.Controllers
         /// </summary>
         public static void PasteFromClipboard()
         {
-            WriteableBitmap image = GetImageFromClipboard();
-            if (image != null)
+            IEnumerable<Layer> layers;
+            try
             {
-                AddImageToLayers(image);
-                int latestLayerIndex = ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.Count - 1;
-                ViewModelMain.Current.BitmapManager.ActiveDocument.UndoManager.AddUndoChange(
-                    new Change(RemoveLayerProcess, new object[] { latestLayerIndex }, AddLayerProcess, new object[] { image }));
+                layers = GetLayersFromClipboard();
+            }
+            catch
+            {
+                return;
             }
-        }
 
+            Document activeDocument = ViewModelMain.Current.BitmapManager.ActiveDocument;
+            int startIndex = activeDocument.Layers.Count;
+
+            foreach (var layer in layers)
+            {
+                activeDocument.Layers.Add(layer);
+            }
+
+            activeDocument.UndoManager.AddUndoChange(
+                new Change(RemoveLayersProcess, new object[] { startIndex }, AddLayersProcess, new object[] { layers }) { DisposeProcess = DisposeProcess });
+        }
 
         /// <summary>
         ///     Gets image from clipboard, supported PNG, Dib and Bitmap.
         /// </summary>
         /// <returns>WriteableBitmap.</returns>
-        public static WriteableBitmap GetImageFromClipboard()
+        private static IEnumerable<Layer> GetLayersFromClipboard()
         {
-            DataObject dao = (DataObject)Clipboard.GetDataObject();
-            WriteableBitmap finalImage = null;
-            if (dao.GetDataPresent("PNG"))
+            DataObject data = ClipboardHelper.TryGetDataObject();
+            if (data == null)
+                yield break;
+
+            //Remove pixi for now
+            /*
+            if (data.GetDataPresent("PIXI"))
             {
-                using (MemoryStream pngStream = dao.GetData("PNG") as MemoryStream)
+                SerializableDocument document = GetSerializable(data, out CropData crop);
+                SKRectI cropRect = SKRectI.Create(crop.OffsetX, crop.OffsetY, crop.Width, crop.Height);
+
+                foreach (SerializableLayer sLayer in document)
                 {
-                    if (pngStream != null)
+                    SKRectI intersect;
+
+                    if (//layer.OffsetX > crop.OffsetX + crop.Width || layer.OffsetY > crop.OffsetY + crop.Height ||
+                        !sLayer.IsVisible || sLayer.Opacity == 0 ||
+                        (intersect = SKRectI.Intersect(cropRect, sLayer.GetRect())) == SKRectI.Empty)
                     {
-                        PngBitmapDecoder decoder = new PngBitmapDecoder(pngStream, BitmapCreateOptions.IgnoreImageCache, BitmapCacheOption.OnLoad);
-                        finalImage = new WriteableBitmap(decoder.Frames[0].Clone());
+                        continue;
                     }
+
+                    var layer = sLayer.ToLayer();
+
+                    layer.Crop(intersect);
+
+                    yield return layer;
                 }
             }
-            else if (dao.GetDataPresent(DataFormats.Dib))
+            else */
+            if (TryFromSingleImage(data, out Surface singleImage))
             {
-                finalImage = new WriteableBitmap(Clipboard.GetImage()!);
+                yield return new Layer("Image", singleImage);
             }
-            else if (dao.GetDataPresent(DataFormats.Bitmap))
+            else if (data.GetDataPresent(DataFormats.FileDrop))
             {
-                finalImage = new WriteableBitmap((dao.GetData(DataFormats.Bitmap) as BitmapSource)!);
-            }
+                foreach (string path in data.GetFileDropList())
+                {
+                    if (!Importer.IsSupportedFile(path))
+                    {
+                        continue;
+                    }
+
+                    Layer layer = null;
+
+                    try
+                    {
+                        layer = new(Path.GetFileName(path), Importer.ImportSurface(path));
+                    }
+                    catch (CorruptedFileException)
+                    {
+                    }
 
-            return finalImage;
+                    yield return layer ?? new($"Corrupt {path}");
+                }
+            }
+            else
+            {
+                yield break;
+            }
         }
 
         public static bool IsImageInClipboard()
         {
-            DataObject dao = (DataObject)Clipboard.GetDataObject();
+            DataObject dao = ClipboardHelper.TryGetDataObject();
             if (dao == null)
-            {
                 return false;
+
+            var files = dao.GetFileDropList();
+
+            if (files != null)
+            {
+                foreach (var file in files)
+                {
+                    if (Importer.IsSupportedFile(file))
+                    {
+                        return true;
+                    }
+                }
             }
 
             return dao.GetDataPresent("PNG") || dao.GetDataPresent(DataFormats.Dib) ||
-                   dao.GetDataPresent(DataFormats.Bitmap);
+                   dao.GetDataPresent(DataFormats.Bitmap) || dao.GetDataPresent(DataFormats.FileDrop) ||
+                   dao.GetDataPresent("PIXI");
         }
 
-        public static BitmapSource BitmapSelectionToBmpSource(WriteableBitmap bitmap, Coordinates[] selection)
+        private static BitmapSource BitmapSelectionToBmpSource(WriteableBitmap bitmap, Coordinates[] selection, out int offsetX, out int offsetY, out int width, out int height)
         {
-            int offsetX = selection.Min(x => x.X);
-            int offsetY = selection.Min(x => x.Y);
-            int width = selection.Max(x => x.X) - offsetX + 1;
-            int height = selection.Max(x => x.Y) - offsetY + 1;
+            offsetX = selection.Min(min => min.X);
+            offsetY = selection.Min(min => min.Y);
+            width = selection.Max(max => max.X) - offsetX + 1;
+            height = selection.Max(max => max.Y) - offsetY + 1;
             return bitmap.Crop(offsetX, offsetY, width, height);
         }
 
-        private static void RemoveLayerProcess(object[] parameters)
+        private static BitmapSource FromPNG(DataObject data)
+        {
+            MemoryStream pngStream = data.GetData("PNG") as MemoryStream;
+            PngBitmapDecoder decoder = new PngBitmapDecoder(pngStream, BitmapCreateOptions.IgnoreImageCache, BitmapCacheOption.OnLoad);
+
+            return decoder.Frames[0];
+        }
+
+        private static unsafe SerializableDocument GetSerializable(DataObject data, out CropData cropData)
+        {
+            MemoryStream pixiStream = data.GetData("PIXI") as MemoryStream;
+            SerializableDocument document = PixiParser.Deserialize(pixiStream);
+
+            if (data.GetDataPresent(typeof(CropData)))
+            {
+                cropData = CropData.FromStream(data.GetData(typeof(CropData)) as MemoryStream);
+            }
+            else
+            {
+                cropData = new CropData(document.Width, document.Height, 0, 0);
+            }
+
+            return document;
+        }
+
+        private static bool TryFromSingleImage(DataObject data, out Surface result)
+        {
+            try
+            {
+                BitmapSource source;
+
+                if (data.GetDataPresent("PNG"))
+                {
+                    source = FromPNG(data);
+                }
+                else if (data.GetDataPresent(DataFormats.Dib) || data.GetDataPresent(DataFormats.Bitmap))
+                {
+                    source = Clipboard.GetImage();
+                }
+                else
+                {
+                    result = null;
+                    return false;
+                }
+
+                if (source.Format.IsSkiaSupported())
+                {
+                    result = new Surface(source);
+                }
+                else
+                {
+                    FormatConvertedBitmap newFormat = new FormatConvertedBitmap();
+                    newFormat.BeginInit();
+                    newFormat.Source = source;
+                    newFormat.DestinationFormat = PixelFormats.Rgba64;
+                    newFormat.EndInit();
+
+                    result = new Surface(newFormat);
+                }
+
+                return true;
+            }
+            catch { }
+
+            result = null;
+            return false;
+        }
+
+        private static void RemoveLayersProcess(object[] parameters)
         {
-            if (parameters.Length == 0 || !(parameters[0] is int))
+            if (parameters.Length == 0 || parameters[0] is not int i)
             {
                 return;
             }
 
-            ViewModelMain.Current.BitmapManager.ActiveDocument.RemoveLayer((int)parameters[0], true);
+            Document document = ViewModelMain.Current.BitmapManager.ActiveDocument;
+
+            while (i < document.Layers.Count)
+            {
+                document.RemoveLayer(i, true);
+            }
         }
 
-        private static void AddLayerProcess(object[] parameters)
+        private static void AddLayersProcess(object[] parameters)
         {
-            if (parameters.Length == 0 || !(parameters[0] is WriteableBitmap))
+            if (parameters.Length == 0 || parameters[0] is not IEnumerable<Layer> layers)
             {
                 return;
             }
 
-            AddImageToLayers((WriteableBitmap)parameters[0]);
+            foreach (var layer in layers)
+            {
+                ViewModelMain.Current.BitmapManager.ActiveDocument.Layers.Add(layer);
+            }
         }
 
-        private static void AddImageToLayers(WriteableBitmap image)
+        private static void DisposeProcess(object[] rev, object[] proc)
         {
-            ViewModelMain.Current.BitmapManager.ActiveDocument.AddNewLayer("Image", image);
+            if (proc[0] is IEnumerable<Layer> layers)
+            {
+                foreach (var layer in layers)
+                {
+                    layer.LayerBitmap.Dispose();
+                }
+            }
         }
     }
 }

+ 15 - 0
PixiEditor/Models/Controllers/ICanvasInputTarget.cs

@@ -0,0 +1,15 @@
+using PixiEditor.Models.Tools;
+using System.Windows.Input;
+
+namespace PixiEditor.Models.Controllers
+{
+    public interface ICanvasInputTarget
+    {
+        void OnToolChange(Tool tool);
+        void OnKeyDown(Key key);
+        void OnKeyUp(Key key);
+        void OnLeftMouseButtonDown(double canvasPosX, double canvasPosY);
+        void OnLeftMouseButtonUp();
+        void OnMouseMove(double newCanvasX, double newCanvasY);
+    }
+}

+ 173 - 0
PixiEditor/Models/Controllers/LayerStackRenderer.cs

@@ -0,0 +1,173 @@
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Layers.Utils;
+using SkiaSharp;
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.Models.Controllers
+{
+    public class LayerStackRenderer : INotifyPropertyChanged, IDisposable
+    {
+        private SKPaint BlendingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+        private SKPaint ClearPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
+
+        private ObservableCollection<Layer> layers;
+        private LayerStructure structure;
+
+        private Surface finalSurface;
+        private SKSurface backingSurface;
+        private WriteableBitmap finalBitmap;
+        public WriteableBitmap FinalBitmap
+        {
+            get => finalBitmap;
+            set
+            {
+                finalBitmap = value;
+                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FinalBitmap)));
+            }
+        }
+
+        public Surface FinalSurface { get => finalSurface; }
+
+        public event PropertyChangedEventHandler PropertyChanged;
+        public LayerStackRenderer(ObservableCollection<Layer> layers, LayerStructure structure, int width, int height)
+        {
+            this.layers = layers;
+            this.structure = structure;
+            layers.CollectionChanged += OnLayersChanged;
+            SubscribeToAllLayers(layers);
+            Resize(width, height);
+        }
+
+        public void Resize(int newWidth, int newHeight)
+        {
+            finalSurface?.Dispose();
+            backingSurface?.Dispose();
+            finalSurface = new Surface(newWidth, newHeight);
+            FinalBitmap = new WriteableBitmap(newWidth, newHeight, 96, 96, PixelFormats.Pbgra32, null);
+            var imageInfo = new SKImageInfo(newWidth, newHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb());
+            backingSurface = SKSurface.Create(imageInfo, finalBitmap.BackBuffer, finalBitmap.BackBufferStride);
+            Update(new Int32Rect(0, 0, newWidth, newHeight));
+        }
+
+        public void SetNewLayersCollection(ObservableCollection<Layer> layers)
+        {
+            layers.CollectionChanged -= OnLayersChanged;
+            UnsubscribeFromAllLayers(this.layers);
+            this.layers = layers;
+            SubscribeToAllLayers(layers);
+            layers.CollectionChanged += OnLayersChanged;
+            Update(new Int32Rect(0, 0, finalSurface.Width, finalSurface.Height));
+        }
+
+        public void ForceRerender()
+        {
+            Update(new Int32Rect(0, 0, finalSurface.Width, finalSurface.Height));
+        }
+
+        public void Dispose()
+        {
+            finalSurface.Dispose();
+            backingSurface.Dispose();
+            BlendingPaint.Dispose();
+            ClearPaint.Dispose();
+            layers.CollectionChanged -= OnLayersChanged;
+        }
+
+        private void SubscribeToAllLayers(ObservableCollection<Layer> layers)
+        {
+            foreach (var layer in layers)
+            {
+                layer.LayerBitmapChanged += OnLayerBitmapChanged;
+            }
+        }
+
+        private void UnsubscribeFromAllLayers(ObservableCollection<Layer> layers)
+        {
+            foreach (var layer in layers)
+            {
+                layer.LayerBitmapChanged -= OnLayerBitmapChanged;
+            }
+        }
+
+        private void Update(Int32Rect dirtyRectangle)
+        {
+            dirtyRectangle = dirtyRectangle.Intersect(new Int32Rect(0, 0, finalBitmap.PixelWidth, finalBitmap.PixelHeight));
+            finalSurface.SkiaSurface.Canvas.DrawRect(
+                new SKRect(
+                    dirtyRectangle.X, dirtyRectangle.Y,
+                    dirtyRectangle.X + dirtyRectangle.Width,
+                    dirtyRectangle.Y + dirtyRectangle.Height
+                    ),
+                ClearPaint
+                );
+            foreach (var layer in layers)
+            {
+                if (!LayerStructureUtils.GetFinalLayerIsVisible(layer, structure))
+                    continue;
+                BlendingPaint.Color = new SKColor(255, 255, 255, (byte)(LayerStructureUtils.GetFinalLayerOpacity(layer, structure) * 255));
+
+                Int32Rect layerRect = new Int32Rect(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height);
+                Int32Rect layerPortion = layerRect.Intersect(dirtyRectangle);
+
+                using (var snapshot = layer.LayerBitmap.SkiaSurface.Snapshot())
+                {
+                    finalSurface.SkiaSurface.Canvas.DrawImage(
+                        snapshot,
+                        new SKRect(
+                            layerPortion.X - layer.OffsetX,
+                            layerPortion.Y - layer.OffsetY,
+                            layerPortion.X - layer.OffsetX + layerPortion.Width,
+                            layerPortion.Y - layer.OffsetY + layerPortion.Height),
+                        new SKRect(
+                            layerPortion.X,
+                            layerPortion.Y,
+                            layerPortion.X + layerPortion.Width,
+                            layerPortion.Y + layerPortion.Height
+                        ),
+                        BlendingPaint);
+                }
+            }
+            finalBitmap.Lock();
+            using (var snapshot = finalSurface.SkiaSurface.Snapshot())
+            {
+                SKRect rect = new(dirtyRectangle.X, dirtyRectangle.Y, dirtyRectangle.X + dirtyRectangle.Width, dirtyRectangle.Y + dirtyRectangle.Height);
+                backingSurface.Canvas.DrawImage(snapshot, rect, rect, Surface.ReplacingPaint);
+            }
+
+            finalBitmap.AddDirtyRect(dirtyRectangle);
+            finalBitmap.Unlock();
+        }
+
+        private void OnLayersChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+        {
+            if (e.NewItems != null)
+            {
+                foreach (var obj in e.NewItems)
+                {
+                    Layer layer = (Layer)obj;
+                    layer.LayerBitmapChanged += OnLayerBitmapChanged;
+                }
+            }
+            if (e.OldItems != null)
+            {
+                foreach (var obj in e.OldItems)
+                {
+                    ((Layer)obj).LayerBitmapChanged -= OnLayerBitmapChanged;
+                }
+            }
+            Update(new Int32Rect(0, 0, finalSurface.Width, finalSurface.Height));
+        }
+
+        private void OnLayerBitmapChanged(object sender, Int32Rect e)
+        {
+            Update(e);
+        }
+    }
+}

+ 53 - 0
PixiEditor/Models/Controllers/MouseInputFilter.cs

@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+
+namespace PixiEditor.Models.Controllers
+{
+    internal class MouseInputFilter
+    {
+        public EventHandler<MouseButton> OnMouseDown;
+        public EventHandler OnMouseMove;
+        public EventHandler<MouseButton> OnMouseUp;
+
+
+        private Dictionary<MouseButton, MouseButtonState> buttonStates = new()
+        {
+            [MouseButton.Left] = MouseButtonState.Released,
+            [MouseButton.Right] = MouseButtonState.Released,
+            [MouseButton.Middle] = MouseButtonState.Released,
+        };
+
+        public void MouseDown(object args) => MouseDown(((MouseButtonEventArgs)args).ChangedButton);
+        public void MouseDown(MouseButton button)
+        {
+            if (button is MouseButton.XButton1 or MouseButton.XButton2)
+                return;
+            if (buttonStates[button] == MouseButtonState.Pressed)
+                return;
+            buttonStates[button] = MouseButtonState.Pressed;
+
+            OnMouseDown?.Invoke(this, button);
+        }
+
+        public void MouseMove(object args) => OnMouseMove?.Invoke(this, EventArgs.Empty);
+        public void MouseMove(MouseEventArgs args) => OnMouseMove?.Invoke(this, EventArgs.Empty);
+
+        public void MouseUp(object args) => MouseUp(((MouseButtonEventArgs)args).ChangedButton);
+        public void MouseUp(object sender, Point p, MouseButton button) => MouseUp(button);
+        public void MouseUp(MouseButton button)
+        {
+            if (button is MouseButton.XButton1 or MouseButton.XButton2)
+                return;
+            if (buttonStates[button] == MouseButtonState.Released)
+                return;
+            buttonStates[button] = MouseButtonState.Released;
+
+            OnMouseUp?.Invoke(this, button);
+        }
+    }
+}

+ 0 - 83
PixiEditor/Models/Controllers/MouseMovementController.cs

@@ -1,83 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Windows.Input;
-using PixiEditor.Models.Position;
-
-namespace PixiEditor.Models.Controllers
-{
-    public class MouseMovementController
-    {
-        public event EventHandler StartedRecordingChanges;
-
-        public event EventHandler<MouseEventArgs> OnMouseDown;
-
-        public event EventHandler<MouseEventArgs> OnMouseUp;
-
-        public event EventHandler<MouseMovementEventArgs> MousePositionChanged;
-
-        public event EventHandler StoppedRecordingChanges;
-
-        public List<Coordinates> LastMouseMoveCoordinates { get; set; } = new List<Coordinates>();
-
-        public bool IsRecordingChanges { get; private set; }
-
-        public bool ClickedOnCanvas { get; set; }
-
-        public void StartRecordingMouseMovementChanges(bool clickedOnCanvas)
-        {
-            if (IsRecordingChanges == false)
-            {
-                LastMouseMoveCoordinates.Clear();
-                IsRecordingChanges = true;
-                ClickedOnCanvas = clickedOnCanvas;
-                StartedRecordingChanges?.Invoke(this, EventArgs.Empty);
-            }
-        }
-
-        public void RecordMouseMovementChange(Coordinates mouseCoordinates)
-        {
-            if (IsRecordingChanges)
-            {
-                if (LastMouseMoveCoordinates.Count == 0 || mouseCoordinates != LastMouseMoveCoordinates[^1])
-                {
-                    LastMouseMoveCoordinates.Insert(0, mouseCoordinates);
-                    MousePositionChanged?.Invoke(this, new MouseMovementEventArgs(mouseCoordinates));
-                }
-            }
-        }
-
-        /// <summary>
-        ///     Plain mouse move, does not affect mouse drag recordings.
-        /// </summary>
-        public void MouseMoved(Coordinates mouseCoordinates)
-        {
-            MousePositionChanged?.Invoke(this, new MouseMovementEventArgs(mouseCoordinates));
-        }
-
-        /// <summary>
-        /// Plain mouse down, does not affect mouse recordings.
-        /// </summary>
-        public void MouseDown(MouseEventArgs args)
-        {
-            OnMouseDown?.Invoke(this, args);
-        }
-
-        /// <summary>
-        /// Plain mouse up, does not affect mouse recordings.
-        /// </summary>
-        public void MouseUp(MouseEventArgs args)
-        {
-            OnMouseUp?.Invoke(this, args);
-        }
-
-        public void StopRecordingMouseMovementChanges()
-        {
-            if (IsRecordingChanges)
-            {
-                IsRecordingChanges = false;
-                ClickedOnCanvas = false;
-                StoppedRecordingChanges?.Invoke(this, EventArgs.Empty);
-            }
-        }
-    }
-}

+ 0 - 105
PixiEditor/Models/Controllers/PixelChangesController.cs

@@ -1,105 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using PixiEditor.Models.DataHolders;
-
-namespace PixiEditor.Models.Controllers
-{
-    public class PixelChangesController
-    {
-        private Dictionary<Guid, LayerChange> LastChanges { get; set; }
-
-        private Dictionary<Guid, LayerChange> LastOldValues { get; set; }
-
-        /// <summary>
-        ///     Adds layer changes to controller.
-        /// </summary>
-        /// <param name="changes">New changes.</param>
-        /// <param name="oldValues">Old values of changes.</param>
-        public void AddChanges(LayerChange changes, LayerChange oldValues)
-        {
-            if (changes.PixelChanges.ChangedPixels.Count > 0)
-            {
-                if (LastChanges == null)
-                {
-                    LastChanges = new Dictionary<Guid, LayerChange> { { changes.LayerGuid, changes } };
-                    LastOldValues = new Dictionary<Guid, LayerChange> { { oldValues.LayerGuid, oldValues } };
-                }
-                else if (LastChanges.ContainsKey(changes.LayerGuid))
-                {
-                    AddToExistingLayerChange(changes, oldValues);
-                }
-                else
-                {
-                    AddNewLayerChange(changes, oldValues);
-                }
-            }
-        }
-
-        /// <summary>
-        ///     Returns all changes and deletes them from controller.
-        /// </summary>
-        /// <returns>Tuple array with new changes and old values.</returns>
-        public Tuple<LayerChange, LayerChange>[] PopChanges()
-        {
-            // Maybe replace Tuple with custom data type
-            if (LastChanges == null)
-            {
-                return null;
-            }
-
-            Tuple<LayerChange, LayerChange>[] result = new Tuple<LayerChange, LayerChange>[LastChanges.Count];
-            int i = 0;
-            foreach (KeyValuePair<Guid, LayerChange> change in LastChanges)
-            {
-                Dictionary<Position.Coordinates, System.Windows.Media.Color> pixelChanges =
-                    change.Value.PixelChanges.ChangedPixels.ToDictionary(entry => entry.Key, entry => entry.Value);
-                Dictionary<Position.Coordinates, System.Windows.Media.Color> oldValues = LastOldValues[change.Key].PixelChanges.ChangedPixels
-                    .ToDictionary(entry => entry.Key, entry => entry.Value);
-
-                LayerChange tmp = new LayerChange(new BitmapPixelChanges(pixelChanges), change.Key);
-                LayerChange oldValuesTmp = new LayerChange(new BitmapPixelChanges(oldValues), change.Key);
-
-                result[i] = new Tuple<LayerChange, LayerChange>(tmp, oldValuesTmp);
-                i++;
-            }
-
-            LastChanges = null;
-            LastOldValues = null;
-            return result;
-        }
-
-        private void AddNewLayerChange(LayerChange changes, LayerChange oldValues)
-        {
-            LastChanges[changes.LayerGuid] = changes;
-            LastOldValues[changes.LayerGuid] = oldValues;
-        }
-
-        private void AddToExistingLayerChange(LayerChange layerChange, LayerChange oldValues)
-        {
-            foreach (KeyValuePair<Position.Coordinates, System.Windows.Media.Color> change in layerChange.PixelChanges.ChangedPixels)
-            {
-                if (LastChanges[layerChange.LayerGuid].PixelChanges.ChangedPixels.ContainsKey(change.Key))
-                {
-                    continue;
-                }
-                else
-                {
-                    LastChanges[layerChange.LayerGuid].PixelChanges.ChangedPixels.Add(change.Key, change.Value);
-                }
-            }
-
-            foreach (KeyValuePair<Position.Coordinates, System.Windows.Media.Color> change in oldValues.PixelChanges.ChangedPixels)
-            {
-                if (LastOldValues[layerChange.LayerGuid].PixelChanges.ChangedPixels.ContainsKey(change.Key))
-                {
-                    continue;
-                }
-                else
-                {
-                    LastOldValues[layerChange.LayerGuid].PixelChanges.ChangedPixels.Add(change.Key, change.Value);
-                }
-            }
-        }
-    }
-}

+ 0 - 14
PixiEditor/Models/Controllers/ReadonlyToolUtility.cs

@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-using PixiEditor.Models.Position;
-using PixiEditor.Models.Tools;
-
-namespace PixiEditor.Models.Controllers
-{
-    public class ReadonlyToolUtility
-    {
-        public void ExecuteTool(List<Coordinates> mouseMove, ReadonlyTool tool)
-        {
-            tool.Use(mouseMove);
-        }
-    }
-}

+ 2 - 3
PixiEditor/Models/Controllers/Shortcuts/ShortcutController.cs

@@ -1,5 +1,4 @@
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
+using System.Collections.ObjectModel;
 using System.Linq;
 using System.Windows.Input;
 
@@ -41,4 +40,4 @@ namespace PixiEditor.Models.Controllers.Shortcuts
             }
         }
     }
-}
+}

+ 101 - 0
PixiEditor/Models/Controllers/SingleLayerRenderer.cs

@@ -0,0 +1,101 @@
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Layers;
+using SkiaSharp;
+using System;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.Models.Controllers
+{
+    public class SingleLayerRenderer : INotifyPropertyChanged, IDisposable
+    {
+        private SKPaint BlendingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+        private SKPaint ClearPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
+        private Layer layer;
+
+        private SKSurface backingSurface;
+        private WriteableBitmap finalBitmap;
+        public WriteableBitmap FinalBitmap
+        {
+            get => finalBitmap;
+            set
+            {
+                finalBitmap = value;
+                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FinalBitmap)));
+            }
+        }
+
+        public event PropertyChangedEventHandler PropertyChanged;
+        public SingleLayerRenderer(Layer layer, int width, int height)
+        {
+            this.layer = layer;
+            layer.LayerBitmapChanged += OnLayerBitmapChanged;
+            Resize(width, height);
+        }
+
+        public void Resize(int newWidth, int newHeight)
+        {
+            backingSurface?.Dispose();
+
+            finalBitmap = new WriteableBitmap(newWidth, newHeight, 96, 96, PixelFormats.Pbgra32, null);
+            var imageInfo = new SKImageInfo(newWidth, newHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb());
+            backingSurface = SKSurface.Create(imageInfo, finalBitmap.BackBuffer, finalBitmap.BackBufferStride);
+            Update(new Int32Rect(0, 0, newWidth, newHeight));
+        }
+
+        public void Dispose()
+        {
+            backingSurface.Dispose();
+            BlendingPaint.Dispose();
+            layer.LayerBitmapChanged -= OnLayerBitmapChanged;
+        }
+
+        private void Update(Int32Rect dirtyRectangle)
+        {
+            dirtyRectangle = dirtyRectangle.Intersect(new Int32Rect(0, 0, finalBitmap.PixelWidth, finalBitmap.PixelHeight));
+            if (!dirtyRectangle.HasArea)
+                return;
+            backingSurface.Canvas.DrawRect(
+                new SKRect(
+                    dirtyRectangle.X, dirtyRectangle.Y,
+                    dirtyRectangle.X + dirtyRectangle.Width,
+                    dirtyRectangle.Y + dirtyRectangle.Height
+                    ),
+                ClearPaint
+                );
+            finalBitmap.Lock();
+            if (layer.IsVisible)
+            {
+                BlendingPaint.Color = new SKColor(255, 255, 255, (byte)(layer.Opacity * 255));
+                var layerDirty = dirtyRectangle.Intersect(new Int32Rect(layer.OffsetX, layer.OffsetY, layer.Width, layer.Height));
+                using (var snapshot = layer.LayerBitmap.SkiaSurface.Snapshot())
+                {
+                    backingSurface.Canvas.DrawImage(
+                        snapshot,
+                        new SKRect(
+                            layerDirty.X - layer.OffsetX,
+                            layerDirty.Y - layer.OffsetY,
+                            layerDirty.X - layer.OffsetX + layerDirty.Width,
+                            layerDirty.Y - layer.OffsetY + layerDirty.Height),
+                        new SKRect(
+                            layerDirty.X,
+                            layerDirty.Y,
+                            layerDirty.X + layerDirty.Width,
+                            layerDirty.Y + layerDirty.Height
+                        ),
+                        BlendingPaint);
+                }
+            }
+
+            finalBitmap.AddDirtyRect(dirtyRectangle);
+            finalBitmap.Unlock();
+        }
+
+        private void OnLayerBitmapChanged(object sender, Int32Rect e)
+        {
+            Update(e);
+        }
+    }
+}

+ 39 - 0
PixiEditor/Models/Controllers/SurfaceRenderer.cs

@@ -0,0 +1,39 @@
+using PixiEditor.Models.DataHolders;
+using SkiaSharp;
+using System;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.Models.Controllers
+{
+    class SurfaceRenderer : IDisposable
+    {
+        public SKSurface BackingSurface { get; private set; }
+        public WriteableBitmap FinalBitmap { get; private set; }
+        private SKPaint BlendingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+        public SurfaceRenderer(int width, int height)
+        {
+            FinalBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Pbgra32, null);
+            var imageInfo = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb());
+            BackingSurface = SKSurface.Create(imageInfo, FinalBitmap.BackBuffer, FinalBitmap.BackBufferStride);
+        }
+
+        public void Dispose()
+        {
+            BackingSurface.Dispose();
+            BlendingPaint.Dispose();
+        }
+
+        public void Draw(Surface otherSurface, byte opacity)
+        {
+            BackingSurface.Canvas.Clear();
+            FinalBitmap.Lock();
+            BlendingPaint.Color = new SKColor(255, 255, 255, opacity);
+            using (var snapshot = otherSurface.SkiaSurface.Snapshot())
+                BackingSurface.Canvas.DrawImage(snapshot, new SKRect(0, 0, FinalBitmap.PixelWidth, FinalBitmap.PixelHeight));
+            FinalBitmap.AddDirtyRect(new Int32Rect(0, 0, FinalBitmap.PixelWidth, FinalBitmap.PixelHeight));
+            FinalBitmap.Unlock();
+        }
+    }
+}

+ 121 - 0
PixiEditor/Models/Controllers/ToolSession.cs

@@ -0,0 +1,121 @@
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using System;
+using System.Collections.Generic;
+using System.Windows.Input;
+using SkiaSharp;
+
+namespace PixiEditor.Models.Controllers
+{
+    public class ToolSession
+    {
+        private List<Coordinates> mouseMovement = new();
+        private bool ended = false;
+
+        public IReadOnlyList<Coordinates> MouseMovement => mouseMovement;
+        public Tool Tool { get; }
+
+        public bool IsCtrlDown { get; private set; }
+        public bool IsShiftDown { get; private set; }
+        public bool IsAltDown { get; private set; }
+
+        private int _smallestX = int.MaxValue;
+        private int _smallestY = int.MaxValue;
+        private int _biggestX = int.MinValue;
+        private int _biggestY = int.MinValue;
+
+        public ToolSession(
+            Tool tool,
+            double mouseXOnCanvas,
+            double mouseYOnCanvas,
+            IReadOnlyDictionary<Key, KeyStates> keyboardStates)
+        {
+            if (tool == null)
+                throw new ArgumentNullException(nameof(tool));
+            Tool = tool;
+
+            Tool.Session = this;
+            InvokeKeyboardEvents(keyboardStates);
+            int x = (int)Math.Floor(mouseXOnCanvas);
+            int y = (int)Math.Floor(mouseYOnCanvas);
+            mouseMovement.Add(new(x, y));
+
+            UpdateMinMax(x, y);
+
+            Tool.BeforeUse();
+        }
+
+        private void InvokeKeyboardEvents(IReadOnlyDictionary<Key, KeyStates> keyboardStates)
+        {
+            foreach (var pair in keyboardStates)
+            {
+                if (pair.Value == KeyStates.None)
+                    OnKeyUp(pair.Key);
+                else if (pair.Value == KeyStates.Down)
+                    OnKeyDown(pair.Key);
+            }
+        }
+
+        public void EndSession(IReadOnlyDictionary<Key, KeyStates> keyboardStates)
+        {
+            if (ended)
+                throw new Exception("Session has ended already");
+            ended = true;
+
+            Tool.AfterUse(SKRectI.Create(
+                _smallestX,
+                _smallestY,
+                _biggestX - _smallestX + 1,
+                _biggestY - _smallestY + 1));
+            InvokeReleaseKeyboardEvents(keyboardStates);
+            Tool.Session = null;
+        }
+
+        private void InvokeReleaseKeyboardEvents(IReadOnlyDictionary<Key, KeyStates> keyboardStates)
+        {
+            foreach (var pair in keyboardStates)
+            {
+                if (pair.Value == KeyStates.Down)
+                    OnKeyUp(pair.Key);
+            }
+        }
+
+        public void OnKeyDown(Key key)
+        {
+            if (key == Key.LeftCtrl)
+                IsCtrlDown = true;
+            else if (key == Key.LeftShift)
+                IsShiftDown = true;
+            else if (key == Key.LeftAlt)
+                IsAltDown = true;
+
+            Tool.OnKeyDown(key);
+        }
+
+        public void OnKeyUp(Key key)
+        {
+            if (key == Key.LeftCtrl)
+                IsCtrlDown = false;
+            else if (key == Key.LeftShift)
+                IsShiftDown = false;
+            else if (key == Key.LeftAlt)
+                IsAltDown = false;
+
+            Tool.OnKeyUp(key);
+        }
+
+        public void OnPixelPositionChange(Coordinates pos)
+        {
+            UpdateMinMax(pos.X, pos.Y);
+            mouseMovement.Add(pos);
+        }
+
+        private void UpdateMinMax(int x, int y)
+        {
+            _smallestX = Math.Min(_smallestX, x);
+            _smallestY = Math.Min(_smallestY, y);
+            _biggestX = Math.Max(_biggestX, x);
+            _biggestY = Math.Max(_biggestY, y);
+        }
+    }
+}

+ 140 - 0
PixiEditor/Models/Controllers/ToolSessionController.cs

@@ -0,0 +1,140 @@
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using System;
+using System.Collections.Generic;
+using System.Windows.Input;
+
+namespace PixiEditor.Models.Controllers
+{
+    public class ToolSessionController : ICanvasInputTarget
+    {
+        public event EventHandler<MouseMovementEventArgs> PixelMousePositionChanged;
+        public event EventHandler<(double, double)> PreciseMousePositionChanged;
+        public event EventHandler<(Key, KeyStates)> KeyStateChanged;
+
+        public event EventHandler<ToolSession> SessionStarted;
+        public event EventHandler<ToolSession> SessionEnded;
+
+        public MouseButtonState LeftMouseState { get; private set; }
+
+        public bool IsShiftDown => keyboardState.ContainsKey(Key.LeftShift) ? keyboardState[Key.LeftShift] == KeyStates.Down : false;
+        public bool IsCtrlDown => keyboardState.ContainsKey(Key.LeftCtrl) ? keyboardState[Key.LeftCtrl] == KeyStates.Down : false;
+        public bool IsAltDown => keyboardState.ContainsKey(Key.LeftAlt) ? keyboardState[Key.LeftAlt] == KeyStates.Down : false;
+
+        public Coordinates LastPixelPosition => new(lastPixelX, lastPixelY);
+
+        private int lastPixelX;
+        private int lastPixelY;
+
+        private Dictionary<Key, KeyStates> keyboardState = new();
+        private Tool currentTool = null;
+        private ToolSession currentSession = null;
+
+        private void TryStartToolSession(Tool tool, double mouseXOnCanvas, double mouseYOnCanvas)
+        {
+            if (currentSession != null)
+                return;
+            currentSession = new(tool, mouseXOnCanvas, mouseYOnCanvas, keyboardState);
+            SessionStarted?.Invoke(this, currentSession);
+        }
+
+        private void TryStopToolSession()
+        {
+            if (currentSession == null)
+                return;
+            currentSession.EndSession(keyboardState);
+            SessionEnded?.Invoke(this, currentSession);
+            currentSession = null;
+        }
+
+        public void OnKeyDown(Key key)
+        {
+            key = ConvertRightKeys(key);
+            UpdateKeyState(key, KeyStates.Down);
+            currentSession?.OnKeyDown(key);
+            KeyStateChanged?.Invoke(this, (key, KeyStates.Down));
+        }
+
+        public void OnKeyUp(Key key)
+        {
+            key = ConvertRightKeys(key);
+            UpdateKeyState(key, KeyStates.None);
+            currentSession?.OnKeyUp(key);
+            KeyStateChanged?.Invoke(this, (key, KeyStates.None));
+        }
+
+        private void UpdateKeyState(Key key, KeyStates state)
+        {
+            key = ConvertRightKeys(key);
+            if (!keyboardState.ContainsKey(key))
+                keyboardState.Add(key, state);
+            else
+                keyboardState[key] = state;
+        }
+
+        private Key ConvertRightKeys(Key key)
+        {
+            if (key == Key.RightAlt)
+                return Key.LeftAlt;
+            if (key == Key.RightCtrl)
+                return Key.LeftCtrl;
+            if (key == Key.RightShift)
+                return Key.LeftShift;
+            return key;
+        }
+
+        public void ForceStopActiveSessionIfAny() => TryStopToolSession();
+
+        public void OnToolChange(Tool tool)
+        {
+            currentTool = tool;
+            TryStopToolSession();
+        }
+
+        public void OnMouseMove(double newCanvasX, double newCanvasY)
+        {
+            //update internal state
+
+            int newX = (int)Math.Floor(newCanvasX);
+            int newY = (int)Math.Floor(newCanvasY);
+            bool pixelPosChanged = false;
+            if (lastPixelX != newX || lastPixelY != newY)
+            {
+                lastPixelX = newX;
+                lastPixelY = newY;
+                pixelPosChanged = true;
+            }
+
+
+            //call session events
+            if (currentSession != null && pixelPosChanged)
+                currentSession.OnPixelPositionChange(new(newX, newY));
+
+            //call internal events
+            PreciseMousePositionChanged?.Invoke(this, (newCanvasX, newCanvasY));
+            if (pixelPosChanged)
+                PixelMousePositionChanged?.Invoke(this, new MouseMovementEventArgs(new Coordinates(newX, newY)));
+        }
+
+        public void OnLeftMouseButtonDown(double canvasPosX, double canvasPosY)
+        {
+            //update internal state
+            LeftMouseState = MouseButtonState.Pressed;
+
+            //call session events
+
+            if (currentTool == null)
+                throw new Exception("Current tool must not be null here");
+            TryStartToolSession(currentTool, canvasPosX, canvasPosY);
+        }
+
+        public void OnLeftMouseButtonUp()
+        {
+            //update internal state
+            LeftMouseState = MouseButtonState.Released;
+
+            //call session events
+            TryStopToolSession();
+        }
+    }
+}

+ 28 - 7
PixiEditor/Models/Controllers/UndoManager.cs

@@ -1,15 +1,15 @@
-using System;
+using PixiEditor.Models.Undo;
+using PixiEditor.ViewModels;
+using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
 using System.Reflection;
-using PixiEditor.Models.Undo;
-using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Controllers
 {
     [DebuggerDisplay("{UndoStack.Count} undo steps, {RedoStack.Count} redo step(s)")]
-    public class UndoManager
+    public class UndoManager : IDisposable
     {
         private bool lastChangeWasUndo;
 
@@ -54,6 +54,10 @@ namespace PixiEditor.Models.Controllers
             // Clears RedoStack if last move wasn't redo or undo and if redo stack is greater than 0.
             if (lastChangeWasUndo == false && RedoStack.Count > 0)
             {
+                foreach (var redo in RedoStack)
+                {
+                    //redo.Dispose();
+                }
                 RedoStack.Clear();
             }
 
@@ -106,6 +110,11 @@ namespace PixiEditor.Models.Controllers
         public void SquashUndoChanges(int amount)
         {
             string description = UndoStack.ElementAt(UndoStack.Count - amount).Description;
+            if (string.IsNullOrEmpty(description))
+            {
+                description = $"Squash {amount} undo changes.";
+            }
+
             SquashUndoChanges(amount, description);
         }
 
@@ -114,7 +123,7 @@ namespace PixiEditor.Models.Controllers
         /// </summary>
         /// <param name="amount">Amount of changes to squash.</param>
         /// <param name="description">Final change description.</param>
-        public void SquashUndoChanges(int amount, string description)
+        public void SquashUndoChanges(int amount, string description, bool reverseOrderOnRedo = true)
         {
             Change[] changes = new Change[amount];
             for (int i = 0; i < amount; i++)
@@ -140,7 +149,9 @@ namespace PixiEditor.Models.Controllers
 
             Action<object[]> process = (object[] props) =>
             {
-                foreach (var prop in props.Reverse())
+                var finalProps = reverseOrderOnRedo ? props.Reverse() : props;
+
+                foreach (var prop in finalProps)
                 {
                     Change change = (Change)prop;
                     if (change.Process == null)
@@ -158,6 +169,16 @@ namespace PixiEditor.Models.Controllers
             AddUndoChange(change);
         }
 
+        public void Dispose()
+        {
+            foreach (Change change in UndoStack.Concat(RedoStack))
+            {
+                change.Dispose();
+            }
+
+            GC.SuppressFinalize(this);
+        }
+
         private bool ChangeIsBlockedProperty(Change change)
         {
             return (change.Root != null || change.FindRootProcess != null)
@@ -195,4 +216,4 @@ namespace PixiEditor.Models.Controllers
             return new Tuple<PropertyInfo, object>(target.GetType().GetProperty(bits.Last()), target);
         }
     }
-}
+}

+ 15 - 19
PixiEditor/Models/DataHolders/BitmapPixelChanges.cs

@@ -1,38 +1,34 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Diagnostics;
-using System.Linq;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using PixiEditor.Exceptions;
+using PixiEditor.Exceptions;
 using PixiEditor.Helpers.Extensions;
-using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
 
 namespace PixiEditor.Models.DataHolders
 {
     public struct BitmapPixelChanges
     {
-        public BitmapPixelChanges(Dictionary<Coordinates, Color> changedPixels)
+        public BitmapPixelChanges(Dictionary<Coordinates, SKColor> changedPixels)
         {
             ChangedPixels = changedPixels;
             WasBuiltAsSingleColored = false;
         }
 
-        public static BitmapPixelChanges Empty => new BitmapPixelChanges(new Dictionary<Coordinates, Color>());
+        public static BitmapPixelChanges Empty => new BitmapPixelChanges(new Dictionary<Coordinates, SKColor>());
 
         public bool WasBuiltAsSingleColored { get; private set; }
 
-        public Dictionary<Coordinates, Color> ChangedPixels { get; set; }
+        public Dictionary<Coordinates, SKColor> ChangedPixels { get; set; }
 
         /// <summary>
         ///     Builds BitmapPixelChanges with only one color for specified coordinates.
         /// </summary>
         /// <returns>Single-colored BitmapPixelChanges.</returns>
-        public static BitmapPixelChanges FromSingleColoredArray(IEnumerable<Coordinates> coordinates, Color color)
+        public static BitmapPixelChanges FromSingleColoredArray(IEnumerable<Coordinates> coordinates, SKColor color)
         {
-            Dictionary<Coordinates, Color> dict = new Dictionary<Coordinates, Color>();
+            Dictionary<Coordinates, SKColor> dict = new Dictionary<Coordinates, SKColor>();
             foreach (Coordinates coordinate in coordinates)
             {
                 if (dict.ContainsKey(coordinate))
@@ -76,16 +72,16 @@ namespace PixiEditor.Models.DataHolders
         /// <summary>
         ///     Builds BitmapPixelChanges using 2 same-length enumerables of coordinates and colors.
         /// </summary>
-        public static BitmapPixelChanges FromArrays(IEnumerable<Coordinates> coordinates, IEnumerable<Color> color)
+        public static BitmapPixelChanges FromArrays(IEnumerable<Coordinates> coordinates, IEnumerable<SKColor> color)
         {
             Coordinates[] coordinateArray = coordinates.ToArray();
-            Color[] colorArray = color.ToArray();
+            SKColor[] colorArray = color.ToArray();
             if (coordinateArray.Length != colorArray.Length)
             {
                 throw new ArrayLengthMismatchException();
             }
 
-            Dictionary<Coordinates, Color> dict = new Dictionary<Coordinates, Color>();
+            Dictionary<Coordinates, SKColor> dict = new Dictionary<Coordinates, SKColor>();
             for (int i = 0; i < coordinateArray.Length; i++)
             {
                 dict.Add(coordinateArray[i], colorArray[i]);
@@ -96,7 +92,7 @@ namespace PixiEditor.Models.DataHolders
 
         public BitmapPixelChanges WithoutTransparentPixels()
         {
-            return new BitmapPixelChanges(ChangedPixels.Where(x => x.Value.A > 0).ToDictionary(y => y.Key, y => y.Value));
+            return new BitmapPixelChanges(ChangedPixels.Where(x => x.Value.Alpha > 0).ToDictionary(y => y.Key, y => y.Value));
         }
     }
-}
+}

+ 12 - 3
PixiEditor/Models/DataHolders/Document/Document.Constructors.cs

@@ -1,6 +1,7 @@
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Layers;
 using PixiEditor.ViewModels;
+using System;
 using System.Linq;
 
 namespace PixiEditor.Models.DataHolders
@@ -10,9 +11,12 @@ namespace PixiEditor.Models.DataHolders
         public Document(int width, int height)
             : this()
         {
+            if (width <= 0 || height <= 0)
+                throw new ArgumentException("Document dimensions must be greater than 0");
             Width = width;
             Height = height;
-            DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(0, 0, width, height));
+            Renderer = new LayerStackRenderer(layers, layerStructure, Width, Height);
+            DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(1, 1, width, height));
         }
 
         private Document()
@@ -25,6 +29,11 @@ namespace PixiEditor.Models.DataHolders
             Layers.CollectionChanged += Layers_CollectionChanged;
             LayerStructure.Groups.CollectionChanged += Groups_CollectionChanged;
             LayerStructure.LayerStructureChanged += LayerStructure_LayerStructureChanged;
+            DocumentSizeChanged += (sender, args) =>
+            {
+                Renderer.Resize(args.NewWidth, args.NewHeight);
+                GeneratePreviewLayer();
+            };
         }
 
         private void LayerStructure_LayerStructureChanged(object sender, LayerStructureChangedEventArgs e)
@@ -32,7 +41,7 @@ namespace PixiEditor.Models.DataHolders
             RaisePropertyChanged(nameof(LayerStructure));
             foreach (var layerGuid in e.AffectedLayerGuids)
             {
-                Layer layer = Layers.First(x => x.LayerGuid == layerGuid);
+                Layer layer = Layers.First(x => x.GuidValue == layerGuid);
                 layer.RaisePropertyChange(nameof(layer.IsVisible));
                 layer.RaisePropertyChange(nameof(layer.Opacity));
             }
@@ -51,4 +60,4 @@ namespace PixiEditor.Models.DataHolders
             RaisePropertyChanged(nameof(Layers));
         }
     }
-}
+}

+ 2 - 1
PixiEditor/Models/DataHolders/Document/Document.Discord.cs

@@ -1,4 +1,5 @@
-using System;
+using PixiEditor.Parser;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;

+ 121 - 89
PixiEditor/Models/DataHolders/Document/Document.Layers.cs

@@ -1,19 +1,17 @@
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Enums;
-using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
+using PixiEditor.Models.Layers.Utils;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Undo;
+using SkiaSharp;
 using System;
 using System.Buffers;
 using System.Collections.Generic;
-using System.Collections.ObjectModel;
 using System.Linq;
 using System.Text.RegularExpressions;
 using System.Windows;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.DataHolders
 {
@@ -25,29 +23,61 @@ namespace PixiEditor.Models.DataHolders
         private Guid activeLayerGuid;
         private LayerStructure layerStructure;
 
-        private ObservableCollection<Layer> layers = new();
+        private WpfObservableRangeCollection<Layer> layers = new();
 
-        public ObservableCollection<Layer> Layers
+        public WpfObservableRangeCollection<Layer> Layers
         {
             get => layers;
             set
             {
                 layers = value;
                 Layers.CollectionChanged += Layers_CollectionChanged;
+                Renderer.SetNewLayersCollection(value);
             }
         }
 
         public LayerStructure LayerStructure
         {
             get => layerStructure;
-            set
+            private set
             {
                 layerStructure = value;
                 RaisePropertyChanged(nameof(LayerStructure));
             }
         }
 
-        public Layer ActiveLayer => Layers.Count > 0 ? Layers.FirstOrDefault(x => x.LayerGuid == ActiveLayerGuid) : null;
+        private LayerStackRenderer renderer;
+        public LayerStackRenderer Renderer
+        {
+            get => renderer;
+            private set
+            {
+                renderer = value;
+                RaisePropertyChanged(nameof(Renderer));
+            }
+        }
+
+        private Layer referenceLayer;
+        private SingleLayerRenderer referenceLayerRenderer;
+        public Layer ReferenceLayer
+        {
+            get => referenceLayer;
+            set
+            {
+                referenceLayer = value;
+                referenceLayerRenderer?.Dispose();
+                referenceLayerRenderer = referenceLayer == null ? null : new SingleLayerRenderer(referenceLayer, referenceLayer.Width, referenceLayer.Height);
+                RaisePropertyChanged(nameof(ReferenceLayer));
+                RaisePropertyChanged(nameof(ReferenceLayerRenderer));
+            }
+        }
+
+        public SingleLayerRenderer ReferenceLayerRenderer
+        {
+            get => referenceLayerRenderer;
+        }
+
+        public Layer ActiveLayer => Layers.Count > 0 ? Layers.FirstOrDefault(x => x.GuidValue == ActiveLayerGuid) : null;
 
         public Guid ActiveLayerGuid
         {
@@ -80,7 +110,7 @@ namespace PixiEditor.Models.DataHolders
                 }
             }
 
-            ActiveLayerGuid = Layers[index].LayerGuid;
+            ActiveLayerGuid = Layers[index].GuidValue;
             ActiveLayer.IsActive = true;
             LayersChanged?.Invoke(this, new LayersChangedEventArgs(ActiveLayerGuid, LayerAction.SetActive));
         }
@@ -90,35 +120,12 @@ namespace PixiEditor.Models.DataHolders
         /// </summary>
         /// <param name="layer">Layer to check.</param>
         /// <returns>True if is visible, false if at least parent is not visible or layer itself is invisible.</returns>
-        public bool GetFinalLayerIsVisible(Layer layer)
-        {
-            if (!layer.IsVisible)
-            {
-                return false;
-            }
-
-            var group = LayerStructure.GetGroupByLayer(layer.LayerGuid);
-            bool atLeastOneParentIsInvisible = false;
-            GuidStructureItem groupToCheck = group;
-            while (groupToCheck != null)
-            {
-                if (!groupToCheck.IsVisible)
-                {
-                    atLeastOneParentIsInvisible = true;
-                    break;
-                }
-
-                groupToCheck = groupToCheck.Parent;
-            }
-
-            return !atLeastOneParentIsInvisible;
-        }
-
+        public bool GetFinalLayerIsVisible(Layer layer) => LayerStructureUtils.GetFinalLayerIsVisible(layer, LayerStructure);
         public void UpdateLayersColor()
         {
             foreach (var layer in Layers)
             {
-                if (layer.LayerGuid == ActiveLayerGuid)
+                if (layer.GuidValue == ActiveLayerGuid)
                 {
                     layer.LayerHighlightColor = MainSelectedLayerColor;
                 }
@@ -129,11 +136,11 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
-        public void MoveLayerInStructure(Guid layerGuid, Guid referenceLayer, bool above = false)
+        public void MoveLayerInStructure(Guid layerGuid, Guid referenceLayer, bool above = false, bool addToUndo = true)
         {
             var args = new object[] { layerGuid, referenceLayer, above };
 
-            Layer layer = Layers.First(x => x.LayerGuid == layerGuid);
+            Layer layer = Layers.First(x => x.GuidValue == layerGuid);
 
             int oldIndex = Layers.IndexOf(layer);
 
@@ -143,6 +150,8 @@ namespace PixiEditor.Models.DataHolders
 
             AddLayerStructureToUndo(oldLayerStrcutureGroups);
 
+            if (!addToUndo) return;
+
             UndoManager.AddUndoChange(new Change(
                 ReverseMoveLayerInStructureProcess,
                 new object[] { oldIndex, layerGuid },
@@ -157,8 +166,8 @@ namespace PixiEditor.Models.DataHolders
         {
             var args = new object[] { groupGuid, referenceLayer, above };
 
-            var topLayer = Layers.First(x => x.LayerGuid == LayerStructure.GetGroupByGuid(groupGuid).EndLayerGuid);
-            var bottomLayer = Layers.First(x => x.LayerGuid == LayerStructure.GetGroupByGuid(groupGuid).StartLayerGuid);
+            var topLayer = Layers.First(x => x.GuidValue == LayerStructure.GetGroupByGuid(groupGuid).EndLayerGuid);
+            var bottomLayer = Layers.First(x => x.GuidValue == LayerStructure.GetGroupByGuid(groupGuid).StartLayerGuid);
 
             int indexOfTopLayer = Layers.IndexOf(topLayer);
             Guid oldReferenceLayerGuid;
@@ -166,12 +175,12 @@ namespace PixiEditor.Models.DataHolders
 
             if (indexOfTopLayer + 1 < Layers.Count)
             {
-                oldReferenceLayerGuid = topLayer.LayerGuid;
+                oldReferenceLayerGuid = topLayer.GuidValue;
             }
             else
             {
                 int indexOfBottomLayer = Layers.IndexOf(bottomLayer);
-                oldReferenceLayerGuid = Layers[indexOfBottomLayer - 1].LayerGuid;
+                oldReferenceLayerGuid = Layers[indexOfBottomLayer - 1].GuidValue;
                 oldAbove = true;
             }
 
@@ -190,26 +199,33 @@ namespace PixiEditor.Models.DataHolders
             UndoManager.SquashUndoChanges(2, "Move group");
         }
 
-        public void AddNewLayer(string name, WriteableBitmap bitmap, bool setAsActive = true)
+        public void AddNewLayer(string name, Surface bitmap, bool setAsActive = true)
         {
-            AddNewLayer(name, bitmap.PixelWidth, bitmap.PixelHeight, setAsActive);
-            Layers.Last().LayerBitmap = bitmap;
+            AddNewLayer(name, bitmap.Width, bitmap.Height, setAsActive, bitmap);
         }
 
         public void AddNewLayer(string name, bool setAsActive = true)
         {
-            AddNewLayer(name, 0, 0, setAsActive);
+            AddNewLayer(name, 1, 1, setAsActive);
         }
 
-        public void AddNewLayer(string name, int width, int height, bool setAsActive = true)
+        public void AddNewLayer(string name, int width, int height, bool setAsActive = true, Surface bitmap = null)
         {
             Layer layer;
 
-            Layers.Add(layer = new Layer(name, width, height)
+            if (bitmap != null)
             {
-                MaxHeight = Height,
-                MaxWidth = Width
-            });
+                if (width != bitmap.Width || height != bitmap.Height)
+                    throw new ArgumentException("Inconsistent width and height");
+            }
+            if (width <= 0 || height <= 0)
+                throw new ArgumentException("Dimensions must be greater than 0");
+
+            layer = bitmap == null ? new Layer(name, width, height) : new Layer(name, bitmap);
+            layer.MaxHeight = Height;
+            layer.MaxWidth = Width;
+
+            Layers.Add(layer);
 
             layer.Name = GetLayerSuffix(layer);
 
@@ -224,12 +240,12 @@ namespace PixiEditor.Models.DataHolders
                 UndoManager.AddUndoChange(
                     storageChange.ToChange(
                         RemoveLayerProcess,
-                        new object[] { Layers[^1].LayerGuid },
+                        new object[] { Layers[^1].GuidValue },
                         RestoreLayersProcess,
                         "Add layer"));
             }
 
-            LayersChanged?.Invoke(this, new LayersChangedEventArgs(Layers[^1].LayerGuid, LayerAction.Add));
+            LayersChanged?.Invoke(this, new LayersChangedEventArgs(Layers[^1].GuidValue, LayerAction.Add));
         }
 
         /// <summary>
@@ -250,7 +266,7 @@ namespace PixiEditor.Models.DataHolders
             UndoManager.AddUndoChange(
                 storageChange.ToChange(
                     RemoveLayerProcess,
-                    new object[] { duplicate.LayerGuid },
+                    new object[] { duplicate.GuidValue },
                     RestoreLayersProcess,
                     "Duplicate Layer"));
 
@@ -277,9 +293,9 @@ namespace PixiEditor.Models.DataHolders
             var selectedLayers = Layers.Where(x => x.IsActive);
             foreach (var layer in selectedLayers)
             {
-                if (layer.LayerGuid != lastLayerGuid)
+                if (layer.GuidValue != lastLayerGuid)
                 {
-                    ActiveLayerGuid = layer.LayerGuid;
+                    ActiveLayerGuid = layer.GuidValue;
                     LayersChanged?.Invoke(this, new LayersChangedEventArgs(ActiveLayerGuid, LayerAction.SetActive));
                     return;
                 }
@@ -296,9 +312,9 @@ namespace PixiEditor.Models.DataHolders
                     return;
                 }
 
-                if (ActiveLayerGuid == layer.LayerGuid)
+                if (ActiveLayerGuid == layer.GuidValue)
                 {
-                    SetNextSelectedLayerAsActive(layer.LayerGuid);
+                    SetNextSelectedLayerAsActive(layer.GuidValue);
                 }
 
                 layer.IsActive = !layer.IsActive;
@@ -341,15 +357,15 @@ namespace PixiEditor.Models.DataHolders
                 return;
             }
 
-            LayerStructure.AssignParent(Layers[layerIndex].LayerGuid, null);
+            LayerStructure.AssignParent(Layers[layerIndex].GuidValue, null);
 
             bool wasActive = Layers[layerIndex].IsActive;
 
-            StorageBasedChange change = new(this, new[] { Layers[layerIndex] });
             if (addToUndo)
             {
+                StorageBasedChange change = new(this, new[] { Layers[layerIndex] });
                 UndoManager.AddUndoChange(
-                    change.ToChange(RestoreLayersProcess, RemoveLayerProcess, new object[] { Layers[layerIndex].LayerGuid }));
+                    change.ToChange(RestoreLayersProcess, RemoveLayerProcess, new object[] { Layers[layerIndex].GuidValue }));
             }
 
             Layers.RemoveAt(layerIndex);
@@ -376,7 +392,7 @@ namespace PixiEditor.Models.DataHolders
             Layer[] layers = Layers.Where(x => x.IsActive).ToArray();
             int firstIndex = Layers.IndexOf(layers[0]);
 
-            object[] guidArgs = new object[] { layers.Select(x => x.LayerGuid).ToArray() };
+            object[] guidArgs = new object[] { layers.Select(x => x.GuidValue).ToArray() };
 
             StorageBasedChange change = new(this, layers);
 
@@ -392,14 +408,14 @@ namespace PixiEditor.Models.DataHolders
 
         }
 
-        public void AddLayerStructureToUndo(ObservableCollection<GuidStructureItem> oldLayerStructureGroups)
+        public void AddLayerStructureToUndo(WpfObservableRangeCollection<GuidStructureItem> oldLayerStructureGroups)
         {
             UndoManager.AddUndoChange(
                 new Change(
                     BuildLayerStructureProcess,
-                    new[] { oldLayerStructureGroups },
+                    new object[] { oldLayerStructureGroups },
                     BuildLayerStructureProcess,
-                    new[] { LayerStructure.CloneGroups() }));
+                    new object[] { LayerStructure.CloneGroups() }, "Reload LayerStructure"));
         }
 
         public Layer MergeLayers(Layer[] layersToMerge, bool nameOfLast, int index)
@@ -423,11 +439,11 @@ namespace PixiEditor.Models.DataHolders
 
             Layer mergedLayer = layersToMerge[0];
 
-            var groupParent = LayerStructure.GetGroupByLayer(layersToMerge[^1].LayerGuid);
+            var groupParent = LayerStructure.GetGroupByLayer(layersToMerge[^1].GuidValue);
 
             Layer placeholderLayer = new("_placeholder");
             Layers.Insert(index, placeholderLayer);
-            LayerStructure.AssignParent(placeholderLayer.LayerGuid, groupParent?.GroupGuid);
+            LayerStructure.AssignParent(placeholderLayer.GuidValue, groupParent?.GroupGuid);
 
             for (int i = 0; i < layersToMerge.Length - 1; i++)
             {
@@ -438,10 +454,9 @@ namespace PixiEditor.Models.DataHolders
             }
 
             Layers.Insert(index, mergedLayer);
-            LayerStructure.AssignParent(mergedLayer.LayerGuid, groupParent?.GroupGuid);
+            LayerStructure.AssignParent(mergedLayer.GuidValue, groupParent?.GroupGuid);
 
             RemoveLayer(placeholderLayer, false);
-
             RemoveLayer(layersToMerge[^1], false);
 
             SetMainActiveLayer(Layers.IndexOf(mergedLayer));
@@ -472,21 +487,36 @@ namespace PixiEditor.Models.DataHolders
                 InsertLayersAtIndexesProcess,
                 new object[] { indexes[0] },
                 MergeLayersProcess,
-                new object[] { indexes, nameIsLastLayers, layer.LayerGuid }));
+                new object[] { indexes, nameIsLastLayers, layer.GuidValue }));
 
-            UndoManager.SquashUndoChanges(2, "Undo merge layers");
+            UndoManager.SquashUndoChanges(2, "Undo merge layers", false);
 
             return layer;
         }
 
-        public Color GetColorAtPoint(int x, int y)
+        public SKColor GetColorAtPoint(int x, int y)
+        {
+            return Renderer.FinalSurface.GetSRGBPixel(x, y);
+        }
+
+        private void DisposeLayerBitmaps()
         {
-            return BitmapUtils.GetColorAtPointCombined(x, y, Layers.ToArray());
+            foreach (var layer in layers)
+            {
+                layer.LayerBitmap.Dispose();
+            }
+
+            referenceLayer?.LayerBitmap.Dispose();
+            previewLayer?.LayerBitmap.Dispose();
+
+            previewLayerRenderer?.Dispose();
+            referenceLayerRenderer?.Dispose();
+            renderer?.Dispose();
         }
 
-        private void BuildLayerStructureProcess(object[] parameters)
+        public void BuildLayerStructureProcess(object[] parameters)
         {
-            if (parameters.Length > 0 && parameters[0] is ObservableCollection<GuidStructureItem> groups)
+            if (parameters.Length > 0 && parameters[0] is WpfObservableRangeCollection<GuidStructureItem> groups)
             {
                 LayerStructure.Groups.CollectionChanged -= Groups_CollectionChanged;
                 LayerStructure.Groups = LayerStructure.CloneGroups(groups);
@@ -500,13 +530,13 @@ namespace PixiEditor.Models.DataHolders
             int indexTo = (int)props[0];
             Guid layerGuid = (Guid)props[1];
 
-            Guid layerAtOldIndex = Layers[indexTo].LayerGuid;
+            Guid layerAtOldIndex = Layers[indexTo].GuidValue;
 
             var startGroup = LayerStructure.GetGroupByLayer(layerGuid);
 
             LayerStructure.PreMoveReassignBounds(new GroupData(startGroup?.GroupGuid), layerGuid);
 
-            Layers.Move(Layers.IndexOf(Layers.First(x => x.LayerGuid == layerGuid)), indexTo);
+            Layers.Move(Layers.IndexOf(Layers.First(x => x.GuidValue == layerGuid)), indexTo);
 
             var newGroup = LayerStructure.GetGroupByLayer(layerAtOldIndex);
 
@@ -522,7 +552,7 @@ namespace PixiEditor.Models.DataHolders
 
             if (Layers.Count == 0)
             {
-                Layer layer = new("Base Layer", 0, 0) { MaxHeight = Height, MaxWidth = Width };
+                Layer layer = new("Base Layer", 1, 1) { MaxHeight = Height, MaxWidth = Width };
                 Layers.Add(layer);
                 undoAction = (Layer[] layers, UndoLayer[] undoData) =>
                 {
@@ -567,7 +597,8 @@ namespace PixiEditor.Models.DataHolders
         {
             if (args.Length > 0 && args[0] is int layerIndex)
             {
-                Layers.RemoveAt(layerIndex);
+                RemoveLayer(layerIndex, false);
+
                 for (int i = 0; i < layers.Length; i++)
                 {
                     Layer layer = layers[i];
@@ -575,7 +606,7 @@ namespace PixiEditor.Models.DataHolders
                     Layers.Insert(data[i].LayerIndex, layer);
                 }
 
-                ActiveLayerGuid = layers.First(x => x.LayerHighlightColor == MainSelectedLayerColor).LayerGuid;
+                ActiveLayerGuid = layers.First(x => x.LayerHighlightColor == MainSelectedLayerColor).GuidValue;
                 // Identifying main layer by highlightColor is a bit hacky, but shhh
             }
         }
@@ -613,15 +644,15 @@ namespace PixiEditor.Models.DataHolders
             GuidStructureItem group = LayerStructure.GetGroupByGuid(groupGuid);
             GuidStructureItem referenceLayerGroup = LayerStructure.GetGroupByLayer(referenceLayerGuid);
 
-            Layer referenceLayer = Layers.First(x => x.LayerGuid == referenceLayerGuid);
+            Layer referenceLayer = Layers.First(x => x.GuidValue == referenceLayerGuid);
 
             int layerIndex = Layers.IndexOf(referenceLayer);
-            int folderTopIndex = Layers.IndexOf(Layers.First(x => x.LayerGuid == group?.EndLayerGuid));
+            int folderTopIndex = Layers.IndexOf(Layers.First(x => x.GuidValue == group?.EndLayerGuid));
             int oldIndex = folderTopIndex;
 
             if (layerIndex < folderTopIndex)
             {
-                int folderBottomIndex = Layers.IndexOf(Layers.First(x => x.LayerGuid == group.StartLayerGuid));
+                int folderBottomIndex = Layers.IndexOf(Layers.First(x => x.GuidValue == group.StartLayerGuid));
                 oldIndex = folderBottomIndex;
             }
 
@@ -658,8 +689,8 @@ namespace PixiEditor.Models.DataHolders
             Guid referenceLayer = (Guid)parameter[1];
             bool above = (bool)parameter[2];
 
-            int layerIndex = Layers.IndexOf(Layers.First(x => x.LayerGuid == referenceLayer));
-            int oldIndex = Layers.IndexOf(Layers.First(x => x.LayerGuid == layer));
+            int layerIndex = Layers.IndexOf(Layers.First(x => x.GuidValue == referenceLayer));
+            int oldIndex = Layers.IndexOf(Layers.First(x => x.GuidValue == layer));
             int newIndex = CalculateNewIndex(layerIndex, above, oldIndex);
 
             var startGroup = LayerStructure.GetGroupByLayer(layer);
@@ -678,6 +709,7 @@ namespace PixiEditor.Models.DataHolders
             }
 
             RaisePropertyChanged(nameof(LayerStructure));
+            Renderer.ForceRerender();
         }
 
         private void RestoreLayersProcess(Layer[] layers, UndoLayer[] layersData)
@@ -696,21 +728,21 @@ namespace PixiEditor.Models.DataHolders
 
         private void RemoveLayerProcess(object[] parameters)
         {
-            if (parameters != null && parameters.Length > 0 && parameters[0] is Guid layerGuid)
+            if (parameters is { Length: > 0 } && parameters[0] is Guid layerGuid)
             {
-                Layer layer = Layers.First(x => x.LayerGuid == layerGuid);
+                Layer layer = Layers.First(x => x.GuidValue == layerGuid);
                 int index = Layers.IndexOf(layer);
                 bool wasActive = layer.IsActive;
 
-                var layerGroup = LayerStructure.GetGroupByLayer(layer.LayerGuid);
+                var layerGroup = LayerStructure.GetGroupByLayer(layer.GuidValue);
 
                 LayerStructure.ExpandParentGroups(layerGroup);
 
-                if (layerGroup?.Parent != null && LayerStructure.GroupContainsOnlyLayer(layer.LayerGuid, layerGroup))
+                if (layerGroup?.Parent != null && LayerStructure.GroupContainsOnlyLayer(layer.GuidValue, layerGroup))
                 {
                     LayerStructure.PreMoveReassignBounds(new GroupData(layerGroup.Parent.GroupGuid), new GroupData(layerGroup.GroupGuid));
                 }
-                LayerStructure.AssignParent(Layers[index].LayerGuid, null);
+                LayerStructure.AssignParent(Layers[index].GuidValue, null);
                 RemoveGroupsIfEmpty(layer, layerGroup);
 
                 Layers.Remove(layer);
@@ -726,7 +758,7 @@ namespace PixiEditor.Models.DataHolders
 
         private void RemoveGroupsIfEmpty(Layer layer, GuidStructureItem layerGroup)
         {
-            if (LayerStructure.GroupContainsOnlyLayer(layer.LayerGuid, layerGroup))
+            if (LayerStructure.GroupContainsOnlyLayer(layer.GuidValue, layerGroup))
             {
                 if (layerGroup.Parent != null)
                 {

+ 138 - 3
PixiEditor/Models/DataHolders/Document/Document.Operations.cs

@@ -1,7 +1,9 @@
-using PixiEditor.Helpers.Extensions;
+using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
 using PixiEditor.Models.Undo;
+using SkiaSharp;
 using System;
 using System.Linq;
 using System.Windows;
@@ -46,6 +48,35 @@ namespace PixiEditor.Models.DataHolders
             DocumentSizeChanged?.Invoke(this, new DocumentSizeChangedEventArgs(oldWidth, oldHeight, width, height));
         }
 
+        public void RotateActiveDocument(float degrees)
+        {
+            object[] processArgs = { degrees };
+            object[] reverseProcessArgs = { -degrees };
+
+            RotateDocumentProcess(processArgs);
+
+            UndoManager.AddUndoChange(new Change(
+                RotateDocumentProcess,
+                reverseProcessArgs,
+                RotateDocumentProcess,
+                processArgs,
+                "Rotate layer"));
+        }
+
+        public void FlipActiveDocument(FlipType flip)
+        {
+            object[] processArgs = { flip };
+
+            FlipDocumentProcess(processArgs);
+
+            UndoManager.AddUndoChange(new Change(
+                FlipDocumentProcess,
+                processArgs,
+                FlipDocumentProcess,
+                processArgs,
+                $"Flip layer: {flip}"));
+        }
+
         /// <summary>
         ///     Resizes all document layers using NearestNeighbor interpolation.
         /// </summary>
@@ -68,6 +99,110 @@ namespace PixiEditor.Models.DataHolders
                     "Resize document"));
         }
 
+
+        private void FlipDocumentProcess(object[] processArgs)
+        {
+            FlipType flip = (FlipType)processArgs[0];
+            foreach (var layer in Layers)
+            {
+                using (new SKAutoCanvasRestore(layer.LayerBitmap.SkiaSurface.Canvas, true))
+                {
+                    var copy = layer.LayerBitmap.SkiaSurface.Snapshot();
+
+                    var canvas = layer.LayerBitmap.SkiaSurface.Canvas;
+
+                    layer.ClipCanvas();
+
+                    if (flip == FlipType.Horizontal)
+                    {
+                        canvas.Translate(layer.Width, 0);
+                        canvas.Scale(-1, 1, 0, 0);
+                    }
+                    else
+                    {
+                        canvas.Translate(0, layer.Width);
+                        canvas.Scale(1, -1, 0, 0);
+                    }
+
+                    // Flip offset based on document and layer center point
+                    var documentCenter = new Coordinates(Width / 2, Height / 2);
+                    var layerCenter = new Coordinates(layer.Width / 2, layer.Height / 2);
+
+                    int newOffsetX = layer.OffsetX;
+                    int newOffsetY = layer.OffsetY;
+
+                    if (flip == FlipType.Horizontal)
+                    {
+                        newOffsetX += layerCenter.X;
+                        int diff = documentCenter.X - newOffsetX;
+                        newOffsetX = layer.OffsetX + (diff * 2);
+                    }
+                    else if(flip == FlipType.Vertical)
+                    {
+                        newOffsetY += layerCenter.Y;
+                        int diff = documentCenter.Y - newOffsetY;
+                        newOffsetY = layer.OffsetY + (diff * 2);
+                    }
+
+                    layer.Offset = new Thickness(newOffsetX, newOffsetY, 0, 0);
+
+                    canvas.DrawImage(copy, default(SKPoint));
+                    copy.Dispose();
+                }
+
+                layer.InvokeLayerBitmapChange();
+            }
+        }
+
+        private void RotateDocumentProcess(object[] parameters)
+        {
+            float degrees = (float)parameters[0];
+
+            int oldWidth = Width;
+            int oldHeight = Height;
+
+            int biggerMaxSize = Math.Max(Width, Height);
+
+            // TODO: Fix v0.2
+            foreach (var layer in Layers)
+            {
+                using (new SKAutoCanvasRestore(layer.LayerBitmap.SkiaSurface.Canvas, true))
+                {
+                    var copy = layer.LayerBitmap.SkiaSurface.Snapshot();
+
+                    double radians = Math.PI * degrees / 180;
+                    float sine = (float)Math.Abs(Math.Sin(radians));
+                    float cosine = (float)Math.Abs(Math.Cos(radians));
+                    int originalWidth = layer.Width;
+                    int originalHeight = layer.Height;
+                    int rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight);
+                    int rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth);
+
+                    layer.CreateNewBitmap(rotatedWidth, rotatedHeight);
+
+                    var surface = layer.LayerBitmap.SkiaSurface.Canvas;
+
+                    surface.Translate(rotatedWidth / 2, rotatedHeight / 2);
+                    surface.RotateDegrees((float)degrees);
+                    surface.Translate(-originalWidth / 2, -originalHeight / 2);
+                    surface.DrawImage(copy, default(SKPoint));
+
+                    layer.MaxHeight = oldWidth;
+                    layer.MaxWidth = oldHeight;
+
+                    copy.Dispose();
+                }
+
+                layer.InvokeLayerBitmapChange();
+            }
+
+            Height = oldWidth;
+            Width = oldHeight;
+            DocumentSizeChanged?.Invoke(
+                this,
+                new DocumentSizeChangedEventArgs(oldWidth, oldHeight, Width, Height));
+        }
+
         private void RestoreDocumentLayersProcess(Layer[] layers, UndoLayer[] data, object[] args)
         {
             Width = (int)args[0];
@@ -112,8 +247,8 @@ namespace PixiEditor.Models.DataHolders
             {
                 float widthRatio = (float)newWidth / Width;
                 float heightRatio = (float)newHeight / Height;
-                int layerWidth = (int)(Layers[i].Width * widthRatio);
-                int layerHeight = (int)(Layers[i].Height * heightRatio);
+                int layerWidth = Math.Max(1, (int)(Layers[i].Width * widthRatio));
+                int layerHeight = Math.Max(1, (int)(Layers[i].Height * heightRatio));
 
                 Layers[i].Resize(layerWidth, layerHeight, newWidth, newHeight);
                 Layers[i].Offset = new Thickness(Math.Floor(Layers[i].OffsetX * widthRatio), Math.Floor(Layers[i].OffsetY * heightRatio), 0, 0);

+ 12 - 2
PixiEditor/Models/DataHolders/Document/Document.Preview.cs

@@ -1,6 +1,7 @@
-using System.Windows.Media.Imaging;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.ImageManipulation;
 using PixiEditor.Models.Layers;
+using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.DataHolders
 {
@@ -14,6 +15,7 @@ namespace PixiEditor.Models.DataHolders
         }
 
         private Layer previewLayer;
+        private SingleLayerRenderer previewLayerRenderer;
 
         public Layer PreviewLayer
         {
@@ -21,10 +23,18 @@ namespace PixiEditor.Models.DataHolders
             set
             {
                 previewLayer = value;
+                previewLayerRenderer?.Dispose();
+                previewLayerRenderer = previewLayer == null ? null : new SingleLayerRenderer(previewLayer, Width, Height);
                 RaisePropertyChanged(nameof(PreviewLayer));
+                RaisePropertyChanged(nameof(PreviewLayerRenderer));
             }
         }
 
+        public SingleLayerRenderer PreviewLayerRenderer
+        {
+            get => previewLayerRenderer;
+        }
+
         public void UpdatePreviewImage()
         {
             previewImage = BitmapUtils.GeneratePreviewBitmap(this, 30, 20);
@@ -40,4 +50,4 @@ namespace PixiEditor.Models.DataHolders
             };
         }
     }
-}
+}

+ 35 - 33
PixiEditor/Models/DataHolders/Document/Document.cs

@@ -2,9 +2,11 @@
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Layers;
+using PixiEditor.Models.Layers.Utils;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Undo;
 using PixiEditor.ViewModels;
+using SkiaSharp;
 using System;
 using System.Buffers;
 using System.Collections.Generic;
@@ -13,13 +15,11 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Windows;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.DataHolders
 {
     [DebuggerDisplay("'{Name, nq}' {width}x{height} {Layers.Count} Layer(s)")]
-    public partial class Document : NotifyableObject
+    public partial class Document : NotifyableObject, IDisposable
     {
 
         private ViewModelMain xamlAccesibleViewModel = null;
@@ -33,36 +33,28 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
-        private Layer referenceLayer;
-
-        public Layer ReferenceLayer
-        {
-            get => referenceLayer;
-            set => SetProperty(ref referenceLayer, value);
-        }
-
         public string Name
         {
             get => (string.IsNullOrEmpty(DocumentFilePath) ? "Untitled" : Path.GetFileName(DocumentFilePath))
                 + (!ChangesSaved ? " *" : string.Empty);
         }
 
-        private int width;
+        private int width = 1;
         public int Width
         {
             get => width;
-            set
+            private set
             {
                 width = value;
-                RaisePropertyChanged("Width");
+                RaisePropertyChanged(nameof(Width));
             }
         }
 
-        private int height;
+        private int height = 1;
         public int Height
         {
             get => height;
-            set
+            private set
             {
                 height = value;
                 RaisePropertyChanged("Height");
@@ -103,12 +95,14 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
+        public bool Disposed { get; private set; } = false;
+
         public ExecutionTrigger<Size> CenterViewportTrigger { get; } = new();
         public ExecutionTrigger<double> ZoomViewportTrigger { get; } = new();
 
         public UndoManager UndoManager { get; set; }
 
-        public ObservableCollection<Color> Swatches { get; set; } = new ObservableCollection<Color>();
+        public ObservableCollection<SKColor> Swatches { get; set; } = new ObservableCollection<SKColor>();
 
         public void RaisePropertyChange(string name)
         {
@@ -120,7 +114,7 @@ namespace PixiEditor.Models.DataHolders
         /// </summary>
         public void ClipCanvas()
         {
-            DoubleCords points = GetEdgePoints(Layers);
+            DoubleCoords points = GetEdgePoints(Layers);
             int smallestX = points.Coords1.X;
             int smallestY = points.Coords1.Y;
             int biggestX = points.Coords2.X;
@@ -159,13 +153,13 @@ namespace PixiEditor.Models.DataHolders
         /// </summary>
         public void CenterContent()
         {
-            var layersToCenter = Layers.Where(x => x.IsActive && GetFinalLayerIsVisible(x));
+            var layersToCenter = Layers.Where(x => x.IsActive && LayerStructureUtils.GetFinalLayerIsVisible(x, LayerStructure));
             if (!layersToCenter.Any())
             {
                 return;
             }
 
-            DoubleCords points = GetEdgePoints(layersToCenter);
+            DoubleCoords points = GetEdgePoints(layersToCenter);
 
             int smallestX = points.Coords1.X;
             int smallestY = points.Coords1.Y;
@@ -193,10 +187,19 @@ namespace PixiEditor.Models.DataHolders
                     "Center content"));
         }
 
+        public void Dispose()
+        {
+            if (Disposed)
+                return;
+            Disposed = true;
+            DisposeLayerBitmaps();
+            UndoManager.Dispose();
+
+            GC.SuppressFinalize(this);
+        }
+
         private void SetAsActiveOnClick(object obj)
         {
-            XamlAccesibleViewModel.BitmapManager.MouseController.StopRecordingMouseMovementChanges();
-            XamlAccesibleViewModel.BitmapManager.MouseController.StartRecordingMouseMovementChanges(true);
             if (XamlAccesibleViewModel.BitmapManager.ActiveDocument != this)
             {
                 XamlAccesibleViewModel.BitmapManager.ActiveDocument = this;
@@ -212,12 +215,12 @@ namespace PixiEditor.Models.DataHolders
         {
             if (anchor.HasFlag(AnchorPoint.Center))
             {
-                return Math.Abs((destWidth / 2) - (srcWidth / 2));
+                return (destWidth / 2) - (srcWidth / 2);
             }
 
             if (anchor.HasFlag(AnchorPoint.Right))
             {
-                return Math.Abs(destWidth - srcWidth);
+                return destWidth - srcWidth;
             }
 
             return 0;
@@ -227,29 +230,28 @@ namespace PixiEditor.Models.DataHolders
         {
             if (anchor.HasFlag(AnchorPoint.Middle))
             {
-                return Math.Abs((destHeight / 2) - (srcHeight / 2));
+                return (destHeight / 2) - (srcHeight / 2);
             }
 
             if (anchor.HasFlag(AnchorPoint.Bottom))
             {
-                return Math.Abs(destHeight - srcHeight);
+                return destHeight - srcHeight;
             }
 
             return 0;
         }
 
-        private DoubleCords GetEdgePoints(IEnumerable<Layer> layers)
+        private DoubleCoords GetEdgePoints(IEnumerable<Layer> layers)
         {
             if (Layers.Count == 0)
             {
                 throw new ArgumentException("Not enough layers");
             }
 
-            Layer firstLayer = layers.First();
-            int smallestX = firstLayer.OffsetX;
-            int smallestY = firstLayer.OffsetY;
-            int biggestX = smallestX + firstLayer.Width;
-            int biggestY = smallestY + firstLayer.Height;
+            int smallestX = int.MaxValue;
+            int smallestY = int.MaxValue;
+            int biggestX = int.MinValue;
+            int biggestY = int.MinValue;
 
             foreach (Layer layer in layers)
             {
@@ -275,7 +277,7 @@ namespace PixiEditor.Models.DataHolders
                 }
             }
 
-            return new DoubleCords(
+            return new DoubleCoords(
                 new Coordinates(smallestX, smallestY),
                 new Coordinates(biggestX, biggestY));
         }

+ 0 - 24
PixiEditor/Models/DataHolders/LayerChange.cs

@@ -1,24 +0,0 @@
-using System;
-using PixiEditor.Models.Layers;
-
-namespace PixiEditor.Models.DataHolders
-{
-    public class LayerChange
-    {
-        public LayerChange(BitmapPixelChanges pixelChanges, Guid layerGuid)
-        {
-            PixelChanges = pixelChanges;
-            LayerGuid = layerGuid;
-        }
-
-        public LayerChange(BitmapPixelChanges pixelChanges, Layer layer)
-        {
-            PixelChanges = pixelChanges;
-            LayerGuid = layer.LayerGuid;
-        }
-
-        public BitmapPixelChanges PixelChanges { get; set; }
-
-        public Guid LayerGuid { get; set; }
-    }
-}

+ 666 - 0
PixiEditor/Models/DataHolders/RangeObservableCollection.cs

@@ -0,0 +1,666 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Linq;
+
+namespace PixiEditor.Models.DataHolders
+{
+  // Licensed to the .NET Foundation under one or more agreements.
+  // The .NET Foundation licenses this file to you under the MIT license.
+  // See the LICENSE file in the project root for more information.
+  /// <summary>
+  /// Implementation of a dynamic data collection based on generic Collection&lt;T&gt;,
+  /// implementing INotifyCollectionChanged to notify listeners
+  /// when items get added, removed or the whole list is refreshed.
+  /// </summary>
+  public class RangeObservableCollection<T> : ObservableCollection<T>
+  {
+    //------------------------------------------------------
+    //
+    //  Private Fields
+    //
+    //------------------------------------------------------
+
+    #region Private Fields    
+    [NonSerialized]
+    private DeferredEventsCollection? _deferredEvents;
+    #endregion Private Fields
+
+
+    //------------------------------------------------------
+    //
+    //  Constructors
+    //
+    //------------------------------------------------------
+
+    #region Constructors
+    /// <summary>
+    /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity.
+    /// </summary>
+    public RangeObservableCollection() { }
+
+    /// <summary>
+    /// Initializes a new instance of the ObservableCollection class that contains
+    /// elements copied from the specified collection and has sufficient capacity
+    /// to accommodate the number of elements copied.
+    /// </summary>
+    /// <param name="collection">The collection whose elements are copied to the new list.</param>
+    /// <remarks>
+    /// The elements are copied onto the ObservableCollection in the
+    /// same order they are read by the enumerator of the collection.
+    /// </remarks>
+    /// <exception cref="ArgumentNullException"> collection is a null reference </exception>
+    public RangeObservableCollection(IEnumerable<T> collection) : base(collection) { }
+
+    /// <summary>
+    /// Initializes a new instance of the ObservableCollection class
+    /// that contains elements copied from the specified list
+    /// </summary>
+    /// <param name="list">The list whose elements are copied to the new list.</param>
+    /// <remarks>
+    /// The elements are copied onto the ObservableCollection in the
+    /// same order they are read by the enumerator of the list.
+    /// </remarks>
+    /// <exception cref="ArgumentNullException"> list is a null reference </exception>
+    public RangeObservableCollection(List<T> list) : base(list) { }
+
+    #endregion Constructors
+
+    //------------------------------------------------------
+    //
+    //  Public Properties
+    //
+    //------------------------------------------------------
+
+    #region Public Properties
+    EqualityComparer<T>? _Comparer;
+    public EqualityComparer<T> Comparer
+    {
+      get => _Comparer ??= EqualityComparer<T>.Default;
+      private set => _Comparer = value;
+    }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether this collection acts as a <see cref="HashSet{T}"/>,
+    /// disallowing duplicate items, based on <see cref="Comparer"/>.
+    /// This might indeed consume background performance, but in the other hand,
+    /// it will pay off in UI performance as less required UI updates are required.
+    /// </summary>
+    public bool AllowDuplicates { get; set; } = true;
+
+    #endregion Public Properties
+
+    //------------------------------------------------------
+    //
+    //  Public Methods
+    //
+    //------------------------------------------------------
+
+    #region Public Methods
+
+    /// <summary>
+    /// Adds the elements of the specified collection to the end of the <see cref="ObservableCollection{T}"/>.
+    /// </summary>
+    /// <param name="collection">
+    /// The collection whose elements should be added to the end of the <see cref="ObservableCollection{T}"/>.
+    /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
+    /// </param>
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    public void AddRange(IEnumerable<T> collection)
+    {
+      InsertRange(Count, collection);
+    }
+
+    /// <summary>
+    /// Inserts the elements of a collection into the <see cref="ObservableCollection{T}"/> at the specified index.
+    /// </summary>
+    /// <param name="index">The zero-based index at which the new elements should be inserted.</param>
+    /// <param name="collection">The collection whose elements should be inserted into the List<T>.
+    /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.</param>                
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is not in the collection range.</exception>
+    public void InsertRange(int index, IEnumerable<T> collection)
+    {
+      if (collection == null)
+        throw new ArgumentNullException(nameof(collection));
+      if (index < 0)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (index > Count)
+        throw new ArgumentOutOfRangeException(nameof(index));
+
+      if (!AllowDuplicates)
+        collection =
+          collection
+          .Distinct(Comparer)
+          .Where(item => !Items.Contains(item, Comparer))
+          .ToList();
+
+      if (collection is ICollection<T> countable)
+      {
+        if (countable.Count == 0)
+          return;
+      }
+      else if (!collection.Any())
+        return;
+
+      CheckReentrancy();
+
+      //expand the following couple of lines when adding more constructors.
+      var target = (List<T>)Items;
+      target.InsertRange(index, collection);
+
+      OnEssentialPropertiesChanged();
+
+      if (!(collection is IList list))
+        list = new List<T>(collection);
+
+      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index));
+    }
+
+
+    /// <summary> 
+    /// Removes the first occurence of each item in the specified collection from the <see cref="ObservableCollection{T}"/>.
+    /// </summary>
+    /// <param name="collection">The items to remove.</param>        
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    public void RemoveRange(IEnumerable<T> collection)
+    {
+      if (collection == null)
+        throw new ArgumentNullException(nameof(collection));
+
+      if (Count == 0)
+        return;
+      else if (collection is ICollection<T> countable)
+      {
+        if (countable.Count == 0)
+          return;
+        else if (countable.Count == 1)
+          using (IEnumerator<T> enumerator = countable.GetEnumerator())
+          {
+            enumerator.MoveNext();
+            Remove(enumerator.Current);
+            return;
+          }
+      }
+      else if (!collection.Any())
+        return;
+
+      CheckReentrancy();
+
+      var clusters = new Dictionary<int, List<T>>();
+      var lastIndex = -1;
+      List<T>? lastCluster = null;
+      foreach (T item in collection)
+      {
+        var index = IndexOf(item);
+        if (index < 0)
+          continue;
+
+        Items.RemoveAt(index);
+
+        if (lastIndex == index && lastCluster != null)
+          lastCluster.Add(item);
+        else
+          clusters[lastIndex = index] = lastCluster = new List<T> { item };
+      }
+
+      OnEssentialPropertiesChanged();
+
+      if (Count == 0)
+        OnCollectionReset();
+      else
+        foreach (KeyValuePair<int, List<T>> cluster in clusters)
+          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster.Value, cluster.Key));
+
+    }
+
+    /// <summary>
+    /// Iterates over the collection and removes all items that satisfy the specified match.
+    /// </summary>
+    /// <remarks>The complexity is O(n).</remarks>
+    /// <param name="match"></param>
+    /// <returns>Returns the number of elements that where </returns>
+    /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
+    public int RemoveAll(Predicate<T> match)
+    {
+      return RemoveAll(0, Count, match);
+    }
+
+    /// <summary>
+    /// Iterates over the specified range within the collection and removes all items that satisfy the specified match.
+    /// </summary>
+    /// <remarks>The complexity is O(n).</remarks>
+    /// <param name="index">The index of where to start performing the search.</param>
+    /// <param name="count">The number of items to iterate on.</param>
+    /// <param name="match"></param>
+    /// <returns>Returns the number of elements that where </returns>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
+    /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
+    public int RemoveAll(int index, int count, Predicate<T> match)
+    {
+      if (index < 0)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (count < 0)
+        throw new ArgumentOutOfRangeException(nameof(count));
+      if (index + count > Count)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (match == null)
+        throw new ArgumentNullException(nameof(match));
+
+      if (Count == 0)
+        return 0;
+
+      List<T>? cluster = null;
+      var clusterIndex = -1;
+      var removedCount = 0;
+
+      using (BlockReentrancy())
+      using (DeferEvents())
+      {
+        for (var i = 0; i < count; i++, index++)
+        {
+          T item = Items[index];
+          if (match(item))
+          {
+            Items.RemoveAt(index);
+            removedCount++;
+
+            if (clusterIndex == index)
+            {
+              Debug.Assert(cluster != null);
+              cluster!.Add(item);
+            }
+            else
+            {
+              cluster = new List<T> { item };
+              clusterIndex = index;
+            }
+
+            index--;
+          }
+          else if (clusterIndex > -1)
+          {
+            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
+            clusterIndex = -1;
+            cluster = null;
+          }
+        }
+
+        if (clusterIndex > -1)
+          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster, clusterIndex));
+      }
+
+      if (removedCount > 0)
+        OnEssentialPropertiesChanged();
+
+      return removedCount;
+    }
+
+    /// <summary>
+    /// Removes a range of elements from the <see cref="ObservableCollection{T}"/>>.
+    /// </summary>
+    /// <param name="index">The zero-based starting index of the range of elements to remove.</param>
+    /// <param name="count">The number of elements to remove.</param>
+    /// <exception cref="ArgumentOutOfRangeException">The specified range is exceeding the collection.</exception>
+    public void RemoveRange(int index, int count)
+    {
+      if (index < 0)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (count < 0)
+        throw new ArgumentOutOfRangeException(nameof(count));
+      if (index + count > Count)
+        throw new ArgumentOutOfRangeException(nameof(index));
+
+      if (count == 0)
+        return;
+
+      if (count == 1)
+      {
+        RemoveItem(index);
+        return;
+      }
+
+      //Items will always be List<T>, see constructors
+      var items = (List<T>)Items;
+      List<T> removedItems = items.GetRange(index, count);
+
+      CheckReentrancy();
+
+      items.RemoveRange(index, count);
+
+      OnEssentialPropertiesChanged();
+
+      if (Count == 0)
+        OnCollectionReset();
+      else
+        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index));
+    }
+
+    /// <summary> 
+    /// Clears the current collection and replaces it with the specified collection,
+    /// using <see cref="Comparer"/>.
+    /// </summary>             
+    /// <param name="collection">The items to fill the collection with, after clearing it.</param>
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    public void ReplaceRange(IEnumerable<T> collection)
+    {
+      ReplaceRange(0, Count, collection);
+    }
+
+    /// <summary>
+    /// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact.
+    /// </summary>
+    /// <param name="index">The index of where to start the replacement.</param>
+    /// <param name="count">The number of items to be replaced.</param>
+    /// <param name="collection">The collection to insert in that location.</param>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
+    /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
+    /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+    /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null.</exception>
+    public void ReplaceRange(int index, int count, IEnumerable<T> collection)
+    {
+      if (index < 0)
+        throw new ArgumentOutOfRangeException(nameof(index));
+      if (count < 0)
+        throw new ArgumentOutOfRangeException(nameof(count));
+      if (index + count > Count)
+        throw new ArgumentOutOfRangeException(nameof(index));
+
+      if (collection == null)
+        throw new ArgumentNullException(nameof(collection));
+
+      if (!AllowDuplicates)
+        collection =
+          collection
+          .Distinct(Comparer)
+          .ToList();
+
+      if (collection is ICollection<T> countable)
+      {
+        if (countable.Count == 0)
+        {
+          RemoveRange(index, count);
+          return;
+        }
+      }
+      else if (!collection.Any())
+      {
+        RemoveRange(index, count);
+        return;
+      }
+
+      if (index + count == 0)
+      {
+        InsertRange(0, collection);
+        return;
+      }
+
+      if (!(collection is IList<T> list))
+        list = new List<T>(collection);
+
+      using (BlockReentrancy())
+      using (DeferEvents())
+      {
+        var rangeCount = index + count;
+        var addedCount = list.Count;
+
+        var changesMade = false;
+        List<T>?
+          newCluster = null,
+          oldCluster = null;
+
+
+        int i = index;
+        for (; i < rangeCount && i - index < addedCount; i++)
+        {
+          //parallel position
+          T old = this[i], @new = list[i - index];
+          if (Comparer.Equals(old, @new))
+          {
+            OnRangeReplaced(i, newCluster!, oldCluster!);
+            continue;
+          }
+          else
+          {
+            Items[i] = @new;
+
+            if (newCluster == null)
+            {
+              Debug.Assert(oldCluster == null);
+              newCluster = new List<T> { @new };
+              oldCluster = new List<T> { old };
+            }
+            else
+            {
+              newCluster.Add(@new);
+              oldCluster!.Add(old);
+            }
+
+            changesMade = true;
+          }
+        }
+
+        OnRangeReplaced(i, newCluster!, oldCluster!);
+
+        //exceeding position
+        if (count != addedCount)
+        {
+          var items = (List<T>)Items;
+          if (count > addedCount)
+          {
+            var removedCount = rangeCount - addedCount;
+            T[] removed = new T[removedCount];
+            items.CopyTo(i, removed, 0, removed.Length);
+            items.RemoveRange(i, removedCount);
+            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed, i));
+          }
+          else
+          {
+            var k = i - index;
+            T[] added = new T[addedCount - k];
+            for (int j = k; j < addedCount; j++)
+            {
+              T @new = list[j];
+              added[j - k] = @new;
+            }
+            items.InsertRange(i, added);
+            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, added, i));
+          }
+
+          OnEssentialPropertiesChanged();
+        }
+        else if (changesMade)
+        {
+          OnIndexerPropertyChanged();
+        }
+      }
+    }
+
+    #endregion Public Methods
+
+
+    //------------------------------------------------------
+    //
+    //  Protected Methods
+    //
+    //------------------------------------------------------
+
+    #region Protected Methods
+
+    /// <summary>
+    /// Called by base class Collection&lt;T&gt; when the list is being cleared;
+    /// raises a CollectionChanged event to any listeners.
+    /// </summary>
+    protected override void ClearItems()
+    {
+      if (Count == 0)
+        return;
+
+      CheckReentrancy();
+      base.ClearItems();
+      OnEssentialPropertiesChanged();
+      OnCollectionReset();
+    }
+
+    /// <inheritdoc/>
+    protected override void InsertItem(int index, T item)
+    {
+      if (!AllowDuplicates && Items.Contains(item))
+        return;
+
+      base.InsertItem(index, item);
+    }
+
+    /// <inheritdoc/>
+    protected override void SetItem(int index, T item)
+    {
+      if (AllowDuplicates)
+      {
+        if (Comparer.Equals(this[index], item))
+          return;
+      }
+      else
+        if (Items.Contains(item, Comparer))
+        return;
+
+      CheckReentrancy();
+      T oldItem = this[index];
+      base.SetItem(index, item);
+
+      OnIndexerPropertyChanged();
+      OnCollectionChanged(NotifyCollectionChangedAction.Replace, oldItem!, item!, index);
+    }
+
+    /// <summary>
+    /// Raise CollectionChanged event to any listeners.
+    /// Properties/methods modifying this ObservableCollection will raise
+    /// a collection changed event through this virtual method.
+    /// </summary>
+    /// <remarks>
+    /// When overriding this method, either call its base implementation
+    /// or call <see cref="BlockReentrancy"/> to guard against reentrant collection changes.
+    /// </remarks>
+    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+    {
+      if (_deferredEvents != null)
+      {
+        _deferredEvents.Add(e);
+        return;
+      }
+      base.OnCollectionChanged(e);
+    }
+
+    protected virtual IDisposable DeferEvents() => new DeferredEventsCollection(this);
+
+    #endregion Protected Methods
+
+
+    //------------------------------------------------------
+    //
+    //  Private Methods
+    //
+    //------------------------------------------------------
+
+    #region Private Methods
+
+    /// <summary>
+    /// Helper to raise Count property and the Indexer property.
+    /// </summary>
+    void OnEssentialPropertiesChanged()
+    {
+      OnPropertyChanged(EventArgsCache.CountPropertyChanged);
+      OnIndexerPropertyChanged();
+    }
+
+    /// <summary>
+    /// /// Helper to raise a PropertyChanged event for the Indexer property
+    /// /// </summary>
+    void OnIndexerPropertyChanged() =>
+     OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
+
+    /// <summary>
+    /// Helper to raise CollectionChanged event to any listeners
+    /// </summary>
+    void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) =>
+     OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
+
+    /// <summary>
+    /// Helper to raise CollectionChanged event with action == Reset to any listeners
+    /// </summary>
+    void OnCollectionReset() =>
+     OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
+
+    /// <summary>
+    /// Helper to raise event for clustered action and clear cluster.
+    /// </summary>
+    /// <param name="followingItemIndex">The index of the item following the replacement block.</param>
+    /// <param name="newCluster"></param>
+    /// <param name="oldCluster"></param>
+    //TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable<T> collection, IEqualityComparer<T> comparer),
+    //move when supported language version updated.
+    void OnRangeReplaced(int followingItemIndex, ICollection<T> newCluster, ICollection<T> oldCluster)
+    {
+      if (oldCluster == null || oldCluster.Count == 0)
+      {
+        Debug.Assert(newCluster == null || newCluster.Count == 0);
+        return;
+      }
+
+      OnCollectionChanged(
+        new NotifyCollectionChangedEventArgs(
+          NotifyCollectionChangedAction.Replace,
+          new List<T>(newCluster),
+          new List<T>(oldCluster),
+          followingItemIndex - oldCluster.Count));
+
+      oldCluster.Clear();
+      newCluster.Clear();
+    }
+
+    #endregion Private Methods
+
+    //------------------------------------------------------
+    //
+    //  Private Types
+    //
+    //------------------------------------------------------
+
+    #region Private Types
+    sealed class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
+    {
+      readonly RangeObservableCollection<T> _collection;
+      public DeferredEventsCollection(RangeObservableCollection<T> collection)
+      {
+        Debug.Assert(collection != null);
+        Debug.Assert(collection._deferredEvents == null);
+        _collection = collection;
+        _collection._deferredEvents = this;
+      }
+
+      public void Dispose()
+      {
+        _collection._deferredEvents = null;
+        foreach (var args in this)
+          _collection.OnCollectionChanged(args);
+      }
+    }
+
+    #endregion Private Types
+
+  }
+
+  /// <remarks>
+  /// To be kept outside <see cref="ObservableCollection{T}"/>, since otherwise, a new instance will be created for each generic type used.
+  /// </remarks>
+  internal static class EventArgsCache
+  {
+    internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs("Count");
+    internal static readonly PropertyChangedEventArgs IndexerPropertyChanged = new PropertyChangedEventArgs("Item[]");
+    internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+  }
+}

+ 16 - 24
PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs

@@ -1,10 +1,12 @@
-using System.Diagnostics;
-using System.IO;
-using System.Windows.Media.Imaging;
-using PixiEditor.Helpers;
-using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Helpers;
 using PixiEditor.Models.IO;
+using PixiEditor.Models.Position;
 using PixiEditor.Parser;
+using PixiEditor.Parser.Skia;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.DataHolders
 {
@@ -49,14 +51,13 @@ namespace PixiEditor.Models.DataHolders
             }
         }
 
-        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
         public WriteableBitmap PreviewBitmap
         {
             get
             {
-                if (previewBitmap == null)
+                if (previewBitmap == null && !Corrupt)
                 {
-                    PreviewBitmap = LoadPreviewBitmap();
+                    previewBitmap = LoadPreviewBitmap();
                 }
 
                 return previewBitmap;
@@ -85,21 +86,12 @@ namespace PixiEditor.Models.DataHolders
                     return null;
                 }
 
-                WriteableBitmap writeableBitmap =
-                    BitmapUtils.GeneratePreviewBitmap(serializableDocument.Layers, serializableDocument.Width, serializableDocument.Height, 80, 50);
-
-                const int MaxWidthInPixels = 1080;
-                const int MaxHeightInPixels = 1080;
-                PixiFileMaxSizeChecker pixiFileMaxSizeChecker = new PixiFileMaxSizeChecker()
-                {
-                    MaxAllowedWidthInPixels = MaxWidthInPixels,
-                    MaxAllowedHeightInPixels = MaxHeightInPixels,
-                    MaxAllowedLayerCount = 5,
-                };
+                using Surface surface = Surface.Combine(serializableDocument.Width, serializableDocument.Height,
+                          serializableDocument.Layers
+                              .Where(x => x.Opacity > 0.8)
+                              .Select(x => (x.ToSKImage(), new Coordinates(x.OffsetX, x.OffsetY))));
 
-                return pixiFileMaxSizeChecker.IsFileUnderMaxSize(serializableDocument) ?
-                    writeableBitmap
-                    : writeableBitmap.Resize(width: MaxWidthInPixels, height: MaxHeightInPixels, WriteableBitmapExtensions.Interpolation.Bilinear);
+                return surface.ToWriteableBitmap();
             }
             else if (FileExtension is ".png" or ".jpg" or ".jpeg")
             {
@@ -107,7 +99,7 @@ namespace PixiEditor.Models.DataHolders
 
                 try
                 {
-                    bitmap = Importer.ImportImage(FilePath);
+                    bitmap = Importer.ImportWriteableBitmap(FilePath);
                 }
                 catch
                 {
@@ -130,4 +122,4 @@ namespace PixiEditor.Models.DataHolders
             return null;
         }
     }
-}
+}

+ 55 - 14
PixiEditor/Models/DataHolders/Selection.cs

@@ -1,26 +1,27 @@
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Diagnostics;
-using System.Linq;
-using System.Windows.Media;
-using PixiEditor.Helpers;
+using PixiEditor.Helpers;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
+using SkiaSharp;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Windows;
 
 namespace PixiEditor.Models.DataHolders
 {
     [DebuggerDisplay("{SelectedPoints.Count} selected Pixels")]
     public class Selection : NotifyableObject
     {
-        private readonly Color selectionBlue;
+        private readonly SKColor selectionBlue;
         private Layer selectionLayer;
 
         public Selection(Coordinates[] selectedPoints)
         {
             SelectedPoints = new ObservableCollection<Coordinates>(selectedPoints);
             SelectionLayer = new Layer("_selectionLayer");
-            selectionBlue = Color.FromArgb(127, 142, 202, 255);
+            selectionBlue = new SKColor(142, 202, 255, 255);
         }
 
         public ObservableCollection<Coordinates> SelectedPoints { get; private set; }
@@ -31,35 +32,75 @@ namespace PixiEditor.Models.DataHolders
             set
             {
                 selectionLayer = value;
-                RaisePropertyChanged("SelectionLayer");
+                RaisePropertyChanged(nameof(SelectionLayer));
             }
         }
 
         public void SetSelection(IEnumerable<Coordinates> selection, SelectionType mode)
         {
-            Color selectionColor = selectionBlue;
+            SKColor selectionColor = selectionBlue;
             switch (mode)
             {
                 case SelectionType.New:
                     SelectedPoints = new ObservableCollection<Coordinates>(selection);
-                    SelectionLayer.Clear();
+                    SelectionLayer.Reset();
                     break;
                 case SelectionType.Add:
                     SelectedPoints = new ObservableCollection<Coordinates>(SelectedPoints.Concat(selection).Distinct());
                     break;
                 case SelectionType.Subtract:
                     SelectedPoints = new ObservableCollection<Coordinates>(SelectedPoints.Except(selection));
-                    selectionColor = System.Windows.Media.Colors.Transparent;
+                    selectionColor = SKColors.Transparent;
                     break;
             }
 
             SelectionLayer.SetPixels(BitmapPixelChanges.FromSingleColoredArray(selection, selectionColor));
         }
 
+        public void TranslateSelection(int dX, int dY)
+        {
+            selectionLayer.Offset = new Thickness(selectionLayer.OffsetX + dX, selectionLayer.OffsetY + dY, 0, 0);
+            for (int i = 0; i < SelectedPoints.Count; i++)
+            {
+                SelectedPoints[i] = new Coordinates(SelectedPoints[i].X + dX, SelectedPoints[i].Y + dY);
+            }
+        }
+
+        public void SetSelection(Int32Rect rect, bool isCirclular, SelectionType mode)
+        {
+            using SKPaint paint = new()
+            {
+                Color = selectionBlue,
+                BlendMode = SKBlendMode.Src,
+                Style = SKPaintStyle.StrokeAndFill,
+            };
+            switch (mode)
+            {
+                case SelectionType.New:
+                    SelectionLayer.Reset();
+                    break;
+                case SelectionType.Subtract:
+                    paint.Color = SKColors.Transparent;
+                    break;
+            }
+
+            SelectionLayer.DynamicResizeAbsolute(new Int32Rect(rect.X, rect.Y, rect.Width, rect.Height));
+            if (isCirclular)
+            {
+                float cx = rect.X + rect.Width / 2f;
+                float cy = rect.Y + rect.Height / 2f;
+                SelectionLayer.LayerBitmap.SkiaSurface.Canvas.DrawOval(cx, cy, rect.Width / 2f, rect.Height / 2f, paint);
+            }
+            else
+            {
+                SelectionLayer.LayerBitmap.SkiaSurface.Canvas.DrawRect(rect.X, rect.Y, rect.Width, rect.Height, paint);
+            }
+        }
+
         public void Clear()
         {
-            SelectionLayer.Clear();
+            SelectionLayer.Reset();
             SelectedPoints.Clear();
         }
     }
-}
+}

+ 287 - 0
PixiEditor/Models/DataHolders/Surface.cs

@@ -0,0 +1,287 @@
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Position;
+using SkiaSharp;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public class Surface : IDisposable
+    {
+        public static SKPaint ReplacingPaint { get; } = new() { BlendMode = SKBlendMode.Src };
+        public static SKPaint BlendingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+        public static SKPaint MaskingPaint { get; } = new() { BlendMode = SKBlendMode.DstIn };
+        public static SKPaint InverseMaskingPaint { get; } = new() { BlendMode = SKBlendMode.DstOut };
+
+        private static readonly SKPaint nearestNeighborReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src, FilterQuality = SKFilterQuality.None };
+
+        public SKSurface SkiaSurface { get; private set; }
+        public int Width { get; }
+        public int Height { get; }
+
+        public bool Disposed { get; private set; } = false;
+
+        private SKPaint drawingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
+        private IntPtr surfaceBuffer;
+
+        public Surface(int w, int h)
+        {
+            if (w <= 0 || h <= 0)
+                throw new ArgumentException("Surface dimensions must be non-zero");
+            InitSurface(w, h);
+            Width = w;
+            Height = h;
+        }
+
+        public Surface(Surface original)
+        {
+            Width = original.Width;
+            Height = original.Height;
+            InitSurface(Width, Height);
+            original.SkiaSurface.Draw(SkiaSurface.Canvas, 0, 0, ReplacingPaint);
+        }
+
+        public Surface(int w, int h, byte[] pbgra32Bytes)
+        {
+            if (w <= 0 || h <= 0)
+                throw new ArgumentException("Surface dimensions must be non-zero");
+            Width = w;
+            Height = h;
+            InitSurface(w, h);
+            DrawBytes(w, h, pbgra32Bytes, SKColorType.Bgra8888, SKAlphaType.Premul);
+        }
+
+        public Surface(BitmapSource original)
+        {
+            SKColorType color = original.Format.ToSkia(out SKAlphaType alpha);
+            if (original.PixelWidth <= 0 || original.PixelHeight <= 0)
+                throw new ArgumentException("Surface dimensions must be non-zero");
+
+            byte[] pixels = new byte[original.PixelWidth * original.PixelHeight * 4];
+            original.CopyPixels(pixels, original.PixelWidth * 4, 0);
+
+            Width = original.PixelWidth;
+            Height = original.PixelHeight;
+            InitSurface(Width, Height);
+            DrawBytes(Width, Height, pixels, color, alpha);
+        }
+
+        public Surface(SKImage image)
+        {
+            Width = image.Width;
+            Height = image.Height;
+            InitSurface(Width, Height);
+            SkiaSurface.Canvas.DrawImage(image, 0, 0);
+        }
+
+        /// <summary>
+        /// Combines the <paramref name="images"/> into a <see cref="Surface"/>
+        /// </summary>
+        /// <param name="width">The width of the <see cref="Surface"/></param>
+        /// <param name="height">The height of the <see cref="Surface"/></param>
+        /// <returns>A surface that has the <paramref name="images"/> drawn on it</returns>
+        public static Surface Combine(int width, int height, IEnumerable<(SKImage image, Coordinates offset)> images)
+        {
+            Surface surface = new Surface(width, height);
+
+            foreach (var image in images)
+            {
+                surface.SkiaSurface.Canvas.DrawImage(image.image, (SKPoint)image.offset);
+            }
+
+            return surface;
+        }
+
+        public Surface ResizeNearestNeighbor(int newW, int newH)
+        {
+            SKImage image = SkiaSurface.Snapshot();
+            Surface newSurface = new(newW, newH);
+            newSurface.SkiaSurface.Canvas.DrawImage(image, new SKRect(0, 0, newW, newH), nearestNeighborReplacingPaint);
+            return newSurface;
+        }
+
+        public Surface Crop(int x, int y, int width, int height)
+        {
+            Surface result = new Surface(width, height);
+            SkiaSurface.Draw(result.SkiaSurface.Canvas, x, y, ReplacingPaint);
+            return result;
+        }
+
+        public unsafe SKColor GetSRGBPixel(int x, int y)
+        {
+            Half* ptr = (Half*)(surfaceBuffer + (x + y * Width) * 8);
+            SKColor color = (SKColor)new SKColorF((float)ptr[0], (float)ptr[1], (float)ptr[2], (float)ptr[3]);
+            return SKPMColor.UnPreMultiply(new SKPMColor((uint)color));
+        }
+
+        public void SetSRGBPixel(int x, int y, SKColor color)
+        {
+            // It's possible that this function can be sped up by writing into surfaceBuffer, not sure if skia will like it though
+            drawingPaint.Color = color;
+            SkiaSurface.Canvas.DrawPoint(x, y, drawingPaint);
+        }
+
+        public unsafe byte[] ToByteArray(SKColorType colorType = SKColorType.Bgra8888, SKAlphaType alphaType = SKAlphaType.Premul)
+        {
+            var imageInfo = new SKImageInfo(Width, Height, colorType, alphaType, SKColorSpace.CreateSrgb());
+
+            byte[] buffer = new byte[Width * Height * imageInfo.BytesPerPixel];
+            fixed (void* pointer = buffer)
+            {
+                if (!SkiaSurface.ReadPixels(imageInfo, new IntPtr(pointer), imageInfo.RowBytes, 0, 0))
+                {
+                    throw new InvalidOperationException("Could not read surface into buffer");
+                }
+            }
+
+            return buffer;
+        }
+
+        public WriteableBitmap ToWriteableBitmap()
+        {
+            WriteableBitmap result = new WriteableBitmap(Width, Height, 96, 96, PixelFormats.Pbgra32, null);
+            result.Lock();
+            var dirty = new Int32Rect(0, 0, Width, Height);
+            result.WritePixels(dirty, ToByteArray(), Width * 4, 0);
+            result.AddDirtyRect(dirty);
+            result.Unlock();
+            return result;
+        }
+
+        public void Dispose()
+        {
+            if (Disposed)
+                return;
+            Disposed = true;
+            SkiaSurface.Dispose();
+            drawingPaint.Dispose();
+            Marshal.FreeHGlobal(surfaceBuffer);
+            GC.SuppressFinalize(this);
+        }
+
+        ~Surface()
+        {
+            Marshal.FreeHGlobal(surfaceBuffer);
+        }
+
+        private static SKSurface CreateSurface(int w, int h, IntPtr buffer)
+        {
+            var surface = SKSurface.Create(new SKImageInfo(w, h, SKColorType.RgbaF16, SKAlphaType.Premul, SKColorSpace.CreateSrgb()), buffer);
+            if (surface == null)
+                throw new Exception("Could not create surface");
+            return surface;
+        }
+        private static SKSurface CreateSurface(int w, int h)
+        {
+            var surface = SKSurface.Create(new SKImageInfo(w, h, SKColorType.RgbaF16, SKAlphaType.Premul, SKColorSpace.CreateSrgb()));
+            if (surface == null)
+                throw new Exception("Could not create surface");
+            return surface;
+        }
+
+        private unsafe void InitSurface(int w, int h)
+        {
+            int byteC = w * h * 8;
+            surfaceBuffer = Marshal.AllocHGlobal(byteC);
+            Unsafe.InitBlockUnaligned((byte*)surfaceBuffer, 0, (uint)byteC);
+            SkiaSurface = CreateSurface(w, h, surfaceBuffer);
+        }
+
+        private unsafe void DrawBytes(int w, int h, byte[] bytes, SKColorType colorType, SKAlphaType alphaType)
+        {
+            SKImageInfo info = new SKImageInfo(w, h, colorType, alphaType);
+
+            fixed (void* pointer = bytes)
+            {
+                using SKPixmap map = new(info, new IntPtr(pointer));
+                using SKSurface surface = SKSurface.Create(map);
+                surface.Draw(SkiaSurface.Canvas, 0, 0, ReplacingPaint);
+            }
+        }
+
+#if DEBUG
+        // Used to iterate the surface's pixels during development
+
+        [Obsolete("Only meant for use in a debugger like Visual Studio", true)]
+        private SurfaceDebugger Debugger => new SurfaceDebugger(this);
+
+        [Obsolete("Only meant for use in a debugger like Visual Studio", true)]
+        private class SurfaceDebugger : IEnumerable
+        {
+            private readonly Surface _surface;
+
+            public SurfaceDebugger(Surface surface)
+            {
+                _surface = surface;
+            }
+
+            IEnumerator IEnumerable.GetEnumerator()
+            {
+                var pixmap = _surface.SkiaSurface.PeekPixels();
+
+                for (int y = 0; y < pixmap.Width; y++)
+                {
+                    yield return new DebugPixel(y);
+
+                    for (int x = 0; x < pixmap.Height; x++)
+                    {
+                        yield return new DebugPixel(x, y, pixmap.GetPixelColor(x, y).ToString());
+                    }
+                }
+            }
+
+            [DebuggerDisplay("{DebuggerDisplay,nq}")]
+            private struct DebugPixel
+            {
+                [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+                private string DebuggerDisplay
+                {
+                    get
+                    {
+                        if (isPixel)
+                        {
+                            return $"X: {x}; Y: {y} - {hex}";
+                        }
+                        else
+                        {
+                            return $"|- Y: {y} -|";
+                        }
+                    }
+                }
+
+                [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+                private readonly int x;
+                [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+                private readonly int y;
+                [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+                private readonly string hex;
+                [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+                private readonly bool isPixel;
+
+                public DebugPixel(int y)
+                {
+                    x = 0;
+                    this.y = y;
+                    hex = null;
+                    isPixel = false;
+                }
+
+                public DebugPixel(int x, int y, string hex)
+                {
+                    this.x = x;
+                    this.y = y;
+                    this.hex = hex;
+                    isPixel = true;
+                }
+            }
+        }
+#endif
+    }
+}

+ 104 - 0
PixiEditor/Models/DataHolders/WpfObservableRangeCollection.cs

@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Windows.Data;
+
+namespace PixiEditor.Models.DataHolders
+{
+public class WpfObservableRangeCollection<T> : RangeObservableCollection<T>
+{
+        public bool SuppressNotify { get; set; } = false;
+  DeferredEventsCollection _deferredEvents;
+
+  public WpfObservableRangeCollection()
+  {
+  }
+
+  public WpfObservableRangeCollection(IEnumerable<T> collection) : base(collection)
+  {
+  }
+
+  public WpfObservableRangeCollection(List<T> list) : base(list)
+  {
+  }
+
+
+  /// <summary>
+  /// Raise CollectionChanged event to any listeners.
+  /// Properties/methods modifying this ObservableCollection will raise
+  /// a collection changed event through this virtual method.
+  /// </summary>
+  /// <remarks>
+  /// When overriding this method, either call its base implementation
+  /// or call <see cref="BlockReentrancy"/> to guard against reentrant collection changes.
+  /// </remarks>
+  protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+  {
+            if (SuppressNotify) return;
+    var _deferredEvents = (ICollection<NotifyCollectionChangedEventArgs>) typeof(RangeObservableCollection<T>)
+      .GetField("_deferredEvents", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this);
+    if (_deferredEvents != null)
+    {
+      _deferredEvents.Add(e);
+      return;
+    }
+
+    foreach (var handler in GetHandlers())
+      if (IsRange(e) && handler.Target is CollectionView cv)
+        cv.Refresh();
+      else
+        handler(this, e);
+  }
+
+  protected override IDisposable DeferEvents() => new DeferredEventsCollection(this);
+
+  bool IsRange(NotifyCollectionChangedEventArgs e) => e.NewItems?.Count > 1 || e.OldItems?.Count > 1;
+
+  IEnumerable<NotifyCollectionChangedEventHandler> GetHandlers()
+  {
+    var info = typeof(ObservableCollection<T>).GetField(nameof(CollectionChanged),
+      BindingFlags.Instance | BindingFlags.NonPublic);
+    var @event = (MulticastDelegate) info.GetValue(this);
+    return @event?.GetInvocationList()
+             .Cast<NotifyCollectionChangedEventHandler>()
+             .Distinct()
+           ?? Enumerable.Empty<NotifyCollectionChangedEventHandler>();
+  }
+
+  class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
+  {
+    private readonly WpfObservableRangeCollection<T> _collection;
+
+    public DeferredEventsCollection(WpfObservableRangeCollection<T> collection)
+    {
+      Debug.Assert(collection != null);
+      Debug.Assert(collection._deferredEvents == null);
+      _collection = collection;
+      _collection._deferredEvents = this;
+    }
+
+    public void Dispose()
+    {
+      _collection._deferredEvents = null;
+
+      var handlers = _collection
+        .GetHandlers()
+        .ToLookup(h => h.Target is CollectionView);
+
+      foreach (var handler in handlers[false])
+      foreach (var e in this)
+        handler(_collection, e);
+
+      foreach (var cv in handlers[true]
+                 .Select(h => h.Target)
+                 .Cast<CollectionView>()
+                 .Distinct())
+        cv.Refresh();
+    }
+  }
+}
+}

+ 14 - 25
PixiEditor/Models/Dialogs/NewFileDialog.cs

@@ -1,53 +1,42 @@
-using System;
-using System.Windows;
-using PixiEditor.Models.UserPreferences;
+using PixiEditor.Models.UserPreferences;
 using PixiEditor.Views;
 
 namespace PixiEditor.Models.Dialogs
 {
     public class NewFileDialog : CustomDialog
     {
-        private int height = (int)IPreferences.Current.GetPreference("DefaultNewFileHeight", 16L);
+        public const int defaultSize = 64;
 
-        private int width = (int)IPreferences.Current.GetPreference("DefaultNewFileWidth", 16L);
+        private int height = IPreferences.Current.GetPreference("DefaultNewFileHeight", defaultSize);
+
+        private int width = IPreferences.Current.GetPreference("DefaultNewFileWidth", defaultSize);
 
         public int Width
         {
             get => width;
-            set
-            {
-                if (width != value)
-                {
-                    width = value;
-                    RaisePropertyChanged("Width");
-                }
-            }
+            set => SetProperty(ref width, value);
         }
 
         public int Height
         {
             get => height;
-            set
-            {
-                if (height != value)
-                {
-                    height = value;
-                    RaisePropertyChanged("Height");
-                }
-            }
+            set => SetProperty(ref height, value);
         }
 
         public override bool ShowDialog()
         {
-            Window popup = new NewFilePopup()
+            NewFilePopup popup = new()
             {
                 FileWidth = Width,
                 FileHeight = Height
             };
+
             popup.ShowDialog();
-            Height = (popup as NewFilePopup).FileHeight;
-            Width = (popup as NewFilePopup).FileWidth;
-            return (bool)popup.DialogResult;
+
+            Height = popup.FileHeight;
+            Width = popup.FileWidth;
+
+            return popup.DialogResult.GetValueOrDefault();
         }
     }
 }

+ 8 - 0
PixiEditor/Models/Enums/FlipType.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Models.Enums
+{
+    public enum FlipType
+    {
+        Horizontal,
+        Vertical
+    }
+}

+ 37 - 7
PixiEditor/Models/IO/Exporter.cs

@@ -1,11 +1,14 @@
-using System;
-using System.IO;
-using System.Windows;
-using System.Windows.Media.Imaging;
-using Microsoft.Win32;
+using Microsoft.Win32;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
+using SkiaSharp;
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.IO
 {
@@ -41,7 +44,7 @@ namespace PixiEditor.Models.IO
         /// <returns>Path.</returns>
         public static string SaveAsEditableFile(Document document, string path)
         {
-            Parser.PixiParser.Serialize(document.ToSerializable(), path);
+            Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
             return path;
         }
 
@@ -68,6 +71,33 @@ namespace PixiEditor.Models.IO
             }
         }
 
+        public static void SaveAsGZippedBytes(string path, Surface surface)
+        {
+            SaveAsGZippedBytes(path, surface, SKRectI.Create(0, 0, surface.Width, surface.Height));
+        }
+
+        public static void SaveAsGZippedBytes(string path, Surface surface, SKRectI rectToSave)
+        {
+            var imageInfo = new SKImageInfo(rectToSave.Width, rectToSave.Height, SKColorType.RgbaF16);
+            var unmanagedBuffer = Marshal.AllocHGlobal(rectToSave.Width * rectToSave.Height * 8);
+            //+8 bytes for width and height
+            var bytes = new byte[rectToSave.Width * rectToSave.Height * 8 + 8];
+            try
+            {
+                surface.SkiaSurface.ReadPixels(imageInfo, unmanagedBuffer, rectToSave.Width * 8, rectToSave.Left, rectToSave.Top);
+                Marshal.Copy(unmanagedBuffer, bytes, 8, rectToSave.Width * rectToSave.Height * 8);
+            }
+            finally
+            {
+                Marshal.FreeHGlobal(unmanagedBuffer);
+            }
+            BitConverter.GetBytes(rectToSave.Width).CopyTo(bytes, 0);
+            BitConverter.GetBytes(rectToSave.Height).CopyTo(bytes, 4);
+            using FileStream outputStream = new(path, FileMode.Create);
+            using GZipStream compressedStream = new GZipStream(outputStream, CompressionLevel.Fastest);
+            compressedStream.Write(bytes);
+        }
+
         /// <summary>
         ///     Saves image to PNG file.
         /// </summary>
@@ -93,4 +123,4 @@ namespace PixiEditor.Models.IO
             }
         }
     }
-}
+}

+ 53 - 11
PixiEditor/Models/IO/Importer.cs

@@ -1,12 +1,14 @@
-using System;
-using System.IO;
-using System.Runtime.Serialization;
-using System.Windows.Media.Imaging;
-using PixiEditor.Exceptions;
+using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Parser;
+using SkiaSharp;
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Runtime.InteropServices;
+using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.IO
 {
@@ -19,12 +21,14 @@ namespace PixiEditor.Models.IO
         /// <param name="width">New width of image.</param>
         /// <param name="height">New height of image.</param>
         /// <returns>WriteableBitmap of imported image.</returns>
-        public static WriteableBitmap ImportImage(string path, int width, int height)
+        public static Surface ImportImage(string path, int width, int height)
         {
-            WriteableBitmap wbmp = ImportImage(path);
-            if (wbmp.PixelWidth != width || wbmp.PixelHeight != height)
+            Surface wbmp = ImportSurface(path);
+            if (wbmp.Width != width || wbmp.Height != height)
             {
-                return wbmp.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+                var resized = wbmp.ResizeNearestNeighbor(width, height);
+                wbmp.Dispose();
+                return resized;
             }
 
             return wbmp;
@@ -34,7 +38,17 @@ namespace PixiEditor.Models.IO
         ///     Imports image from path and resizes it to given dimensions.
         /// </summary>
         /// <param name="path">Path of image.</param>
-        public static WriteableBitmap ImportImage(string path)
+        public static Surface ImportSurface(string path)
+        {
+            using var image = SKImage.FromEncodedData(path);
+            if (image == null)
+                throw new CorruptedFileException();
+            Surface surface = new Surface(image.Width, image.Height);
+            surface.SkiaSurface.Canvas.DrawImage(image, new SKPoint(0, 0));
+            return surface;
+        }
+
+        public static WriteableBitmap ImportWriteableBitmap(string path)
         {
             try
             {
@@ -76,5 +90,33 @@ namespace PixiEditor.Models.IO
             path = path.ToLower();
             return path.EndsWith(".pixi") || path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg");
         }
+
+        public static Surface LoadFromGZippedBytes(string path)
+        {
+            using FileStream compressedData = new(path, FileMode.Open);
+            using GZipStream uncompressedData = new(compressedData, CompressionMode.Decompress);
+            using MemoryStream resultBytes = new();
+            uncompressedData.CopyTo(resultBytes);
+
+            byte[] bytes = resultBytes.ToArray();
+            int width = BitConverter.ToInt32(bytes, 0);
+            int height = BitConverter.ToInt32(bytes, 4);
+
+            SKImageInfo info = new SKImageInfo(width, height, SKColorType.RgbaF16);
+            var ptr = Marshal.AllocHGlobal(bytes.Length - 8);
+            try
+            {
+                Marshal.Copy(bytes, 8, ptr, bytes.Length - 8);
+                SKPixmap map = new(info, ptr);
+                SKSurface surface = SKSurface.Create(map);
+                var finalSurface = new Surface(width, height);
+                surface.Draw(finalSurface.SkiaSurface.Canvas, 0, 0, Surface.ReplacingPaint);
+                return finalSurface;
+            }
+            finally
+            {
+                Marshal.FreeHGlobal(ptr);
+            }
+        }
     }
-}
+}

+ 78 - 98
PixiEditor/Models/ImageManipulation/BitmapUtils.cs

@@ -3,90 +3,80 @@ using PixiEditor.Models.Layers;
 using PixiEditor.Models.Layers.Utils;
 using PixiEditor.Models.Position;
 using PixiEditor.Parser;
+using PixiEditor.Parser.Skia;
+using SkiaSharp;
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Runtime.CompilerServices;
 using System.Windows;
-using System.Windows.Media;
 using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.ImageManipulation
 {
     public static class BitmapUtils
     {
-        /// <summary>
-        ///     Converts pixel bytes to WriteableBitmap.
-        /// </summary>
-        /// <param name="currentBitmapWidth">Width of bitmap.</param>
-        /// <param name="currentBitmapHeight">Height of bitmap.</param>
-        /// <param name="byteArray">Bitmap byte array.</param>
-        /// <returns>WriteableBitmap.</returns>
-        public static WriteableBitmap BytesToWriteableBitmap(int currentBitmapWidth, int currentBitmapHeight, byte[] byteArray)
-        {
-            WriteableBitmap bitmap = BitmapFactory.New(currentBitmapWidth, currentBitmapHeight);
-            if (byteArray != null)
-            {
-                bitmap.FromByteArray(byteArray);
-            }
-
-            return bitmap;
-        }
-
-        /// <summary>
-        ///     Converts layers bitmaps into one bitmap.
-        /// </summary>
-        /// <param name="width">Width of final bitmap.</param>
-        /// <param name="height">Height of final bitmap.</param>.
-        /// <param name="layers">Layers to combine.</param>
-        /// <returns>WriteableBitmap of layered bitmaps.</returns>
-        public static WriteableBitmap CombineLayers(int width, int height, IEnumerable<Layer> layers, LayerStructure structure = null)
+        public static Surface CombineLayers(Int32Rect portion, IEnumerable<Layer> layers, LayerStructure structure = null)
         {
-            WriteableBitmap finalBitmap = BitmapFactory.New(width, height);
+            Surface finalSurface = new(portion.Width, portion.Height);
+            using SKPaint paint = new();
 
-            using (finalBitmap.GetBitmapContext())
+            for (int i = 0; i < layers.Count(); i++)
             {
-                for (int i = 0; i < layers.Count(); i++)
+                Layer layer = layers.ElementAt(i);
+                if (structure != null && !LayerStructureUtils.GetFinalLayerIsVisible(layer, structure))
+                    continue;
+                float layerOpacity = structure == null ? layer.Opacity : LayerStructureUtils.GetFinalLayerOpacity(layer, structure);
+                paint.Color = new(255, 255, 255, (byte)(layerOpacity * 255));
+
+                if (layer.OffsetX < 0 || layer.OffsetY < 0 ||
+                    layer.Width + layer.OffsetX > layer.MaxWidth ||
+                    layer.Height + layer.OffsetY > layer.MaxHeight)
                 {
-                    Layer layer = layers.ElementAt(i);
-                    float layerOpacity = structure == null ? layer.Opacity : LayerStructureUtils.GetFinalLayerOpacity(layer, structure);
-
-                    if (layer.OffsetX < 0 || layer.OffsetY < 0 ||
-                        layer.Width + layer.OffsetX > layer.MaxWidth ||
-                        layer.Height + layer.OffsetY > layer.MaxHeight)
-                    {
-                        throw new InvalidOperationException("Layers must not extend beyond canvas borders");
-                    }
-
-                    for (int y = 0; y < layer.Height; y++)
-                    {
-                        for (int x = 0; x < layer.Width; x++)
-                        {
-                            Color previousColor = finalBitmap.GetPixel(x + layer.OffsetX, y + layer.OffsetY);
-                            Color color = layer.GetPixel(x, y);
-
-                            finalBitmap.SetPixel(x + layer.OffsetX, y + layer.OffsetY, BlendColor(previousColor, color, layerOpacity));
-                        }
-                    }
+                    throw new InvalidOperationException("Layers must not extend beyond canvas borders");
                 }
+
+                using SKImage snapshot = layer.LayerBitmap.SkiaSurface.Snapshot();
+                int x = portion.X - layer.OffsetX;
+                int y = portion.Y - layer.OffsetY;
+                finalSurface.SkiaSurface.Canvas.DrawImage(
+                    snapshot,
+                    new SKRect(x, y, portion.Width + x, portion.Height + y),
+                    new SKRect(0, 0, portion.Width, portion.Height),
+                    paint);
             }
 
-            return finalBitmap;
+            return finalSurface;
         }
 
-        public static Color GetColorAtPointCombined(int x, int y, params Layer[] layers)
+        public static Surface[] ExtractSelectedPortions(Layer selLayer, Layer[] extractFrom, bool eraceFromLayers)
         {
-            Color prevColor = Color.FromArgb(0, 0, 0, 0);
+            using var selSnap = selLayer.LayerBitmap.SkiaSurface.Snapshot();
+            Surface[] output = new Surface[extractFrom.Length];
 
-            for (int i = 0; i < layers.Length; i++)
+            int count = 0;
+            foreach (Layer layer in extractFrom)
             {
-                Color color = layers[i].GetPixelWithOffset(x, y);
-                float layerOpacity = layers[i].Opacity;
+                Surface portion = new Surface(selLayer.Width, selLayer.Height);
+                SKRect selLayerRect = new SKRect(0, 0, selLayer.Width, selLayer.Height);
 
-                prevColor = BlendColor(prevColor, color, layerOpacity);
-            }
+                int x = selLayer.OffsetX - layer.OffsetX;
+                int y = selLayer.OffsetY - layer.OffsetY;
+
+                using (var layerSnap = layer.LayerBitmap.SkiaSurface.Snapshot())
+                    portion.SkiaSurface.Canvas.DrawImage(layerSnap, new SKRect(x, y, x + selLayer.Width, y + selLayer.Height), selLayerRect, Surface.ReplacingPaint);
+                portion.SkiaSurface.Canvas.DrawImage(selSnap, 0, 0, Surface.MaskingPaint);
+                output[count] = portion;
+                count++;
 
-            return prevColor;
+                if (eraceFromLayers)
+                {
+                    layer.LayerBitmap.SkiaSurface.Canvas.DrawImage(selSnap, new SKRect(0, 0, selLayer.Width, selLayer.Height),
+                        new SKRect(selLayer.OffsetX - layer.OffsetX, selLayer.OffsetY - layer.OffsetY, selLayer.OffsetX - layer.OffsetX + selLayer.Width, selLayer.OffsetY - layer.OffsetY + selLayer.Height),
+                        Surface.InverseMaskingPaint);
+                    layer.InvokeLayerBitmapChange(new Int32Rect(selLayer.OffsetX, selLayer.OffsetY, selLayer.Width, selLayer.Height));
+                }
+            }
+            return output;
         }
 
         /// <summary>
@@ -126,10 +116,11 @@ namespace PixiEditor.Models.ImageManipulation
 
         public static WriteableBitmap GeneratePreviewBitmap(IEnumerable<SerializableLayer> layers, int width, int height, int maxPreviewWidth, int maxPreviewHeight)
         {
-            var opacityLayers = layers.Where(x => x.IsVisible && x.Opacity > 0.8f);
+            var opacityLayers = layers.Where(x => x.IsVisible && x.Opacity > 0.8f
+            && x.Height > 0 && x.Width > 0);
 
             return GeneratePreviewBitmap(
-                opacityLayers.Select(x => BytesToWriteableBitmap(x.Width, x.Height, x.BitmapBytes)),
+                opacityLayers.Select(x => new Surface(x.ToSKImage())),
                 opacityLayers.Select(x => x.OffsetX),
                 opacityLayers.Select(x => x.OffsetY),
                 width,
@@ -138,62 +129,48 @@ namespace PixiEditor.Models.ImageManipulation
                 maxPreviewHeight);
         }
 
-        public static Dictionary<Guid, Color[]> GetPixelsForSelection(Layer[] layers, Coordinates[] selection)
+        public static Dictionary<Guid, SKColor[]> GetPixelsForSelection(Layer[] layers, Coordinates[] selection)
         {
-            Dictionary<Guid, Color[]> result = new();
+            Dictionary<Guid, SKColor[]> result = new();
 
             foreach (Layer layer in layers)
             {
-                Color[] pixels = new Color[selection.Length];
+                SKColor[] pixels = new SKColor[selection.Length];
 
-                using (layer.LayerBitmap.GetBitmapContext())
+                for (int j = 0; j < pixels.Length; j++)
                 {
-                    for (int j = 0; j < pixels.Length; j++)
+                    Coordinates position = layer.GetRelativePosition(selection[j]);
+                    if (position.X < 0 || position.X > layer.Width - 1 || position.Y < 0 ||
+                        position.Y > layer.Height - 1)
                     {
-                        Coordinates position = layer.GetRelativePosition(selection[j]);
-                        if (position.X < 0 || position.X > layer.Width - 1 || position.Y < 0 ||
-                            position.Y > layer.Height - 1)
-                        {
-                            continue;
-                        }
-
-                        pixels[j] = layer.GetPixel(position.X, position.Y);
+                        continue;
                     }
-                }
 
-                result[layer.LayerGuid] = pixels;
+                    var cl = layer.GetPixel(position.X, position.Y);
+                    pixels[j] = cl;
+                }
+                result[layer.GuidValue] = pixels;
             }
 
             return result;
         }
 
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private static Color BlendColor(Color previousColor, Color color, float opacity)
+        public static SKColor BlendColors(SKColor bottomColor, SKColor topColor)
         {
-            if ((color.A < 255 && color.A > 0) || (opacity < 1f && opacity > 0 && color.A > 0))
-            {
-                byte pixelA = (byte)(color.A * opacity);
-                byte r = (byte)((color.R * pixelA / 255) + (previousColor.R * previousColor.A * (255 - pixelA) / (255 * 255)));
-                byte g = (byte)((color.G * pixelA / 255) + (previousColor.G * previousColor.A * (255 - pixelA) / (255 * 255)));
-                byte b = (byte)((color.B * pixelA / 255) + (previousColor.B * previousColor.A * (255 - pixelA) / (255 * 255)));
-                byte a = (byte)(pixelA + (previousColor.A * (255 - pixelA) / 255));
-                color = Color.FromArgb(a, r, g, b);
-            }
-            else
-            {
-                color = Color.FromArgb(color.A, color.R, color.G, color.B);
-            }
-
-            if (color.A > 0)
+            if ((topColor.Alpha < 255 && topColor.Alpha > 0))
             {
-                return color;
+                byte r = (byte)((topColor.Red * topColor.Alpha / 255) + (bottomColor.Red * bottomColor.Alpha * (255 - topColor.Alpha) / (255 * 255)));
+                byte g = (byte)((topColor.Green * topColor.Alpha / 255) + (bottomColor.Green * bottomColor.Alpha * (255 - topColor.Alpha) / (255 * 255)));
+                byte b = (byte)((topColor.Blue * topColor.Alpha / 255) + (bottomColor.Blue * bottomColor.Alpha * (255 - topColor.Alpha) / (255 * 255)));
+                byte a = (byte)(topColor.Alpha + (bottomColor.Alpha * (255 - topColor.Alpha) / 255));
+                return new SKColor(r, g, b, a);
             }
 
-            return previousColor;
+            return topColor.Alpha == 255 ? topColor : bottomColor;
         }
 
         private static WriteableBitmap GeneratePreviewBitmap(
-            IEnumerable<WriteableBitmap> layerBitmaps,
+            IEnumerable<Surface> layerBitmaps,
             IEnumerable<int> offsetsX,
             IEnumerable<int> offsetsY,
             int width,
@@ -208,6 +185,9 @@ namespace PixiEditor.Models.ImageManipulation
                 throw new ArgumentException("There were not the same amount of bitmaps and offsets", nameof(layerBitmaps));
             }
 
+            using Surface previewSurface = new Surface(maxPreviewWidth, maxPreviewHeight);
+            return previewSurface.ToWriteableBitmap();
+            /*
             WriteableBitmap previewBitmap = BitmapFactory.New(width, height);
 
             var layerBitmapsEnumerator = layerBitmaps.GetEnumerator();
@@ -231,7 +211,7 @@ namespace PixiEditor.Models.ImageManipulation
 
             int newWidth = width >= height ? maxPreviewWidth : (int)Math.Ceiling(width / ((float)height / maxPreviewHeight));
             int newHeight = height > width ? maxPreviewHeight : (int)Math.Ceiling(height / ((float)width / maxPreviewWidth));
-            return previewBitmap.Resize(newWidth, newHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+            return previewBitmap.Redesize(newWidth, newHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);*/
         }
     }
 }

+ 257 - 0
PixiEditor/Models/ImageManipulation/ToolCalculator.cs

@@ -0,0 +1,257 @@
+using PixiEditor.Helpers;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools;
+using PixiEditor.Models.Tools.Tools;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Windows;
+using System.Windows.Input;
+
+namespace PixiEditor.Models.ImageManipulation
+{
+    public static class ToolCalculator
+    {
+        /// <summary>
+        /// This function calculates fill and outputs it into Coordinates list.
+        /// </summary>
+        /// <remarks>All coordinates are calculated without layer offset.
+        /// If you want to take consideration offset, just do it in CalculateBresenhamLine in PerformLinearFill function.</remarks>
+        /// <param name="layer">Layer to calculate fill from.</param>
+        /// <param name="startingCoords">Starting fill coordinates</param>
+        /// <param name="maxWidth">Maximum fill width</param>
+        /// <param name="maxHeight">Maximum fill height</param>
+        /// <param name="newColor">Replacement color to stop on</param>
+        /// <param name="output">List with output coordinates.</param>
+        public static void GetLinearFillAbsolute(Layer layer, Coordinates startingCoords, int maxWidth, int maxHeight, SKColor newColor, List<Coordinates> output)
+        {
+            Queue<FloodFillRange> floodFillQueue = new Queue<FloodFillRange>();
+            SKColor colorToReplace = layer.GetPixelWithOffset(startingCoords.X, startingCoords.Y);
+            if ((colorToReplace.Alpha == 0 && newColor.Alpha == 0) ||
+                colorToReplace == newColor)
+                return;
+
+            int width = maxWidth;
+            int height = maxHeight;
+            if (startingCoords.X < 0 || startingCoords.Y < 0 || startingCoords.X >= width || startingCoords.Y >= height)
+                return;
+            var visited = new bool[width * height];
+
+            Int32Rect dirtyRect = new Int32Rect(startingCoords.X, startingCoords.Y, 1, 1);
+
+            PerformLinearFill(layer, floodFillQueue, startingCoords, width, colorToReplace, ref dirtyRect, visited, output);
+            PerformFloodFIll(layer, floodFillQueue, colorToReplace, ref dirtyRect, width, height, visited, output);
+        }
+
+        public static void GenerateEllipseNonAlloc(Coordinates start, Coordinates end, bool fill,
+            List<Coordinates> output)
+        {
+            DoubleCoords fixedCoordinates = CalculateCoordinatesForShapeRotation(start, end);
+            
+            EllipseGenerator.GenerateEllipseFromRect(fixedCoordinates, output);
+            if (fill)
+            {
+                CalculateFillForEllipse(output);
+            }
+        }
+
+        public static void GenerateRectangleNonAlloc(
+            Coordinates start,
+            Coordinates end, bool fill, int thickness, List<Coordinates> output)
+        {
+            DoubleCoords fixedCoordinates = CalculateCoordinatesForShapeRotation(start, end);
+            CalculateRectanglePoints(fixedCoordinates, output);
+
+            for (int i = 1; i < (int)Math.Floor(thickness / 2f) + 1; i++)
+            {
+                CalculateRectanglePoints(
+                    new DoubleCoords(
+                    new Coordinates(fixedCoordinates.Coords1.X - i, fixedCoordinates.Coords1.Y - i),
+                    new Coordinates(fixedCoordinates.Coords2.X + i, fixedCoordinates.Coords2.Y + i)), output);
+            }
+
+            for (int i = 1; i < (int)Math.Ceiling(thickness / 2f); i++)
+            {
+                CalculateRectanglePoints(
+                    new DoubleCoords(
+                    new Coordinates(fixedCoordinates.Coords1.X + i, fixedCoordinates.Coords1.Y + i),
+                    new Coordinates(fixedCoordinates.Coords2.X - i, fixedCoordinates.Coords2.Y - i)), output);
+            }
+
+            if (fill)
+            {
+                CalculateRectangleFillNonAlloc(start, end, thickness, output);
+            }
+        }
+
+        public static DoubleCoords CalculateCoordinatesForShapeRotation(
+            Coordinates startingCords,
+            Coordinates secondCoordinates)
+        {
+            Coordinates currentCoordinates = secondCoordinates;
+
+            if (startingCords.X > currentCoordinates.X && startingCords.Y > currentCoordinates.Y)
+            {
+                return new DoubleCoords(
+                    new Coordinates(currentCoordinates.X, currentCoordinates.Y),
+                    new Coordinates(startingCords.X, startingCords.Y));
+            }
+
+            if (startingCords.X < currentCoordinates.X && startingCords.Y < currentCoordinates.Y)
+            {
+                return new DoubleCoords(
+                    new Coordinates(startingCords.X, startingCords.Y),
+                    new Coordinates(currentCoordinates.X, currentCoordinates.Y));
+            }
+
+            if (startingCords.Y > currentCoordinates.Y)
+            {
+                return new DoubleCoords(
+                    new Coordinates(startingCords.X, currentCoordinates.Y),
+                    new Coordinates(currentCoordinates.X, startingCords.Y));
+            }
+
+            if (startingCords.X > currentCoordinates.X && startingCords.Y <= currentCoordinates.Y)
+            {
+                return new DoubleCoords(
+                    new Coordinates(currentCoordinates.X, startingCords.Y),
+                    new Coordinates(startingCords.X, currentCoordinates.Y));
+            }
+
+            return new DoubleCoords(startingCords, secondCoordinates);
+        }
+
+        private static void PerformLinearFill(
+            Layer layer, Queue<FloodFillRange> floodFillQueue,
+            Coordinates coords, int width, SKColor colorToReplace, ref Int32Rect dirtyRect, bool[] visited, List<Coordinates> output)
+        {
+            // Find the Left Edge of the Color Area
+            int fillXLeft = coords.X;
+            while (true)
+            {
+                // Indicate that this pixel has been checked
+                int pixelIndex = (coords.Y * width) + fillXLeft;
+                visited[pixelIndex] = true;
+
+                // Move one pixel to the left
+                fillXLeft--;
+                // Exit the loop if we're at edge of the bitmap or the color area
+                if (fillXLeft < 0 || visited[pixelIndex - 1] || layer.GetPixelWithOffset(fillXLeft, coords.Y) != colorToReplace)
+                    break;
+            }
+            int lastCheckedPixelLeft = fillXLeft + 1;
+
+            // Find the Right Edge of the Color Area
+            int fillXRight = coords.X;
+            while (true)
+            {
+                int pixelIndex = (coords.Y * width) + fillXRight;
+                visited[pixelIndex] = true;
+
+                fillXRight++;
+                if (fillXRight >= width || visited[pixelIndex + 1] || layer.GetPixelWithOffset(fillXRight, coords.Y) != colorToReplace)
+                    break;
+            }
+            int lastCheckedPixelRight = fillXRight - 1;
+
+            int relativeY = coords.Y;
+            LineTool.CalculateBresenhamLine(new Coordinates(lastCheckedPixelLeft, relativeY), new Coordinates(lastCheckedPixelRight, relativeY), output);
+            dirtyRect = dirtyRect.Expand(new Int32Rect(lastCheckedPixelLeft, coords.Y, lastCheckedPixelRight - lastCheckedPixelLeft + 1, 1));
+
+            FloodFillRange range = new FloodFillRange(lastCheckedPixelLeft, lastCheckedPixelRight, coords.Y);
+            floodFillQueue.Enqueue(range);
+        }
+
+        private static void PerformFloodFIll(
+            Layer layer, Queue<FloodFillRange> floodFillQueue,
+            SKColor colorToReplace, ref Int32Rect dirtyRect, int width, int height, bool[] pixelsVisited, List<Coordinates> output)
+        {
+            while (floodFillQueue.Count > 0)
+            {
+                FloodFillRange range = floodFillQueue.Dequeue();
+
+                //START THE LOOP UPWARDS AND DOWNWARDS
+                int upY = range.Y - 1; //so we can pass the y coord by ref
+                int downY = range.Y + 1;
+                int downPixelxIndex = (width * (range.Y + 1)) + range.StartX;
+                int upPixelIndex = (width * (range.Y - 1)) + range.StartX;
+                for (int i = range.StartX; i <= range.EndX; i++)
+                {
+                    //START LOOP UPWARDS
+                    //if we're not above the top of the bitmap and the pixel above this one is within the color tolerance
+                    if (range.Y > 0 && (!pixelsVisited[upPixelIndex]) && layer.GetPixelWithOffset(i, upY) == colorToReplace)
+                        PerformLinearFill(layer, floodFillQueue, new Coordinates(i, upY), width, colorToReplace, ref dirtyRect, pixelsVisited, output);
+                    //START LOOP DOWNWARDS
+                    if (range.Y < (height - 1) && (!pixelsVisited[downPixelxIndex]) && layer.GetPixelWithOffset(i, downY) == colorToReplace)
+                        PerformLinearFill(layer, floodFillQueue, new Coordinates(i, downY), width, colorToReplace, ref dirtyRect, pixelsVisited, output);
+                    downPixelxIndex++;
+                    upPixelIndex++;
+                }
+            }
+        }
+
+        private static void CalculateFillForEllipse(List<Coordinates> outlineCoordinates)
+        {
+            if (!outlineCoordinates.Any())
+                return;
+
+            var lines = EllipseGenerator.SplitEllipseIntoLines(outlineCoordinates);
+            foreach (var line in lines)
+            {
+                for (int i = line.Coords1.X; i <= line.Coords2.X; i++)
+                {
+                    outlineCoordinates.Add(new Coordinates(i, line.Coords1.Y));
+                }
+            }
+        }
+
+        private static void CalculateRectangleFillNonAlloc(Coordinates start, Coordinates end, int thickness, List<Coordinates> output)
+        {
+            int offset = (int)Math.Ceiling(thickness / 2f);
+            DoubleCoords fixedCords = CalculateCoordinatesForShapeRotation(start, end);
+
+            DoubleCoords innerCords = new DoubleCoords
+            {
+                Coords1 = new Coordinates(fixedCords.Coords1.X + offset, fixedCords.Coords1.Y + offset),
+                Coords2 = new Coordinates(fixedCords.Coords2.X - (offset - 1), fixedCords.Coords2.Y - (offset - 1))
+            };
+
+            int height = innerCords.Coords2.Y - innerCords.Coords1.Y;
+            int width = innerCords.Coords2.X - innerCords.Coords1.X;
+
+            if (height < 1 || width < 1)
+            {
+                return;
+            }
+
+            int i = 0;
+            for (int y = 0; y < height; y++)
+            {
+                for (int x = 0; x < width; x++)
+                {
+                    output.Add(new Coordinates(innerCords.Coords1.X + x, innerCords.Coords1.Y + y));
+                    i++;
+                }
+            }
+        }
+
+        private static void CalculateRectanglePoints(DoubleCoords coordinates, List<Coordinates> output)
+        {
+            for (int i = coordinates.Coords1.X; i < coordinates.Coords2.X + 1; i++)
+            {
+                output.Add(new Coordinates(i, coordinates.Coords1.Y));
+                output.Add(new Coordinates(i, coordinates.Coords2.Y));
+            }
+
+            for (int i = coordinates.Coords1.Y + 1; i <= coordinates.Coords2.Y - 1; i++)
+            {
+                output.Add(new Coordinates(coordinates.Coords1.X, i));
+                output.Add(new Coordinates(coordinates.Coords2.X, i));
+            }
+        }
+    }
+}

+ 5 - 5
PixiEditor/Models/Layers/BasicLayer.cs

@@ -1,10 +1,10 @@
-using System;
-using PixiEditor.Helpers;
+using PixiEditor.Helpers;
+using System;
 
 namespace PixiEditor.Models.Layers
 {
     [Serializable]
-    public class BasicLayer : NotifyableObject
+    public class BasicLayer : NotifyableObject, IHasGuid
     {
         private int height;
 
@@ -30,6 +30,6 @@ namespace PixiEditor.Models.Layers
             }
         }
 
-        public Guid LayerGuid { get; protected set; }
+        public Guid GuidValue { get; protected set; }
     }
-}
+}

+ 3 - 1
PixiEditor/Models/Layers/GuidStructureItem.cs

@@ -146,7 +146,9 @@ namespace PixiEditor.Models.Layers
             {
                 GroupGuid = GroupGuid,
                 IsExpanded = isExpanded,
-                IsRenaming = isRenaming
+                IsRenaming = isRenaming,
+                IsVisible = isVisible,
+                Opacity = opacity
             };
 
             if(Subgroups.Count > 0)

+ 9 - 0
PixiEditor/Models/Layers/IHasGuid.cs

@@ -0,0 +1,9 @@
+using System;
+
+namespace PixiEditor.Models.Layers
+{
+    public interface IHasGuid
+    {
+        Guid GuidValue { get; }
+    }
+}

+ 226 - 122
PixiEditor/Models/Layers/Layer.cs

@@ -1,28 +1,27 @@
-using PixiEditor.Models.DataHolders;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Undo;
 using PixiEditor.ViewModels;
+using SkiaSharp;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
 using System.Windows;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.Layers
 {
     [DebuggerDisplay("'{name,nq}' {width}x{height}")]
     public class Layer : BasicLayer
     {
-        private const int SizeOfArgb = 4;
         private bool clipRequested;
 
         private bool isActive;
 
         private bool isRenaming;
         private bool isVisible = true;
-        private WriteableBitmap layerBitmap;
+        private Surface layerBitmap;
 
         private string name;
 
@@ -35,31 +34,33 @@ namespace PixiEditor.Models.Layers
         public Layer(string name)
         {
             Name = name;
-            LayerBitmap = BitmapFactory.New(0, 0);
-            Width = 0;
-            Height = 0;
-            LayerGuid = Guid.NewGuid();
+            LayerBitmap = new Surface(1, 1);
+            IsReset = true;
+            Width = 1;
+            Height = 1;
+            GuidValue = Guid.NewGuid();
         }
 
         public Layer(string name, int width, int height)
         {
             Name = name;
-            LayerBitmap = BitmapFactory.New(width, height);
+            LayerBitmap = new Surface(width, height);
+            IsReset = true;
             Width = width;
             Height = height;
-            LayerGuid = Guid.NewGuid();
+            GuidValue = Guid.NewGuid();
         }
 
-        public Layer(string name, WriteableBitmap layerBitmap)
+        public Layer(string name, Surface layerBitmap)
         {
             Name = name;
             LayerBitmap = layerBitmap;
-            Width = layerBitmap.PixelWidth;
-            Height = layerBitmap.PixelHeight;
-            LayerGuid = Guid.NewGuid();
+            Width = layerBitmap.Width;
+            Height = layerBitmap.Height;
+            GuidValue = Guid.NewGuid();
         }
 
-        public Dictionary<Coordinates, Color> LastRelativeCoordinates { get; set; }
+        public Dictionary<Coordinates, SKColor> LastRelativeCoordinates { get; set; }
 
         public string LayerHighlightColor
         {
@@ -96,11 +97,11 @@ namespace PixiEditor.Models.Layers
             get => isVisible;
             set
             {
-                if (SetProperty(ref isVisible, value))
-                {
-                    RaisePropertyChanged(nameof(IsVisibleUndoTriggerable));
-                    ViewModelMain.Current.ToolsSubViewModel.TriggerCacheOutdated();
-                }
+                isVisible = value;
+                RaisePropertyChanged(nameof(IsVisibleUndoTriggerable));
+                RaisePropertyChanged(nameof(IsVisible));
+                ViewModelMain.Current?.ToolsSubViewModel?.TriggerCacheOutdated();
+                InvokeLayerBitmapChange();
             }
         }
 
@@ -118,9 +119,10 @@ namespace PixiEditor.Models.Layers
                             isVisible,
                             value,
                             LayerHelper.FindLayerByGuidProcess,
-                            new object[] { LayerGuid },
+                            new object[] { GuidValue },
                             "Change layer visibility"));
                     IsVisible = value;
+                    InvokeLayerBitmapChange();
                 }
             }
         }
@@ -135,13 +137,18 @@ namespace PixiEditor.Models.Layers
             }
         }
 
-        public WriteableBitmap LayerBitmap
+        public Surface LayerBitmap
         {
             get => layerBitmap;
             set
             {
+                Int32Rect prevRect = new Int32Rect(OffsetX, OffsetY, Width, Height);
                 layerBitmap = value;
+                Width = layerBitmap.Width;
+                Height = layerBitmap.Height;
+                Int32Rect curRect = new Int32Rect(OffsetX, OffsetY, Width, Height);
                 RaisePropertyChanged(nameof(LayerBitmap));
+                InvokeLayerBitmapChange(prevRect.Expand(curRect));
             }
         }
 
@@ -150,11 +157,10 @@ namespace PixiEditor.Models.Layers
             get => opacity;
             set
             {
-                if (SetProperty(ref opacity, value))
-                {
-                    RaisePropertyChanged(nameof(OpacityUndoTriggerable));
-                    ViewModelMain.Current.ToolsSubViewModel.TriggerCacheOutdated();
-                }
+                opacity = value;
+                RaisePropertyChanged(nameof(OpacityUndoTriggerable));
+                ViewModelMain.Current?.ToolsSubViewModel?.TriggerCacheOutdated();
+                InvokeLayerBitmapChange();
             }
         }
 
@@ -172,7 +178,7 @@ namespace PixiEditor.Models.Layers
                                    opacity,
                                    value,
                                    LayerHelper.FindLayerByGuidProcess,
-                                   new object[] { LayerGuid },
+                                   new object[] { GuidValue },
                                    "Change layer opacity"));
                     Opacity = value;
                 }
@@ -188,8 +194,11 @@ namespace PixiEditor.Models.Layers
             get => offset;
             set
             {
+                Int32Rect prevRect = new Int32Rect(OffsetX, OffsetY, Width, Height);
                 offset = value;
-                RaisePropertyChanged("Offset");
+                Int32Rect curRect = new Int32Rect(OffsetX, OffsetY, Width, Height);
+                RaisePropertyChanged(nameof(Offset));
+                InvokeLayerBitmapChange(prevRect.Expand(curRect));
             }
         }
 
@@ -197,6 +206,23 @@ namespace PixiEditor.Models.Layers
 
         public int MaxHeight { get; set; } = int.MaxValue;
 
+        public bool IsReset { get; private set; }
+
+        public event EventHandler<Int32Rect> LayerBitmapChanged;
+
+        public void InvokeLayerBitmapChange()
+        {
+            IsReset = false;
+            LayerBitmapChanged?.Invoke(this, new Int32Rect(OffsetX, OffsetY, Width, Height));
+        }
+
+        public void InvokeLayerBitmapChange(Int32Rect dirtyArea)
+        {
+            IsReset = false;
+            LayerBitmapChanged?.Invoke(this, dirtyArea);
+        }
+
+
         /// <summary>
         /// Changes Guid of layer.
         /// </summary>
@@ -204,7 +230,7 @@ namespace PixiEditor.Models.Layers
         /// <remarks>This is potentially destructive operation, use when absolutelly necessary.</remarks>
         public void ChangeGuid(Guid newGuid)
         {
-            LayerGuid = newGuid;
+            GuidValue = newGuid;
         }
 
         public IEnumerable<Layer> GetLayers()
@@ -217,7 +243,7 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         public Layer Clone(bool generateNewGuid = false)
         {
-            return new Layer(Name, LayerBitmap.Clone())
+            return new Layer(Name, new Surface(LayerBitmap))
             {
                 IsVisible = IsVisible,
                 Offset = Offset,
@@ -226,7 +252,7 @@ namespace PixiEditor.Models.Layers
                 Opacity = Opacity,
                 IsActive = IsActive,
                 IsRenaming = IsRenaming,
-                LayerGuid = generateNewGuid ? Guid.NewGuid() : LayerGuid
+                GuidValue = generateNewGuid ? Guid.NewGuid() : GuidValue
             };
         }
 
@@ -244,7 +270,7 @@ namespace PixiEditor.Models.Layers
         /// <param name="newMaxHeight">New layer maximum height, this should be document height.</param>
         public void Resize(int width, int height, int newMaxWidth, int newMaxHeight)
         {
-            LayerBitmap = LayerBitmap.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+            LayerBitmap = LayerBitmap.ResizeNearestNeighbor(width, height);
             Width = width;
             Height = height;
             MaxWidth = newMaxWidth;
@@ -265,10 +291,10 @@ namespace PixiEditor.Models.Layers
         /// <param name="x">Viewport relative X.</param>
         /// <param name="y">Viewport relative Y.</param>
         /// <returns>Color of a pixel.</returns>
-        public Color GetPixelWithOffset(int x, int y)
+        public SKColor GetPixelWithOffset(int x, int y)
         {
-            Coordinates cords = GetRelativePosition(new Coordinates(x, y));
-            return GetPixel(cords.X, cords.Y);
+            //This does not use GetRelativePosition for better performance
+            return GetPixel(x - OffsetX, y - OffsetY);
         }
 
         /// <summary>
@@ -277,26 +303,24 @@ namespace PixiEditor.Models.Layers
         /// <param name="x">X coordinate.</param>
         /// <param name="y">Y Coordinate.</param>
         /// <returns>Color of pixel, if out of bounds, returns transparent pixel.</returns>
-        public Color GetPixel(int x, int y)
+        public SKColor GetPixel(int x, int y)
         {
             if (x > Width - 1 || x < 0 || y > Height - 1 || y < 0)
             {
-                return Color.FromArgb(0, 0, 0, 0);
+                return SKColors.Empty;
             }
 
-            return LayerBitmap.GetPixel(x, y);
+            return LayerBitmap.GetSRGBPixel(x, y);
         }
 
-        /// <summary>
-        ///     Applies pixel to layer.
-        /// </summary>
-        /// <param name="coordinates">Position of pixel.</param>
-        /// <param name="color">Color of pixel.</param>
-        /// <param name="dynamicResize">Resizes bitmap to fit content.</param>
-        /// <param name="applyOffset">Converts pixels coordinates to relative to bitmap.</param>
-        public void SetPixel(Coordinates coordinates, Color color, bool dynamicResize = true, bool applyOffset = true)
+        public void SetPixelWithOffset(Coordinates coordinates, SKColor color)
+        {
+            LayerBitmap.SetSRGBPixel(coordinates.X - OffsetX, coordinates.Y - OffsetY, color);
+        }
+
+        public void SetPixelWithOffset(int x, int y, SKColor color)
         {
-            SetPixels(BitmapPixelChanges.FromSingleColoredArray(new[] { coordinates }, color), dynamicResize, applyOffset);
+            LayerBitmap.SetSRGBPixel(x - OffsetX, y - OffsetY, color);
         }
 
         /// <summary>
@@ -312,32 +336,40 @@ namespace PixiEditor.Models.Layers
                 return;
             }
 
-            if (dynamicResize)
+            if (applyOffset)
             {
-                DynamicResize(pixels);
+                pixels.ChangedPixels = GetRelativePosition(pixels.ChangedPixels);
             }
 
-            if (applyOffset)
+            if (dynamicResize)
             {
-                pixels.ChangedPixels = GetRelativePosition(pixels.ChangedPixels);
+                DynamicResize(pixels);
             }
 
             LastRelativeCoordinates = pixels.ChangedPixels;
 
-            using (BitmapContext ctx = LayerBitmap.GetBitmapContext())
+            int minX = int.MaxValue;
+            int maxX = int.MinValue;
+            int minY = int.MaxValue;
+            int maxY = int.MinValue;
+
+            foreach (KeyValuePair<Coordinates, SKColor> coords in pixels.ChangedPixels)
             {
-                foreach (KeyValuePair<Coordinates, Color> coords in pixels.ChangedPixels)
+                if (OutOfBounds(coords.Key))
                 {
-                    if (OutOfBounds(coords.Key))
-                    {
-                        continue;
-                    }
-
-                    ctx.WriteableBitmap.SetPixel(coords.Key.X, coords.Key.Y, coords.Value);
+                    continue;
                 }
+
+                LayerBitmap.SetSRGBPixel(coords.Key.X, coords.Key.Y, coords.Value);
+                minX = Math.Min(minX, coords.Key.X);
+                minY = Math.Min(minY, coords.Key.Y);
+                maxX = Math.Max(maxX, coords.Key.X);
+                maxY = Math.Max(maxY, coords.Key.Y);
             }
 
             ClipIfNecessary();
+            if (minX != int.MaxValue)
+                InvokeLayerBitmapChange(new Int32Rect(minX + OffsetX, minY + OffsetY, maxX - minX + 1, maxY - minY + 1));
         }
 
         /// <summary>
@@ -355,6 +387,15 @@ namespace PixiEditor.Models.Layers
             return result;
         }
 
+        public void CreateNewBitmap(int width, int height)
+        {
+            LayerBitmap = new Surface(width, height);
+
+            Width = width;
+            Height = height;
+        }
+
+
         /// <summary>
         ///     Resizes canvas to fit pixels outside current bounds. Clamped to MaxHeight and MaxWidth.
         /// </summary>
@@ -366,24 +407,16 @@ namespace PixiEditor.Models.Layers
             }
 
             ResetOffset(pixels);
-            Tuple<DoubleCords, bool> borderData = ExtractBorderData(pixels);
-            DoubleCords minMaxCords = borderData.Item1;
-            int newMaxX = minMaxCords.Coords2.X - OffsetX;
-            int newMaxY = minMaxCords.Coords2.Y - OffsetY;
-            int newMinX = minMaxCords.Coords1.X - OffsetX;
-            int newMinY = minMaxCords.Coords1.Y - OffsetY;
-
-            if (!(pixels.WasBuiltAsSingleColored && pixels.ChangedPixels.First().Value.A == 0))
+            Tuple<DoubleCoords, bool> borderData = ExtractBorderData(pixels);
+            DoubleCoords minMaxCords = borderData.Item1;
+            int newMaxX = minMaxCords.Coords2.X;
+            int newMaxY = minMaxCords.Coords2.Y;
+            int newMinX = minMaxCords.Coords1.X;
+            int newMinY = minMaxCords.Coords1.Y;
+
+            if (!(pixels.WasBuiltAsSingleColored && pixels.ChangedPixels.First().Value.Alpha == 0))
             {
-                if ((newMaxX + 1 > Width && Width < MaxWidth) || (newMaxY + 1 > Height && Height < MaxHeight))
-                {
-                    IncreaseSizeToBottomAndRight(newMaxX, newMaxY);
-                }
-
-                if ((newMinX < 0 && Width < MaxWidth) || (newMinY < 0 && Height < MaxHeight))
-                {
-                    IncreaseSizeToTopAndLeft(newMinX, newMinY);
-                }
+                DynamicResizeRelative(newMaxX, newMaxY, newMinX, newMinY);
             }
 
             // if clip is requested
@@ -393,12 +426,47 @@ namespace PixiEditor.Models.Layers
             }
         }
 
+        public void DynamicResizeAbsolute(Int32Rect newSize)
+        {
+            newSize = newSize.Intersect(new Int32Rect(0, 0, MaxWidth, MaxHeight));
+            if (newSize.IsEmpty)
+                return;
+            if (IsReset)
+            {
+                Offset = new Thickness(newSize.X, newSize.Y, 0, 0);
+            }
+
+            int relX = newSize.X - OffsetX;
+            int relY = newSize.Y - OffsetY;
+            int maxX = relX + newSize.Width - 1;
+            int maxY = relY + newSize.Height - 1;
+
+            DynamicResizeRelative(maxX, maxY, relX, relY);
+        }
+
         /// <summary>
-        ///     Changes size of bitmap to fit content.
+        ///     Resizes canvas to fit pixels outside current bounds. Clamped to MaxHeight and MaxWidth.
         /// </summary>
-        public void ClipCanvas()
+        public void DynamicResizeRelative(int newMaxX, int newMaxY, int newMinX, int newMinY)
+        {
+            if ((newMaxX + 1 > Width && Width < MaxWidth) || (newMaxY + 1 > Height && Height < MaxHeight))
+            {
+                newMaxX = Math.Max(newMaxX, (int)(Width * 1.5f));
+                newMaxY = Math.Max(newMaxY, (int)(Height * 1.5f));
+                IncreaseSizeToBottomAndRight(newMaxX, newMaxY);
+            }
+
+            if ((newMinX < 0 && Width < MaxWidth) || (newMinY < 0 && Height < MaxHeight))
+            {
+                newMinX = Math.Min(newMinX, Width - (int)(Width * 1.5f));
+                newMinY = Math.Min(newMinY, Height - (int)(Height * 1.5f));
+                IncreaseSizeToTopAndLeft(newMinX, newMinY);
+            }
+        }
+
+        public Int32Rect GetContentDimensions()
         {
-            DoubleCords points = GetEdgePoints();
+            DoubleCoords points = GetEdgePoints();
             int smallestX = points.Coords1.X;
             int smallestY = points.Coords1.Y;
             int biggestX = points.Coords2.X;
@@ -406,22 +474,46 @@ namespace PixiEditor.Models.Layers
 
             if (smallestX < 0 && smallestY < 0 && biggestX < 0 && biggestY < 0)
             {
-                return;
+                return Int32Rect.Empty;
             }
 
             int width = biggestX - smallestX + 1;
             int height = biggestY - smallestY + 1;
-            ResizeCanvas(0, 0, smallestX, smallestY, width, height);
-            Offset = new Thickness(OffsetX + smallestX, OffsetY + smallestY, 0, 0);
+            return new Int32Rect(smallestX, smallestY, width, height);
         }
 
         /// <summary>
-        ///     Clears bitmap.
+        ///     Changes size of bitmap to fit content.
         /// </summary>
-        public void Clear()
+        public void ClipCanvas()
         {
-            LayerBitmap.Clear();
-            ClipCanvas();
+            var dimensions = GetContentDimensions();
+            if (dimensions == Int32Rect.Empty) return;
+
+            ResizeCanvas(0, 0, dimensions.X, dimensions.Y, dimensions.Width, dimensions.Height);
+            Offset = new Thickness(OffsetX + dimensions.X, OffsetY + dimensions.Y, 0, 0);
+        }
+
+        public void Reset()
+        {
+            if (IsReset)
+                return;
+            var dirtyRect = new Int32Rect(OffsetX, OffsetY, Width, Height);
+            LayerBitmap?.Dispose();
+            LayerBitmap = new Surface(1, 1);
+            Width = 1;
+            Height = 1;
+            Offset = new Thickness(0, 0, 0, 0);
+            IsReset = true;
+            LayerBitmapChanged?.Invoke(this, dirtyRect);
+        }
+
+        public void ClearCanvas()
+        {
+            if (IsReset)
+                return;
+            LayerBitmap.SkiaSurface.Canvas.Clear();
+            InvokeLayerBitmapChange();
         }
 
         /// <summary>
@@ -429,20 +521,46 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         public byte[] ConvertBitmapToBytes()
         {
-            LayerBitmap.Lock();
-            byte[] byteArray = LayerBitmap.ToByteArray();
-            LayerBitmap.Unlock();
-            return byteArray;
+            return LayerBitmap.ToByteArray();
+        }
+
+        public SKRectI GetRect() => SKRectI.Create(OffsetX, OffsetY, Width, Height);
+
+        public void CropIntersect(SKRectI rect)
+        {
+            SKRectI layerRect = GetRect();
+            SKRectI intersect = SKRectI.Intersect(layerRect, rect);
+
+            Crop(intersect);
         }
 
-        private Dictionary<Coordinates, Color> GetRelativePosition(Dictionary<Coordinates, Color> changedPixels)
+        public void Crop(SKRectI intersect)
+        {
+            if (intersect == SKRectI.Empty)
+            {
+                return;
+            }
+
+            using var oldSurface = LayerBitmap;
+
+            int offsetX = (int)(Offset.Left - intersect.Left);
+            int offsetY = (int)(Offset.Top - intersect.Top);
+
+            Width = intersect.Width;
+            Height = intersect.Height;
+            LayerBitmap = LayerBitmap.Crop(offsetX, offsetY, Width, Height);
+
+            Offset = new(intersect.Left, intersect.Top, 0, 0);
+        }
+
+        private Dictionary<Coordinates, SKColor> GetRelativePosition(Dictionary<Coordinates, SKColor> changedPixels)
         {
             return changedPixels.ToDictionary(
                 d => new Coordinates(d.Key.X - OffsetX, d.Key.Y - OffsetY),
                 d => d.Value);
         }
 
-        private Tuple<DoubleCords, bool> ExtractBorderData(BitmapPixelChanges pixels)
+        private Tuple<DoubleCoords, bool> ExtractBorderData(BitmapPixelChanges pixels)
         {
             Coordinates firstCords = pixels.ChangedPixels.First().Key;
             int minX = firstCords.X;
@@ -451,7 +569,7 @@ namespace PixiEditor.Models.Layers
             int maxY = minY;
             bool clipRequested = false;
 
-            foreach (KeyValuePair<Coordinates, Color> pixel in pixels.ChangedPixels)
+            foreach (KeyValuePair<Coordinates, SKColor> pixel in pixels.ChangedPixels)
             {
                 if (pixel.Key.X < minX)
                 {
@@ -471,14 +589,14 @@ namespace PixiEditor.Models.Layers
                     maxY = pixel.Key.Y;
                 }
 
-                if (clipRequested == false && IsBorderPixel(pixel.Key) && pixel.Value.A == 0)
+                if (clipRequested == false && IsBorderPixel(pixel.Key) && pixel.Value.Alpha == 0)
                 {
                     clipRequested = true;
                 }
             }
 
-            return new Tuple<DoubleCords, bool>(
-                new DoubleCords(new Coordinates(minX, minY), new Coordinates(maxX, maxY)), clipRequested);
+            return new Tuple<DoubleCoords, bool>(
+                new DoubleCoords(new Coordinates(minX, minY), new Coordinates(maxX, maxY)), clipRequested);
         }
 
         private bool IsBorderPixel(Coordinates cords)
@@ -508,8 +626,8 @@ namespace PixiEditor.Models.Layers
                 return;
             }
 
-            newMaxX = Math.Clamp(Math.Max(newMaxX + 1, Width), 0, MaxWidth - OffsetX);
-            newMaxY = Math.Clamp(Math.Max(newMaxY + 1, Height), 0, MaxHeight - OffsetY);
+            newMaxX = Math.Clamp(Math.Max(newMaxX + 1, Width), 1, MaxWidth - OffsetX);
+            newMaxY = Math.Clamp(Math.Max(newMaxY + 1, Height), 1, MaxHeight - OffsetY);
 
             ResizeCanvas(0, 0, 0, 0, newMaxX, newMaxY);
         }
@@ -534,12 +652,12 @@ namespace PixiEditor.Models.Layers
             ResizeCanvas(offsetX, offsetY, 0, 0, newWidth, newHeight);
         }
 
-        private DoubleCords GetEdgePoints()
+        private DoubleCoords GetEdgePoints()
         {
             Coordinates smallestPixel = CoordinatesCalculator.FindMinEdgeNonTransparentPixel(LayerBitmap);
             Coordinates biggestPixel = CoordinatesCalculator.FindMostEdgeNonTransparentPixel(LayerBitmap);
 
-            return new DoubleCords(smallestPixel, biggestPixel);
+            return new DoubleCoords(smallestPixel, biggestPixel);
         }
 
         private void ResetOffset(BitmapPixelChanges pixels)
@@ -557,26 +675,12 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         private void ResizeCanvas(int offsetX, int offsetY, int offsetXSrc, int offsetYSrc, int newWidth, int newHeight)
         {
-            int iteratorHeight = Height > newHeight ? newHeight : Height;
-            int count = Width > newWidth ? newWidth : Width;
-
-            using (BitmapContext srcContext = LayerBitmap.GetBitmapContext(ReadWriteMode.ReadOnly))
-            {
-                WriteableBitmap result = BitmapFactory.New(newWidth, newHeight);
-                using (BitmapContext destContext = result.GetBitmapContext())
-                {
-                    for (int line = 0; line < iteratorHeight; line++)
-                    {
-                        int srcOff = (((offsetYSrc + line) * Width) + offsetXSrc) * SizeOfArgb;
-                        int dstOff = (((offsetY + line) * newWidth) + offsetX) * SizeOfArgb;
-                        BitmapContext.BlockCopy(srcContext, srcOff, destContext, dstOff, count * SizeOfArgb);
-                    }
-
-                    LayerBitmap = result;
-                    Width = newWidth;
-                    Height = newHeight;
-                }
-            }
+            Surface result = new Surface(newWidth, newHeight);
+            LayerBitmap.SkiaSurface.Draw(result.SkiaSurface.Canvas, offsetX - offsetXSrc, offsetY - offsetYSrc, Surface.ReplacingPaint);
+            LayerBitmap?.Dispose();
+            LayerBitmap = result;
+            Width = newWidth;
+            Height = newHeight;
         }
     }
 }

+ 21 - 16
PixiEditor/Models/Layers/LayerGroup.cs

@@ -1,28 +1,33 @@
-using System;
-using System.Collections;
+using PixiEditor.Helpers;
+using PixiEditor.ViewModels;
+using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
-using PixiEditor.Helpers;
-using PixiEditor.ViewModels;
 
 namespace PixiEditor.Models.Layers
 {
-    public class LayerGroup : NotifyableObject
+    public class LayerGroup : NotifyableObject, IHasGuid
     {
-        public Guid GroupGuid { get; init; }
+        public Guid GuidValue { get; init; }
 
-        public GuidStructureItem StructureData { get; init; }
+        private GuidStructureItem structureData;
+        public GuidStructureItem StructureData
+        {
+            get => structureData;
+            set => SetProperty(ref structureData, value);
+        }
 
-        public ObservableCollection<Layer> Layers { get; set; } = new ObservableCollection<Layer>();
+        private ObservableCollection<Layer> Layers { get; set; } = new ObservableCollection<Layer>();
 
-        public ObservableCollection<LayerGroup> Subfolders { get; set; } = new ObservableCollection<LayerGroup>();
+        private ObservableCollection<LayerGroup> Subfolders { get; set; } = new ObservableCollection<LayerGroup>();
 
-        public IEnumerable Items => BuildItems();
+        private ObservableCollection<IHasGuid> items = null;
+        public ObservableCollection<IHasGuid> Items => items ??= BuildItems();
 
-        private IEnumerable BuildItems()
+        private ObservableCollection<IHasGuid> BuildItems()
         {
-            List<object> obj = new(Layers.Reverse());
+            List<IHasGuid> obj = new(Layers.Reverse());
             foreach (var subfolder in Subfolders)
             {
                 obj.Insert(Math.Clamp(subfolder.DisplayIndex - DisplayIndex, 0, obj.Count), subfolder);
@@ -30,7 +35,7 @@ namespace PixiEditor.Models.Layers
 
             obj.Reverse();
 
-            return obj;
+            return new ObservableCollection<IHasGuid>(obj);
         }
 
         private string name;
@@ -95,7 +100,7 @@ namespace PixiEditor.Models.Layers
 
         private void UpdateIsExpandedInDocument(bool value)
         {
-            var folder = ViewModelMain.Current.BitmapManager.ActiveDocument.LayerStructure.GetGroupByGuid(GroupGuid);
+            var folder = ViewModelMain.Current.BitmapManager.ActiveDocument.LayerStructure.GetGroupByGuid(GuidValue);
             if (folder != null)
             {
                 folder.IsExpanded = value;
@@ -114,10 +119,10 @@ namespace PixiEditor.Models.Layers
             Layers = new ObservableCollection<Layer>(layers);
             Subfolders = new ObservableCollection<LayerGroup>(subfolders);
             Name = name;
-            GroupGuid = guid;
+            GuidValue = guid;
             DisplayIndex = displayIndex;
             TopIndex = topIndex;
             StructureData = structureData;
         }
     }
-}
+}

+ 11 - 19
PixiEditor/Models/Layers/LayerHelper.cs

@@ -1,10 +1,10 @@
-using System;
-using System.Linq;
-using System.Windows;
-using System.Windows.Media.Imaging;
+using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.ImageManipulation;
 using PixiEditor.ViewModels;
+using System;
+using System.Linq;
+using System.Windows;
 
 namespace PixiEditor.Models.Layers
 {
@@ -12,7 +12,7 @@ namespace PixiEditor.Models.Layers
     {
         public static Layer FindLayerByGuid(Document document, Guid guid)
         {
-            return document.Layers.FirstOrDefault(x => x.LayerGuid == guid);
+            return document.Layers.FirstOrDefault(x => x.GuidValue == guid);
         }
 
         public static object FindLayerByGuidProcess(object[] parameters)
@@ -59,24 +59,16 @@ namespace PixiEditor.Models.Layers
 
         public static Layer MergeWith(this Layer thisLayer, Layer otherLayer, string newName, Vector documentsSize)
         {
-            thisLayer.GetCloser(otherLayer, out Layer xCloser, out Layer yCloser, out Layer xOther, out Layer yOther);
-
-            // Calculate the offset to the other layer
-            int offsetX = Math.Abs(xCloser.OffsetX + xCloser.Width - xOther.OffsetX);
-            int offsetY = Math.Abs(yCloser.OffsetY + yCloser.Height - yOther.OffsetY);
+            Int32Rect thisRect = new(thisLayer.OffsetX, thisLayer.OffsetY, thisLayer.Width, thisLayer.Height);
+            Int32Rect otherRect = new(otherLayer.OffsetX, otherLayer.OffsetY, otherLayer.Width, otherLayer.Height);
 
-            // Calculate the needed width and height of the new layer
-            int width = xCloser.Width + offsetX + xOther.Width;
-            int height = yCloser.Height + offsetY + yOther.Height;
+            Int32Rect combined = thisRect.Expand(otherRect);
 
-            // Merge both layers into a bitmap
-            WriteableBitmap mergedBitmap = BitmapUtils.CombineLayers((int)documentsSize.X, (int)documentsSize.Y, new Layer[] { thisLayer, otherLayer });
-            mergedBitmap = mergedBitmap.Crop(xCloser.OffsetX, yCloser.OffsetY, width, height);
+            Surface mergedBitmap = BitmapUtils.CombineLayers(combined, new Layer[] { thisLayer, otherLayer });
 
-            // Create the new layer with the merged bitmap
             Layer mergedLayer = new Layer(newName, mergedBitmap)
             {
-                Offset = new Thickness(xCloser.OffsetX, yCloser.OffsetY, 0, 0)
+                Offset = new Thickness(combined.X, combined.Y, 0, 0)
             };
 
             return mergedLayer;
@@ -87,4 +79,4 @@ namespace PixiEditor.Models.Layers
             return MergeWith(thisLayer, otherLayer, newName, new Vector(documentWidth, documentHeight));
         }
     }
-}
+}

+ 24 - 24
PixiEditor/Models/Layers/LayerStructure.cs

@@ -18,7 +18,7 @@ namespace PixiEditor.Models.Layers
     {
         public event EventHandler<LayerStructureChangedEventArgs> LayerStructureChanged;
 
-        public ObservableCollection<GuidStructureItem> Groups { get; set; }
+        public WpfObservableRangeCollection<GuidStructureItem> Groups { get; set; }
 
         private Document Owner { get; }
 
@@ -38,9 +38,9 @@ namespace PixiEditor.Models.Layers
         /// </summary>
         /// <param name="groups">Groups to clone.</param>
         /// <returns>ObservableCollection with cloned groups.</returns>
-        public static ObservableCollection<GuidStructureItem> CloneGroups(ObservableCollection<GuidStructureItem> groups)
+        public static WpfObservableRangeCollection<GuidStructureItem> CloneGroups(WpfObservableRangeCollection<GuidStructureItem> groups)
         {
-            ObservableCollection<GuidStructureItem> outputGroups = new();
+            WpfObservableRangeCollection<GuidStructureItem> outputGroups = new();
             foreach (var group in groups.ToArray())
             {
                 outputGroups.Add(group.CloneGroup());
@@ -69,7 +69,7 @@ namespace PixiEditor.Models.Layers
             return GetGroupByGuid(groupGuid, Groups);
         }
 
-        public ObservableCollection<GuidStructureItem> CloneGroups()
+        public WpfObservableRangeCollection<GuidStructureItem> CloneGroups()
         {
             return CloneGroups(Groups);
         }
@@ -175,8 +175,8 @@ namespace PixiEditor.Models.Layers
             var group = GetGroupByGuid(groupGuid);
             var parentGroup = group.Parent;
             bool reverseOrder = true;
-            int groupTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == group.EndLayerGuid));
-            int groupBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == group.StartLayerGuid));
+            int groupTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == group.EndLayerGuid));
+            int groupBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == group.StartLayerGuid));
 
             int difference = newIndex - groupTopIndex;
 
@@ -336,7 +336,7 @@ namespace PixiEditor.Models.Layers
             var layerGuids = GetGroupLayerGuids(group);
             foreach (var layerGuid in layerGuids)
             {
-                layers.Add(Owner.Layers.First(x => x.LayerGuid == layerGuid));
+                layers.Add(Owner.Layers.First(x => x.GuidValue == layerGuid));
             }
 
             return layers;
@@ -390,8 +390,8 @@ namespace PixiEditor.Models.Layers
         /// <returns>List of layer guids.</returns>
         private List<Guid> GetGroupLayerGuids(GuidStructureItem group)
         {
-            Layer layerTop = Owner.Layers.FirstOrDefault(x => x.LayerGuid == group.EndLayerGuid);
-            Layer layerBottom = Owner.Layers.FirstOrDefault(x => x.LayerGuid == group.StartLayerGuid);
+            Layer layerTop = Owner.Layers.FirstOrDefault(x => x.GuidValue == group.EndLayerGuid);
+            Layer layerBottom = Owner.Layers.FirstOrDefault(x => x.GuidValue == group.StartLayerGuid);
 
             if (layerTop == null || layerBottom == null)
             {
@@ -412,7 +412,7 @@ namespace PixiEditor.Models.Layers
 
             for (int i = minIndex; i <= maxIndex; i++)
             {
-                layerGuids.Add(Owner.Layers[i].LayerGuid);
+                layerGuids.Add(Owner.Layers[i].GuidValue);
             }
 
             return layerGuids;
@@ -488,10 +488,10 @@ namespace PixiEditor.Models.Layers
                 Guid? oldStart = parentGroup.StartLayerGuid;
                 Guid? oldEnd = parentGroup.EndLayerGuid;
 
-                int layerIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == layerGuid));
+                int layerIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == layerGuid));
 
-                int folderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentGroup.EndLayerGuid));
-                int folderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentGroup.StartLayerGuid));
+                int folderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == parentGroup.EndLayerGuid));
+                int folderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == parentGroup.StartLayerGuid));
 
                 int finalTopIndex = Math.Max(folderTopIndex, layerIndex);
                 int finalBottomIndex = Math.Min(folderBottomIndex, layerIndex);
@@ -535,11 +535,11 @@ namespace PixiEditor.Models.Layers
             {
                 Guid oldStart = parentGroup.StartLayerGuid;
                 Guid oldEnd = parentGroup.EndLayerGuid;
-                int folderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == group.EndLayerGuid));
-                int folderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == group.StartLayerGuid));
+                int folderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == group.EndLayerGuid));
+                int folderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == group.StartLayerGuid));
 
-                int parentFolderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentGroup.EndLayerGuid));
-                int parentFolderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentGroup.StartLayerGuid));
+                int parentFolderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == parentGroup.EndLayerGuid));
+                int parentFolderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == parentGroup.StartLayerGuid));
 
                 int finalTopIndex = Math.Max(folderTopIndex, parentFolderTopIndex);
                 int finalBottomIndex = Math.Min(folderBottomIndex, parentFolderBottomIndex);
@@ -639,8 +639,8 @@ namespace PixiEditor.Models.Layers
 
         private Guid FindBoundLayer(GuidStructureItem parentFolder, Guid layerGuid, bool above)
         {
-            int parentFolderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentFolder.EndLayerGuid));
-            int parentFolderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.LayerGuid == parentFolder.StartLayerGuid));
+            int parentFolderTopIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == parentFolder.EndLayerGuid));
+            int parentFolderBottomIndex = Owner.Layers.IndexOf(Owner.Layers.First(x => x.GuidValue == parentFolder.StartLayerGuid));
 
             return FindBoundLayer(layerGuid, parentFolderTopIndex, parentFolderBottomIndex, above);
         }
@@ -652,7 +652,7 @@ namespace PixiEditor.Models.Layers
             for (int i = 0; i < layers.Count; i++)
             {
                 Guid layerGuid = layerGuids[i];
-                var layer = Owner.Layers.First(x => x.LayerGuid == layerGuid);
+                var layer = Owner.Layers.First(x => x.GuidValue == layerGuid);
                 int layerIndex = Owner.Layers.IndexOf(layer);
                 Owner.Layers.Move(layerIndex, layerIndex + moveBy);
             }
@@ -662,8 +662,8 @@ namespace PixiEditor.Models.Layers
         {
             foreach (var currentGroup in groups)
             {
-                var endLayer = Owner.Layers.First(x => x.LayerGuid == currentGroup.EndLayerGuid);
-                var startLayer = Owner.Layers.First(x => x.LayerGuid == currentGroup.StartLayerGuid);
+                var endLayer = Owner.Layers.First(x => x.GuidValue == currentGroup.EndLayerGuid);
+                var startLayer = Owner.Layers.First(x => x.GuidValue == currentGroup.StartLayerGuid);
 
                 int topIndex = Owner.Layers.IndexOf(endLayer);
                 int bottomIndex = Owner.Layers.IndexOf(startLayer);
@@ -709,7 +709,7 @@ namespace PixiEditor.Models.Layers
             return null;
         }
 
-        public LayerStructure(ObservableCollection<GuidStructureItem> items, Document owner)
+        public LayerStructure(WpfObservableRangeCollection<GuidStructureItem> items, Document owner)
         {
             Groups = items;
             Owner = owner;
@@ -717,7 +717,7 @@ namespace PixiEditor.Models.Layers
 
         public LayerStructure(Document owner)
         {
-            Groups = new ObservableCollection<GuidStructureItem>();
+            Groups = new WpfObservableRangeCollection<GuidStructureItem>();
             Owner = owner;
         }
     }

+ 19 - 21
PixiEditor/Models/Layers/StructuredLayerTree.cs

@@ -1,5 +1,5 @@
 using PixiEditor.Helpers;
-using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.DataHolders;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
@@ -11,16 +11,14 @@ namespace PixiEditor.Models.Layers
     {
         private List<Guid> layersInStructure = new();
 
-        public ObservableCollection<object> RootDirectoryItems { get; } = new ObservableCollection<object>();
+        public WpfObservableRangeCollection<IHasGuid> RootDirectoryItems { get; } = new WpfObservableRangeCollection<IHasGuid>();
 
         private static void Swap(ref int startIndex, ref int endIndex)
         {
-            int tmp = startIndex;
-            startIndex = endIndex;
-            endIndex = tmp;
+            (startIndex, endIndex) = (endIndex, startIndex);
         }
 
-        public StructuredLayerTree(ObservableCollection<Layer> layers, LayerStructure structure)
+        public StructuredLayerTree(WpfObservableRangeCollection<Layer> layers, LayerStructure structure)
         {
             if (layers == null || structure == null)
             {
@@ -50,7 +48,7 @@ namespace PixiEditor.Models.Layers
 
             for (int i = 0; i < layers.Count; i++)
             {
-                if (currentFolder != null && layers[i].LayerGuid == currentFolder.StructureData.EndLayerGuid)
+                if (currentFolder != null && layers[i].GuidValue == currentFolder.StructureData.EndLayerGuid)
                 {
                     if (unfinishedFolders.Count > 0)
                     {
@@ -66,7 +64,7 @@ namespace PixiEditor.Models.Layers
 
                 AssignGroup(parsedFolders, layers, ref currentFolder, ref groupsAtIndex, unfinishedFolders, i);
 
-                if (currentFolder == null && !layersInStructure.Contains(layers[i].LayerGuid))
+                if (currentFolder == null && !layersInStructure.Contains(layers[i].GuidValue))
                 {
                     RootDirectoryItems.Add(layers[i]);
                 }
@@ -79,9 +77,9 @@ namespace PixiEditor.Models.Layers
 
         private void AssignGroup(List<LayerGroup> parsedFolders, ObservableCollection<Layer> layers, ref LayerGroup currentFolder, ref List<LayerGroup> groupsAtIndex, Stack<LayerGroup> unfinishedFolders, int i)
         {
-            if (parsedFolders.Any(x => x.StructureData.StartLayerGuid == layers[i].LayerGuid))
+            if (parsedFolders.Any(x => x.StructureData.StartLayerGuid == layers[i].GuidValue))
             {
-                groupsAtIndex = parsedFolders.Where(x => x.StructureData.StartLayerGuid == layers[i].LayerGuid).ToList();
+                groupsAtIndex = parsedFolders.Where(x => x.StructureData.StartLayerGuid == layers[i].GuidValue).ToList();
                 for (int j = 0; j < groupsAtIndex.Count; j++)
                 {
                     LayerGroup group = groupsAtIndex[j];
@@ -91,10 +89,10 @@ namespace PixiEditor.Models.Layers
                         unfinishedFolders.Push(currentFolder);
                     }
 
-                    groupsAtIndex[j] = parsedFolders.First(x => x.StructureData.StartLayerGuid == layers[i].LayerGuid);
+                    groupsAtIndex[j] = parsedFolders.First(x => x.StructureData.StartLayerGuid == layers[i].GuidValue);
                     groupsAtIndex[j].DisplayIndex = RootDirectoryItems.Count;
                     groupsAtIndex[j].TopIndex = CalculateTopIndex(group.DisplayIndex, group.StructureData, layers);
-                    if (groupsAtIndex[j].StructureData.EndLayerGuid != layers[i].LayerGuid)
+                    if (groupsAtIndex[j].StructureData.EndLayerGuid != layers[i].GuidValue)
                     {
                         currentFolder = groupsAtIndex[j];
                     }
@@ -104,8 +102,8 @@ namespace PixiEditor.Models.Layers
 
         private int CalculateTopIndex(int displayIndex, GuidStructureItem structureData, ObservableCollection<Layer> layers)
         {
-            var endLayer = layers.FirstOrDefault(x => x.LayerGuid == structureData.EndLayerGuid);
-            var bottomLayer = layers.FirstOrDefault(x => x.LayerGuid == structureData.StartLayerGuid);
+            var endLayer = layers.FirstOrDefault(x => x.GuidValue == structureData.EndLayerGuid);
+            var bottomLayer = layers.FirstOrDefault(x => x.GuidValue == structureData.StartLayerGuid);
             int originalTopIndex = 0;
             int originalBottomIndex = 0;
             if (endLayer != null)
@@ -146,18 +144,18 @@ namespace PixiEditor.Models.Layers
 
             foreach (var guid in layersInFolder)
             {
-                var layer = layers.FirstOrDefault(x => x.LayerGuid == guid);
+                var layer = layers.FirstOrDefault(x => x.GuidValue == guid);
                 if (layer != null)
                 {
-                    if (!layersInStructure.Contains(layer.LayerGuid))
+                    if (!layersInStructure.Contains(layer.GuidValue))
                     {
-                        layersInStructure.Add(layer.LayerGuid);
+                        layersInStructure.Add(layer.GuidValue);
                         structureItemLayers.Add(layer);
                     }
                 }
             }
 
-            int displayIndex = layersInFolder.Length > 0 ? layers.IndexOf(layers.First(x => x.LayerGuid == structureItem.StartLayerGuid)) : 0;
+            int displayIndex = layersInFolder.Length > 0 ? layers.IndexOf(layers.First(x => x.GuidValue == structureItem.StartLayerGuid)) : 0;
 
             structureItemLayers.Reverse();
 
@@ -172,8 +170,8 @@ namespace PixiEditor.Models.Layers
 
         private Guid[] GetLayersInGroup(ObservableCollection<Layer> layers, GuidStructureItem structureItem)
         {
-            var startLayer = layers.FirstOrDefault(x => x.LayerGuid == structureItem.StartLayerGuid);
-            var endLayer = layers.FirstOrDefault(x => x.LayerGuid == structureItem.EndLayerGuid);
+            var startLayer = layers.FirstOrDefault(x => x.GuidValue == structureItem.StartLayerGuid);
+            var endLayer = layers.FirstOrDefault(x => x.GuidValue == structureItem.EndLayerGuid);
 
             if (startLayer == null || endLayer == null)
             {
@@ -194,7 +192,7 @@ namespace PixiEditor.Models.Layers
 
             for (int i = 0; i < len; i++)
             {
-                guids[i] = layers[i + startIndex].LayerGuid;
+                guids[i] = layers[i + startIndex].GuidValue;
             }
 
             return guids;

+ 31 - 6
PixiEditor/Models/Layers/Utils/LayerStructureUtils.cs

@@ -1,8 +1,4 @@
 using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 
 namespace PixiEditor.Models.Layers.Utils
 {
@@ -20,7 +16,7 @@ namespace PixiEditor.Models.Layers.Utils
                 return 0f;
             }
 
-            var group = structure.GetGroupByLayer(layer.LayerGuid);
+            var group = structure.GetGroupByLayer(layer.GuidValue);
             GuidStructureItem groupToCheck = group;
             float finalOpacity = layer.Opacity;
 
@@ -32,5 +28,34 @@ namespace PixiEditor.Models.Layers.Utils
 
             return Math.Clamp(finalOpacity, 0f, 1f);
         }
+
+        /// <summary>
+        /// Gets final layer IsVisible taking into consideration group visibility.
+        /// </summary>
+        /// <param name="layer">Layer to check.</param>
+        /// <returns>True if is visible, false if at least parent is not visible or layer itself is invisible.</returns>
+        public static bool GetFinalLayerIsVisible(Layer layer, LayerStructure structure)
+        {
+            if (!layer.IsVisible)
+            {
+                return false;
+            }
+
+            var group = structure.GetGroupByLayer(layer.GuidValue);
+            bool atLeastOneParentIsInvisible = false;
+            GuidStructureItem groupToCheck = group;
+            while (groupToCheck != null)
+            {
+                if (!groupToCheck.IsVisible)
+                {
+                    atLeastOneParentIsInvisible = true;
+                    break;
+                }
+
+                groupToCheck = groupToCheck.Parent;
+            }
+
+            return !atLeastOneParentIsInvisible;
+        }
     }
-}
+}

+ 55 - 7
PixiEditor/Models/Position/Coordinates.cs

@@ -1,9 +1,17 @@
-using System;
+using SkiaSharp;
+using System;
+using System.Diagnostics;
+using System.Windows;
 
 namespace PixiEditor.Models.Position
 {
+    [DebuggerDisplay("({DebuggerDisplay,nq})")]
     public struct Coordinates
     {
+        public static Coordinates Zero => new Coordinates(0, 0);
+
+        private string DebuggerDisplay => ToString();
+
         public Coordinates(int x, int y)
         {
             X = x;
@@ -14,6 +22,31 @@ namespace PixiEditor.Models.Position
 
         public int Y { get; set; }
 
+        public static implicit operator Coordinates((int width, int height) tuple)
+        {
+            return new Coordinates(tuple.width, tuple.height);
+        }
+
+        public static Coordinates operator -(Coordinates coordiantes, Thickness thickness)
+        {
+            return new Coordinates(coordiantes.X - (int)thickness.Left, coordiantes.Y - (int)thickness.Top);
+        }
+
+        public static Coordinates operator -(Coordinates coordiantes, int size)
+        {
+            return new Coordinates(coordiantes.X - size, coordiantes.Y - size);
+        }
+
+        public static Coordinates operator +(Coordinates coordiantes, int size)
+        {
+            return new Coordinates(coordiantes.X + size, coordiantes.Y + size);
+        }
+
+        public static Coordinates operator -(Coordinates coordiantes1, Coordinates coordinates2)
+        {
+            return new Coordinates(coordiantes1.X - coordinates2.X, coordiantes1.Y - coordinates2.Y);
+        }
+
         public static bool operator ==(Coordinates c1, Coordinates c2)
         {
             return c2.X == c1.X && c2.Y == c1.Y;
@@ -29,19 +62,34 @@ namespace PixiEditor.Models.Position
             return $"{X}, {Y}";
         }
 
-        public override bool Equals(object obj)
+        public string ToString(IFormatProvider provider)
+        {
+            return $"{X.ToString(provider)}, {Y.ToString(provider)}";
+        }
+
+        public static explicit operator SKPoint(Coordinates coordinates)
         {
-            if (obj is Coordinates coords)
-            {
-                return this == coords;
-            }
+            return new SKPoint(coordinates.X, coordinates.Y);
+        }
 
-            return false;
+        public static explicit operator SKPointI(Coordinates coordinates)
+        {
+            return new SKPointI(coordinates.X, coordinates.Y);
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is Coordinates coords && this == coords;
         }
 
         public override int GetHashCode()
         {
             return HashCode.Combine(X, Y);
         }
+
+        public SKPoint ToSKPoint()
+        {
+            return new SKPoint(X, Y);
+        }
     }
 }

+ 40 - 34
PixiEditor/Models/Position/CoordinatesCalculator.cs

@@ -1,7 +1,8 @@
-using System;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using SkiaSharp;
+using System;
 using System.Collections.Generic;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
 
 namespace PixiEditor.Models.Position
 {
@@ -12,7 +13,7 @@ namespace PixiEditor.Models.Position
         /// </summary>
         /// <param name="startPosition">Top left position of rectangle.</param>
         /// <param name="thickness">Thickness of rectangle.</param>
-        public static DoubleCords CalculateThicknessCenter(Coordinates startPosition, int thickness)
+        public static DoubleCoords CalculateThicknessCenter(Coordinates startPosition, int thickness)
         {
             int x1, x2, y1, y2;
             if (thickness % 2 == 0)
@@ -30,7 +31,7 @@ namespace PixiEditor.Models.Position
                 y1 = y2 - thickness;
             }
 
-            return new DoubleCords(new Coordinates(x1, y1), new Coordinates(x2 - 1, y2 - 1));
+            return new DoubleCoords(new Coordinates(x1, y1), new Coordinates(x2 - 1, y2 - 1));
         }
 
         public static Coordinates GetCenterPoint(Coordinates startingPoint, Coordinates endPoint)
@@ -63,7 +64,21 @@ namespace PixiEditor.Models.Position
             return coordinates;
         }
 
-        public static IEnumerable<Coordinates> RectangleToCoordinates(DoubleCords coordinates)
+        public static void DrawRectangle(Layer layer, SKColor color, int x1, int y1, int x2, int y2)
+        {
+            //TODO: use some kind of context
+            x2++;
+            y2++;
+            for (int y = y1; y < y1 + (y2 - y1); y++)
+            {
+                for (int x = x1; x < x1 + (x2 - x1); x++)
+                {
+                    layer.SetPixelWithOffset(x, y, color);
+                }
+            }
+        }
+
+        public static IEnumerable<Coordinates> RectangleToCoordinates(DoubleCoords coordinates)
         {
             return RectangleToCoordinates(coordinates.Coords1.X, coordinates.Coords1.Y, coordinates.Coords2.X, coordinates.Coords2.Y);
         }
@@ -71,7 +86,7 @@ namespace PixiEditor.Models.Position
         /// <summary>
         ///     Returns first pixel coordinates in bitmap that is most top left on canvas.
         /// </summary>
-        public static Coordinates FindMinEdgeNonTransparentPixel(WriteableBitmap bitmap)
+        public static Coordinates FindMinEdgeNonTransparentPixel(Surface bitmap)
         {
             return new Coordinates(FindMinXNonTransparent(bitmap), FindMinYNonTransparent(bitmap));
         }
@@ -79,20 +94,18 @@ namespace PixiEditor.Models.Position
         /// <summary>
         ///     Returns last pixel coordinates that is most bottom right.
         /// </summary>
-        public static Coordinates FindMostEdgeNonTransparentPixel(WriteableBitmap bitmap)
+        public static Coordinates FindMostEdgeNonTransparentPixel(Surface bitmap)
         {
             return new Coordinates(FindMaxXNonTransparent(bitmap), FindMaxYNonTransparent(bitmap));
         }
 
-        public static int FindMinYNonTransparent(WriteableBitmap bitmap)
+        public static int FindMinYNonTransparent(Surface bitmap)
         {
-            Color transparent = Color.FromArgb(0, 0, 0, 0);
-            using BitmapContext ctx = bitmap.GetBitmapContext(ReadWriteMode.ReadOnly);
-            for (int y = 0; y < ctx.Height; y++)
+            for (int y = 0; y < bitmap.Height; y++)
             {
-                for (int x = 0; x < ctx.Width; x++)
+                for (int x = 0; x < bitmap.Width; x++)
                 {
-                    if (ctx.WriteableBitmap.GetPixel(x, y) != transparent)
+                    if (bitmap.GetSRGBPixel(x, y).Alpha != 0)
                     {
                         return y;
                     }
@@ -102,15 +115,13 @@ namespace PixiEditor.Models.Position
             return -1;
         }
 
-        public static int FindMinXNonTransparent(WriteableBitmap bitmap)
+        public static int FindMinXNonTransparent(Surface bitmap)
         {
-            Color transparent = Color.FromArgb(0, 0, 0, 0);
-            using BitmapContext ctx = bitmap.GetBitmapContext(ReadWriteMode.ReadOnly);
-            for (int x = 0; x < ctx.Width; x++)
+            for (int x = 0; x < bitmap.Width; x++)
             {
-                for (int y = 0; y < ctx.Height; y++)
+                for (int y = 0; y < bitmap.Height; y++)
                 {
-                    if (bitmap.GetPixel(x, y) != transparent)
+                    if (bitmap.GetSRGBPixel(x, y).Alpha != 0)
                     {
                         return x;
                     }
@@ -120,15 +131,13 @@ namespace PixiEditor.Models.Position
             return -1;
         }
 
-        public static int FindMaxYNonTransparent(WriteableBitmap bitmap)
+        public static int FindMaxYNonTransparent(Surface bitmap)
         {
-            Color transparent = Color.FromArgb(0, 0, 0, 0);
-            using BitmapContext ctx = bitmap.GetBitmapContext(ReadWriteMode.ReadOnly);
-            for (int y = ctx.Height - 1; y >= 0; y--)
+            for (int y = bitmap.Height - 1; y >= 0; y--)
             {
-                for (int x = ctx.Width - 1; x >= 0; x--)
+                for (int x = bitmap.Width - 1; x >= 0; x--)
                 {
-                    if (bitmap.GetPixel(x, y) != transparent)
+                    if (bitmap.GetSRGBPixel(x, y).Alpha != 0)
                     {
                         return y;
                     }
@@ -138,16 +147,13 @@ namespace PixiEditor.Models.Position
             return -1;
         }
 
-        public static int FindMaxXNonTransparent(WriteableBitmap bitmap)
+        public static int FindMaxXNonTransparent(Surface bitmap)
         {
-            Color transparent = Color.FromArgb(0, 0, 0, 0);
-
-            using BitmapContext ctx = bitmap.GetBitmapContext(ReadWriteMode.ReadOnly);
-            for (int x = ctx.Width - 1; x >= 0; x--)
+            for (int x = bitmap.Width - 1; x >= 0; x--)
             {
-                for (int y = ctx.Height - 1; y >= 0; y--)
+                for (int y = bitmap.Height - 1; y >= 0; y--)
                 {
-                    if (bitmap.GetPixel(x, y) != transparent)
+                    if (bitmap.GetSRGBPixel(x, y).Alpha != 0)
                     {
                         return x;
                     }
@@ -157,4 +163,4 @@ namespace PixiEditor.Models.Position
             return -1;
         }
     }
-}
+}

+ 82 - 0
PixiEditor/Models/Position/CropData.cs

@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace PixiEditor.Models.Position
+{
+    [StructLayout(LayoutKind.Explicit)]
+    public unsafe struct CropData
+    {
+        [FieldOffset(0)]
+        private readonly int _width;
+        [FieldOffset(4)]
+        private readonly int _height;
+        [FieldOffset(8)]
+        private readonly int _offsetX;
+        [FieldOffset(12)]
+        private readonly int _offsetY;
+
+        public int Width => _width;
+
+        public int Height => _height;
+
+        public int OffsetX => _offsetX;
+
+        public int OffsetY => _offsetY;
+
+        public CropData(int width, int height, int offsetX, int offsetY)
+        {
+            _width = width;
+            _height = height;
+            _offsetX = offsetX;
+            _offsetY = offsetY;
+        }
+
+        public static CropData FromByteArray(byte[] data)
+        {
+            if (data.Length != sizeof(CropData))
+            {
+                throw new ArgumentOutOfRangeException(nameof(data), $"data must be {sizeof(CropData)} long");
+            }
+
+            fixed (void* ptr = data)
+            {
+                return Marshal.PtrToStructure<CropData>(new IntPtr(ptr));
+            }
+        }
+
+        public static CropData FromStream(Stream stream)
+        {
+            if (stream.Length < sizeof(CropData))
+            {
+                throw new ArgumentOutOfRangeException(nameof(stream), $"The specified stream must be at least {sizeof(CropData)} bytes long");
+            }
+
+            byte[] buffer = new byte[sizeof(CropData)];
+            stream.Read(buffer);
+
+            return FromByteArray(buffer);
+        }
+
+        public byte[] ToByteArray()
+        {
+            IntPtr ptr = Marshal.AllocHGlobal(sizeof(CropData));
+            Marshal.StructureToPtr(this, ptr, true);
+
+            Span<byte> bytes = new Span<byte>(ptr.ToPointer(), sizeof(CropData));
+            byte[] array = bytes.ToArray();
+
+            Marshal.FreeHGlobal(ptr);
+
+            return array;
+        }
+
+        public MemoryStream ToStream()
+        {
+            MemoryStream stream = new();
+            stream.Write(ToByteArray());
+            return stream;
+        }
+
+    }
+}

+ 14 - 14
PixiEditor/Models/Position/DoubleCords.cs → PixiEditor/Models/Position/DoubleCoords.cs

@@ -1,15 +1,15 @@
-namespace PixiEditor.Models.Position
-{
-    public struct DoubleCords
-    {
-        public DoubleCords(Coordinates cords1, Coordinates cords2)
-        {
-            Coords1 = cords1;
-            Coords2 = cords2;
-        }
-
-        public Coordinates Coords1 { get; set; }
-
-        public Coordinates Coords2 { get; set; }
-    }
+namespace PixiEditor.Models.Position
+{
+    public struct DoubleCoords
+    {
+        public DoubleCoords(Coordinates cords1, Coordinates cords2)
+        {
+            Coords1 = cords1;
+            Coords2 = cords2;
+        }
+
+        public Coordinates Coords1 { get; set; }
+
+        public Coordinates Coords2 { get; set; }
+    }
 }

+ 62 - 0
PixiEditor/Models/Services/DocumentProvider.cs

@@ -0,0 +1,62 @@
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Layers;
+using System.Collections.Generic;
+
+namespace PixiEditor.Models.Services
+{
+    /// <summary>
+    /// Provides the active document and it's values like active layer and reference layer
+    /// </summary>
+    public class DocumentProvider
+    {
+        private readonly BitmapManager _bitmapManager;
+
+        public DocumentProvider(BitmapManager bitmapManager)
+        {
+            _bitmapManager = bitmapManager;
+        }
+
+        /// <summary>
+        /// Gets all opened documents
+        /// </summary>
+        public IEnumerable<Document> GetDocuments() => _bitmapManager.Documents;
+
+        /// <summary>
+        /// Gets the active document
+        /// </summary>
+        public Document GetDocument() => _bitmapManager.ActiveDocument;
+
+        /// <summary>
+        /// Get the layers of the opened document
+        /// </summary>
+        public IEnumerable<Layer> GetLayers() => _bitmapManager.ActiveDocument?.Layers;
+
+        /// <summary>
+        /// Gets the active layer
+        /// </summary>
+        public Layer GetLayer() => _bitmapManager.ActiveLayer;
+
+        /// <summary>
+        /// Gets the surface of the active layer
+        /// </summary>
+        public Surface GetSurface() => _bitmapManager.ActiveLayer?.LayerBitmap;
+
+        /// <summary>
+        /// Gets the reference layer of the active document
+        /// </summary>
+        public Layer GetReferenceLayer() => _bitmapManager.ActiveDocument?.ReferenceLayer;
+
+        public Surface GetReferenceSurface() => _bitmapManager.ActiveDocument?.ReferenceLayer?.LayerBitmap;
+
+        /// <summary>
+        /// Gets the renderer for the active document
+        /// </summary>
+        public LayerStackRenderer GetRenderer() => _bitmapManager.ActiveDocument?.Renderer;
+
+        /// <summary>
+        /// Gets the renderer for the reference layer of the active document
+        /// </summary>
+        public SingleLayerRenderer GetReferenceRenderer() => _bitmapManager.ActiveDocument?.ReferenceLayerRenderer;
+    }
+}

+ 55 - 14
PixiEditor/Models/Tools/BitmapOperationTool.cs

@@ -1,10 +1,9 @@
-using System;
-using System.Collections.Generic;
-using System.Windows.Documents;
-using System.Windows.Media;
-using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
+using PixiEditor.Models.Undo;
+using SkiaSharp;
+using System.Collections.Generic;
 
 namespace PixiEditor.Models.Tools
 {
@@ -15,22 +14,64 @@ namespace PixiEditor.Models.Tools
         public bool ClearPreviewLayerOnEachIteration { get; set; } = true;
 
         public bool UseDefaultUndoMethod { get; set; } = true;
-        public virtual bool UsesShift => true;
 
-        private readonly LayerChange[] onlyLayerArr = new LayerChange[] { new LayerChange(BitmapPixelChanges.Empty, Guid.Empty) };
+        public bool UseDocumentRectForUndo { get; set; } = false;
+
+        private StorageBasedChange _change;
 
-        public abstract LayerChange[] Use(Layer layer, List<Coordinates> mouseMove, Color color);
+        public abstract void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color);
 
-        protected LayerChange[] Only(BitmapPixelChanges changes, Layer layer)
+        public override void BeforeUse()
         {
-            onlyLayerArr[0] = new LayerChange(changes, layer);
-            return onlyLayerArr;
+            if (UseDefaultUndoMethod && !RequiresPreviewLayer)
+            {
+                InitializeStorageBasedChange(SKRectI.Empty);
+            }
         }
 
-        protected LayerChange[] Only(BitmapPixelChanges changes, Guid layerGuid)
+        /// <summary>
+        /// Executes undo adding procedure.
+        /// </summary>
+        /// <remarks>When overriding, set UseDefaultUndoMethod to false.</remarks>
+        public override void AfterUse(SKRectI sessionRect)
         {
-            onlyLayerArr[0] = new LayerChange(changes, layerGuid);
-            return onlyLayerArr;
+            if (!UseDefaultUndoMethod)
+                return;
+
+            if (RequiresPreviewLayer)
+            {
+                InitializeStorageBasedChange(sessionRect);
+            }
+
+            var document = ViewModels.ViewModelMain.Current.BitmapManager.ActiveDocument;
+            var args = new object[] { _change.Document };
+            document.UndoManager.AddUndoChange(_change.ToChange(StorageBasedChange.BasicUndoProcess, args));
+            _change = null;
+        }
+
+        private void InitializeStorageBasedChange(SKRectI toolSessionRect)
+        {
+            Document doc = ViewModels.ViewModelMain.Current.BitmapManager.ActiveDocument;
+            //var toolSize = Toolbar.GetSetting<SizeSetting>("ToolSize");
+            //SKRectI finalRect = toolSessionRect;
+            //if (toolSize != null)
+            //{
+            //    int halfSize = (int)Math.Ceiling(toolSize.Value / 2f);
+            //    finalRect.Inflate(halfSize, halfSize);
+            //}
+
+            //if (toolSessionRect.IsEmpty)
+            //{
+            //    finalRect = SKRectI.Create(doc.ActiveLayer.OffsetX, doc.ActiveLayer.OffsetY, doc.ActiveLayer.Width, doc.ActiveLayer.Height);
+            //}
+
+            //Commented, because rect based undo is still a little buggy
+            //if (UseDocumentRectForUndo)
+            //{
+            //    finalRect = SKRectI.Create(0, 0, doc.Width, doc.Height);
+            //}
+
+            _change = new StorageBasedChange(doc, new[] { doc.ActiveLayer });
         }
     }
 }

+ 13 - 0
PixiEditor/Models/Tools/Brushes/Brush.cs

@@ -0,0 +1,13 @@
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+
+namespace PixiEditor.Models.Tools.Brushes
+{
+    public abstract class Brush
+    {
+        public abstract void Draw(Layer layer, int toolSize, Coordinates coordinates, SKPaint paint);
+    }
+}

+ 22 - 0
PixiEditor/Models/Tools/Brushes/CircleBrush.cs

@@ -0,0 +1,22 @@
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Models.Tools.ToolSettings.Settings;
+using SkiaSharp;
+using System;
+
+namespace PixiEditor.Models.Tools.Brushes
+{
+    public class CircleBrush : Brush
+    {
+        public override void Draw(Layer layer, int toolSize, Coordinates coordinates, SKPaint paint)
+        {
+            int halfSize = (int)Math.Ceiling(toolSize / 2f);
+            int modifier = toolSize % 2 != 0 ? 1 : 0;
+            Coordinates topLeft = new Coordinates(coordinates.X - halfSize + modifier, coordinates.Y - halfSize + modifier);
+            Coordinates bottomRight = new Coordinates(coordinates.X + halfSize - 1, coordinates.Y + halfSize - 1);
+
+            CircleTool.DrawEllipseFromCoordinates(layer, topLeft, bottomRight, paint.Color, paint.Color, 1, true);
+        }
+    }
+}

+ 14 - 0
PixiEditor/Models/Tools/Brushes/CircleBrushOverrides/InterestingShapeBrush.cs

@@ -0,0 +1,14 @@
+namespace PixiEditor.Models.Tools.Brushes
+{
+    public class InterestingShapeBrush : MatrixBrush
+    {
+        public static readonly int[,] InterestingShapeMatrix = new int[,]
+        {
+            { 1, 1, 1 },
+            { 0, 1, 0 },
+            { 0, 1, 0 }
+        };
+
+        public override int[,] BrushMatrix => InterestingShapeMatrix;
+    }
+}

+ 25 - 0
PixiEditor/Models/Tools/FloodFillRange.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.Tools
+{
+    /// <summary>
+    /// Represents a linear range to be filled and branched from.
+    /// </summary>
+    public struct FloodFillRange
+    {
+        public int StartX;
+        public int EndX;
+        public int Y;
+
+        public FloodFillRange(int startX, int endX, int y)
+        {
+            StartX = startX;
+            EndX = endX;
+            Y = y;
+        }
+    }
+}

+ 94 - 0
PixiEditor/Models/Tools/MatrixBrush.cs

@@ -0,0 +1,94 @@
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools.Brushes;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+
+namespace PixiEditor.Models.Tools
+{
+    public abstract class MatrixBrush : Brush
+    {
+        public abstract int[,] BrushMatrix { get; }
+
+        public SKPoint[] BakedMatrix => _cachedBakedMatrix;
+
+
+        private SKPoint[] _cachedBakedMatrix;
+        private SKPoint[] _cachedGetAtPointBakedMatrix;
+
+        public MatrixBrush()
+        {
+            InitMatrix();
+        }
+
+        public override void Draw(Layer layer, int toolSize, Coordinates coordinates, SKPaint paint)
+        {
+            layer.LayerBitmap.SkiaSurface.Canvas.DrawPoints(SKPointMode.Points, GetAtPoint(coordinates, layer.OffsetX, layer.OffsetY), paint);
+        }
+
+        //We can easily handle .pixi to brush parsing
+
+        /// <summary>
+        ///     Creates an SKPoint[] array from BrushMatrix. All values grater than 0 will be placed in final array.
+        /// </summary>
+        /// <returns>Points array, ready to be drawn on Skia canvas.</returns>
+        public virtual SKPoint[] BakeMatrix()
+        {
+            List<SKPoint> result = new List<SKPoint>();
+
+            int brushHeight = BrushMatrix.GetLength(0);
+            int brushWidth = BrushMatrix.GetLength(1);
+
+            int centerX = (int)Math.Floor(brushWidth / 2f);
+            int centerY = (int)Math.Floor(brushHeight / 2f);
+
+            for (int i = 0; i < brushHeight; i++)
+            {
+                for (int j = 0; j < brushWidth; j++)
+                {
+                    if (BrushMatrix[i, j] > 0)
+                    {
+                        result.Add(new SKPoint(centerX - j, centerY - i));
+                    }
+                }
+            }
+
+            return result.ToArray();
+        }
+
+        /// <summary>
+        ///     Calculates BrushMatrix for given point.
+        /// </summary>
+        /// <param name="point">Point to calculate BrushMatrix for.</param>
+        /// <returns>SKPoints for given coordinate.</returns>
+        public SKPoint[] GetAtPoint(Coordinates point, int offsetX, int offsetY)
+        {
+            if (_cachedGetAtPointBakedMatrix == null)
+            {
+                InitMatrix();
+            }
+
+            for (int i = 0; i < _cachedGetAtPointBakedMatrix.Length; i++)
+            {
+                _cachedGetAtPointBakedMatrix[i] = new SKPoint(
+                    _cachedBakedMatrix[i].X + point.X - offsetX,
+                    _cachedBakedMatrix[i].Y + point.Y - offsetY);
+
+            }
+
+            return _cachedGetAtPointBakedMatrix;
+        }
+
+        private void InitMatrix()
+        {
+            if (_cachedBakedMatrix == null)
+            {
+                _cachedBakedMatrix = BakeMatrix();
+                _cachedGetAtPointBakedMatrix = new SKPoint[_cachedBakedMatrix.Length];
+                Array.Copy(_cachedBakedMatrix, _cachedGetAtPointBakedMatrix, BakedMatrix.Length);
+            }
+        }
+
+    }
+}

+ 4 - 4
PixiEditor/Models/Tools/ReadonlyTool.cs

@@ -1,10 +1,10 @@
-using System.Collections.Generic;
-using PixiEditor.Models.Position;
+using PixiEditor.Models.Position;
+using System.Collections.Generic;
 
 namespace PixiEditor.Models.Tools
 {
     public abstract class ReadonlyTool : Tool
     {
-        public abstract void Use(List<Coordinates> pixels);
+        public abstract void Use(IReadOnlyList<Coordinates> pixels);
     }
-}
+}

+ 14 - 18
PixiEditor/Models/Tools/ShapeTool.cs

@@ -1,17 +1,15 @@
-using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
+using SkiaSharp;
 using System.Collections.Generic;
-using System.Linq;
 using System.Windows.Input;
-using System.Windows.Media;
 
 namespace PixiEditor.Models.Tools
 {
     public abstract class ShapeTool : BitmapOperationTool
     {
-        public static DoubleCords CalculateCoordinatesForShapeRotation(
+        public static DoubleCoords CalculateCoordinatesForShapeRotation(
             Coordinates startingCords,
             Coordinates secondCoordinates)
         {
@@ -19,33 +17,33 @@ namespace PixiEditor.Models.Tools
 
             if (startingCords.X > currentCoordinates.X && startingCords.Y > currentCoordinates.Y)
             {
-                return new DoubleCords(
+                return new DoubleCoords(
                     new Coordinates(currentCoordinates.X, currentCoordinates.Y),
                     new Coordinates(startingCords.X, startingCords.Y));
             }
 
             if (startingCords.X < currentCoordinates.X && startingCords.Y < currentCoordinates.Y)
             {
-                return new DoubleCords(
+                return new DoubleCoords(
                     new Coordinates(startingCords.X, startingCords.Y),
                     new Coordinates(currentCoordinates.X, currentCoordinates.Y));
             }
 
             if (startingCords.Y > currentCoordinates.Y)
             {
-                return new DoubleCords(
+                return new DoubleCoords(
                     new Coordinates(startingCords.X, currentCoordinates.Y),
                     new Coordinates(currentCoordinates.X, startingCords.Y));
             }
 
             if (startingCords.X > currentCoordinates.X && startingCords.Y <= currentCoordinates.Y)
             {
-                return new DoubleCords(
+                return new DoubleCoords(
                     new Coordinates(currentCoordinates.X, startingCords.Y),
                     new Coordinates(startingCords.X, currentCoordinates.Y));
             }
 
-            return new DoubleCords(startingCords, secondCoordinates);
+            return new DoubleCoords(startingCords, secondCoordinates);
         }
 
         public ShapeTool()
@@ -55,20 +53,18 @@ namespace PixiEditor.Models.Tools
             Toolbar = new BasicShapeToolbar();
         }
 
-        // TODO: Add cache for lines 31, 32 (hopefully it would speed up calculation)
-        public abstract override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color);
-
-        protected static IEnumerable<Coordinates> GetThickShape(IEnumerable<Coordinates> shape, int thickness)
+        public static void ThickenShape(Layer layer, SKColor color, IEnumerable<Coordinates> shape, int thickness)
         {
-            List<Coordinates> output = new List<Coordinates>();
             foreach (Coordinates item in shape)
             {
-                output.AddRange(
-                    CoordinatesCalculator.RectangleToCoordinates(
-                        CoordinatesCalculator.CalculateThicknessCenter(item, thickness)));
+                ThickenShape(layer, color, item, thickness);
             }
+        }
 
-            return output.Distinct();
+        protected static void ThickenShape(Layer layer, SKColor color, Coordinates coords, int thickness)
+        {
+            var dcords = CoordinatesCalculator.CalculateThicknessCenter(coords, thickness);
+            CoordinatesCalculator.DrawRectangle(layer, color, dcords.Coords1.X, dcords.Coords1.Y, dcords.Coords2.X, dcords.Coords2.Y);
         }
     }
 }

+ 46 - 67
PixiEditor/Models/Tools/Tool.cs

@@ -1,27 +1,24 @@
-using System;
-using System.Text;
-using System.Windows.Input;
-using PixiEditor.Helpers;
+using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Controllers;
-using PixiEditor.Models.Position;
-using PixiEditor.Models.Tools.ToolSettings;
-using PixiEditor.Models.Tools.ToolSettings.Toolbars;
-
-namespace PixiEditor.Models.Tools
-{
-    public abstract class Tool : NotifyableObject
-    {
-        private bool isActive;
-        private string actionDisplay = string.Empty;
+using PixiEditor.Models.Tools.ToolSettings;
+using PixiEditor.Models.Tools.ToolSettings.Toolbars;
+using System.Windows.Input;
+using SkiaSharp;
 
+namespace PixiEditor.Models.Tools
+{
+    public abstract class Tool : NotifyableObject
+    {
         public virtual string ToolName => GetType().Name.Replace("Tool", string.Empty);
 
         public virtual string DisplayName => ToolName.AddSpacesBeforeUppercaseLetters();
 
-        public virtual string ImagePath => $"/Images/Tools/{ToolName}Image.png";
+        public virtual string ImagePath => $"/Images/Tools/{ToolName}Image.png";
+
+        public virtual bool HideHighlight { get; }
 
-        public virtual bool HideHighlight { get; }
+        public virtual bool RequiresPreciseMouseData { get; }
 
         public abstract string Tooltip { get; }
 
@@ -34,57 +31,39 @@ namespace PixiEditor.Models.Tools
                 RaisePropertyChanged("ActionDisplay");
             }
         }
-
-        public bool IsActive
-        {
-            get => isActive;
-            set
-            {
-                isActive = value;
-                RaisePropertyChanged("IsActive");
-            }
-        }
-
-        public Cursor Cursor { get; set; } = Cursors.Arrow;
-
-        public Toolbar Toolbar { get; set; } = new EmptyToolbar();
-
-        public bool CanStartOutsideCanvas { get; set; } = false;
-
-        public virtual void OnMouseDown(MouseEventArgs e)
-        {
-        }
 
-        public virtual void OnMouseUp(MouseEventArgs e)
-        {
-        }
-
-        public virtual void OnKeyDown(KeyEventArgs e)
-        {
-        }
-
-        public virtual void OnKeyUp(KeyEventArgs e)
-        {
-        }
-
-        public virtual void OnStart(Coordinates clickPosition)
-        {
-        }
-
-        public virtual void OnRecordingLeftMouseDown(MouseEventArgs e)
-        {
-        }
-
-        public virtual void OnStoppedRecordingMouseUp(MouseEventArgs e)
-        {
-        }
-
-        public virtual void OnMouseMove(MouseEventArgs e)
-        {
-        }
-
-        public virtual void AfterAddedUndo(UndoManager undoManager)
+        public bool IsActive
         {
+            get => isActive;
+            set
+            {
+                isActive = value;
+                RaisePropertyChanged("IsActive");
+            }
         }
-    }
-}
+
+        public Cursor Cursor { get; set; } = Cursors.Arrow;
+
+        public Toolbar Toolbar { get; set; } = new EmptyToolbar();
+
+        public ToolSession Session { get; set; }
+
+        private bool isActive;
+        private string actionDisplay = string.Empty;
+
+
+        public virtual void OnKeyDown(Key key) { }
+
+        public virtual void OnKeyUp(Key key) { }
+
+        public virtual void BeforeUse() { }
+
+        /// <summary>
+        ///     Called when the tool finished executing
+        /// </summary>
+        /// <param name="sessionRect">A rectangle which was created during session</param>
+        public virtual void AfterUse(SKRectI sessionRect) { }
+
+        public virtual void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) { }
+    }
+}

+ 0 - 71
PixiEditor/Models/Tools/ToolBuilder.cs

@@ -1,71 +0,0 @@
-using PixiEditor.Helpers;
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-
-namespace PixiEditor.Models.Tools
-{
-    /// <summary>
-    /// Handles Depdency Injection of tools
-    /// </summary>
-    public class ToolBuilder
-    {
-        private readonly IServiceProvider services;
-
-        private readonly List<Type> toBuild = new List<Type>();
-
-        public ToolBuilder(IServiceProvider services)
-        {
-            this.services = services;
-        }
-
-        /// <summary>
-        /// Constructs a new tool of type <typeparamref name="T"/> and injects all services of <paramref name="services"/>
-        /// </summary>
-        public static T BuildTool<T>(IServiceProvider services)
-            where T : Tool
-            => (T)BuildTool(typeof(T), services);
-
-        /// <summary>
-        /// Constructs a new tool of type <paramref name="type"/> and injects all services of <paramref name="services"/>
-        /// </summary>
-        public static Tool BuildTool(Type type, IServiceProvider services)
-        {
-            Tool tool = (Tool)services.Inject(type);
-
-            return tool;
-        }
-
-        /// <summary>
-        /// Adds a new tool of type <typeparamref name="T"/> to the building chain.
-        /// </summary>
-        public ToolBuilder Add<T>()
-            where T : Tool
-            => Add(typeof(T));
-
-        /// <summary>
-        /// Adds a new tool of type <paramref name="type"/> to the building chain.
-        /// </summary>
-        public ToolBuilder Add(Type type)
-        {
-            toBuild.Add(type);
-
-            return this;
-        }
-
-        /// <summary>
-        /// Builds all added tools.
-        /// </summary>
-        public IEnumerable<Tool> Build()
-        {
-            List<Tool> tools = new List<Tool>();
-
-            foreach (Type type in toBuild)
-            {
-                tools.Add(BuildTool(type, services));
-            }
-
-            return tools;
-        }
-    }
-}

+ 3 - 5
PixiEditor/Models/Tools/ToolSettings/Settings/BoolSetting.cs

@@ -2,17 +2,15 @@
 using System.Windows.Controls;
 using System.Windows.Controls.Primitives;
 using System.Windows.Data;
-
+using System.Windows.Media;
+
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
 {
     public class BoolSetting : Setting<bool>
     {
         public BoolSetting(string name, string label = "")
-            : base(name)
+            : this(name, false, label)
         {
-            Label = label;
-            Value = false;
-            SettingControl = GenerateCheckBox();
         }
 
         public BoolSetting(string name, bool isChecked, string label = "")

+ 7 - 9
PixiEditor/Models/Tools/ToolSettings/Settings/SizeSetting.cs

@@ -1,10 +1,6 @@
-using System.Windows;
-using System.Windows.Controls;
+using PixiEditor.Views;
+using System.Windows;
 using System.Windows.Data;
-using System.Windows.Interactivity;
-using PixiEditor.Helpers;
-using PixiEditor.Helpers.Behaviours;
-using PixiEditor.Views;
 
 namespace PixiEditor.Models.Tools.ToolSettings.Settings
 {
@@ -22,9 +18,11 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
         {
             SizeInput tb = new SizeInput
             {
-                Width = 40,
+                Width = 65,
                 Height = 20,
-                FontSize = 12,
+                VerticalAlignment = VerticalAlignment.Center,
+                MaxSize = 9999,
+                IsEnabled = true
             };
 
             Binding binding = new Binding("Value")
@@ -35,4 +33,4 @@ namespace PixiEditor.Models.Tools.ToolSettings.Settings
             return tb;
         }
     }
-}
+}

+ 56 - 55
PixiEditor/Models/Tools/Tools/BrightnessTool.cs

@@ -1,16 +1,15 @@
-using System;
-using System.Collections.Generic;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Media;
+using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Colors;
-using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools.ToolSettings.Settings;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Windows;
 
 namespace PixiEditor.Models.Tools.Tools
 {
@@ -18,91 +17,93 @@ namespace PixiEditor.Models.Tools.Tools
     {
         private const float CorrectionFactor = 5f; // Initial correction factor
 
+        private readonly string defaultActionDisplay = "Draw on pixels to make them brighter. Hold Ctrl to darken.";
         private readonly List<Coordinates> pixelsVisited = new List<Coordinates>();
+        private List<DoubleCoords> circleCache = new List<DoubleCoords>();
+        private int cachedCircleSize = -1;
 
         public BrightnessTool()
         {
-            ActionDisplay = "Draw on pixel to make it brighter. Hold Ctrl to darken.";
+            ActionDisplay = defaultActionDisplay;
             Toolbar = new BrightnessToolToolbar(CorrectionFactor);
         }
 
-		public override bool UsesShift => false;
-		public override string Tooltip => "Makes pixel brighter or darker pixel (U). Hold Ctrl to make pixel darker.";
+        public override string Tooltip => "Makes pixels brighter or darker (U). Hold Ctrl to make pixels darker.";
 
         public BrightnessMode Mode { get; set; } = BrightnessMode.Default;
 
-        public override void OnRecordingLeftMouseDown(MouseEventArgs e)
-        {
-            pixelsVisited.Clear();
-        }
-
-        public override void OnKeyDown(KeyEventArgs e)
+        public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
         {
-            if (e.Key == Key.LeftCtrl)
-            {
-                ActionDisplay = "Draw on pixel to make it darker. Release Ctrl to brighten.";
-            }
+            if (!ctrlIsDown)
+                ActionDisplay = defaultActionDisplay;
+            else
+                ActionDisplay = "Draw on pixels to make them darker. Release Ctrl to brighten.";
         }
 
-        public override void OnKeyUp(KeyEventArgs e)
+        public override void BeforeUse()
         {
-            if (e.Key == Key.LeftCtrl)
-            {
-                ActionDisplay = "Draw on pixel to make it brighter. Hold Ctrl to darken.";
-            }
+            base.BeforeUse();
+            pixelsVisited.Clear();
         }
 
-        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
+        public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color)
         {
             int toolSize = Toolbar.GetSetting<SizeSetting>("ToolSize").Value;
             float correctionFactor = Toolbar.GetSetting<FloatSetting>("CorrectionFactor").Value;
             Mode = Toolbar.GetEnumSetting<BrightnessMode>("BrightnessMode").Value;
 
-            LayerChange[] layersChanges = new LayerChange[1];
-            if (Keyboard.IsKeyDown(Key.LeftCtrl))
+            if (Session.IsCtrlDown)
             {
-                layersChanges[0] = new LayerChange(ChangeBrightness(layer, coordinates[0], toolSize, -correctionFactor), layer);
+                ChangeBrightness(activeLayer, recordedMouseMovement[^1], toolSize, -correctionFactor);
             }
             else
             {
-                layersChanges[0] = new LayerChange(ChangeBrightness(layer, coordinates[0], toolSize, correctionFactor), layer);
+                ChangeBrightness(activeLayer, recordedMouseMovement[^1], toolSize, correctionFactor);
             }
-
-            return layersChanges;
         }
 
-        public BitmapPixelChanges ChangeBrightness(Layer layer, Coordinates coordinates, int toolSize, float correctionFactor)
+        private void ChangeBrightness(Layer layer, Coordinates coordinates, int toolSize, float correctionFactor)
         {
-            DoubleCords centeredCoords = CoordinatesCalculator.CalculateThicknessCenter(coordinates, toolSize);
-            IEnumerable<Coordinates> rectangleCoordinates = CoordinatesCalculator.RectangleToCoordinates(
-                centeredCoords.Coords1.X,
-                centeredCoords.Coords1.Y,
-                centeredCoords.Coords2.X,
-                centeredCoords.Coords2.Y);
-            BitmapPixelChanges changes = new BitmapPixelChanges(new Dictionary<Coordinates, Color>());
-
-            foreach (Coordinates coordinate in rectangleCoordinates)
+            if (cachedCircleSize != toolSize)
+                UpdateCircleCache(toolSize);
+
+            int radius = (int)Math.Ceiling(toolSize / 2f);
+            Int32Rect dirtyRect = new(coordinates.X - radius, coordinates.Y - radius, radius * 2, radius * 2);
+            layer.DynamicResizeAbsolute(dirtyRect);
+
+            foreach (var pair in circleCache)
             {
-                if (Mode == BrightnessMode.Default)
+                Coordinates left = pair.Coords1;
+                Coordinates right = pair.Coords2;
+                int y = left.Y + coordinates.Y;
+
+                for (int x = left.X + coordinates.X; x <= right.X + coordinates.X; x++)
                 {
-                    if (pixelsVisited.Contains(coordinate))
+                    if (Mode == BrightnessMode.Default)
                     {
-                        continue;
+                        Coordinates here = new(x, y);
+                        if (pixelsVisited.Contains(here))
+                            continue;
+
+                        pixelsVisited.Add(here);
                     }
 
-                    pixelsVisited.Add(coordinate);
+                    SKColor pixel = layer.GetPixelWithOffset(x, y);
+                    SKColor newColor = ExColor.ChangeColorBrightness(
+                        pixel,
+                        correctionFactor);
+                    layer.LayerBitmap.SkiaSurface.Canvas.DrawPoint(x - layer.OffsetX, y - layer.OffsetY, newColor);
                 }
-
-                Color pixel = layer.GetPixelWithOffset(coordinate.X, coordinate.Y);
-                Color newColor = ExColor.ChangeColorBrightness(
-                    Color.FromArgb(pixel.A, pixel.R, pixel.G, pixel.B),
-                    correctionFactor);
-                changes.ChangedPixels.Add(
-                    new Coordinates(coordinate.X, coordinate.Y),
-                    newColor);
             }
+            layer.InvokeLayerBitmapChange(dirtyRect);
+        }
 
-            return changes;
+        private void UpdateCircleCache(int newCircleSize)
+        {
+            cachedCircleSize = newCircleSize;
+            DoubleCoords rect = CoordinatesCalculator.CalculateThicknessCenter(new Coordinates(0, 0), newCircleSize);
+            List<Coordinates> circle = EllipseGenerator.GenerateEllipseFromRect(rect);
+            circleCache = EllipseGenerator.SplitEllipseIntoLines(circle);
         }
     }
-}
+}

+ 71 - 162
PixiEditor/Models/Tools/Tools/CircleTool.cs

@@ -1,215 +1,124 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Windows.Input;
-using System.Windows.Media;
-using PixiEditor.Helpers.Extensions;
-using PixiEditor.Models.DataHolders;
+using PixiEditor.Helpers;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools.ToolSettings.Settings;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media;
 
 namespace PixiEditor.Models.Tools.Tools
 {
     public class CircleTool : ShapeTool
     {
+        private string defaultActionDisplay = "Click and move mouse to draw a circle. Hold Shift to draw an even one.";
+
         public CircleTool()
         {
-            ActionDisplay = "Click and move mouse to draw a circle. Hold Shift to draw an even one.";
+            ActionDisplay = defaultActionDisplay;
         }
 
         public override string Tooltip => "Draws circle on canvas (C). Hold Shift to draw even circle.";
 
-        public override void OnKeyDown(KeyEventArgs e)
+        public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
         {
-            if (e.Key == Key.LeftShift)
-            {
+            if (shiftIsDown)
                 ActionDisplay = "Click and move mouse to draw an even circle.";
-            }
-        }
-
-        public override void OnKeyUp(KeyEventArgs e)
-        {
-            if (e.Key == Key.LeftShift)
-            {
-                ActionDisplay = "Click and move mouse to draw a circle. Hold Shift to draw an even one.";
-            }
+            else
+                ActionDisplay = defaultActionDisplay;
         }
 
-        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
+        public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color)
         {
             int thickness = Toolbar.GetSetting<SizeSetting>("ToolSize").Value;
-            DoubleCords fixedCoordinates = CalculateCoordinatesForShapeRotation(coordinates[^1], coordinates[0]);
-            IEnumerable<Coordinates> outline = CreateEllipse(fixedCoordinates.Coords1, fixedCoordinates.Coords2, thickness);
-            BitmapPixelChanges pixels = BitmapPixelChanges.FromSingleColoredArray(outline, color);
-            if (Toolbar.GetSetting<BoolSetting>("Fill").Value)
-            {
-                Color fillColor = Toolbar.GetSetting<ColorSetting>("FillColor").Value;
-                pixels.ChangedPixels.AddRangeNewOnly(
-                    BitmapPixelChanges.FromSingleColoredArray(CalculateFillForEllipse(outline), fillColor)
-                        .ChangedPixels);
-            }
+            var hasFillColor = Toolbar.GetSetting<BoolSetting>("Fill").Value;
+            Color temp = Toolbar.GetSetting<ColorSetting>("FillColor").Value;
+            SKColor fill = new SKColor(temp.R, temp.G, temp.B, temp.A);
 
-            return new[] { new LayerChange(pixels, layer) };
-        }
-
-        /// <summary>
-        ///     Calculates ellipse points for specified coordinates and thickness.
-        /// </summary>
-        /// <param name="startCoordinates">Top left coordinate of ellipse.</param>
-        /// <param name="endCoordinates">Bottom right coordinate of ellipse.</param>
-        /// <param name="thickness">Thickness of ellipse.</param>
-        /// <param name="filled">Should ellipse be filled.</param>
-        /// <returns>Coordinates for ellipse.</returns>
-        public IEnumerable<Coordinates> CreateEllipse(Coordinates startCoordinates, Coordinates endCoordinates, int thickness, bool filled)
-        {
-            List<Coordinates> output = new List<Coordinates>();
-            IEnumerable<Coordinates> outline = CreateEllipse(startCoordinates, endCoordinates, thickness);
-            output.AddRange(outline);
-            if (filled)
-            {
-                output.AddRange(CalculateFillForEllipse(outline));
-                return output.Distinct();
-            }
+            var (start, end) = Session.IsShiftDown ?
+                CoordinatesHelper.GetSquareCoordiantes(recordedMouseMovement) :
+                (recordedMouseMovement[0], recordedMouseMovement[^1]);
 
-            return output;
+            DrawEllipseFromCoordinates(previewLayer, start, end, color, fill, thickness, hasFillColor);
         }
 
-        /// <summary>
-        ///     Calculates ellipse points for specified coordinates and thickness.
-        /// </summary>
-        /// <param name="startCoordinates">Top left coordinate of ellipse.</param>
-        /// <param name="endCoordinates">Bottom right coordinate of ellipse.</param>
-        /// <param name="thickness">Thickness of ellipse.</param>
-        /// <returns>Coordinates for ellipse.</returns>
-        public IEnumerable<Coordinates> CreateEllipse(Coordinates startCoordinates, Coordinates endCoordinates, int thickness)
+        public static void DrawEllipseFromCoordinates(Layer layer, Coordinates first, Coordinates second,
+            SKColor color, SKColor fillColor, int thickness, bool hasFillColor)
         {
-            double radiusX = (endCoordinates.X - startCoordinates.X) / 2.0;
-            double radiusY = (endCoordinates.Y - startCoordinates.Y) / 2.0;
-            double centerX = (startCoordinates.X + endCoordinates.X + 1) / 2.0;
-            double centerY = (startCoordinates.Y + endCoordinates.Y + 1) / 2.0;
-
-            List<Coordinates> output = new List<Coordinates>();
-            IEnumerable<Coordinates> ellipse = MidpointEllipse(radiusX, radiusY, centerX, centerY);
-            if (thickness == 1)
-            {
-                output.AddRange(ellipse);
-            }
-            else
+            DoubleCoords corners = CalculateCoordinatesForShapeRotation(first, second);
+            corners.Coords2 = new(corners.Coords2.X, corners.Coords2.Y);
+
+            int halfThickness = (int)Math.Ceiling(thickness / 2.0);
+            Int32Rect dirtyRect = new Int32Rect(
+                corners.Coords1.X - halfThickness,
+                corners.Coords1.Y - halfThickness,
+                corners.Coords2.X + halfThickness * 2 - corners.Coords1.X,
+                corners.Coords2.Y + halfThickness * 2 - corners.Coords1.Y);
+            layer.DynamicResizeAbsolute(dirtyRect);
+
+            using (SKPaint paint = new SKPaint())
             {
-                output.AddRange(GetThickShape(ellipse, thickness));
-            }
-
-            return output.Distinct();
-        }
-
-        public IEnumerable<Coordinates> MidpointEllipse(double halfWidth, double halfHeight, double centerX, double centerY)
-        {
-            if (halfWidth < 1 || halfHeight < 1)
-            {
-                return FallbackRectangle(halfWidth, halfHeight, centerX, centerY);
-            }
-
-            // ellipse formula: halfHeight^2 * x^2 + halfWidth^2 * y^2 - halfHeight^2 * halfWidth^2 = 0
-
-            // Make sure we are always at the center of a pixel
-            double currentX = Math.Ceiling(centerX - 0.5) + 0.5;
-            double currentY = centerY + halfHeight;
-
-            List<Coordinates> outputCoordinates = new List<Coordinates>();
-
-            double currentSlope;
-
-            // from PI/2 to middle
-            do
-            {
-                outputCoordinates.AddRange(GetRegionPoints(currentX, centerX, currentY, centerY));
-
-                // calculate next pixel coords
-                currentX++;
-
-                if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX, 2)) +
-                    (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY - 0.5, 2)) -
-                    (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) >= 0)
+                List<Coordinates> outline = EllipseGenerator.GenerateEllipseFromRect(corners);
+                if (hasFillColor)
                 {
-                    currentY--;
+                    DrawEllipseFill(layer, fillColor, outline);
                 }
+                DrawEllipseOutline(layer, color, outline, thickness);
 
-                // calculate how far we've advanced
-                double derivativeX = 2 * Math.Pow(halfHeight, 2) * (currentX - centerX);
-                double derivativeY = 2 * Math.Pow(halfWidth, 2) * (currentY - centerY);
-                currentSlope = -(derivativeX / derivativeY);
+                // An Idea, use Skia DrawOval for bigger sizes.
             }
-            while (currentSlope > -1 && currentY - centerY > 0.5);
 
-            // from middle to 0
-            while (currentY - centerY >= 0)
-            {
-                outputCoordinates.AddRange(GetRegionPoints(currentX, centerX, currentY, centerY));
-
-                currentY--;
-                if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX + 0.5, 2)) +
-                    (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY, 2)) -
-                    (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) < 0)
-                {
-                    currentX++;
-                }
-            }
-
-            return outputCoordinates;
+            layer.InvokeLayerBitmapChange(dirtyRect);
         }
 
-        public IEnumerable<Coordinates> CalculateFillForEllipse(IEnumerable<Coordinates> outlineCoordinates)
+        public static void DrawEllipseFill(Layer layer, SKColor color, List<Coordinates> outlineCoordinates)
         {
-            List<Coordinates> finalCoordinates = new List<Coordinates>();
-
             if (!outlineCoordinates.Any())
-            {
-                return finalCoordinates;
-            }
+                return;
 
             int bottom = outlineCoordinates.Max(x => x.Y);
             int top = outlineCoordinates.Min(x => x.Y);
+
+            using SKPaint fillPaint = new();
+            fillPaint.Color = color;
+            fillPaint.BlendMode = SKBlendMode.Src;
+
             for (int i = top + 1; i < bottom; i++)
             {
                 IEnumerable<Coordinates> rowCords = outlineCoordinates.Where(x => x.Y == i);
                 int right = rowCords.Max(x => x.X);
                 int left = rowCords.Min(x => x.X);
-                for (int j = left + 1; j < right; j++)
-                {
-                    finalCoordinates.Add(new Coordinates(j, i));
-                }
+                layer.LayerBitmap.SkiaSurface.Canvas.DrawLine(left - layer.OffsetX, i - layer.OffsetY, right - layer.OffsetX, i - layer.OffsetY, fillPaint);
             }
-
-            return finalCoordinates;
         }
 
-        private Coordinates[] FallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY)
+        /// <summary>
+        ///     Calculates ellipse points for specified coordinates and thickness.
+        /// </summary>
+        /// <param name="thickness">Thickness of ellipse.</param>
+        public static void DrawEllipseOutline(Layer layer, SKColor color, List<Coordinates> ellipse, int thickness)
         {
-            List<Coordinates> coordinates = new List<Coordinates>();
-            for (double x = centerX - halfWidth; x <= centerX + halfWidth; x++)
+            if (thickness == 1)
             {
-                coordinates.Add(new Coordinates((int)x, (int)(centerY - halfHeight)));
-                coordinates.Add(new Coordinates((int)x, (int)(centerY + halfHeight)));
+                foreach (var coords in ellipse)
+                {
+                    layer.LayerBitmap.SetSRGBPixel(coords.X - layer.OffsetX, coords.Y - layer.OffsetY, color);
+                }
             }
-
-            for (double y = centerY - halfHeight + 1; y <= centerY + halfHeight - 1; y++)
+            else
             {
-                coordinates.Add(new Coordinates((int)(centerX - halfWidth), (int)y));
-                coordinates.Add(new Coordinates((int)(centerX + halfWidth), (int)y));
+                using SKPaint paint = new();
+                paint.Color = color;
+                paint.BlendMode = SKBlendMode.Src;
+                float offsetX = thickness % 2 == 1 ? layer.OffsetX - 0.5f : layer.OffsetX;
+                float offsetY = thickness % 2 == 1 ? layer.OffsetY - 0.5f : layer.OffsetY;
+                foreach (var coords in ellipse)
+                {
+                    layer.LayerBitmap.SkiaSurface.Canvas.DrawCircle(coords.X - offsetX, coords.Y - offsetY, thickness / 2f, paint);
+                }
             }
-
-            return coordinates.ToArray();
-        }
-
-        private Coordinates[] GetRegionPoints(double x, double xc, double y, double yc)
-        {
-            Coordinates[] outputCoordinates = new Coordinates[4];
-            outputCoordinates[0] = new Coordinates((int)Math.Floor(x), (int)Math.Floor(y));
-            outputCoordinates[1] = new Coordinates((int)Math.Floor(-(x - xc) + xc), (int)Math.Floor(y));
-            outputCoordinates[2] = new Coordinates((int)Math.Floor(x), (int)Math.Floor(-(y - yc) + yc));
-            outputCoordinates[3] = new Coordinates((int)Math.Floor(-(x - xc) + xc), (int)Math.Floor(-(y - yc) + yc));
-            return outputCoordinates;
         }
     }
-}
+}

+ 119 - 23
PixiEditor/Models/Tools/Tools/ColorPickerTool.cs

@@ -1,48 +1,144 @@
-using System.Collections.Generic;
-using System.Drawing;
-using System.Windows.Input;
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.ImageManipulation;
+using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
+using PixiEditor.Models.Services;
 using PixiEditor.ViewModels;
-using Color = System.Windows.Media.Color;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
 
 namespace PixiEditor.Models.Tools.Tools
 {
-    public class ColorPickerTool : ReadonlyTool
+    internal class ColorPickerTool : ReadonlyTool
     {
-        public ColorPickerTool()
+        private readonly DocumentProvider _docProvider;
+        private readonly BitmapManager _bitmapManager;
+        private readonly string defaultActionDisplay = "Click to pick colors. Hold Ctrl to hide the canvas. Hold Shift to hide the reference layer";
+
+        public ColorPickerTool(DocumentProvider documentProvider, BitmapManager bitmapManager)
         {
-            ActionDisplay = "Press on pixel to make it the primary color.";
+            ActionDisplay = defaultActionDisplay;
+            _docProvider = documentProvider;
+            _bitmapManager = bitmapManager;
         }
 
         public override bool HideHighlight => true;
 
-        public override string Tooltip => "Swaps primary color with selected on canvas. (O)";
+        public override bool RequiresPreciseMouseData => true;
+
+        public override string Tooltip => "Picks the primary color from the canvas. (O)";
+
+        public override void Use(IReadOnlyList<Coordinates> recordedMouseMovement)
+        {
+            var coords = recordedMouseMovement[^1];
+            var doc = _docProvider.GetDocument();
+            if (coords.X < 0 || coords.Y < 0 || coords.X >= doc.Width || coords.Y >= doc.Height)
+                return;
+
+            ViewModelMain.Current.ColorsSubViewModel.PrimaryColor = GetColorAt(coords.X, coords.Y);
+        }
+
+        public SKColor GetColorAt(int x, int y)
+        {
+            Layer referenceLayer = _docProvider.GetReferenceLayer();
+
+            if (referenceLayer != null && referenceLayer.IsVisible)
+            {
+                double preciseX = _docProvider.GetDocument().MouseXOnCanvas;
+                double preciseY = _docProvider.GetDocument().MouseYOnCanvas;
+
+                if (Session.IsCtrlDown)
+                    return GetReferenceColor(preciseX, preciseY);
+                if (Session.IsShiftDown)
+                    return GetCanvasColor(x, y);
+                return GetCombinedColor(x, y, preciseX, preciseY);
+            }
 
-        public override void OnMouseDown(MouseEventArgs e)
+            return GetCanvasColor(x, y);
+        }
+
+        private SKColor GetCombinedColor(int x, int y, double preciseX, double preciseY)
         {
-            base.OnMouseDown(e);
-            ViewModelMain.Current.ColorsSubViewModel.PrimaryColor = GetColorUnderMouse();
+            SKColor top = GetCanvasColor(x, y);
+            SKColor bottom = GetReferenceColor(preciseX, preciseY);
+            return BitmapUtils.BlendColors(bottom, top);
         }
 
-        public override void Use(List<Coordinates> coordinates)
+        private SKColor GetCanvasColor(int x, int y)
         {
-            ViewModelMain.Current.ColorsSubViewModel.PrimaryColor = GetColorUnderMouse();
+            return _docProvider.GetRenderer()?.FinalSurface.GetSRGBPixel(x, y) ?? SKColors.Transparent;
         }
 
-        public Color GetColorUnderMouse()
+        private SKColor GetReferenceColor(double x, double y)
         {
-            System.Drawing.Color color;
-            using (Bitmap bitmap = new Bitmap(1, 1))
+            Document activeDocument = _docProvider.GetDocument();
+            Layer referenceLayer = _docProvider.GetReferenceLayer();
+            Coordinates refPos = CanvasSpaceToReferenceSpace(
+                    x, y,
+                    activeDocument.Width, activeDocument.Height,
+                    referenceLayer.Width, referenceLayer.Height);
+
+            if (refPos.X >= 0 && refPos.Y >= 0 && refPos.X < referenceLayer.Width && refPos.Y < referenceLayer.Height)
+                return referenceLayer.LayerBitmap.GetSRGBPixel(refPos.X, refPos.Y);
+            return SKColors.Transparent;
+        }
+
+        private Coordinates CanvasSpaceToReferenceSpace(double canvasX, double canvasY, int canvasW, int canvasH, int referenceW, int referenceH)
+        {
+            double canvasRatio = canvasW / (double)canvasH;
+            double referenceRatio = referenceW / (double)referenceH;
+            bool blackBarsAreOnTopAndBottom = referenceRatio > canvasRatio;
+            if (blackBarsAreOnTopAndBottom)
             {
-                using (Graphics graphics = Graphics.FromImage(bitmap))
-                {
-                    graphics.CopyFromScreen(MousePositionConverter.GetCursorPosition(), new Point(0, 0), new Size(1, 1));
-                }
+                double combinedBlackBarsHeight = (1 - canvasRatio / referenceRatio) * canvasH;
+                double refScale = referenceH / ((double)canvasH - combinedBlackBarsHeight);
+
+                int outX = (int)Math.Floor(canvasX * referenceW / canvasW);
+                int outY = (int)Math.Floor((canvasY - combinedBlackBarsHeight / 2) * refScale);
 
-                color = bitmap.GetPixel(0, 0);
+                return new Coordinates(outX, outY);
             }
+            else
+            {
+                double combinedBlackBarsWidth = (1 - referenceRatio / canvasRatio) * canvasW;
+                double refScale = referenceW / ((double)canvasW - combinedBlackBarsWidth);
 
-            return Color.FromArgb(color.A, color.R, color.G, color.B);
+                int outX = (int)Math.Floor((canvasX - combinedBlackBarsWidth / 2) * refScale);
+                int outY = (int)Math.Floor(canvasY * referenceH / canvasH);
+                return new Coordinates(outX, outY);
+            }
+        }
+
+        public override void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown, bool altIsDown)
+        {
+            if (!IsActive)
+            {
+                _bitmapManager.HideReferenceLayer = false;
+                _bitmapManager.OnlyReferenceLayer = false;
+                return;
+            }
+
+            if (ctrlIsDown)
+            {
+                _bitmapManager.HideReferenceLayer = false;
+                _bitmapManager.OnlyReferenceLayer = true;
+                ActionDisplay = "Click to pick colors from the reference layer.";
+            }
+            else if (shiftIsDown)
+            {
+                _bitmapManager.HideReferenceLayer = true;
+                _bitmapManager.OnlyReferenceLayer = false;
+                ActionDisplay = "Click to pick colors from the canvas.";
+                return;
+            }
+            else
+            {
+                _bitmapManager.HideReferenceLayer = false;
+                _bitmapManager.OnlyReferenceLayer = false;
+                ActionDisplay = defaultActionDisplay;
+            }
         }
     }
-}
+}

+ 32 - 36
PixiEditor/Models/Tools/Tools/EraserTool.cs

@@ -1,38 +1,34 @@
-using System.Collections.Generic;
-using System.Windows.Media;
-using PixiEditor.Models.Controllers;
-using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Position;
-using PixiEditor.Models.Tools.ToolSettings.Settings;
-using PixiEditor.Models.Tools.ToolSettings.Toolbars;
-
-namespace PixiEditor.Models.Tools.Tools
-{
-    public class EraserTool : BitmapOperationTool
-    {
-        private readonly PenTool pen;
-
-        public EraserTool(BitmapManager bitmapManager)
-        {
-            ActionDisplay = "Draw to remove color from a pixel.";
-            Toolbar = new BasicToolbar();
-            pen = new PenTool(bitmapManager);
+using PixiEditor.Models.Controllers;
+using PixiEditor.Models.Layers;
+using PixiEditor.Models.Position;
+using PixiEditor.Models.Tools.ToolSettings.Settings;
+using PixiEditor.Models.Tools.ToolSettings.Toolbars;
+using SkiaSharp;
+using System.Collections.Generic;
+
+namespace PixiEditor.Models.Tools.Tools
+{
+    internal class EraserTool : BitmapOperationTool
+    {
+        private readonly PenTool pen;
+
+        public EraserTool(BitmapManager bitmapManager)
+        {
+            ActionDisplay = "Draw to remove color from a pixel.";
+            Toolbar = new BasicToolbar();
+            pen = new PenTool(bitmapManager);
+        }

+        public override string Tooltip => "Erasers color from pixel. (E)";
+
+        public override void Use(Layer activeLayer, Layer previewLayer, IEnumerable<Layer> allLayers, IReadOnlyList<Coordinates> recordedMouseMovement, SKColor color)
+        {
+            Erase(activeLayer, recordedMouseMovement, Toolbar.GetSetting<SizeSetting>("ToolSize").Value);
         }
 
-        public override bool UsesShift => false;
-		public override string Tooltip => "Erasers color from pixel. (E)";
-
-        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
-        {
-            return Erase(layer, coordinates, Toolbar.GetSetting<SizeSetting>("ToolSize").Value);
-        }
-
-        public LayerChange[] Erase(Layer layer, List<Coordinates> coordinates, int toolSize)
-        {
-            Coordinates startingCords = coordinates.Count > 1 ? coordinates[1] : coordinates[0];
-            BitmapPixelChanges pixels = pen.Draw(startingCords, coordinates[0], System.Windows.Media.Colors.Transparent, toolSize);
-            return Only(pixels, layer);
-        }
-    }
-}
+        public void Erase(Layer layer, IReadOnlyList<Coordinates> coordinates, int toolSize)
+        {
+            Coordinates startingCords = coordinates.Count > 1 ? coordinates[^2] : coordinates[0];
+            pen.Draw(layer, startingCords, coordinates[^1], SKColors.Transparent, toolSize, false, null, SKBlendMode.Src);
+        }
+    }
+}

+ 0 - 80
PixiEditor/Models/Tools/Tools/FloodFill.cs

@@ -1,80 +0,0 @@
-using System.Collections.Generic;
-using System.Windows.Media;
-using PixiEditor.Models.Controllers;
-using PixiEditor.Models.DataHolders;
-using PixiEditor.Models.Layers;
-using PixiEditor.Models.Position;
-
-namespace PixiEditor.Models.Tools.Tools
-{
-    public class FloodFill : BitmapOperationTool
-    {
-        private BitmapManager BitmapManager { get; }
-
-        public FloodFill(BitmapManager bitmapManager)
-        {
-            ActionDisplay = "Press on a area to fill it.";
-            BitmapManager = bitmapManager;
-        }
-
-        public override string Tooltip => "Fills area with color. (G)";
-
-        public override LayerChange[] Use(Layer layer, List<Coordinates> coordinates, Color color)
-        {
-            return Only(ForestFire(layer, coordinates[0], color), layer);
-        }
-
-        public BitmapPixelChanges ForestFire(Layer layer, Coordinates startingCoords, Color newColor)
-        {
-            List<Coordinates> changedCoords = new List<Coordinates>();
-
-            int width = BitmapManager.ActiveDocument.Width;
-            int height = BitmapManager.ActiveDocument.Height;
-
-            var visited = new bool[width, height];
-
-            Color colorToReplace = layer.GetPixelWithOffset(startingCoords.X, startingCoords.Y);
-
-            var stack = new Stack<Coordinates>();
-            stack.Push(new Coordinates(startingCoords.X, startingCoords.Y));
-
-            while (stack.Count > 0)
-            {
-                var cords = stack.Pop();
-                var relativeCords = layer.GetRelativePosition(cords);
-
-                if (cords.X < 0 || cords.X > width - 1)
-                {
-                    continue;
-                }
-
-                if (cords.Y < 0 || cords.Y > height - 1)
-                {
-                    continue;
-                }
-
-                if (visited[cords.X, cords.Y])
-                {
-                    continue;
-                }
-
-                if (layer.GetPixel(relativeCords.X, relativeCords.Y) == newColor)
-                {
-                    continue;
-                }
-
-                if (layer.GetPixel(relativeCords.X, relativeCords.Y) == colorToReplace)
-                {
-                    changedCoords.Add(new Coordinates(cords.X, cords.Y));
-                    visited[cords.X, cords.Y] = true;
-                    stack.Push(new Coordinates(cords.X, cords.Y - 1));
-                    stack.Push(new Coordinates(cords.X, cords.Y + 1));
-                    stack.Push(new Coordinates(cords.X - 1, cords.Y));
-                    stack.Push(new Coordinates(cords.X + 1, cords.Y));
-                }
-            }
-
-            return BitmapPixelChanges.FromSingleColoredArray(changedCoords, newColor);
-        }
-    }
-}

部分文件因文件數量過多而無法顯示