소스 검색

Started populating DocumentViewModel

flabbet 3 년 전
부모
커밋
6dc3547e3c
100개의 변경된 파일1872개의 추가작업 그리고 45개의 파일을 삭제
  1. 1 0
      src/PixiEditor/App.xaml.cs
  2. 2 1
      src/PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs
  3. 1 1
      src/PixiEditor/Helpers/DesignCommandHelpers.cs
  4. 30 0
      src/PixiEditor/Helpers/Extensions/BlendModeEx.cs
  5. 1 0
      src/PixiEditor/Helpers/Extensions/ParserHelpers.cs
  6. 2 1
      src/PixiEditor/Helpers/IconEvaluators.cs
  7. 41 0
      src/PixiEditor/Helpers/SurfaceHelpers.cs
  8. 1 0
      src/PixiEditor/Helpers/UI/DocumentsTemplateSelector.cs
  9. 1 0
      src/PixiEditor/Helpers/UI/PanelsStyleSelector.cs
  10. 7 0
      src/PixiEditor/Models/BitmapActions/RefreshViewport_PassthroughAction.cs
  11. 5 0
      src/PixiEditor/Models/BitmapActions/RemoveViewport_PassthroughAction.cs
  12. 1 1
      src/PixiEditor/Models/Commands/Attributes/Commands/BasicAttribute.cs
  13. 1 1
      src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs
  14. 1 1
      src/PixiEditor/Models/Commands/Attributes/Commands/DebugAttribute.cs
  15. 1 1
      src/PixiEditor/Models/Commands/Attributes/Commands/GroupAttribute.cs
  16. 1 1
      src/PixiEditor/Models/Commands/Attributes/Commands/InternalAttribute.cs
  17. 1 1
      src/PixiEditor/Models/Commands/Attributes/Commands/ToolAttribute.cs
  18. 1 1
      src/PixiEditor/Models/Commands/Attributes/Evaluators/CanExecuteAttribute.cs
  19. 1 1
      src/PixiEditor/Models/Commands/Attributes/Evaluators/EvaluatorAttribute.cs
  20. 1 1
      src/PixiEditor/Models/Commands/Attributes/Evaluators/IconAttribute.cs
  21. 1 0
      src/PixiEditor/Models/Commands/CommandCollection.cs
  22. 3 1
      src/PixiEditor/Models/Commands/CommandController.cs
  23. 1 0
      src/PixiEditor/Models/Commands/CommandGroup.cs
  24. 2 1
      src/PixiEditor/Models/Commands/CommandMethods.cs
  25. 1 1
      src/PixiEditor/Models/Commands/Commands/BasicCommand.cs
  26. 1 2
      src/PixiEditor/Models/Commands/Commands/Command.cs
  27. 1 1
      src/PixiEditor/Models/Commands/Commands/ToolCommand.cs
  28. 3 1
      src/PixiEditor/Models/Commands/Evaluators/CanExecuteEvaluator.cs
  29. 1 0
      src/PixiEditor/Models/Commands/Evaluators/Evaluator.cs
  30. 1 0
      src/PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs
  31. 1 0
      src/PixiEditor/Models/Commands/Search/CommandSearchResult.cs
  32. 1 3
      src/PixiEditor/Models/Commands/Templates/ShortcutProvider.cs
  33. 1 1
      src/PixiEditor/Models/Commands/XAML/ShortcutBinding.cs
  34. 1 0
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  35. 1 0
      src/PixiEditor/Models/Controllers/ShortcutController.cs
  36. 2 2
      src/PixiEditor/Models/DataHolders/CrashReport.cs
  37. 1 1
      src/PixiEditor/Models/DataHolders/Document/Document.Commands.cs
  38. 1 1
      src/PixiEditor/Models/DataHolders/Document/Document.Discord.cs
  39. 1 0
      src/PixiEditor/Models/Dialogs/ConfirmationDialog.cs
  40. 1 0
      src/PixiEditor/Models/Dialogs/ExportFileDialog.cs
  41. 2 1
      src/PixiEditor/Models/Dialogs/ImportFileDialog.cs
  42. 1 0
      src/PixiEditor/Models/Dialogs/NewFileDialog.cs
  43. 1 0
      src/PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs
  44. 188 0
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  45. 21 0
      src/PixiEditor/Models/DocumentModels/DocumentHelpers.cs
  46. 8 0
      src/PixiEditor/Models/DocumentModels/DocumentState.cs
  47. 144 0
      src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs
  48. 319 0
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  49. 1 0
      src/PixiEditor/Models/IO/Exporter.cs
  50. 1 0
      src/PixiEditor/Models/IO/Importer.cs
  51. 12 0
      src/PixiEditor/Models/Position/ViewportInfo.cs
  52. 224 0
      src/PixiEditor/Models/Rendering/AffectedChunkGatherer.cs
  53. 3 0
      src/PixiEditor/Models/Rendering/RenderInfos/CanvasPreviewDirty_RenderInfo.cs
  54. 5 0
      src/PixiEditor/Models/Rendering/RenderInfos/DirtyRect_RenderInfo.cs
  55. 5 0
      src/PixiEditor/Models/Rendering/RenderInfos/IRenderInfo.cs
  56. 3 0
      src/PixiEditor/Models/Rendering/RenderInfos/MaskPreviewDirty_RenderInfo.cs
  57. 3 0
      src/PixiEditor/Models/Rendering/RenderInfos/PreviewDirty_RenderInfo.cs
  58. 301 0
      src/PixiEditor/Models/Rendering/WriteableBitmapUpdater.cs
  59. 1 0
      src/PixiEditor/Models/Services/CommandProvider.cs
  60. 1 0
      src/PixiEditor/Models/Tools/ToolSettings/Settings/ColorSetting.cs
  61. 1 0
      src/PixiEditor/Models/Tools/ToolSettings/Settings/FloatSetting.cs
  62. 1 0
      src/PixiEditor/Models/Tools/ToolSettings/Settings/SizeSetting.cs
  63. 1 3
      src/PixiEditor/Models/Tools/ToolSettings/Toolbars/EmptyToolbar.cs
  64. 1 0
      src/PixiEditor/Models/Tools/Tools/BrightnessTool.cs
  65. 1 0
      src/PixiEditor/Models/Tools/Tools/CircleTool.cs
  66. 1 0
      src/PixiEditor/Models/Tools/Tools/ColorPickerTool.cs
  67. 1 0
      src/PixiEditor/Models/Tools/Tools/EraserTool.cs
  68. 1 0
      src/PixiEditor/Models/Tools/Tools/FloodFillTool.cs
  69. 1 0
      src/PixiEditor/Models/Tools/Tools/LineTool.cs
  70. 1 0
      src/PixiEditor/Models/Tools/Tools/MagicWandTool.cs
  71. 1 0
      src/PixiEditor/Models/Tools/Tools/MoveTool.cs
  72. 1 0
      src/PixiEditor/Models/Tools/Tools/MoveViewportTool.cs
  73. 1 0
      src/PixiEditor/Models/Tools/Tools/PenTool.cs
  74. 1 0
      src/PixiEditor/Models/Tools/Tools/RectangleTool.cs
  75. 1 0
      src/PixiEditor/Models/Tools/Tools/SelectTool.cs
  76. 1 0
      src/PixiEditor/Models/Tools/Tools/ZoomTool.cs
  77. 2 2
      src/PixiEditor/NotifyableObject.cs
  78. 1 0
      src/PixiEditor/ViewModels/CrashReportViewModel.cs
  79. 97 0
      src/PixiEditor/ViewModels/Prototype/DocumentTransformViewModel.cs
  80. 11 0
      src/PixiEditor/ViewModels/Prototype/FolderViewModel.cs
  81. 23 0
      src/PixiEditor/ViewModels/Prototype/LayerViewModel.cs
  82. 176 0
      src/PixiEditor/ViewModels/Prototype/StructureMemberViewModel.cs
  83. 7 6
      src/PixiEditor/ViewModels/SettingsWindowViewModel.cs
  84. 2 0
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs
  85. 144 4
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  86. 2 0
      src/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs
  87. 2 0
      src/PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs
  88. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs
  89. 2 1
      src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  90. 2 0
      src/PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs
  91. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  92. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs
  93. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Main/SearchViewModel.cs
  94. 2 0
      src/PixiEditor/ViewModels/SubViewModels/Main/SelectionViewModel.cs
  95. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Main/StylusViewModel.cs
  96. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs
  97. 2 0
      src/PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs
  98. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs
  99. 1 0
      src/PixiEditor/ViewModels/SubViewModels/Main/ViewportViewModel.cs
  100. 2 1
      src/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs

+ 1 - 0
src/PixiEditor/App.xaml.cs

@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
 using System.Windows;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor;

+ 2 - 1
src/PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs

@@ -2,6 +2,7 @@
 using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Interactivity;
+using PixiEditor.Views;
 
 namespace PixiEditor.Helpers.Behaviours;
 
@@ -129,4 +130,4 @@ internal class TextBoxFocusBehavior : Behavior<TextBox>
             e.Handled = true;
         }
     }
-}
+}

+ 1 - 1
src/PixiEditor/Helpers/DesignCommandHelpers.cs

@@ -1,7 +1,7 @@
 using System.Reflection;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands.Exceptions;
-using CommandAttribute = PixiEditor.Models.Commands.Attributes.Command;
+using CommandAttribute = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
 namespace PixiEditor.Helpers;
 

+ 30 - 0
src/PixiEditor/Helpers/Extensions/BlendModeEx.cs

@@ -0,0 +1,30 @@
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditor.Helpers.Extensions;
+internal static class BlendModeEx
+{
+    public static string EnglishName(this BlendMode mode)
+    {
+        return mode switch
+        {
+            BlendMode.Normal => "Normal",
+            BlendMode.Darken => "Darken",
+            BlendMode.Multiply => "Multiply",
+            BlendMode.ColorBurn => "Color Burn",
+            BlendMode.Lighten => "Lighten",
+            BlendMode.Screen => "Screen",
+            BlendMode.ColorDodge => "Color Dodge",
+            BlendMode.LinearDodge => "Linear Dodge (Add)",
+            BlendMode.Overlay => "Overlay",
+            BlendMode.SoftLight => "Soft Light",
+            BlendMode.HardLight => "Hard Light",
+            BlendMode.Difference => "Difference",
+            BlendMode.Exclusion => "Exclusion",
+            BlendMode.Hue => "Hue",
+            BlendMode.Saturation => "Saturation",
+            BlendMode.Luminosity => "Luminosity",
+            BlendMode.Color => "Color",
+            _ => "<no name>",
+        };
+    }
+}

+ 1 - 0
src/PixiEditor/Helpers/Extensions/ParserHelpers.cs

@@ -1,4 +1,5 @@
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Document;
 using PixiEditor.Parser;
 
 namespace PixiEditor.Helpers.Extensions;

+ 2 - 1
src/PixiEditor/Helpers/IconEvaluators.cs

@@ -3,8 +3,9 @@ using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Media;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Commands.Search;
-using Command = PixiEditor.Models.Commands.Command;
+using Command = PixiEditor.Models.Commands.Commands.Command;
 
 namespace PixiEditor.Helpers;
 

+ 41 - 0
src/PixiEditor/Helpers/SurfaceHelpers.cs

@@ -0,0 +1,41 @@
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using ChunkyImageLib;
+using SkiaSharp;
+
+namespace PixiEditor.Helpers;
+
+public static class SurfaceHelpers
+{
+    public static WriteableBitmap ToWriteableBitmap(this Surface surface)
+    {
+        int width = surface.Size.X;
+        int height = surface.Size.Y;
+        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(surface), width * 4, 0);
+        result.AddDirtyRect(dirty);
+        result.Unlock();
+        return result;
+    }
+
+    private static unsafe byte[] ToByteArray(Surface surface, SKColorType colorType = SKColorType.Bgra8888, SKAlphaType alphaType = SKAlphaType.Premul)
+    {
+        int width = surface.Size.X;
+        int height = surface.Size.Y;
+        var imageInfo = new SKImageInfo(width, height, colorType, alphaType, SKColorSpace.CreateSrgb());
+
+        byte[] buffer = new byte[width * height * imageInfo.BytesPerPixel];
+        fixed (void* pointer = buffer)
+        {
+            if (!surface.SkiaSurface.ReadPixels(imageInfo, new IntPtr(pointer), imageInfo.RowBytes, 0, 0))
+            {
+                throw new InvalidOperationException("Could not read surface into buffer");
+            }
+        }
+
+        return buffer;
+    }
+}

+ 1 - 0
src/PixiEditor/Helpers/UI/DocumentsTemplateSelector.cs

@@ -1,6 +1,7 @@
 using System.Windows;
 using System.Windows.Controls;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Document;
 
 namespace PixiEditor.Helpers.UI;
 

+ 1 - 0
src/PixiEditor/Helpers/UI/PanelsStyleSelector.cs

@@ -1,6 +1,7 @@
 using System.Windows;
 using System.Windows.Controls;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Document;
 
 namespace PixiEditor.Helpers.UI;
 

+ 7 - 0
src/PixiEditor/Models/BitmapActions/RefreshViewport_PassthroughAction.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.Models.Position;
+
+namespace PixiEditor.Models.BitmapActions;
+
+internal record class RefreshViewport_PassthroughAction(ViewportInfo Info) : IAction, IChangeInfo;

+ 5 - 0
src/PixiEditor/Models/BitmapActions/RemoveViewport_PassthroughAction.cs

@@ -0,0 +1,5 @@
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+
+namespace PixiEditor.Models.BitmapActions;
+internal record class RemoveViewport_PassthroughAction(Guid GuidValue) : IAction, IChangeInfo;

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/BasicAttribute.cs

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.Commands.Attributes;
+namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 internal partial class Command
 {

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -1,7 +1,7 @@
 using System.Windows.Input;
 using PixiEditor.Models.DataHolders;
 
-namespace PixiEditor.Models.Commands.Attributes;
+namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 internal partial class Command
 {

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/DebugAttribute.cs

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.Commands.Attributes;
+namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 internal partial class Command
 {

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/GroupAttribute.cs

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.Commands.Attributes;
+namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 internal partial class Command
 {

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/InternalAttribute.cs

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.Commands.Attributes;
+namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 internal partial class Command
 {

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/ToolAttribute.cs

@@ -1,6 +1,6 @@
 using System.Windows.Input;
 
-namespace PixiEditor.Models.Commands.Attributes;
+namespace PixiEditor.Models.Commands.Attributes.Commands;
 
 internal partial class Command
 {

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Evaluators/CanExecuteAttribute.cs

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.Commands.Attributes;
+namespace PixiEditor.Models.Commands.Attributes.Evaluators;
 
 internal partial class Evaluator
 {

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Evaluators/EvaluatorAttribute.cs

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.Commands.Attributes;
+namespace PixiEditor.Models.Commands.Attributes.Evaluators;
 
 internal static partial class Evaluator
 {

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Evaluators/IconAttribute.cs

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.Commands.Attributes;
+namespace PixiEditor.Models.Commands.Attributes.Evaluators;
 
 internal partial class Evaluator
 {

+ 1 - 0
src/PixiEditor/Models/Commands/CommandCollection.cs

@@ -1,6 +1,7 @@
 using System.Collections;
 using System.Diagnostics;
 using System.Windows.Input;
+using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.DataHolders;
 
 namespace PixiEditor.Models.Commands;

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

@@ -3,10 +3,12 @@ using System.Reflection;
 using System.Windows.Media;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Evaluators;
+using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Tools;
-using CommandAttribute = PixiEditor.Models.Commands.Attributes.Command;
+using CommandAttribute = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
 namespace PixiEditor.Models.Commands;
 

+ 1 - 0
src/PixiEditor/Models/Commands/CommandGroup.cs

@@ -1,5 +1,6 @@
 using System.Collections;
 using System.Windows.Input;
+using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.DataHolders;
 
 namespace PixiEditor.Models.Commands;

+ 2 - 1
src/PixiEditor/Models/Commands/CommandMethods.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Models.Commands.Evaluators;
+using PixiEditor.Models.Commands.Commands;
+using PixiEditor.Models.Commands.Evaluators;
 
 namespace PixiEditor.Models.Commands;
 

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

@@ -1,6 +1,6 @@
 using PixiEditor.Models.Commands.Evaluators;
 
-namespace PixiEditor.Models.Commands;
+namespace PixiEditor.Models.Commands.Commands;
 
 internal partial class Command
 {

+ 1 - 2
src/PixiEditor/Models/Commands/Commands/Command.cs

@@ -1,10 +1,9 @@
 using System.Diagnostics;
 using System.Windows.Media;
-using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Evaluators;
 using PixiEditor.Models.DataHolders;
 
-namespace PixiEditor.Models.Commands;
+namespace PixiEditor.Models.Commands.Commands;
 
 [DebuggerDisplay("{InternalName,nq} ('{DisplayName,nq}')")]
 internal abstract partial class Command : NotifyableObject

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

@@ -1,7 +1,7 @@
 using System.Windows.Input;
 using PixiEditor.ViewModels;
 
-namespace PixiEditor.Models.Commands;
+namespace PixiEditor.Models.Commands.Commands;
 
 internal partial class Command
 {

+ 3 - 1
src/PixiEditor/Models/Commands/Evaluators/CanExecuteEvaluator.cs

@@ -1,4 +1,6 @@
-namespace PixiEditor.Models.Commands.Evaluators;
+using PixiEditor.Models.Commands.Commands;
+
+namespace PixiEditor.Models.Commands.Evaluators;
 
 internal class CanExecuteEvaluator : Evaluator<bool>
 {

+ 1 - 0
src/PixiEditor/Models/Commands/Evaluators/Evaluator.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using PixiEditor.Models.Commands.Commands;
 
 namespace PixiEditor.Models.Commands.Evaluators;
 

+ 1 - 0
src/PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs

@@ -3,6 +3,7 @@ using System.Diagnostics;
 using System.Reflection;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
+using PixiEditor.Models.Commands.Commands;
 
 namespace PixiEditor.Models.Commands.Evaluators;
 

+ 1 - 0
src/PixiEditor/Models/Commands/Search/CommandSearchResult.cs

@@ -1,4 +1,5 @@
 using System.Windows.Media;
+using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.DataHolders;
 
 namespace PixiEditor.Models.Commands.Search;

+ 1 - 3
src/PixiEditor/Models/Commands/Templates/ShortcutProvider.cs

@@ -1,6 +1,4 @@
-using System.Collections;
-using System.Windows.Input;
-using PixiEditor.Models.DataHolders;
+using System.Diagnostics;
 
 namespace PixiEditor.Models.Commands.Templates;
 

+ 1 - 1
src/PixiEditor/Models/Commands/XAML/ShortcutBinding.cs

@@ -3,7 +3,7 @@ using System.Windows.Markup;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.ViewModels;
-using ActualCommand = PixiEditor.Models.Commands.Command;
+using ActualCommand = PixiEditor.Models.Commands.Commands.Command;
 
 namespace PixiEditor.Models.Commands.XAML;
 

+ 1 - 0
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
 using System.Windows;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Document;
 using PixiEditor.Models.IO;
 
 namespace PixiEditor.Models.Controllers;

+ 1 - 0
src/PixiEditor/Models/Controllers/ShortcutController.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Tools;
 

+ 2 - 2
src/PixiEditor/Models/DataHolders/CrashReport.cs

@@ -85,9 +85,9 @@ internal class CrashReport : IDisposable
 
     public int GetDocumentCount() => ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")).Count();
 
-    public List<Document> RecoverDocuments()
+    public List<Document.Document> RecoverDocuments()
     {
-        return new List<Document>();
+        return new List<Document.Document>();
         /*
         List<Document> documents = new();
         foreach (ZipArchiveEntry entry in ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")))

+ 1 - 1
src/PixiEditor/Models/DataHolders/Document/Document.Commands.cs

@@ -1,6 +1,6 @@
 using PixiEditor.Helpers;
 
-namespace PixiEditor.Models.DataHolders;
+namespace PixiEditor.Models.DataHolders.Document;
 
 internal partial class Document
 {

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

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.DataHolders;
+namespace PixiEditor.Models.DataHolders.Document;
 
 internal partial class Document
 {

+ 1 - 0
src/PixiEditor/Models/Dialogs/ConfirmationDialog.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.Enums;
 using PixiEditor.Views;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.Models.Dialogs;
 

+ 1 - 0
src/PixiEditor/Models/Dialogs/ExportFileDialog.cs

@@ -1,6 +1,7 @@
 using System.Windows;
 using PixiEditor.Models.Enums;
 using PixiEditor.Views;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.Models.Dialogs;
 

+ 2 - 1
src/PixiEditor/Models/Dialogs/ImportFileDialog.cs

@@ -1,4 +1,5 @@
 using PixiEditor.Views;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.Models.Dialogs;
 
@@ -64,4 +65,4 @@ internal class ImportFileDialog : CustomDialog
 
         return (bool)popup.DialogResult;
     }
-}
+}

+ 1 - 0
src/PixiEditor/Models/Dialogs/NewFileDialog.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Views;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.Models.Dialogs;
 

+ 1 - 0
src/PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.Enums;
 using PixiEditor.Views;
+using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.Models.Dialogs;
 

+ 188 - 0
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -0,0 +1,188 @@
+using System.Windows;
+using System.Windows.Threading;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.Actions.Undo;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.Models.Rendering;
+using PixiEditor.Models.Rendering.RenderInfos;
+using PixiEditor.ViewModels.Prototype;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.Models.DocumentModels;
+
+internal class ActionAccumulator
+{
+    private bool executing = false;
+
+    private List<IAction> queuedActions = new();
+    private DocumentViewModel document;
+    private DocumentHelpers helpers;
+
+    private WriteableBitmapUpdater renderer;
+
+    public ActionAccumulator(DocumentViewModel doc, DocumentHelpers helpers)
+    {
+        this.document = doc;
+        this.helpers = helpers;
+
+        renderer = new(doc, helpers);
+    }
+
+    public void AddFinishedActions(params IAction[] actions)
+    {
+        queuedActions.AddRange(actions);
+        queuedActions.Add(new ChangeBoundary_Action());
+        TryExecuteAccumulatedActions();
+    }
+
+    public void AddActions(params IAction[] actions)
+    {
+        queuedActions.AddRange(actions);
+        TryExecuteAccumulatedActions();
+    }
+
+    private async void TryExecuteAccumulatedActions()
+    {
+        if (executing || queuedActions.Count == 0)
+            return;
+        executing = true;
+        DispatcherTimer busyTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(2000) };
+        busyTimer.Tick += (_, _) =>
+        {
+            busyTimer.Stop();
+            document.Busy = true;
+        };
+        busyTimer.Start();
+
+        while (queuedActions.Count > 0)
+        {
+            // select actions to be processed
+            var toExecute = queuedActions;
+            queuedActions = new List<IAction>();
+
+            // pass them to changeabledocument for processing
+            List<IChangeInfo?> changes;
+            if (AreAllPassthrough(toExecute))
+                changes = toExecute.Select(a => (IChangeInfo?)a).ToList();
+            else
+                changes = await helpers.Tracker.ProcessActions(toExecute);
+
+            // update viewmodels based on changes
+            foreach (IChangeInfo? info in changes)
+            {
+                helpers.Updater.ApplyChangeFromChangeInfo(info);
+            }
+
+            // lock bitmaps that need to be updated
+            var affectedChunks = new AffectedChunkGatherer(helpers.Tracker, changes);
+
+            foreach (var (_, bitmap) in document.Bitmaps)
+            {
+                bitmap.Lock();
+            }
+            bool refreshDelayed = toExecute.Any(static action => action is ChangeBoundary_Action or Redo_Action or Undo_Action);
+            if (refreshDelayed)
+                LockPreviewBitmaps(document.StructureRoot);
+
+            // update bitmaps
+            var renderResult = await renderer.UpdateGatheredChunks(affectedChunks, refreshDelayed);
+            AddDirtyRects(renderResult);
+
+            // unlock bitmaps
+            foreach (var (_, bitmap) in document.Bitmaps)
+            {
+                bitmap.Unlock();
+            }
+            if (refreshDelayed)
+                UnlockPreviewBitmaps(document.StructureRoot);
+
+            // force refresh viewports for better responsiveness
+            foreach (var (_, value) in helpers.State.Viewports)
+            {
+                value.InvalidateVisual();
+            }
+        }
+
+        busyTimer.Stop();
+        if (document.Busy)
+            document.Busy = false;
+        executing = false;
+    }
+
+    private bool AreAllPassthrough(List<IAction> actions)
+    {
+        foreach (var action in actions)
+        {
+            if (action is not IChangeInfo)
+                return false;
+        }
+        return true;
+    }
+
+    private void LockPreviewBitmaps(FolderViewModel root)
+    {
+        foreach (var child in root.Children)
+        {
+            child.PreviewBitmap.Lock();
+            if (child.MaskPreviewBitmap is not null)
+                child.MaskPreviewBitmap.Lock();
+            if (child is FolderViewModel innerFolder)
+                LockPreviewBitmaps(innerFolder);
+        }
+        document.PreviewBitmap.Lock();
+    }
+
+    private void UnlockPreviewBitmaps(FolderViewModel root)
+    {
+        foreach (var child in root.Children)
+        {
+            child.PreviewBitmap.Unlock();
+            if (child.MaskPreviewBitmap is not null)
+                child.MaskPreviewBitmap.Unlock();
+            if (child is FolderViewModel innerFolder)
+                UnlockPreviewBitmaps(innerFolder);
+        }
+        document.PreviewBitmap.Unlock();
+    }
+
+    private void AddDirtyRects(List<IRenderInfo> changes)
+    {
+        foreach (IRenderInfo renderInfo in changes)
+        {
+            switch (renderInfo)
+            {
+                case DirtyRect_RenderInfo info:
+                {
+                    var bitmap = document.Bitmaps[info.Resolution];
+                    RectI finalRect = new RectI(VecI.Zero, new(bitmap.PixelWidth, bitmap.PixelHeight));
+
+                    RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
+                    bitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
+                }
+                break;
+                case PreviewDirty_RenderInfo info:
+                {
+                    var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.PreviewBitmap;
+                    if (bitmap is null)
+                        continue;
+                    bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
+                }
+                break;
+                case MaskPreviewDirty_RenderInfo info:
+                {
+                    var bitmap = helpers.StructureHelper.Find(info.GuidValue)?.MaskPreviewBitmap;
+                    if (bitmap is null)
+                        continue;
+                    bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
+                }
+                break;
+                case CanvasPreviewDirty_RenderInfo:
+                {
+                    document.PreviewBitmap.AddDirtyRect(new Int32Rect(0, 0, document.PreviewBitmap.PixelWidth, document.PreviewBitmap.PixelHeight));
+                }
+                break;
+            }
+        }
+    }
+}

+ 21 - 0
src/PixiEditor/Models/DocumentModels/DocumentHelpers.cs

@@ -0,0 +1,21 @@
+using PixiEditor.ChangeableDocument;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.Models.DocumentModels;
+
+internal class DocumentHelpers
+{
+    public DocumentHelpers(DocumentViewModel doc)
+    {
+        Tracker = new DocumentChangeTracker();
+        StructureHelper = new DocumentStructureHelper(doc, this);
+        Updater = new DocumentUpdater(doc, this);
+        ActionAccumulator = new ActionAccumulator(doc, this);
+        State = new DocumentState();
+    }
+    public ActionAccumulator ActionAccumulator { get; }
+    public DocumentChangeTracker Tracker { get; }
+    public DocumentStructureHelper StructureHelper { get; }
+    public DocumentUpdater Updater { get; }
+    public DocumentState State { get; }
+}

+ 8 - 0
src/PixiEditor/Models/DocumentModels/DocumentState.cs

@@ -0,0 +1,8 @@
+using PixiEditor.Models.Position;
+
+namespace PixiEditor.Models.DocumentModels;
+
+internal class DocumentState
+{
+    public Dictionary<Guid, ViewportInfo> Viewports { get; set; } = new();
+}

+ 144 - 0
src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs

@@ -0,0 +1,144 @@
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.ViewModels.Prototype;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.Models.DocumentModels;
+
+internal class DocumentStructureHelper
+{
+    private DocumentViewModel doc;
+    private DocumentHelpers helpers;
+    public DocumentStructureHelper(DocumentViewModel doc, DocumentHelpers helpers)
+    {
+        this.doc = doc;
+        this.helpers = helpers;
+    }
+
+    public void CreateNewStructureMember(StructureMemberType type)
+    {
+        var member = doc.FindFirstSelectedMember();
+        if (member is null)
+        {
+            var guid = Guid.NewGuid();
+            //put member on top
+            helpers.ActionAccumulator.AddActions(new CreateStructureMember_Action(doc.StructureRoot.GuidValue, guid, doc.StructureRoot.Children.Count, type));
+            helpers.ActionAccumulator.AddFinishedActions(new StructureMemberName_Action(guid, type == StructureMemberType.Layer ? "New Layer" : "New Folder"));
+            return;
+        }
+        if (member is FolderViewModel folder)
+        {
+            var guid = Guid.NewGuid();
+            //put member inside folder on top
+            helpers.ActionAccumulator.AddActions(new CreateStructureMember_Action(folder.GuidValue, guid, folder.Children.Count, type));
+            helpers.ActionAccumulator.AddFinishedActions(new StructureMemberName_Action(guid, type == StructureMemberType.Layer ? "New Layer" : "New Folder"));
+            return;
+        }
+        if (member is LayerViewModel layer)
+        {
+            var guid = Guid.NewGuid();
+            //put member above the layer
+            var path = FindPath(layer.GuidValue);
+            if (path.Count < 2)
+                throw new InvalidOperationException("Couldn't find a path to the selected member");
+            var parent = (FolderViewModel)path[1];
+            helpers.ActionAccumulator.AddActions(new CreateStructureMember_Action(parent.GuidValue, guid, parent.Children.IndexOf(layer) + 1, type));
+            helpers.ActionAccumulator.AddFinishedActions(new StructureMemberName_Action(guid, type == StructureMemberType.Layer ? "New Layer" : "New Folder"));
+            return;
+        }
+        throw new ArgumentException($"Unknown member type: {type}");
+    }
+
+    public StructureMemberViewModel FindOrThrow(Guid guid) => Find(guid) ?? throw new ArgumentException("Could not find member with guid " + guid.ToString());
+    public StructureMemberViewModel? Find(Guid guid)
+    {
+        var list = FindPath(guid);
+        return list.Count > 0 ? list[0] : null;
+    }
+
+    public StructureMemberViewModel? FindFirstWhere(Predicate<StructureMemberViewModel> predicate)
+    {
+        return FindFirstWhere(predicate, doc.StructureRoot);
+    }
+    private StructureMemberViewModel? FindFirstWhere(Predicate<StructureMemberViewModel> predicate, FolderViewModel folderVM)
+    {
+        foreach (var child in folderVM.Children)
+        {
+            if (predicate(child))
+                return child;
+            if (child is FolderViewModel innerFolderVM)
+            {
+                var result = FindFirstWhere(predicate, innerFolderVM);
+                if (result is not null)
+                    return result;
+            }
+        }
+        return null;
+    }
+
+    public (StructureMemberViewModel, FolderViewModel) FindChildAndParentOrThrow(Guid childGuid)
+    {
+        var path = FindPath(childGuid);
+        if (path.Count < 2)
+            throw new ArgumentException("Couldn't find child and parent");
+        return (path[0], (FolderViewModel)path[1]);
+    }
+    public List<StructureMemberViewModel> FindPath(Guid guid)
+    {
+        var list = new List<StructureMemberViewModel>();
+        if (FillPath(doc.StructureRoot, guid, list))
+            list.Add(doc.StructureRoot);
+        return list;
+    }
+
+    private bool FillPath(FolderViewModel folder, Guid guid, List<StructureMemberViewModel> toFill)
+    {
+        if (folder.GuidValue == guid)
+        {
+            return true;
+        }
+        foreach (var member in folder.Children)
+        {
+            if (member is LayerViewModel childLayer && childLayer.GuidValue == guid)
+            {
+                toFill.Add(member);
+                return true;
+            }
+            if (member is FolderViewModel childFolder)
+            {
+                if (FillPath(childFolder, guid, toFill))
+                {
+                    toFill.Add(childFolder);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public void MoveStructureMember(Guid guid, bool toSmallerIndex)
+    {
+        var path = FindPath(guid);
+        if (path.Count < 2)
+            throw new ArgumentException("Couldn't find the member to be moved");
+        if (path.Count == 2)
+        {
+            int curIndex = doc.StructureRoot.Children.IndexOf(path[0]);
+            if ((curIndex == 0 && toSmallerIndex) || (curIndex == doc.StructureRoot.Children.Count - 1 && !toSmallerIndex))
+                return;
+            helpers.ActionAccumulator.AddFinishedActions(new MoveStructureMember_Action(guid, doc.StructureRoot.GuidValue, toSmallerIndex ? curIndex - 1 : curIndex + 1));
+            return;
+        }
+        var folder = (FolderViewModel)path[1];
+        int index = folder.Children.IndexOf(path[0]);
+        if ((toSmallerIndex && index > 0) || (!toSmallerIndex && index < folder.Children.Count - 1))
+        {
+            helpers.ActionAccumulator.AddFinishedActions(new MoveStructureMember_Action(guid, path[1].GuidValue, toSmallerIndex ? index - 1 : index + 1));
+        }
+        else
+        {
+            int parentIndex = ((FolderViewModel)path[2]).Children.IndexOf(folder);
+            helpers.ActionAccumulator.AddFinishedActions(new MoveStructureMember_Action(guid, path[2].GuidValue, toSmallerIndex ? parentIndex : parentIndex + 1));
+        }
+    }
+}

+ 319 - 0
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -0,0 +1,319 @@
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Models.BitmapActions;
+using PixiEditor.ViewModels.Prototype;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using SkiaSharp;
+
+namespace PixiEditor.Models.DocumentModels;
+
+internal class DocumentUpdater
+{
+    private DocumentViewModel doc;
+    private DocumentHelpers helper;
+
+    public DocumentUpdater(DocumentViewModel doc, DocumentHelpers helper)
+    {
+        this.doc = doc;
+        this.helper = helper;
+    }
+
+    /// <summary>
+    /// Don't call this outside ActionAccumulator
+    /// </summary>
+    public void ApplyChangeFromChangeInfo(IChangeInfo? arbitraryInfo)
+    {
+        if (arbitraryInfo is null)
+            return;
+
+        switch (arbitraryInfo)
+        {
+            case CreateStructureMember_ChangeInfo info:
+                ProcessCreateStructureMember(info);
+                break;
+            case DeleteStructureMember_ChangeInfo info:
+                ProcessDeleteStructureMember(info);
+                break;
+            case StructureMemberName_ChangeInfo info:
+                ProcessUpdateStructureMemberName(info);
+                break;
+            case StructureMemberIsVisible_ChangeInfo info:
+                ProcessUpdateStructureMemberIsVisible(info);
+                break;
+            case StructureMemberOpacity_ChangeInfo info:
+                ProcessUpdateStructureMemberOpacity(info);
+                break;
+            case MoveStructureMember_ChangeInfo info:
+                ProcessMoveStructureMember(info);
+                break;
+            case Size_ChangeInfo info:
+                ProcessSize(info);
+                break;
+            case RefreshViewport_PassthroughAction info:
+                ProcessRefreshViewport(info);
+                break;
+            case RemoveViewport_PassthroughAction info:
+                ProcessRemoveViewport(info);
+                break;
+            case StructureMemberMask_ChangeInfo info:
+                ProcessStructureMemberMask(info);
+                break;
+            case StructureMemberBlendMode_ChangeInfo info:
+                ProcessStructureMemberBlendMode(info);
+                break;
+            case LayerLockTransparency_ChangeInfo info:
+                ProcessLayerLockTransparency(info);
+                break;
+            case Selection_ChangeInfo info:
+                ProcessSelection(info);
+                break;
+            case SymmetryAxisState_ChangeInfo info:
+                ProcessSymmetryState(info);
+                break;
+            case SymmetryAxisPosition_ChangeInfo info:
+                ProcessSymmetryPosition(info);
+                break;
+            case StructureMemberClipToMemberBelow_ChangeInfo info:
+                ProcessClipToMemberBelow(info);
+                break;
+            case StructureMemberMaskIsVisible_ChangeInfo info:
+                ProcessMaskIsVisible(info);
+                break;
+            case CreateReferenceLayer_ChangeInfo info:
+                ProcessCreateReferenceLayer(info);
+                break;
+        }
+    }
+
+    private void ProcessCreateReferenceLayer(CreateReferenceLayer_ChangeInfo info)
+    {
+        doc.RaisePropertyChanged(nameof(doc.ReferenceLayer));
+        doc.RaisePropertyChanged(nameof(doc.ReferenceBitmap));
+        doc.RaisePropertyChanged(nameof(doc.ReferenceBitmapSize));
+        doc.RaisePropertyChanged(nameof(doc.ReferenceTransformMatrix));
+        doc.RaisePropertyChanged(nameof(doc.ReferenceShape));
+    }
+
+    private void ProcessMaskIsVisible(StructureMemberMaskIsVisible_ChangeInfo info)
+    {
+        var member = helper.StructureHelper.FindOrThrow(info.GuidValue);
+        member.SetMaskIsVisible(info.IsVisible);
+    }
+
+    private void ProcessClipToMemberBelow(StructureMemberClipToMemberBelow_ChangeInfo info)
+    {
+        var member = helper.StructureHelper.FindOrThrow(info.GuidValue);
+        member.SetClipToMemberBelowEnabled(info.ClipToMemberBelow);
+    }
+
+    private void ProcessSymmetryPosition(SymmetryAxisPosition_ChangeInfo info)
+    {
+        if (info.Direction == SymmetryAxisDirection.Horizontal)
+            doc.SetHorizontalSymmetryAxisY(info.NewPosition);
+        else if (info.Direction == SymmetryAxisDirection.Vertical)
+            doc.SetVerticalSymmetryAxisX(info.NewPosition);
+    }
+
+    private void ProcessSymmetryState(SymmetryAxisState_ChangeInfo info)
+    {
+        if (info.Direction == SymmetryAxisDirection.Horizontal)
+            doc.SetHorizontalSymmetryAxisEnabled(info.State);
+        else if (info.Direction == SymmetryAxisDirection.Vertical)
+            doc.SetVerticalSymmetryAxisEnabled(info.State);
+    }
+
+    private void ProcessSelection(Selection_ChangeInfo info)
+    {
+        doc.SetSelectionPath(info.NewPath);
+    }
+
+    private void ProcessLayerLockTransparency(LayerLockTransparency_ChangeInfo info)
+    {
+        var layer = (LayerViewModel)helper.StructureHelper.FindOrThrow(info.GuidValue);
+        layer.SetLockTransparency(info.LockTransparency);
+    }
+
+    private void ProcessStructureMemberBlendMode(StructureMemberBlendMode_ChangeInfo info)
+    {
+        var memberVm = helper.StructureHelper.FindOrThrow(info.GuidValue);
+        memberVm.SetBlendMode(info.BlendMode);
+    }
+
+    private void ProcessStructureMemberMask(StructureMemberMask_ChangeInfo info)
+    {
+        var memberVm = helper.StructureHelper.FindOrThrow(info.GuidValue);
+        memberVm.MaskPreviewSurface?.Dispose();
+        memberVm.MaskPreviewSurface = null;
+        memberVm.MaskPreviewBitmap = null;
+
+        if (info.HasMask)
+        {
+            var size = StructureMemberViewModel.CalculatePreviewSize(doc.SizeBindable);
+            memberVm.MaskPreviewBitmap = CreateBitmap(size);
+            memberVm.MaskPreviewSurface = CreateSKSurface(memberVm.MaskPreviewBitmap);
+        }
+        memberVm.SetHasMask(info.HasMask);
+        memberVm.RaisePropertyChanged(nameof(memberVm.MaskPreviewBitmap));
+    }
+
+    private void ProcessRefreshViewport(RefreshViewport_PassthroughAction info)
+    {
+        helper.State.Viewports[info.Info.GuidValue] = info.Info;
+    }
+
+    private void ProcessRemoveViewport(RemoveViewport_PassthroughAction info)
+    {
+        helper.State.Viewports.Remove(info.GuidValue);
+    }
+
+    private void UpdateMemberBitmapsRecursively(FolderViewModel folder, VecI newSize)
+    {
+        foreach (var member in folder.Children)
+        {
+            member.PreviewSurface.Dispose();
+            member.PreviewBitmap = CreateBitmap(newSize);
+            member.PreviewSurface = CreateSKSurface(member.PreviewBitmap);
+            member.RaisePropertyChanged(nameof(member.PreviewBitmap));
+
+            member.MaskPreviewSurface?.Dispose();
+            member.MaskPreviewSurface = null;
+            member.MaskPreviewBitmap = null;
+            if (member.HasMaskBindable)
+            {
+                member.MaskPreviewBitmap = CreateBitmap(newSize);
+                member.MaskPreviewSurface = CreateSKSurface(member.MaskPreviewBitmap);
+            }
+            member.RaisePropertyChanged(nameof(member.MaskPreviewBitmap));
+
+            if (member is FolderViewModel innerFolder)
+            {
+                UpdateMemberBitmapsRecursively(innerFolder, newSize);
+            }
+        }
+    }
+
+    private void ProcessSize(Size_ChangeInfo info)
+    {
+        Dictionary<ChunkResolution, WriteableBitmap> newBitmaps = new();
+        foreach (var (res, surf) in doc.Surfaces)
+        {
+            surf.Dispose();
+            newBitmaps[res] = CreateBitmap((VecI)(info.Size * res.Multiplier()));
+            doc.Surfaces[res] = CreateSKSurface(newBitmaps[res]);
+        }
+
+        doc.Bitmaps = newBitmaps;
+
+        doc.SetSize(info.Size);
+        doc.SetVerticalSymmetryAxisX(info.VerticalSymmetryAxisX);
+        doc.SetHorizontalSymmetryAxisY(info.HorizontalSymmetryAxisY);
+
+        var previewSize = StructureMemberViewModel.CalculatePreviewSize(info.Size);
+        doc.PreviewSurface.Dispose();
+        doc.PreviewBitmap = CreateBitmap(previewSize);
+        doc.PreviewSurface = CreateSKSurface(doc.PreviewBitmap);
+
+        doc.RaisePropertyChanged(nameof(doc.Bitmaps));
+        doc.RaisePropertyChanged(nameof(doc.PreviewBitmap));
+
+        UpdateMemberBitmapsRecursively(doc.StructureRoot, previewSize);
+    }
+
+    private WriteableBitmap CreateBitmap(VecI size)
+    {
+        return new WriteableBitmap(Math.Max(size.X, 1), Math.Max(size.Y, 1), 96, 96, PixelFormats.Pbgra32, null);
+    }
+
+    private SKSurface CreateSKSurface(WriteableBitmap bitmap)
+    {
+        return SKSurface.Create(
+            new SKImageInfo(bitmap.PixelWidth, bitmap.PixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb()),
+            bitmap.BackBuffer,
+            bitmap.BackBufferStride);
+    }
+
+    private void ProcessCreateStructureMember(CreateStructureMember_ChangeInfo info)
+    {
+        var parentFolderVM = (FolderViewModel)helper.StructureHelper.FindOrThrow(info.ParentGuid);
+
+        StructureMemberViewModel memberVM;
+        if (info is CreateLayer_ChangeInfo layerInfo)
+        {
+            memberVM = new LayerViewModel(doc, helper, info.GuidValue);
+            ((LayerViewModel)memberVM).SetLockTransparency(layerInfo.LockTransparency);
+        }
+        else if (info is CreateFolder_ChangeInfo)
+        {
+            memberVM = new FolderViewModel(doc, helper, info.GuidValue);
+        }
+        else
+        {
+            throw new NotSupportedException();
+        }
+        memberVM.SetOpacity(info.Opacity);
+        memberVM.SetIsVisible(info.IsVisible);
+        memberVM.SetClipToMemberBelowEnabled(info.ClipToMemberBelow);
+        memberVM.SetName(info.Name);
+        memberVM.SetHasMask(info.HasMask);
+        memberVM.SetMaskIsVisible(info.MaskIsVisible);
+        memberVM.SetBlendMode(info.BlendMode);
+
+        if (info.HasMask)
+        {
+            var size = StructureMemberViewModel.CalculatePreviewSize(doc.SizeBindable);
+            memberVM.MaskPreviewBitmap = CreateBitmap(size);
+            memberVM.MaskPreviewSurface = CreateSKSurface(memberVM.MaskPreviewBitmap);
+        }
+
+        parentFolderVM.Children.Insert(info.Index, memberVM);
+
+        if (info is CreateFolder_ChangeInfo folderInfo)
+        {
+            foreach (CreateStructureMember_ChangeInfo childInfo in folderInfo.Children)
+            {
+                ProcessCreateStructureMember(childInfo);
+            }
+        }
+    }
+
+    private void ProcessDeleteStructureMember(DeleteStructureMember_ChangeInfo info)
+    {
+        var (memberVM, folderVM) = helper.StructureHelper.FindChildAndParentOrThrow(info.GuidValue);
+        folderVM.Children.Remove(memberVM);
+    }
+
+    private void ProcessUpdateStructureMemberIsVisible(StructureMemberIsVisible_ChangeInfo info)
+    {
+        var memberVM = helper.StructureHelper.FindOrThrow(info.GuidValue);
+        memberVM.SetIsVisible(info.IsVisible);
+    }
+
+    private void ProcessUpdateStructureMemberName(StructureMemberName_ChangeInfo info)
+    {
+        var memberVM = helper.StructureHelper.FindOrThrow(info.GuidValue);
+        memberVM.SetName(info.Name);
+    }
+
+    private void ProcessUpdateStructureMemberOpacity(StructureMemberOpacity_ChangeInfo info)
+    {
+        var memberVM = helper.StructureHelper.FindOrThrow(info.GuidValue);
+        memberVM.SetOpacity(info.Opacity);
+    }
+
+    private void ProcessMoveStructureMember(MoveStructureMember_ChangeInfo info)
+    {
+        var (memberVM, curFolderVM) = helper.StructureHelper.FindChildAndParentOrThrow(info.GuidValue);
+
+        var targetFolderVM = (FolderViewModel)helper.StructureHelper.FindOrThrow(info.ParentToGuid);
+
+        curFolderVM.Children.Remove(memberVM);
+        targetFolderVM.Children.Insert(info.NewIndex, memberVM);
+    }
+}

+ 1 - 0
src/PixiEditor/Models/IO/Exporter.cs

@@ -6,6 +6,7 @@ using System.Windows.Media.Imaging;
 using Microsoft.Win32;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Document;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using SkiaSharp;

+ 1 - 0
src/PixiEditor/Models/IO/Importer.cs

@@ -5,6 +5,7 @@ using System.Windows.Media.Imaging;
 using PixiEditor.Exceptions;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.DataHolders.Document;
 using SkiaSharp;
 
 namespace PixiEditor.Models.IO;

+ 12 - 0
src/PixiEditor/Models/Position/ViewportInfo.cs

@@ -0,0 +1,12 @@
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditor.Models.Position;
+internal readonly record struct ViewportInfo(
+    double Angle,
+    VecD Center,
+    VecD RealDimensions,
+    VecD Dimensions,
+    ChunkResolution Resolution,
+    Guid GuidValue,
+    bool Delayed,
+    Action InvalidateVisual);

+ 224 - 0
src/PixiEditor/Models/Rendering/AffectedChunkGatherer.cs

@@ -0,0 +1,224 @@
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+
+namespace PixiEditor.Models.Rendering;
+
+internal class AffectedChunkGatherer
+{
+    private readonly DocumentChangeTracker tracker;
+
+    public HashSet<VecI> mainImageChunks { get; private set; } = new();
+    public Dictionary<Guid, HashSet<VecI>> imagePreviewChunks { get; private set; } = new();
+    public Dictionary<Guid, HashSet<VecI>> maskPreviewChunks { get; private set; } = new();
+
+    public AffectedChunkGatherer(DocumentChangeTracker tracker, IReadOnlyList<IChangeInfo?> changes)
+    {
+        this.tracker = tracker;
+        ProcessChanges(changes);
+    }
+
+    private void ProcessChanges(IReadOnlyList<IChangeInfo?> changes)
+    {
+        foreach (var change in changes)
+        {
+            switch (change)
+            {
+                case MaskChunks_ChangeInfo info:
+                    if (info.Chunks is null)
+                        throw new InvalidOperationException("Chunks must not be null");
+                    AddToMainImage(info.Chunks);
+                    AddToImagePreviews(info.GuidValue, info.Chunks, true);
+                    AddToMaskPreview(info.GuidValue, info.Chunks);
+                    break;
+                case LayerImageChunks_ChangeInfo info:
+                    if (info.Chunks is null)
+                        throw new InvalidOperationException("Chunks must not be null");
+                    AddToMainImage(info.Chunks);
+                    AddToImagePreviews(info.GuidValue, info.Chunks);
+                    break;
+                case CreateStructureMember_ChangeInfo info:
+                    AddAllToMainImage(info.GuidValue);
+                    AddAllToImagePreviews(info.GuidValue);
+                    AddAllToMaskPreview(info.GuidValue);
+                    break;
+                case DeleteStructureMember_ChangeInfo info:
+                    AddWholeCanvasToMainImage();
+                    AddWholeCanvasToImagePreviews(info.ParentGuid);
+                    break;
+                case MoveStructureMember_ChangeInfo info:
+                    AddAllToMainImage(info.GuidValue);
+                    AddAllToImagePreviews(info.GuidValue, true);
+                    if (info.ParentFromGuid != info.ParentToGuid)
+                        AddWholeCanvasToImagePreviews(info.ParentFromGuid);
+                    break;
+                case Size_ChangeInfo:
+                    AddWholeCanvasToMainImage();
+                    AddWholeCanvasToEveryImagePreview();
+                    AddWholeCanvasToEveryMaskPreview();
+                    break;
+                case StructureMemberMask_ChangeInfo info:
+                    AddWholeCanvasToMainImage();
+                    AddWholeCanvasToMaskPreview(info.GuidValue);
+                    AddWholeCanvasToImagePreviews(info.GuidValue, true);
+                    break;
+                case StructureMemberBlendMode_ChangeInfo info:
+                    AddAllToMainImage(info.GuidValue);
+                    AddAllToImagePreviews(info.GuidValue, true);
+                    break;
+                case StructureMemberClipToMemberBelow_ChangeInfo info:
+                    AddAllToMainImage(info.GuidValue);
+                    AddAllToImagePreviews(info.GuidValue, true);
+                    break;
+                case StructureMemberOpacity_ChangeInfo info:
+                    AddAllToMainImage(info.GuidValue);
+                    AddAllToImagePreviews(info.GuidValue, true);
+                    break;
+                case StructureMemberIsVisible_ChangeInfo info:
+                    AddAllToMainImage(info.GuidValue);
+                    AddAllToImagePreviews(info.GuidValue, true);
+                    break;
+                case StructureMemberMaskIsVisible_ChangeInfo info:
+                    AddAllToMainImage(info.GuidValue, false);
+                    AddAllToImagePreviews(info.GuidValue, true);
+                    break;
+            }
+        }
+    }
+
+    private void AddAllToImagePreviews(Guid memberGuid, bool ignoreSelf = false)
+    {
+        var member = tracker.Document.FindMember(memberGuid);
+        if (member is IReadOnlyLayer layer)
+        {
+            var chunks = layer.LayerImage.FindAllChunks();
+            AddToImagePreviews(memberGuid, chunks, ignoreSelf);
+        }
+        else if (member is IReadOnlyFolder folder)
+        {
+            AddWholeCanvasToImagePreviews(memberGuid, ignoreSelf);
+            foreach (var child in folder.Children)
+                AddAllToImagePreviews(child.GuidValue);
+        }
+    }
+
+    private void AddAllToMainImage(Guid memberGuid, bool useMask = true)
+    {
+        var member = tracker.Document.FindMember(memberGuid);
+        if (member is IReadOnlyLayer layer)
+        {
+            var chunks = layer.LayerImage.FindAllChunks();
+            if (layer.Mask is not null && layer.MaskIsVisible && useMask)
+                chunks.IntersectWith(layer.Mask.FindAllChunks());
+            AddToMainImage(chunks);
+        }
+        else
+        {
+            AddWholeCanvasToMainImage();
+        }
+    }
+
+    private void AddAllToMaskPreview(Guid memberGuid)
+    {
+        if (!tracker.Document.TryFindMember(memberGuid, out var member))
+            return;
+        if (member.Mask is not null)
+        {
+            var chunks = member.Mask.FindAllChunks();
+            AddToMaskPreview(memberGuid, chunks);
+        }
+        if (member is IReadOnlyFolder folder)
+        {
+            foreach (var child in folder.Children)
+                AddAllToMaskPreview(child.GuidValue);
+        }
+    }
+
+
+    private void AddToMainImage(HashSet<VecI> chunks)
+    {
+        mainImageChunks.UnionWith(chunks);
+    }
+
+    private void AddToImagePreviews(Guid memberGuid, HashSet<VecI> chunks, bool ignoreSelf = false)
+    {
+        var path = tracker.Document.FindMemberPath(memberGuid);
+        if (path.Count < 2)
+            return;
+        for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
+        {
+            var member = path[i];
+            if (!imagePreviewChunks.ContainsKey(member.GuidValue))
+                imagePreviewChunks[member.GuidValue] = new HashSet<VecI>(chunks);
+            else
+                imagePreviewChunks[member.GuidValue].UnionWith(chunks);
+        }
+    }
+
+    private void AddToMaskPreview(Guid memberGuid, HashSet<VecI> chunks)
+    {
+        if (!maskPreviewChunks.ContainsKey(memberGuid))
+            maskPreviewChunks[memberGuid] = new HashSet<VecI>(chunks);
+        else
+            maskPreviewChunks[memberGuid].UnionWith(chunks);
+    }
+
+
+    private void AddWholeCanvasToMainImage()
+    {
+        AddAllChunks(mainImageChunks);
+    }
+
+    private void AddWholeCanvasToImagePreviews(Guid memberGuid, bool ignoreSelf = false)
+    {
+        var path = tracker.Document.FindMemberPath(memberGuid);
+        if (path.Count < 2)
+            return;
+        // skip root folder
+        for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
+        {
+            var member = path[i];
+            if (!imagePreviewChunks.ContainsKey(member.GuidValue))
+                imagePreviewChunks[member.GuidValue] = new HashSet<VecI>();
+            AddAllChunks(imagePreviewChunks[member.GuidValue]);
+        }
+    }
+
+    private void AddWholeCanvasToMaskPreview(Guid memberGuid)
+    {
+        if (!maskPreviewChunks.ContainsKey(memberGuid))
+            maskPreviewChunks[memberGuid] = new HashSet<VecI>();
+        AddAllChunks(maskPreviewChunks[memberGuid]);
+    }
+
+
+    private void AddWholeCanvasToEveryImagePreview()
+    {
+        tracker.Document.ForEveryReadonlyMember((member) => AddWholeCanvasToImagePreviews(member.GuidValue));
+    }
+
+    private void AddWholeCanvasToEveryMaskPreview()
+    {
+        tracker.Document.ForEveryReadonlyMember((member) => AddWholeCanvasToMaskPreview(member.GuidValue));
+    }
+
+    private void AddAllChunks(HashSet<VecI> chunks)
+    {
+        VecI size = new(
+            (int)Math.Ceiling(tracker.Document.Size.X / (float)ChunkyImage.FullChunkSize),
+            (int)Math.Ceiling(tracker.Document.Size.Y / (float)ChunkyImage.FullChunkSize));
+        for (int i = 0; i < size.X; i++)
+        {
+            for (int j = 0; j < size.Y; j++)
+            {
+                chunks.Add(new(i, j));
+            }
+        }
+    }
+}

+ 3 - 0
src/PixiEditor/Models/Rendering/RenderInfos/CanvasPreviewDirty_RenderInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.Models.Rendering.RenderInfos;
+
+internal record CanvasPreviewDirty_RenderInfo : IRenderInfo;

+ 5 - 0
src/PixiEditor/Models/Rendering/RenderInfos/DirtyRect_RenderInfo.cs

@@ -0,0 +1,5 @@
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditor.Models.Rendering.RenderInfos;
+
+public record class DirtyRect_RenderInfo(VecI Pos, VecI Size, ChunkResolution Resolution) : IRenderInfo;

+ 5 - 0
src/PixiEditor/Models/Rendering/RenderInfos/IRenderInfo.cs

@@ -0,0 +1,5 @@
+namespace PixiEditor.Models.Rendering.RenderInfos;
+
+public interface IRenderInfo
+{
+}

+ 3 - 0
src/PixiEditor/Models/Rendering/RenderInfos/MaskPreviewDirty_RenderInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.Models.Rendering.RenderInfos;
+
+public record class MaskPreviewDirty_RenderInfo(Guid GuidValue) : IRenderInfo;

+ 3 - 0
src/PixiEditor/Models/Rendering/RenderInfos/PreviewDirty_RenderInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.Models.Rendering.RenderInfos;
+
+public record class PreviewDirty_RenderInfo(Guid GuidValue) : IRenderInfo;

+ 301 - 0
src/PixiEditor/Models/Rendering/WriteableBitmapUpdater.cs

@@ -0,0 +1,301 @@
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using ChunkyImageLib.Operations;
+using OneOf;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.Models.DocumentModels;
+using PixiEditor.Models.Rendering.RenderInfos;
+using PixiEditor.ViewModels.Prototype;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using SkiaSharp;
+
+namespace PixiEditor.Models.Rendering;
+
+internal class WriteableBitmapUpdater
+{
+    private readonly DocumentViewModel doc;
+    private readonly DocumentHelpers helpers;
+
+    private static readonly SKPaint ReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
+    private static readonly SKPaint SmoothReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src, FilterQuality = SKFilterQuality.Medium, IsAntialias = true };
+    private static readonly SKPaint ClearPaint = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
+
+    /// <summary>
+    /// Chunks that have been updated but don't need to be re-rendered because they are out of view
+    /// </summary>
+    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalPostponedChunks = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+    /// <summary>
+    /// The state of globalPostponedChunks during the last update of global delayed chunks (when you finish using a tool)
+    /// It is required in case the viewport is moved while you are using a tool. In this case the newly visible chunks on delayed viewports
+    /// need to be re-rendered, even though normally re-render only happens after you're done with some tool.
+    /// Because the viewport still has the old version of the image there is no point in re-rendering everything from globalPostponedChunks.
+    /// It's enough to re-render the chunks that were postponed back when the delayed viewports were last updated fully.
+    /// </summary>
+    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalPostponedForDelayed = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+    /// <summary>
+    /// Chunks that have been updated but don't need to be re-rendered because all viewports that see them have Delayed == true
+    /// These chunks are updated after you finish using a tool
+    /// </summary>
+    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalDelayedChunks = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+    private Dictionary<Guid, HashSet<VecI>> previewDelayedChunks = new();
+    private Dictionary<Guid, HashSet<VecI>> maskPreviewDelayedChunks = new();
+
+    public WriteableBitmapUpdater(DocumentViewModel doc, DocumentHelpers helpers)
+    {
+        this.doc = doc;
+        this.helpers = helpers;
+    }
+
+    /// <summary>
+    /// Don't call this outside ActionAccumulator
+    /// </summary>
+    public async Task<List<IRenderInfo>> UpdateGatheredChunks
+        (AffectedChunkGatherer chunkGatherer, bool updateDelayed)
+    {
+        return await Task.Run(() => Render(chunkGatherer, updateDelayed)).ConfigureAwait(true);
+    }
+
+    private Dictionary<ChunkResolution, HashSet<VecI>> FindGlobalChunksToRerender(AffectedChunkGatherer chunkGatherer, bool renderDelayed)
+    {
+        // add all affected chunks to postponed
+        foreach (var (_, postponed) in globalPostponedChunks)
+        {
+            postponed.UnionWith(chunkGatherer.mainImageChunks);
+        }
+
+        // find all chunks that are on viewports and on delayed viewports
+        var chunksToUpdate = new Dictionary<ChunkResolution, HashSet<VecI>>() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+        var chunksOnDelayedViewports = new Dictionary<ChunkResolution, HashSet<VecI>>() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+        foreach (var (_, viewport) in helpers.State.Viewports)
+        {
+            var viewportChunks = OperationHelper.FindChunksTouchingRectangle(
+                viewport.Center,
+                viewport.Dimensions,
+                -viewport.Angle,
+                ChunkResolution.Full.PixelSize());
+            if (viewport.Delayed)
+                chunksOnDelayedViewports[viewport.Resolution].UnionWith(viewportChunks);
+            else
+                chunksToUpdate[viewport.Resolution].UnionWith(viewportChunks);
+        }
+
+        // exclude the chunks that don't need to be updated, remove chunks that will be updated from postponed
+        foreach (var (res, postponed) in globalPostponedChunks)
+        {
+            chunksToUpdate[res].IntersectWith(postponed);
+            chunksOnDelayedViewports[res].IntersectWith(postponed);
+            postponed.ExceptWith(chunksToUpdate[res]);
+        }
+
+        // decide what to do about the delayed chunks
+        if (renderDelayed)
+        {
+            foreach (var (res, postponed) in globalPostponedChunks)
+            {
+                chunksToUpdate[res].UnionWith(chunksOnDelayedViewports[res]);
+                postponed.ExceptWith(chunksOnDelayedViewports[res]);
+                globalPostponedForDelayed[res] = new HashSet<VecI>(postponed);
+            }
+        }
+        else
+        {
+            foreach (var (res, postponed) in globalPostponedChunks)
+            {
+                chunksOnDelayedViewports[res].IntersectWith(globalPostponedForDelayed[res]);
+                globalPostponedForDelayed[res].ExceptWith(chunksOnDelayedViewports[res]);
+
+                chunksToUpdate[res].UnionWith(chunksOnDelayedViewports[res]);
+                postponed.ExceptWith(chunksOnDelayedViewports[res]);
+            }
+        }
+
+        return chunksToUpdate;
+    }
+
+
+    private static void AddChunks(Dictionary<Guid, HashSet<VecI>> from, Dictionary<Guid, HashSet<VecI>> to)
+    {
+        foreach ((Guid guid, HashSet<VecI> chunks) in from)
+        {
+            if (!to.ContainsKey(guid))
+                to[guid] = new HashSet<VecI>();
+            to[guid].UnionWith(chunks);
+        }
+    }
+
+    private (Dictionary<Guid, HashSet<VecI>> image, Dictionary<Guid, HashSet<VecI>> mask) FindPreviewChunksToRerender
+        (AffectedChunkGatherer chunkGatherer, bool postpone)
+    {
+        AddChunks(chunkGatherer.imagePreviewChunks, previewDelayedChunks);
+        AddChunks(chunkGatherer.maskPreviewChunks, maskPreviewDelayedChunks);
+        if (postpone)
+            return (new(), new());
+        var result = (previewPostponedChunks: previewDelayedChunks, maskPostponedChunks: maskPreviewDelayedChunks);
+        previewDelayedChunks = new();
+        maskPreviewDelayedChunks = new();
+        return result;
+    }
+
+    private List<IRenderInfo> Render(AffectedChunkGatherer chunkGatherer, bool updateDelayed)
+    {
+        Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender = FindGlobalChunksToRerender(chunkGatherer, updateDelayed);
+
+        List<IRenderInfo> infos = new();
+        UpdateMainImage(chunksToRerender, infos);
+
+        var (imagePreviewChunksToRerender, maskPreviewChunksToRerender) = FindPreviewChunksToRerender(chunkGatherer, !updateDelayed);
+        var previewSize = StructureMemberViewModel.CalculatePreviewSize(helpers.Tracker.Document.Size);
+        float scaling = (float)previewSize.X / doc.SizeBindable.X;
+        UpdateImagePreviews(imagePreviewChunksToRerender, scaling, infos);
+        UpdateMaskPreviews(maskPreviewChunksToRerender, scaling, infos);
+
+        return infos;
+    }
+
+    private void UpdateImagePreviews(Dictionary<Guid, HashSet<VecI>> imagePreviewChunks, float scaling, List<IRenderInfo> infos)
+    {
+        // update preview of the whole canvas
+        var cumulative = imagePreviewChunks.Aggregate(new HashSet<VecI>(), (set, pair) =>
+        {
+            set.UnionWith(pair.Value);
+            return set;
+        });
+        bool somethingChanged = false;
+        foreach (var chunkPos in cumulative)
+        {
+            somethingChanged = true;
+            ChunkResolution resolution = scaling switch
+            {
+                > 1 / 2f => ChunkResolution.Full,
+                > 1 / 4f => ChunkResolution.Half,
+                > 1 / 8f => ChunkResolution.Quarter,
+                _ => ChunkResolution.Eighth,
+            };
+            var pos = chunkPos * resolution.PixelSize();
+            var rendered = ChunkRenderer.MergeWholeStructure(chunkPos, resolution, helpers.Tracker.Document.StructureRoot);
+            doc.PreviewSurface.Canvas.Save();
+            doc.PreviewSurface.Canvas.Scale(scaling);
+            doc.PreviewSurface.Canvas.Scale(1 / (float)resolution.Multiplier());
+            if (rendered.IsT1)
+            {
+                doc.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(), resolution.PixelSize(), ClearPaint);
+                return;
+            }
+            using var renderedChunk = rendered.AsT0;
+            renderedChunk.DrawOnSurface(doc.PreviewSurface, pos, SmoothReplacingPaint);
+            doc.PreviewSurface.Canvas.Restore();
+        }
+        if (somethingChanged)
+            infos.Add(new CanvasPreviewDirty_RenderInfo());
+
+        // update previews of individual members
+        foreach (var (guid, chunks) in imagePreviewChunks)
+        {
+            var memberVM = helpers.StructureHelper.Find(guid);
+            if (memberVM is null)
+                continue;
+            var member = helpers.Tracker.Document.FindMemberOrThrow(guid);
+
+            memberVM.PreviewSurface.Canvas.Save();
+            memberVM.PreviewSurface.Canvas.Scale(scaling);
+            if (memberVM is LayerViewModel)
+            {
+                var layer = (IReadOnlyLayer)member;
+                foreach (var chunk in chunks)
+                {
+                    var pos = chunk * ChunkResolution.Full.PixelSize();
+                    // the full res chunks are already rendered so drawing them again should be fast
+                    if (!layer.LayerImage.DrawMostUpToDateChunkOn
+                            (chunk, ChunkResolution.Full, memberVM.PreviewSurface, pos, SmoothReplacingPaint))
+                        memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize, ClearPaint);
+                }
+                infos.Add(new PreviewDirty_RenderInfo(guid));
+            }
+            else if (memberVM is FolderViewModel)
+            {
+                var folder = (IReadOnlyFolder)member;
+                foreach (var chunk in chunks)
+                {
+                    var pos = chunk * ChunkResolution.Full.PixelSize();
+                    // drawing in full res here is kinda slow
+                    // we could switch to a lower resolution based on (canvas size / preview size) to make it run faster
+                    OneOf<Chunk, EmptyChunk> rendered = ChunkRenderer.MergeWholeStructure(chunk, ChunkResolution.Full, folder);
+                    if (rendered.IsT0)
+                    {
+                        memberVM.PreviewSurface.Canvas.DrawSurface(rendered.AsT0.Surface.SkiaSurface, pos, SmoothReplacingPaint);
+                        rendered.AsT0.Dispose();
+                    }
+                    else
+                    {
+                        memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(), ChunkResolution.Full.PixelSize(), ClearPaint);
+                    }
+                }
+                infos.Add(new PreviewDirty_RenderInfo(guid));
+            }
+            memberVM.PreviewSurface.Canvas.Restore();
+        }
+    }
+
+    private void UpdateMaskPreviews(Dictionary<Guid, HashSet<VecI>> maskPreviewChunks, float scaling, List<IRenderInfo> infos)
+    {
+        foreach (var (guid, chunks) in maskPreviewChunks)
+        {
+            var memberVM = helpers.StructureHelper.Find(guid);
+            if (memberVM is null || !memberVM.HasMaskBindable)
+                continue;
+
+            var member = helpers.Tracker.Document.FindMemberOrThrow(guid);
+            memberVM.MaskPreviewSurface!.Canvas.Save();
+            memberVM.MaskPreviewSurface.Canvas.Scale(scaling);
+
+            foreach (var chunk in chunks)
+            {
+                var pos = chunk * ChunkResolution.Full.PixelSize();
+                member.Mask!.DrawMostUpToDateChunkOn
+                    (chunk, ChunkResolution.Full, memberVM.MaskPreviewSurface, pos, SmoothReplacingPaint);
+            }
+
+            memberVM.MaskPreviewSurface.Canvas.Restore();
+            infos.Add(new MaskPreviewDirty_RenderInfo(guid));
+        }
+    }
+
+    private void UpdateMainImage(Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender, List<IRenderInfo> infos)
+    {
+        foreach (var (resolution, chunks) in chunksToRerender)
+        {
+            int chunkSize = resolution.PixelSize();
+            SKSurface screenSurface = doc.Surfaces[resolution];
+            foreach (var chunkPos in chunks)
+            {
+                RenderChunk(chunkPos, screenSurface, resolution);
+                infos.Add(new DirtyRect_RenderInfo(
+                    chunkPos * chunkSize,
+                    new(chunkSize, chunkSize),
+                    resolution
+                ));
+            }
+        }
+    }
+
+    private void RenderChunk(VecI chunkPos, SKSurface screenSurface, ChunkResolution resolution)
+    {
+        ChunkRenderer.MergeWholeStructure(chunkPos, resolution, helpers.Tracker.Document.StructureRoot).Switch(
+            (Chunk chunk) =>
+            {
+                screenSurface.Canvas.DrawSurface(chunk.Surface.SkiaSurface, chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
+                chunk.Dispose();
+            },
+            (EmptyChunk _) =>
+            {
+                var pos = chunkPos * resolution.PixelSize();
+                screenSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(), resolution.PixelSize(), ClearPaint);
+            });
+    }
+}

+ 1 - 0
src/PixiEditor/Models/Services/CommandProvider.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using System.Windows.Media;
 using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Commands.Evaluators;
 using XAMLCommand = PixiEditor.Models.Commands.XAML.Command;
 

+ 1 - 0
src/PixiEditor/Models/Tools/ToolSettings/Settings/ColorSetting.cs

@@ -5,6 +5,7 @@ using System.Windows.Interactivity;
 using System.Windows.Media;
 using PixiEditor.Helpers.Behaviours;
 using PixiEditor.Views;
+using PixiEditor.Views.UserControls;
 
 namespace PixiEditor.Models.Tools.ToolSettings.Settings;
 

+ 1 - 0
src/PixiEditor/Models/Tools/ToolSettings/Settings/FloatSetting.cs

@@ -1,6 +1,7 @@
 using System.Windows.Controls;
 using System.Windows.Data;
 using PixiEditor.Views;
+using PixiEditor.Views.UserControls;
 
 namespace PixiEditor.Models.Tools.ToolSettings.Settings;
 

+ 1 - 0
src/PixiEditor/Models/Tools/ToolSettings/Settings/SizeSetting.cs

@@ -2,6 +2,7 @@
 using System.Windows.Controls;
 using System.Windows.Data;
 using PixiEditor.Views;
+using PixiEditor.Views.UserControls;
 
 namespace PixiEditor.Models.Tools.ToolSettings.Settings;
 

+ 1 - 3
src/PixiEditor/Models/Tools/ToolSettings/Toolbars/EmptyToolbar.cs

@@ -1,6 +1,4 @@
-using PixiEditor.Models.Tools.ToolSettings.Toolbars;
-
-namespace PixiEditor.Models.Tools.ToolSettings;
+namespace PixiEditor.Models.Tools.ToolSettings.Toolbars;
 
 internal class EmptyToolbar : Toolbar
 {

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/BrightnessTool.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/CircleTool.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.Models.Tools.Tools;
 

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/ColorPickerTool.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.Models.Tools.Tools;
 

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/EraserTool.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.Models.Tools.Tools;

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/FloodFillTool.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using SkiaSharp;
 
 namespace PixiEditor.Models.Tools.Tools;

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/LineTool.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.Models.Tools.Tools;

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/MagicWandTool.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.Models.Tools.Tools;

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/MoveTool.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.Models.Tools.Tools;
 

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/MoveViewportTool.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.Models.Tools.Tools;
 

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/PenTool.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Tools.ToolSettings.Settings;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/RectangleTool.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.Models.Tools.Tools;
 

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/SelectTool.cs

@@ -2,6 +2,7 @@
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Tools.ToolSettings.Toolbars;
 using SkiaSharp;
 

+ 1 - 0
src/PixiEditor/Models/Tools/Tools/ZoomTool.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.Models.Tools.Tools;
 

+ 2 - 2
src/PixiEditor/NotifyableObject.cs

@@ -1,7 +1,7 @@
 using System.ComponentModel;
 using System.Runtime.CompilerServices;
 
-namespace PixiEditor.Helpers;
+namespace PixiEditor;
 
 [Serializable]
 internal class NotifyableObject : INotifyPropertyChanged
@@ -31,7 +31,7 @@ internal class NotifyableObject : INotifyPropertyChanged
         };
     }
 
-    protected void RaisePropertyChanged(string property)
+    public void RaisePropertyChanged(string property)
     {
         if (property != null)
         {

+ 1 - 0
src/PixiEditor/ViewModels/CrashReportViewModel.cs

@@ -2,6 +2,7 @@
 using System.Windows;
 using GalaSoft.MvvmLight.CommandWpf;
 using PixiEditor.Models.DataHolders;
+using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels;

+ 97 - 0
src/PixiEditor/ViewModels/Prototype/DocumentTransformViewModel.cs

@@ -0,0 +1,97 @@
+using System.ComponentModel;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.Views.UserControls.TransformOverlay;
+
+namespace PixiEditor.ViewModels.Prototype;
+internal class DocumentTransformViewModel : INotifyPropertyChanged
+{
+    private TransformState internalState;
+    public TransformState InternalState
+    {
+        get => internalState;
+        set
+        {
+            internalState = value;
+            PropertyChanged?.Invoke(this, new(nameof(InternalState)));
+        }
+    }
+
+    private TransformCornerFreedom cornerFreedom;
+    public TransformCornerFreedom CornerFreedom
+    {
+        get => cornerFreedom;
+        set
+        {
+            cornerFreedom = value;
+            PropertyChanged?.Invoke(this, new(nameof(CornerFreedom)));
+        }
+    }
+
+    private TransformSideFreedom sideFreedom;
+    public TransformSideFreedom SideFreedom
+    {
+        get => sideFreedom;
+        set
+        {
+            sideFreedom = value;
+            PropertyChanged?.Invoke(this, new(nameof(SideFreedom)));
+        }
+    }
+
+    private bool transformActive;
+    public bool TransformActive
+    {
+        get => transformActive;
+        set
+        {
+            transformActive = value;
+            PropertyChanged?.Invoke(this, new(nameof(TransformActive)));
+        }
+    }
+
+    private ShapeCorners requestedCorners;
+    public ShapeCorners RequestedCorners
+    {
+        get => requestedCorners;
+        set
+        {
+            requestedCorners = value;
+            PropertyChanged?.Invoke(this, new(nameof(RequestedCorners)));
+        }
+    }
+
+    private ShapeCorners corners;
+    public ShapeCorners Corners
+    {
+        get => corners;
+        set
+        {
+            corners = value;
+            PropertyChanged?.Invoke(this, new(nameof(Corners)));
+            TransformMoved?.Invoke(this, value);
+        }
+    }
+    public event PropertyChangedEventHandler? PropertyChanged;
+    public event EventHandler<ShapeCorners>? TransformMoved;
+
+    public void HideTransform()
+    {
+        TransformActive = false;
+    }
+
+    public void ShowShapeTransform(ShapeCorners initPos)
+    {
+        CornerFreedom = TransformCornerFreedom.Scale;
+        SideFreedom = TransformSideFreedom.ScaleProportionally;
+        RequestedCorners = initPos;
+        TransformActive = true;
+    }
+
+    public void ShowFreeTransform(ShapeCorners initPos)
+    {
+        CornerFreedom = TransformCornerFreedom.Free;
+        SideFreedom = TransformSideFreedom.Free;
+        RequestedCorners = initPos;
+        TransformActive = true;
+    }
+}

+ 11 - 0
src/PixiEditor/ViewModels/Prototype/FolderViewModel.cs

@@ -0,0 +1,11 @@
+using System.Collections.ObjectModel;
+using PixiEditor.Models.DocumentModels;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.ViewModels.Prototype;
+
+internal class FolderViewModel : StructureMemberViewModel
+{
+    public ObservableCollection<StructureMemberViewModel> Children { get; } = new();
+    public FolderViewModel(DocumentViewModel doc, DocumentHelpers helpers, Guid guidValue) : base(doc, helpers, guidValue) { }
+}

+ 23 - 0
src/PixiEditor/ViewModels/Prototype/LayerViewModel.cs

@@ -0,0 +1,23 @@
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.Models.DocumentModels;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.ViewModels.Prototype;
+
+internal class LayerViewModel : StructureMemberViewModel
+{
+    bool lockTransparency;
+    public void SetLockTransparency(bool lockTransparency)
+    {
+        this.lockTransparency = lockTransparency;
+        RaisePropertyChanged(nameof(LockTransparencyBindable));
+    }
+    public bool LockTransparencyBindable
+    {
+        get => lockTransparency;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new LayerLockTransparency_Action(GuidValue, value));
+    }
+    public LayerViewModel(DocumentViewModel doc, DocumentHelpers helpers, Guid guidValue) : base(doc, helpers, guidValue)
+    {
+    }
+}

+ 176 - 0
src/PixiEditor/ViewModels/Prototype/StructureMemberViewModel.cs

@@ -0,0 +1,176 @@
+using System.ComponentModel;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DocumentModels;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using SkiaSharp;
+
+namespace PixiEditor.ViewModels.Prototype;
+
+internal abstract class StructureMemberViewModel : INotifyPropertyChanged
+{
+    public event PropertyChangedEventHandler? PropertyChanged;
+    protected DocumentViewModel Document { get; }
+    protected DocumentHelpers Helpers { get; }
+
+
+    private string name = "";
+    public void SetName(string name)
+    {
+        this.name = name;
+        RaisePropertyChanged(nameof(NameBindable));
+    }
+    public string NameBindable
+    {
+        get => name;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new StructureMemberName_Action(GuidValue, value));
+    }
+
+    private bool isVisible;
+    public void SetIsVisible(bool isVisible)
+    {
+        this.isVisible = isVisible;
+        RaisePropertyChanged(nameof(IsVisibleBindable));
+    }
+    public bool IsVisibleBindable
+    {
+        get => isVisible;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new StructureMemberIsVisible_Action(value, GuidValue));
+    }
+
+    private bool maskIsVisible;
+    public void SetMaskIsVisible(bool maskIsVisible)
+    {
+        this.maskIsVisible = maskIsVisible;
+        RaisePropertyChanged(nameof(MaskIsVisibleBindable));
+    }
+    public bool MaskIsVisibleBindable
+    {
+        get => maskIsVisible;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new StructureMemberMaskIsVisible_Action(value, GuidValue));
+    }
+
+    private BlendMode blendMode;
+    public void SetBlendMode(BlendMode blendMode)
+    {
+        this.blendMode = blendMode;
+        RaisePropertyChanged(nameof(BlendModeBindable));
+    }
+    public BlendMode BlendModeBindable
+    {
+        get => blendMode;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new StructureMemberBlendMode_Action(value, GuidValue));
+    }
+
+    private bool clipToMemberBelowEnabled;
+    public void SetClipToMemberBelowEnabled(bool clipToMemberBelowEnabled)
+    {
+        this.clipToMemberBelowEnabled = clipToMemberBelowEnabled;
+        RaisePropertyChanged(nameof(ClipToMemberBelowEnabledBindable));
+    }
+    public bool ClipToMemberBelowEnabledBindable
+    {
+        get => clipToMemberBelowEnabled;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new StructureMemberClipToMemberBelow_Action(value, GuidValue));
+    }
+
+    private bool hasMask;
+    public void SetHasMask(bool hasMask)
+    {
+        this.hasMask = hasMask;
+        RaisePropertyChanged(nameof(HasMaskBindable));
+    }
+    public bool HasMaskBindable
+    {
+        get => hasMask;
+    }
+
+    private Guid guidValue;
+    public Guid GuidValue
+    {
+        get => guidValue;
+    }
+
+    private float opacity;
+
+    public void SetOpacity(float opacity)
+    {
+        this.opacity = opacity;
+        RaisePropertyChanged(nameof(OpacityBindable));
+    }
+    public float OpacityBindable
+    {
+        get => opacity;
+    }
+
+    private bool isSelected;
+    public bool IsSelected
+    {
+        get => isSelected;
+        set
+        {
+            isSelected = value;
+            Document.RaisePropertyChanged(nameof(Document.SelectedStructureMember));
+        }
+    }
+    public bool ShouldDrawOnMask { get; set; }
+
+
+    public const int PreviewSize = 48;
+    public WriteableBitmap PreviewBitmap { get; set; }
+    public SKSurface PreviewSurface { get; set; }
+
+    public WriteableBitmap? MaskPreviewBitmap { get; set; }
+    public SKSurface? MaskPreviewSurface { get; set; }
+
+    public RelayCommand MoveUpCommand { get; }
+    public RelayCommand MoveDownCommand { get; }
+    public RelayCommand UpdateOpacityCommand { get; }
+    public RelayCommand EndOpacityUpdateCommand { get; }
+
+    public void RaisePropertyChanged(string name)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+    }
+
+    public static VecI CalculatePreviewSize(VecI docSize)
+    {
+        double proportions = docSize.Y / (double)docSize.X;
+        const int prSize = StructureMemberViewModel.PreviewSize;
+        return proportions > 1 ?
+            new VecI((int)Math.Round(prSize / proportions), prSize) :
+            new VecI(prSize, (int)Math.Round(prSize * proportions));
+    }
+
+    public StructureMemberViewModel(DocumentViewModel doc, DocumentHelpers helpers, Guid guidValue)
+    {
+        Document = doc;
+        Helpers = helpers;
+
+        MoveUpCommand = new(_ => Helpers.StructureHelper.MoveStructureMember(GuidValue, false));
+        MoveDownCommand = new(_ => Helpers.StructureHelper.MoveStructureMember(GuidValue, true));
+        UpdateOpacityCommand = new(UpdateOpacity);
+        EndOpacityUpdateCommand = new(EndOpacityUpdate);
+
+        this.guidValue = guidValue;
+        var previewSize = CalculatePreviewSize(doc.SizeBindable);
+        PreviewBitmap = new WriteableBitmap(previewSize.X, previewSize.Y, 96, 96, PixelFormats.Pbgra32, null);
+        PreviewSurface = SKSurface.Create(new SKImageInfo(previewSize.X, previewSize.Y, SKColorType.Bgra8888), PreviewBitmap.BackBuffer, PreviewBitmap.BackBufferStride);
+    }
+
+    private void EndOpacityUpdate(object? opacity)
+    {
+        Helpers.ActionAccumulator.AddFinishedActions(new EndStructureMemberOpacity_Action());
+    }
+
+    private void UpdateOpacity(object? opacity)
+    {
+        if (opacity is not double value)
+            throw new ArgumentException("The passed value isn't a double");
+        Helpers.ActionAccumulator.AddActions(new StructureMemberOpacity_Action(GuidValue, (float)value));
+    }
+}

+ 7 - 6
src/PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -6,6 +6,7 @@ using PixiEditor.ViewModels.SubViewModels.UserPreferences;
 using System.Windows;
 using System.Windows.Input;
 using Microsoft.Win32;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Views.Dialogs;
 
@@ -59,7 +60,7 @@ internal class SettingsWindowViewModel : ViewModelBase
 
     public List<GroupSearchResult> Commands { get; }
 
-    [Models.Commands.Attributes.Command.Internal("PixiEditor.Shortcuts.Reset")]
+    [Command.Internal("PixiEditor.Shortcuts.Reset")]
     public static void ResetCommand()
     {
         var dialog = new OptionsDialog<string>("Are you sure?", "Are you sure you want to reset all shortcuts to their default value?")
@@ -69,7 +70,7 @@ internal class SettingsWindowViewModel : ViewModelBase
         }.ShowDialog();
     }
 
-    [Models.Commands.Attributes.Command.Internal("PixiEditor.Shortcuts.Export")]
+    [Command.Internal("PixiEditor.Shortcuts.Export")]
     public static void ExportShortcuts()
     {
         var dialog = new SaveFileDialog();
@@ -83,7 +84,7 @@ internal class SettingsWindowViewModel : ViewModelBase
         Keyboard.ClearFocus();
     }
 
-    [Models.Commands.Attributes.Command.Internal("PixiEditor.Shortcuts.Import")]
+    [Command.Internal("PixiEditor.Shortcuts.Import")]
     public static void ImportShortcuts()
     {
         var dialog = new OpenFileDialog();
@@ -106,7 +107,7 @@ internal class SettingsWindowViewModel : ViewModelBase
         Keyboard.ClearFocus();
     }
 
-    [Models.Commands.Attributes.Command.Internal("PixiEditor.Shortcuts.OpenTemplatePopup")]
+    [Command.Internal("PixiEditor.Shortcuts.OpenTemplatePopup")]
     public static void OpenTemplatePopup()
     {
         new ImportShortcutTemplatePopup().ShowDialog();
@@ -193,7 +194,7 @@ internal class SettingsWindowViewModel : ViewModelBase
     {
         private Visibility visibility;
 
-        public Command Command { get; set; }
+        public Models.Commands.Commands.Command Command { get; set; }
 
         public Visibility Visibility
         {
@@ -201,7 +202,7 @@ internal class SettingsWindowViewModel : ViewModelBase
             set => SetProperty(ref visibility, value);
         }
 
-        public CommandSearchResult(Command command)
+        public CommandSearchResult(Models.Commands.Commands.Command command)
         {
             Command = command;
         }

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs

@@ -2,6 +2,8 @@
 using System.Windows.Input;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Events;
 using PixiEditor.Models.Tools;

+ 144 - 4
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -1,12 +1,152 @@
-namespace PixiEditor.ViewModels.SubViewModels.Document;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using ChunkyImageLib.DataHolders;
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DocumentModels;
+using PixiEditor.ViewModels.Prototype;
+using SkiaSharp;
 
-internal class DocumentViewModel : SubViewModel<ViewModelMain>
+namespace PixiEditor.ViewModels.SubViewModels.Document;
+
+internal class DocumentViewModel : NotifyableObject
 {
     public const string ConfirmationDialogTitle = "Unsaved changes";
     public const string ConfirmationDialogMessage = "The document has been modified. Do you want to save changes?";
 
-    public DocumentViewModel(ViewModelMain owner, string name)
-        : base(owner)
+    public bool Busy
+    {
+        get => busy;
+        set
+        {
+            busy = value;
+            RaisePropertyChanged(nameof(Busy));
+        }
+    }
+    
+    public FolderViewModel StructureRoot { get; }
+
+    public int Width => size.X;
+    public int Height => size.Y;
+    
+    public StructureMemberViewModel? SelectedStructureMember => FindFirstSelectedMember();
+    
+    public Dictionary<ChunkResolution, WriteableBitmap> Bitmaps { get; set; } = new()
+    {
+        [ChunkResolution.Full] = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null),
+        [ChunkResolution.Half] = new WriteableBitmap(32, 32, 96, 96, PixelFormats.Pbgra32, null),
+        [ChunkResolution.Quarter] = new WriteableBitmap(16, 16, 96, 96, PixelFormats.Pbgra32, null),
+        [ChunkResolution.Eighth] = new WriteableBitmap(8, 8, 96, 96, PixelFormats.Pbgra32, null),
+    };
+
+    public WriteableBitmap PreviewBitmap { get; set; }
+    public SKSurface PreviewSurface { get; set; }
+
+    public Dictionary<ChunkResolution, SKSurface> Surfaces { get; set; } = new();
+
+    public VecI SizeBindable => size;
+    
+    public StructureMemberViewModel? FindFirstSelectedMember() => Helpers.StructureHelper.FindFirstWhere(member => member.IsSelected);
+
+    public int HorizontalSymmetryAxisYBindable => horizontalSymmetryAxisY;
+    public int VerticalSymmetryAxisXBindable => verticalSymmetryAxisX;
+    
+    public bool HorizontalSymmetryAxisEnabledBindable
+    {
+        get => horizontalSymmetryAxisEnabled;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new SymmetryAxisState_Action(SymmetryAxisDirection.Horizontal, value));
+    }
+    
+    public bool VerticalSymmetryAxisEnabledBindable
+    {
+        get => verticalSymmetryAxisEnabled;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new SymmetryAxisState_Action(SymmetryAxisDirection.Vertical, value));
+    }
+    
+    public IReadOnlyReferenceLayer? ReferenceLayer => Helpers.Tracker.Document.ReferenceLayer;
+
+    public BitmapSource? ReferenceBitmap => ReferenceLayer?.Image.ToWriteableBitmap();
+    public VecI ReferenceBitmapSize => ReferenceLayer?.Image.Size ?? VecI.Zero;
+    public ShapeCorners ReferenceShape => ReferenceLayer?.Shape ?? default;
+
+    public Matrix ReferenceTransformMatrix
+    {
+        get
+        {
+            if (ReferenceLayer is null)
+                return Matrix.Identity;
+            var skiaMatrix = OperationHelper.CreateMatrixFromPoints(ReferenceLayer.Shape, ReferenceLayer.Image.Size);
+            return new Matrix(skiaMatrix.ScaleX, skiaMatrix.SkewY, skiaMatrix.SkewX, skiaMatrix.ScaleY, skiaMatrix.TransX, skiaMatrix.TransY);
+        }
+    }
+    
+    public SKPath SelectionPathBindable => selectionPath;
+
+    private DocumentHelpers Helpers { get; }
+
+    private readonly ViewModelMain owner;
+
+    private int verticalSymmetryAxisX;
+    
+    private bool horizontalSymmetryAxisEnabled;
+
+    private bool verticalSymmetryAxisEnabled;
+    
+    private bool busy = false;
+    
+    private VecI size = new VecI(64, 64);
+
+    private int horizontalSymmetryAxisY;
+    
+    private SKPath selectionPath = new SKPath();
+    
+    public DocumentViewModel(string name)
+    {
+    }
+    
+    public void SetSize(VecI size)
+    {
+        this.size = size;
+        RaisePropertyChanged(nameof(SizeBindable));
+        RaisePropertyChanged(nameof(Width));
+        RaisePropertyChanged(nameof(Height));
+    }
+
+    #region Symmetry
+    
+    public void SetVerticalSymmetryAxisEnabled(bool verticalSymmetryAxisEnabled)
+    {
+        this.verticalSymmetryAxisEnabled = verticalSymmetryAxisEnabled;
+        RaisePropertyChanged(nameof(VerticalSymmetryAxisEnabledBindable));
+    }
+    
+    public void SetHorizontalSymmetryAxisEnabled(bool horizontalSymmetryAxisEnabled)
+    {
+        this.horizontalSymmetryAxisEnabled = horizontalSymmetryAxisEnabled;
+        RaisePropertyChanged(nameof(HorizontalSymmetryAxisEnabledBindable));
+    }
+    
+    public void SetVerticalSymmetryAxisX(int verticalSymmetryAxisX)
+    {
+        this.verticalSymmetryAxisX = verticalSymmetryAxisX;
+        RaisePropertyChanged(nameof(VerticalSymmetryAxisXBindable));
+    }
+    
+    public void SetHorizontalSymmetryAxisY(int horizontalSymmetryAxisY)
+    {
+        this.horizontalSymmetryAxisY = horizontalSymmetryAxisY;
+        RaisePropertyChanged(nameof(HorizontalSymmetryAxisYBindable));
+    }
+    
+    #endregion
+    
+    public void SetSelectionPath(SKPath selectionPath)
     {
+        (var toDispose, this.selectionPath) = (this.selectionPath, selectionPath);
+        toDispose.Dispose();
+        RaisePropertyChanged(nameof(SelectionPathBindable));
     }
 }

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/ClipboardViewModel.cs

@@ -4,6 +4,8 @@ using System.Windows.Input;
 using System.Windows.Media;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Commands.Search;
 using PixiEditor.Models.Controllers;
 using SkiaSharp;

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs

@@ -3,6 +3,8 @@ using System.Windows.Media;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Commands.Search;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -3,6 +3,7 @@ using System.IO;
 using System.Reflection;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.UserPreferences;
 

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

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using Newtonsoft.Json.Linq;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.UserPreferences;
@@ -69,7 +70,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
     public void NewDocument(int width, int height, bool addBaseLayer = true)
     {
-        Owner.DocumentManagerSubViewModel.Documents.Add(new DocumentViewModel(Owner, "Unnamed"));
+        Owner.DocumentManagerSubViewModel.Documents.Add(new DocumentViewModel("Unnamed"));
         Owner.DocumentManagerSubViewModel.ActiveDocument = Owner.DocumentManagerSubViewModel.Documents[^1];
         /*
         if (addBaseLayer)

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs

@@ -2,10 +2,12 @@
 using System.Windows.Input;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
+using PixiEditor.Views;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/SearchViewModel.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/SelectionViewModel.cs

@@ -1,5 +1,7 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Commands.Attributes.Evaluators;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/StylusViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using GalaSoft.MvvmLight.CommandWpf;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
 using PixiEditor.Models.UserPreferences;

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/ToolsViewModel.cs

@@ -1,6 +1,7 @@
 using System.Windows.Input;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Events;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;

+ 2 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/UndoViewModel.cs

@@ -1,5 +1,7 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Commands.Attributes.Evaluators;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using System.Windows;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.UpdateModule;

+ 1 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/ViewportViewModel.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.Models.Commands.Attributes;
+using PixiEditor.Models.Commands.Attributes.Commands;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 

+ 2 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs

@@ -2,8 +2,9 @@
 using AvalonDock.Layout;
 using GalaSoft.MvvmLight.CommandWpf;
 using PixiEditor.Models.Commands;
+using PixiEditor.Views;
 using PixiEditor.Views.Dialogs;
-using Command = PixiEditor.Models.Commands.Attributes.Command;
+using Command = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.