Browse Source

Basic selection implementation

Equbuxu 3 years ago
parent
commit
131a5ecd57
54 changed files with 725 additions and 286 deletions
  1. 1 9
      src/ChangeableDocument/Actions/Drawing/Rectangle/DrawRectangle_Action.cs
  2. 13 0
      src/ChangeableDocument/Actions/Drawing/Rectangle/EndDrawRectangle_Action.cs
  3. 13 0
      src/ChangeableDocument/Actions/Drawing/Selection/ClearSelection_Action.cs
  4. 13 0
      src/ChangeableDocument/Actions/Drawing/Selection/EndSelectRectangle_Action.cs
  5. 27 0
      src/ChangeableDocument/Actions/Drawing/Selection/SelectRectangle_Action.cs
  6. 0 9
      src/ChangeableDocument/Actions/MiscActions.cs
  7. 9 0
      src/ChangeableDocument/Actions/Properties/EndOpacityChange_Action.cs
  8. 1 6
      src/ChangeableDocument/Actions/Properties/OpacityChange_Action.cs
  9. 20 0
      src/ChangeableDocument/Actions/Properties/SetStructureMemberName_Action.cs
  10. 20 0
      src/ChangeableDocument/Actions/Properties/SetStructureMemberVisibility_Action.cs
  11. 22 0
      src/ChangeableDocument/Actions/Structure/CreateStructureMember_Action.cs
  12. 18 0
      src/ChangeableDocument/Actions/Structure/DeleteStructureMember_Action.cs
  13. 22 0
      src/ChangeableDocument/Actions/Structure/MoveStructureMember_Action.cs
  14. 0 89
      src/ChangeableDocument/Actions/StructureActions.cs
  15. 5 0
      src/ChangeableDocument/Actions/Undo/RedoAction.cs
  16. 5 0
      src/ChangeableDocument/Actions/Undo/UndoAction.cs
  17. 9 0
      src/ChangeableDocument/ChangeInfos/Selection_ChangeInfo.cs
  18. 5 3
      src/ChangeableDocument/Changeables/Document.cs
  19. 4 4
      src/ChangeableDocument/Changeables/Folder.cs
  20. 1 0
      src/ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  21. 1 1
      src/ChangeableDocument/Changeables/Interfaces/IReadOnlyLayer.cs
  22. 10 0
      src/ChangeableDocument/Changeables/Interfaces/IReadOnlySelection.cs
  23. 4 4
      src/ChangeableDocument/Changeables/Interfaces/IReadOnlyStructureMember.cs
  24. 6 6
      src/ChangeableDocument/Changeables/Layer.cs
  25. 16 0
      src/ChangeableDocument/Changeables/Selection.cs
  26. 4 4
      src/ChangeableDocument/Changeables/StructureMember.cs
  27. 3 3
      src/ChangeableDocument/Changes/CreateStructureMember_Change.cs
  28. 1 1
      src/ChangeableDocument/Changes/DeleteStructureMember_Change.cs
  29. 49 0
      src/ChangeableDocument/Changes/Drawing/ClearSelection_Change.cs
  30. 4 2
      src/ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs
  31. 64 0
      src/ChangeableDocument/Changes/Drawing/SelectRectangle_UpdateableChange.cs
  32. 1 1
      src/ChangeableDocument/Changes/MoveStructureMember_Change.cs
  33. 3 3
      src/ChangeableDocument/Changes/StructureMemberOpacity_UpdateableChange.cs
  34. 8 8
      src/ChangeableDocument/Changes/StructureMemberProperties_Change.cs
  35. 1 0
      src/ChangeableDocument/DocumentChangeTracker.cs
  36. 4 3
      src/ChunkyImageLib/Chunk.cs
  37. 129 49
      src/ChunkyImageLib/ChunkyImage.cs
  38. 2 2
      src/ChunkyImageLib/CommitedChunkStorage.cs
  39. 1 1
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  40. 19 0
      src/ChunkyImageLib/Operations/ClearOperation.cs
  41. 10 0
      src/ChunkyImageLib/Operations/IChunkOperation.cs
  42. 1 5
      src/ChunkyImageLib/Operations/IOperation.cs
  43. 2 2
      src/ChunkyImageLib/Operations/ImageOperation.cs
  44. 13 0
      src/ChunkyImageLib/Operations/RasterClipOperation.cs
  45. 2 2
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  46. 1 1
      src/ChunkyImageLib/Surface.cs
  47. 1 1
      src/PixiEditorPrototype/MainWindow.xaml
  48. 1 1
      src/PixiEditorPrototype/Models/DocumentStructureHelper.cs
  49. 3 3
      src/PixiEditorPrototype/Models/DocumentUpdater.cs
  50. 8 0
      src/PixiEditorPrototype/Models/Tool.cs
  51. 89 36
      src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs
  52. 7 7
      src/PixiEditorPrototype/ViewModels/StructureMemberViewModel.cs
  53. 14 6
      src/PixiEditorPrototype/Views/DocumentView.xaml
  54. 35 14
      src/StructureRenderer/Renderer.cs

+ 1 - 9
src/ChangeableDocument/Actions/Drawing/RectangleActions.cs → src/ChangeableDocument/Actions/Drawing/Rectangle/DrawRectangle_Action.cs

@@ -2,7 +2,7 @@
 using ChangeableDocument.Changes.Drawing;
 using ChunkyImageLib.DataHolders;
 
-namespace ChangeableDocument.Actions.Drawing
+namespace ChangeableDocument.Actions.Drawing.Rectangle
 {
     public record struct DrawRectangle_Action : IStartOrUpdateChangeAction
     {
@@ -25,12 +25,4 @@ namespace ChangeableDocument.Actions.Drawing
             return new DrawRectangle_UpdateableChange(LayerGuid, Rectangle);
         }
     }
-
-    public record struct EndDrawRectangle_Action : IEndChangeAction
-    {
-        bool IEndChangeAction.IsChangeTypeMatching(IChange change)
-        {
-            return change is DrawRectangle_UpdateableChange;
-        }
-    }
 }

+ 13 - 0
src/ChangeableDocument/Actions/Drawing/Rectangle/EndDrawRectangle_Action.cs

@@ -0,0 +1,13 @@
+using ChangeableDocument.Changes;
+using ChangeableDocument.Changes.Drawing;
+
+namespace ChangeableDocument.Actions.Drawing.Rectangle
+{
+    public record struct EndDrawRectangle_Action : IEndChangeAction
+    {
+        bool IEndChangeAction.IsChangeTypeMatching(IChange change)
+        {
+            return change is DrawRectangle_UpdateableChange;
+        }
+    }
+}

+ 13 - 0
src/ChangeableDocument/Actions/Drawing/Selection/ClearSelection_Action.cs

@@ -0,0 +1,13 @@
+using ChangeableDocument.Changes;
+using ChangeableDocument.Changes.Drawing;
+
+namespace ChangeableDocument.Actions.Drawing.Selection
+{
+    public record struct ClearSelection_Action : IMakeChangeAction
+    {
+        IChange IMakeChangeAction.CreateCorrespondingChange()
+        {
+            return new ClearSelection_Change();
+        }
+    }
+}

+ 13 - 0
src/ChangeableDocument/Actions/Drawing/Selection/EndSelectRectangle_Action.cs

@@ -0,0 +1,13 @@
+using ChangeableDocument.Changes;
+using ChangeableDocument.Changes.Drawing;
+
+namespace ChangeableDocument.Actions.Drawing.Selection
+{
+    public record struct EndSelectRectangle_Action : IEndChangeAction
+    {
+        bool IEndChangeAction.IsChangeTypeMatching(IChange change)
+        {
+            return change is SelectRectangle_UpdateableChange;
+        }
+    }
+}

+ 27 - 0
src/ChangeableDocument/Actions/Drawing/Selection/SelectRectangle_Action.cs

@@ -0,0 +1,27 @@
+using ChangeableDocument.Changes;
+using ChangeableDocument.Changes.Drawing;
+using ChunkyImageLib.DataHolders;
+
+namespace ChangeableDocument.Actions.Drawing.Selection
+{
+    public record struct SelectRectangle_Action : IStartOrUpdateChangeAction
+    {
+        public Vector2i Pos { get; }
+        public Vector2i Size { get; }
+        public SelectRectangle_Action(Vector2i pos, Vector2i size)
+        {
+            Pos = pos;
+            Size = size;
+        }
+
+        IUpdateableChange IStartOrUpdateChangeAction.CreateCorrespondingChange()
+        {
+            return new SelectRectangle_UpdateableChange(Pos, Size);
+        }
+
+        void IStartOrUpdateChangeAction.UpdateCorrespodingChange(IUpdateableChange change)
+        {
+            ((SelectRectangle_UpdateableChange)change).Update(Pos, Size);
+        }
+    }
+}

+ 0 - 9
src/ChangeableDocument/Actions/MiscActions.cs

@@ -1,9 +0,0 @@
-namespace ChangeableDocument.Actions;
-
-public record struct RedoAction : IAction
-{
-}
-
-public record struct UndoAction : IAction
-{
-}

+ 9 - 0
src/ChangeableDocument/Actions/Properties/EndOpacityChange_Action.cs

@@ -0,0 +1,9 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions.Properties
+{
+    public record struct EndOpacityChange_Action : IEndChangeAction
+    {
+        bool IEndChangeAction.IsChangeTypeMatching(IChange change) => change is StructureMemberOpacity_UpdateableChange;
+    }
+}

+ 1 - 6
src/ChangeableDocument/Actions/OpacityActions.cs → src/ChangeableDocument/Actions/Properties/OpacityChange_Action.cs

@@ -1,6 +1,6 @@
 using ChangeableDocument.Changes;
 
-namespace ChangeableDocument.Actions
+namespace ChangeableDocument.Actions.Properties
 {
     public record struct OpacityChange_Action : IStartOrUpdateChangeAction
     {
@@ -23,9 +23,4 @@ namespace ChangeableDocument.Actions
             ((StructureMemberOpacity_UpdateableChange)change).Update(Opacity);
         }
     }
-
-    public record struct EndOpacityChange_Action : IEndChangeAction
-    {
-        bool IEndChangeAction.IsChangeTypeMatching(IChange change) => change is StructureMemberOpacity_UpdateableChange;
-    }
 }

+ 20 - 0
src/ChangeableDocument/Actions/Properties/SetStructureMemberName_Action.cs

@@ -0,0 +1,20 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions.Properties;
+
+public record struct SetStructureMemberName_Action : IMakeChangeAction
+{
+    public SetStructureMemberName_Action(string name, Guid guidValue)
+    {
+        Name = name;
+        GuidValue = guidValue;
+    }
+
+    public string Name { get; init; }
+    public Guid GuidValue { get; init; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new StructureMemberProperties_Change(GuidValue) { NewName = Name };
+    }
+}

+ 20 - 0
src/ChangeableDocument/Actions/Properties/SetStructureMemberVisibility_Action.cs

@@ -0,0 +1,20 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions.Properties;
+
+public record struct SetStructureMemberVisibility_Action : IMakeChangeAction
+{
+    public SetStructureMemberVisibility_Action(bool isVisible, Guid guidValue)
+    {
+        this.isVisible = isVisible;
+        GuidValue = guidValue;
+    }
+
+    public bool isVisible { get; init; }
+    public Guid GuidValue { get; init; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new StructureMemberProperties_Change(GuidValue) { NewIsVisible = isVisible };
+    }
+}

+ 22 - 0
src/ChangeableDocument/Actions/Structure/CreateStructureMember_Action.cs

@@ -0,0 +1,22 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions.Structure;
+
+public record struct CreateStructureMember_Action : IMakeChangeAction
+{
+    public CreateStructureMember_Action(Guid parentGuid, int index, StructureMemberType type)
+    {
+        ParentGuid = parentGuid;
+        Index = index;
+        Type = type;
+    }
+
+    public Guid ParentGuid { get; init; }
+    public int Index { get; init; }
+    public StructureMemberType Type { get; init; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new CreateStructureMember_Change(ParentGuid, Index, Type);
+    }
+}

+ 18 - 0
src/ChangeableDocument/Actions/Structure/DeleteStructureMember_Action.cs

@@ -0,0 +1,18 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions.Structure;
+
+public record struct DeleteStructureMember_Action : IMakeChangeAction
+{
+    public DeleteStructureMember_Action(Guid guidValue)
+    {
+        GuidValue = guidValue;
+    }
+
+    public Guid GuidValue { get; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new DeleteStructureMember_Change(GuidValue);
+    }
+}

+ 22 - 0
src/ChangeableDocument/Actions/Structure/MoveStructureMember_Action.cs

@@ -0,0 +1,22 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions.Structure;
+
+public record struct MoveStructureMember_Action : IMakeChangeAction
+{
+    public MoveStructureMember_Action(Guid member, Guid targetFolder, int index)
+    {
+        Member = member;
+        TargetFolder = targetFolder;
+        Index = index;
+    }
+
+    public Guid Member { get; init; }
+    public Guid TargetFolder { get; init; }
+    public int Index { get; init; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new MoveStructureMember_Change(Member, TargetFolder, Index);
+    }
+}

+ 0 - 89
src/ChangeableDocument/Actions/StructureActions.cs

@@ -1,89 +0,0 @@
-using ChangeableDocument.Changes;
-
-namespace ChangeableDocument.Actions;
-public record struct CreateStructureMember_Action : IMakeChangeAction
-{
-    public CreateStructureMember_Action(Guid parentGuid, int index, StructureMemberType type)
-    {
-        ParentGuid = parentGuid;
-        Index = index;
-        Type = type;
-    }
-
-    public Guid ParentGuid { get; init; }
-    public int Index { get; init; }
-    public StructureMemberType Type { get; init; }
-
-    IChange IMakeChangeAction.CreateCorrespondingChange()
-    {
-        return new CreateStructureMember_Change(ParentGuid, Index, Type);
-    }
-}
-
-public record struct DeleteStructureMember_Action : IMakeChangeAction
-{
-    public DeleteStructureMember_Action(Guid guidValue)
-    {
-        GuidValue = guidValue;
-    }
-
-    public Guid GuidValue { get; }
-
-    IChange IMakeChangeAction.CreateCorrespondingChange()
-    {
-        return new DeleteStructureMember_Change(GuidValue);
-    }
-}
-
-public record struct MoveStructureMember_Action : IMakeChangeAction
-{
-    public MoveStructureMember_Action(Guid member, Guid targetFolder, int index)
-    {
-        Member = member;
-        TargetFolder = targetFolder;
-        Index = index;
-    }
-
-    public Guid Member { get; init; }
-    public Guid TargetFolder { get; init; }
-    public int Index { get; init; }
-
-    IChange IMakeChangeAction.CreateCorrespondingChange()
-    {
-        return new MoveStructureMember_Change(Member, TargetFolder, Index);
-    }
-}
-
-public record struct SetStructureMemberName_Action : IMakeChangeAction
-{
-    public SetStructureMemberName_Action(string name, Guid guidValue)
-    {
-        Name = name;
-        GuidValue = guidValue;
-    }
-
-    public string Name { get; init; }
-    public Guid GuidValue { get; init; }
-
-    IChange IMakeChangeAction.CreateCorrespondingChange()
-    {
-        return new StructureMemberProperties_Change(GuidValue) { NewName = Name };
-    }
-}
-
-public record struct SetStructureMemberVisibility_Action : IMakeChangeAction
-{
-    public SetStructureMemberVisibility_Action(bool isVisible, Guid guidValue)
-    {
-        this.isVisible = isVisible;
-        GuidValue = guidValue;
-    }
-
-    public bool isVisible { get; init; }
-    public Guid GuidValue { get; init; }
-
-    IChange IMakeChangeAction.CreateCorrespondingChange()
-    {
-        return new StructureMemberProperties_Change(GuidValue) { NewIsVisible = isVisible };
-    }
-}

+ 5 - 0
src/ChangeableDocument/Actions/Undo/RedoAction.cs

@@ -0,0 +1,5 @@
+namespace ChangeableDocument.Actions.Undo;
+
+public record struct RedoAction : IAction
+{
+}

+ 5 - 0
src/ChangeableDocument/Actions/Undo/UndoAction.cs

@@ -0,0 +1,5 @@
+namespace ChangeableDocument.Actions.Undo;
+
+public record struct UndoAction : IAction
+{
+}

+ 9 - 0
src/ChangeableDocument/ChangeInfos/Selection_ChangeInfo.cs

@@ -0,0 +1,9 @@
+using ChunkyImageLib.DataHolders;
+
+namespace ChangeableDocument.ChangeInfos
+{
+    public record struct Selection_ChangeInfo : IChangeInfo
+    {
+        public HashSet<Vector2i>? Chunks { get; init; }
+    }
+}

+ 5 - 3
src/ChangeableDocument/Changeables/Document.cs

@@ -5,12 +5,14 @@ namespace ChangeableDocument.Changeables
     internal class Document : IChangeable, IReadOnlyDocument
     {
         public IReadOnlyFolder ReadOnlyStructureRoot => StructureRoot;
+        public IReadOnlySelection ReadOnlySelection => Selection;
         IReadOnlyStructureMember? IReadOnlyDocument.FindMember(Guid guid) => FindMember(guid);
         IReadOnlyList<IReadOnlyStructureMember> IReadOnlyDocument.FindMemberPath(Guid guid) => FindMemberPath(guid);
         IReadOnlyStructureMember IReadOnlyDocument.FindMemberOrThrow(Guid guid) => FindMemberOrThrow(guid);
         (IReadOnlyStructureMember, IReadOnlyFolder) IReadOnlyDocument.FindChildAndParentOrThrow(Guid guid) => FindChildAndParentOrThrow(guid);
 
-        internal Folder StructureRoot { get; set; } = new() { GuidValue = Guid.Empty };
+        internal Folder StructureRoot { get; } = new() { ReadOnlyGuidValue = Guid.Empty };
+        internal Selection Selection { get; } = new();
 
         public StructureMember FindMemberOrThrow(Guid guid) => FindMember(guid) ?? throw new Exception("Could not find member with guid " + guid.ToString());
         public StructureMember? FindMember(Guid guid)
@@ -37,13 +39,13 @@ namespace ChangeableDocument.Changeables
 
         private bool FillMemberPath(Folder folder, Guid guid, List<StructureMember> toFill)
         {
-            if (folder.GuidValue == guid)
+            if (folder.ReadOnlyGuidValue == guid)
             {
                 return true;
             }
             foreach (var member in folder.Children)
             {
-                if (member is Layer childLayer && childLayer.GuidValue == guid)
+                if (member is Layer childLayer && childLayer.ReadOnlyGuidValue == guid)
                 {
                     toFill.Add(member);
                     return true;

+ 4 - 4
src/ChangeableDocument/Changeables/Folder.cs

@@ -17,10 +17,10 @@ namespace ChangeableDocument.Changeables
 
             return new Folder()
             {
-                GuidValue = GuidValue,
-                IsVisible = IsVisible,
-                Name = Name,
-                Opacity = Opacity,
+                ReadOnlyGuidValue = ReadOnlyGuidValue,
+                ReadOnlyIsVisible = ReadOnlyIsVisible,
+                ReadOnlyName = ReadOnlyName,
+                ReadOnlyOpacity = ReadOnlyOpacity,
                 Children = clonedChildren
             };
         }

+ 1 - 0
src/ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs

@@ -3,6 +3,7 @@
     public interface IReadOnlyDocument
     {
         IReadOnlyFolder ReadOnlyStructureRoot { get; }
+        IReadOnlySelection ReadOnlySelection { get; }
         IReadOnlyStructureMember? FindMember(Guid guid);
         IReadOnlyStructureMember FindMemberOrThrow(Guid guid);
         (IReadOnlyStructureMember, IReadOnlyFolder) FindChildAndParentOrThrow(Guid guid);

+ 1 - 1
src/ChangeableDocument/Changeables/Interfaces/IReadOnlyLayer.cs

@@ -4,6 +4,6 @@ namespace ChangeableDocument.Changeables.Interfaces
 {
     public interface IReadOnlyLayer : IReadOnlyStructureMember
     {
-        IReadOnlyChunkyImage LayerImage { get; }
+        IReadOnlyChunkyImage ReadOnlyLayerImage { get; }
     }
 }

+ 10 - 0
src/ChangeableDocument/Changeables/Interfaces/IReadOnlySelection.cs

@@ -0,0 +1,10 @@
+using ChunkyImageLib;
+
+namespace ChangeableDocument.Changeables.Interfaces
+{
+    public interface IReadOnlySelection
+    {
+        public IReadOnlyChunkyImage ReadOnlySelectionImage { get; }
+        public bool ReadOnlyIsEmptyAndInactive { get; }
+    }
+}

+ 4 - 4
src/ChangeableDocument/Changeables/Interfaces/IReadOnlyStructureMember.cs

@@ -2,9 +2,9 @@
 {
     public interface IReadOnlyStructureMember
     {
-        bool IsVisible { get; }
-        string Name { get; }
-        Guid GuidValue { get; }
-        float Opacity { get; }
+        bool ReadOnlyIsVisible { get; }
+        string ReadOnlyName { get; }
+        Guid ReadOnlyGuidValue { get; }
+        float ReadOnlyOpacity { get; }
     }
 }

+ 6 - 6
src/ChangeableDocument/Changeables/Layer.cs

@@ -6,17 +6,17 @@ namespace ChangeableDocument.Changeables
     internal class Layer : StructureMember, IReadOnlyLayer
     {
         public ChunkyImage LayerImage { get; set; } = new();
-        IReadOnlyChunkyImage IReadOnlyLayer.LayerImage => LayerImage;
+        IReadOnlyChunkyImage IReadOnlyLayer.ReadOnlyLayerImage => LayerImage;
 
         internal override Layer Clone()
         {
             return new Layer()
             {
-                GuidValue = GuidValue,
-                IsVisible = IsVisible,
-                Name = Name,
-                Opacity = Opacity,
-                LayerImage = LayerImage.Clone()
+                ReadOnlyGuidValue = ReadOnlyGuidValue,
+                ReadOnlyIsVisible = ReadOnlyIsVisible,
+                ReadOnlyName = ReadOnlyName,
+                ReadOnlyOpacity = ReadOnlyOpacity,
+                LayerImage = LayerImage.CloneFromLatest()
             };
         }
     }

+ 16 - 0
src/ChangeableDocument/Changeables/Selection.cs

@@ -0,0 +1,16 @@
+using ChangeableDocument.Changeables.Interfaces;
+using ChunkyImageLib;
+using SkiaSharp;
+
+namespace ChangeableDocument.Changeables
+{
+    internal class Selection : IReadOnlySelection
+    {
+        public static SKColor SelectionColor { get; } = SKColors.CornflowerBlue;
+        public bool IsEmptyAndInactive { get; set; } = true;
+        public ChunkyImage SelectionImage { get; set; } = new();
+
+        public IReadOnlyChunkyImage ReadOnlySelectionImage => SelectionImage;
+        public bool ReadOnlyIsEmptyAndInactive => IsEmptyAndInactive;
+    }
+}

+ 4 - 4
src/ChangeableDocument/Changeables/StructureMember.cs

@@ -4,10 +4,10 @@ namespace ChangeableDocument.Changeables
 {
     internal abstract class StructureMember : IChangeable, IReadOnlyStructureMember
     {
-        public float Opacity { get; set; } = 1f;
-        public bool IsVisible { get; set; } = true;
-        public string Name { get; set; } = "Unnamed";
-        public Guid GuidValue { get; init; }
+        public float ReadOnlyOpacity { get; set; } = 1f;
+        public bool ReadOnlyIsVisible { get; set; } = true;
+        public string ReadOnlyName { get; set; } = "Unnamed";
+        public Guid ReadOnlyGuidValue { get; init; }
 
         internal abstract StructureMember Clone();
     }

+ 3 - 3
src/ChangeableDocument/Changes/CreateStructureMember_Change.cs

@@ -29,8 +29,8 @@ namespace ChangeableDocument.Changes
 
             StructureMember member = type switch
             {
-                StructureMemberType.Layer => new Layer() { GuidValue = newMemberGuid },
-                StructureMemberType.Folder => new Folder() { GuidValue = newMemberGuid },
+                StructureMemberType.Layer => new Layer() { ReadOnlyGuidValue = newMemberGuid },
+                StructureMemberType.Folder => new Folder() { ReadOnlyGuidValue = newMemberGuid },
                 _ => throw new Exception("Cannon create member of type " + type.ToString())
             };
 
@@ -42,7 +42,7 @@ namespace ChangeableDocument.Changes
         public IChangeInfo Revert(Document document)
         {
             var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
-            folder.Children.RemoveAt(folder.Children.FindIndex(child => child.GuidValue == newMemberGuid));
+            folder.Children.RemoveAt(folder.Children.FindIndex(child => child.ReadOnlyGuidValue == newMemberGuid));
 
             return new DeleteStructureMember_ChangeInfo() { GuidValue = newMemberGuid };
         }

+ 1 - 1
src/ChangeableDocument/Changes/DeleteStructureMember_Change.cs

@@ -19,7 +19,7 @@ namespace ChangeableDocument.Changes
             var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
 
             originalIndex = parent.Children.IndexOf(member);
-            parentGuid = parent.GuidValue;
+            parentGuid = parent.ReadOnlyGuidValue;
             savedCopy = member.Clone();
         }
 

+ 49 - 0
src/ChangeableDocument/Changes/Drawing/ClearSelection_Change.cs

@@ -0,0 +1,49 @@
+using ChangeableDocument.Changeables;
+using ChangeableDocument.ChangeInfos;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+
+namespace ChangeableDocument.Changes.Drawing
+{
+    internal class ClearSelection_Change : IChange
+    {
+        private bool originalIsEmpty;
+        private CommitedChunkStorage? savedSelection;
+        public void Initialize(Document target)
+        {
+            originalIsEmpty = target.Selection.IsEmptyAndInactive;
+            if (!originalIsEmpty)
+                savedSelection = new(target.Selection.SelectionImage, target.Selection.SelectionImage.FindAllChunks());
+        }
+
+        public IChangeInfo? Apply(Document target)
+        {
+            if (originalIsEmpty)
+                return new Selection_ChangeInfo() { Chunks = new() };
+            target.Selection.IsEmptyAndInactive = true;
+
+            target.Selection.SelectionImage.CancelChanges();
+            target.Selection.SelectionImage.Clear();
+            HashSet<Vector2i> affChunks = target.Selection.SelectionImage.FindAffectedChunks();
+            target.Selection.SelectionImage.CommitChanges();
+
+            return new Selection_ChangeInfo() { Chunks = affChunks };
+        }
+
+        public IChangeInfo? Revert(Document target)
+        {
+            if (originalIsEmpty)
+                return new Selection_ChangeInfo() { Chunks = new() };
+            if (savedSelection == null)
+                throw new Exception("No saved selection to restore");
+            target.Selection.IsEmptyAndInactive = false;
+
+            target.Selection.SelectionImage.CancelChanges();
+            savedSelection.ApplyChunksToImage(target.Selection.SelectionImage);
+            HashSet<Vector2i> affChunks = target.Selection.SelectionImage.FindAffectedChunks();
+            target.Selection.SelectionImage.CommitChanges();
+
+            return new Selection_ChangeInfo() { Chunks = affChunks };
+        }
+    }
+}

+ 4 - 2
src/ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs

@@ -9,7 +9,7 @@ namespace ChangeableDocument.Changes.Drawing
     {
         private Guid layerGuid;
         private ShapeData rect;
-        private ChunkStorage? storedChunks;
+        private CommitedChunkStorage? storedChunks;
         public DrawRectangle_UpdateableChange(Guid layerGuid, ShapeData rectangle)
         {
             this.layerGuid = layerGuid;
@@ -28,6 +28,8 @@ namespace ChangeableDocument.Changes.Drawing
             Layer layer = (Layer)target.FindMemberOrThrow(layerGuid);
             var oldChunks = layer.LayerImage.FindAffectedChunks();
             layer.LayerImage.CancelChanges();
+            if (!target.Selection.IsEmptyAndInactive)
+                layer.LayerImage.ApplyRasterClip(target.Selection.SelectionImage);
             layer.LayerImage.DrawRectangle(rect);
             var newChunks = layer.LayerImage.FindAffectedChunks();
             newChunks.UnionWith(oldChunks);
@@ -42,7 +44,7 @@ namespace ChangeableDocument.Changes.Drawing
         {
             Layer layer = (Layer)target.FindMemberOrThrow(layerGuid);
             var changes = ApplyTemporarily(target);
-            storedChunks = new ChunkStorage(layer.LayerImage, layer.LayerImage.FindAffectedChunks());
+            storedChunks = new CommitedChunkStorage(layer.LayerImage, ((LayerImageChunks_ChangeInfo)changes!).Chunks!);
             layer.LayerImage.CommitChanges();
             return changes;
         }

+ 64 - 0
src/ChangeableDocument/Changes/Drawing/SelectRectangle_UpdateableChange.cs

@@ -0,0 +1,64 @@
+using ChangeableDocument.Changeables;
+using ChangeableDocument.ChangeInfos;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using SkiaSharp;
+
+namespace ChangeableDocument.Changes.Drawing
+{
+    internal class SelectRectangle_UpdateableChange : IUpdateableChange
+    {
+        private bool originalIsEmpty;
+        private Vector2i pos;
+        private Vector2i size;
+        private CommitedChunkStorage? originalSelectionState;
+        public SelectRectangle_UpdateableChange(Vector2i pos, Vector2i size)
+        {
+            Update(pos, size);
+        }
+        public void Initialize(Document target)
+        {
+            originalIsEmpty = target.Selection.IsEmptyAndInactive;
+        }
+
+        public void Update(Vector2i pos, Vector2i size)
+        {
+            this.pos = pos;
+            this.size = size;
+        }
+
+        public IChangeInfo? ApplyTemporarily(Document target)
+        {
+            var oldChunks = target.Selection.SelectionImage.FindAffectedChunks();
+            target.Selection.SelectionImage.CancelChanges();
+            target.Selection.IsEmptyAndInactive = false;
+            target.Selection.SelectionImage.DrawRectangle(new ShapeData(pos, size, 0, SKColors.Transparent, Selection.SelectionColor));
+
+            oldChunks.UnionWith(target.Selection.SelectionImage.FindAffectedChunks());
+            return new Selection_ChangeInfo() { Chunks = oldChunks };
+        }
+
+        public IChangeInfo? Apply(Document target)
+        {
+            var changes = ApplyTemporarily(target);
+            originalSelectionState = new CommitedChunkStorage(target.Selection.SelectionImage, ((Selection_ChangeInfo)changes!).Chunks!);
+            target.Selection.SelectionImage.CommitChanges();
+            target.Selection.IsEmptyAndInactive = target.Selection.SelectionImage.CheckIfCommitedIsEmpty();
+            return changes;
+        }
+
+        public IChangeInfo? Revert(Document target)
+        {
+            if (originalSelectionState == null)
+                throw new Exception("No stored chunks to revert to");
+
+            target.Selection.IsEmptyAndInactive = originalIsEmpty;
+            originalSelectionState.ApplyChunksToImage(target.Selection.SelectionImage);
+            originalSelectionState.Dispose();
+            originalSelectionState = null;
+            var changes = new Selection_ChangeInfo() { Chunks = target.Selection.SelectionImage.FindAffectedChunks() };
+            target.Selection.SelectionImage.CommitChanges();
+            return changes;
+        }
+    }
+}

+ 1 - 1
src/ChangeableDocument/Changes/MoveStructureMember_Change.cs

@@ -23,7 +23,7 @@ namespace ChangeableDocument.Changes
         public void Initialize(Document document)
         {
             var (member, curFolder) = document.FindChildAndParentOrThrow(memberGuid);
-            originalFolderGuid = curFolder.GuidValue;
+            originalFolderGuid = curFolder.ReadOnlyGuidValue;
             originalFolderIndex = curFolder.Children.IndexOf(member);
         }
 

+ 3 - 3
src/ChangeableDocument/Changes/StructureMemberOpacity_UpdateableChange.cs

@@ -24,7 +24,7 @@ namespace ChangeableDocument.Changes
         public void Initialize(Document document)
         {
             var member = document.FindMemberOrThrow(memberGuid);
-            originalOpacity = member.Opacity;
+            originalOpacity = member.ReadOnlyOpacity;
         }
 
         public IChangeInfo? ApplyTemporarily(Document target) => Apply(target);
@@ -32,7 +32,7 @@ namespace ChangeableDocument.Changes
         public IChangeInfo? Apply(Document document)
         {
             var member = document.FindMemberOrThrow(memberGuid);
-            member.Opacity = NewOpacity;
+            member.ReadOnlyOpacity = NewOpacity;
 
             return new StructureMemberOpacity_ChangeInfo() { GuidValue = memberGuid };
         }
@@ -40,7 +40,7 @@ namespace ChangeableDocument.Changes
         public IChangeInfo? Revert(Document document)
         {
             var member = document.FindMemberOrThrow(memberGuid);
-            member.Opacity = originalOpacity;
+            member.ReadOnlyOpacity = originalOpacity;
 
             return new StructureMemberOpacity_ChangeInfo() { GuidValue = memberGuid };
         }

+ 8 - 8
src/ChangeableDocument/Changes/StructureMemberProperties_Change.cs

@@ -21,19 +21,19 @@ namespace ChangeableDocument.Changes
         public void Initialize(Document document)
         {
             var member = document.FindMemberOrThrow(memberGuid);
-            if (NewIsVisible != null) originalIsVisible = member.IsVisible;
-            if (NewName != null) originalName = member.Name;
+            if (NewIsVisible != null) originalIsVisible = member.ReadOnlyIsVisible;
+            if (NewName != null) originalName = member.ReadOnlyName;
         }
 
         public IChangeInfo? Apply(Document document)
         {
             var member = document.FindMemberOrThrow(memberGuid);
-            if (NewIsVisible != null) member.IsVisible = NewIsVisible.Value;
-            if (NewName != null) member.Name = NewName;
+            if (NewIsVisible != null) member.ReadOnlyIsVisible = NewIsVisible.Value;
+            if (NewName != null) member.ReadOnlyName = NewName;
 
             return new StructureMemberProperties_ChangeInfo()
             {
-                GuidValue = member.GuidValue,
+                GuidValue = member.ReadOnlyGuidValue,
                 IsVisibleChanged = NewIsVisible != null,
                 NameChanged = NewName != null
             };
@@ -42,12 +42,12 @@ namespace ChangeableDocument.Changes
         public IChangeInfo? Revert(Document document)
         {
             var member = document.FindMemberOrThrow(memberGuid);
-            if (NewIsVisible != null) member.IsVisible = originalIsVisible;
-            if (NewName != null) member.Name = originalName!;
+            if (NewIsVisible != null) member.ReadOnlyIsVisible = originalIsVisible;
+            if (NewName != null) member.ReadOnlyName = originalName!;
 
             return new StructureMemberProperties_ChangeInfo()
             {
-                GuidValue = member.GuidValue,
+                GuidValue = member.ReadOnlyGuidValue,
                 IsVisibleChanged = NewIsVisible != null,
                 NameChanged = NewName != null,
             };

+ 1 - 0
src/ChangeableDocument/DocumentChangeTracker.cs

@@ -1,4 +1,5 @@
 using ChangeableDocument.Actions;
+using ChangeableDocument.Actions.Undo;
 using ChangeableDocument.Changeables;
 using ChangeableDocument.Changeables.Interfaces;
 using ChangeableDocument.ChangeInfos;

+ 4 - 3
src/ChunkyImageLib/Chunk.cs

@@ -1,4 +1,5 @@
-using SkiaSharp;
+using ChunkyImageLib.DataHolders;
+using SkiaSharp;
 
 namespace ChunkyImageLib
 {
@@ -10,9 +11,9 @@ namespace ChunkyImageLib
             Surface = new Surface(ChunkPool.ChunkSize, ChunkPool.ChunkSize);
         }
 
-        public SKImage Snapshot()
+        public void DrawOnSurface(SKSurface surface, Vector2i pos, SKPaint? paint = null)
         {
-            return Surface.SkiaSurface.Snapshot();
+            surface.Canvas.DrawSurface(Surface.SkiaSurface, pos.X, pos.Y, paint);
         }
 
         public void Dispose()

+ 129 - 49
src/ChunkyImageLib/ChunkyImage.cs

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
+using SkiaSharp;
 using System.Runtime.CompilerServices;
 
 [assembly: InternalsVisibleTo("ChunkyImageLibTest")]
@@ -7,22 +8,28 @@ namespace ChunkyImageLib
 {
     public class ChunkyImage : IReadOnlyChunkyImage
     {
-        private bool locked = false; //todo implement locking
-
         private Queue<(IOperation, HashSet<Vector2i>)> queuedOperations = new();
 
-        private Dictionary<Vector2i, Chunk> chunks = new();
-        private Dictionary<Vector2i, Chunk> uncommitedChunks = new();
+        private Dictionary<Vector2i, Chunk> commitedChunks = new();
+        private Dictionary<Vector2i, Chunk> latestChunks = new();
+        private Chunk tempChunk;
+
+        private static SKPaint ClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstIn };
 
         public static int ChunkSize => ChunkPool.ChunkSize;
 
-        public ChunkyImage Clone()
+        public ChunkyImage()
+        {
+            tempChunk = ChunkPool.Instance.BorrowChunk();
+        }
+
+        public ChunkyImage CloneFromLatest()
         {
             ChunkyImage output = new();
             var chunks = FindAllChunks();
             foreach (var chunk in chunks)
             {
-                var image = GetChunk(chunk);
+                var image = GetLatestChunk(chunk);
                 if (image != null)
                     output.DrawImage(chunk * ChunkSize, image.Surface);
             }
@@ -30,17 +37,17 @@ namespace ChunkyImageLib
             return output;
         }
 
-        public Chunk? GetChunk(Vector2i pos)
+        public Chunk? GetLatestChunk(Vector2i pos)
         {
             if (queuedOperations.Count == 0)
-                return MaybeGetChunk(pos, chunks);
+                return MaybeGetChunk(pos, commitedChunks);
             ProcessQueue(pos);
-            return MaybeGetChunk(pos, uncommitedChunks) ?? MaybeGetChunk(pos, chunks);
+            return MaybeGetChunk(pos, latestChunks) ?? MaybeGetChunk(pos, commitedChunks);
         }
 
         internal Chunk? GetCommitedChunk(Vector2i pos)
         {
-            return MaybeGetChunk(pos, chunks);
+            return MaybeGetChunk(pos, commitedChunks);
         }
 
         private Chunk? MaybeGetChunk(Vector2i pos, Dictionary<Vector2i, Chunk> from) => from.ContainsKey(pos) ? from[pos] : null;
@@ -48,13 +55,25 @@ namespace ChunkyImageLib
         public void DrawRectangle(ShapeData rect)
         {
             RectangleOperation operation = new(rect);
-            queuedOperations.Enqueue((operation, operation.FindAffectedChunks()));
+            queuedOperations.Enqueue((operation, operation.FindAffectedChunks(this)));
         }
 
         internal void DrawImage(Vector2i pos, Surface image)
         {
             ImageOperation operation = new(pos, image);
-            queuedOperations.Enqueue((operation, operation.FindAffectedChunks()));
+            queuedOperations.Enqueue((operation, operation.FindAffectedChunks(this)));
+        }
+
+        public void Clear()
+        {
+            ClearOperation operation = new();
+            queuedOperations.Enqueue((operation, operation.FindAffectedChunks(this)));
+        }
+
+        public void ApplyRasterClip(ChunkyImage clippingMask)
+        {
+            RasterClipOperation operation = new(clippingMask);
+            queuedOperations.Enqueue((operation, new()));
         }
 
         public void CancelChanges()
@@ -62,23 +81,30 @@ namespace ChunkyImageLib
             foreach (var operation in queuedOperations)
                 operation.Item1.Dispose();
             queuedOperations.Clear();
-            foreach (var (_, chunk) in uncommitedChunks)
+            foreach (var (_, chunk) in latestChunks)
             {
                 ChunkPool.Instance.ReturnChunk(chunk);
             }
-            uncommitedChunks.Clear();
+            latestChunks.Clear();
         }
 
         public void CommitChanges()
         {
-            SwapUncommitedChunks();
-            ProcessQueueFinal();
+            var affectedChunks = FindAffectedChunks();
+            foreach (var chunk in affectedChunks)
+            {
+                ProcessQueue(chunk);
+            }
+            foreach (var (operation, operChunks) in queuedOperations)
+                operation.Dispose();
+            queuedOperations.Clear();
+            CommitLatestChunks();
         }
 
         public HashSet<Vector2i> FindAllChunks()
         {
-            var allChunks = chunks.Select(chunk => chunk.Key).ToHashSet();
-            allChunks.UnionWith(uncommitedChunks.Select(chunk => chunk.Key).ToHashSet());
+            var allChunks = commitedChunks.Select(chunk => chunk.Key).ToHashSet();
+            allChunks.UnionWith(latestChunks.Select(chunk => chunk.Key).ToHashSet());
             foreach (var (operation, opChunks) in queuedOperations)
             {
                 allChunks.UnionWith(opChunks);
@@ -88,7 +114,7 @@ namespace ChunkyImageLib
 
         public HashSet<Vector2i> FindAffectedChunks()
         {
-            var chunks = uncommitedChunks.Select(chunk => chunk.Key).ToHashSet();
+            var chunks = latestChunks.Select(chunk => chunk.Key).ToHashSet();
             foreach (var (operation, opChunks) in queuedOperations)
             {
                 chunks.UnionWith(opChunks);
@@ -96,76 +122,130 @@ namespace ChunkyImageLib
             return chunks;
         }
 
-        private void ProcessQueueFinal()
+        private void CommitLatestChunks()
         {
-            foreach (var (operation, operChunks) in queuedOperations)
+            foreach (var (pos, chunk) in latestChunks)
             {
-                foreach (var pos in operChunks)
+                if (commitedChunks.ContainsKey(pos))
                 {
-                    operation.DrawOnChunk(GetOrCreateCommitedChunk(pos), pos);
+                    var oldChunk = commitedChunks[pos];
+                    commitedChunks.Remove(pos);
+                    ChunkPool.Instance.ReturnChunk(oldChunk);
                 }
-                operation.Dispose();
+                commitedChunks.Add(pos, chunk);
             }
-            queuedOperations.Clear();
+            latestChunks.Clear();
         }
 
-        private void SwapUncommitedChunks()
+        private void ProcessQueue(Vector2i chunkPos)
         {
-            foreach (var (pos, chunk) in uncommitedChunks)
+            Chunk? targetChunk = null;
+            List<RasterClipOperation> clips = new();
+            foreach (var (operation, operChunks) in queuedOperations)
             {
-                if (chunks.ContainsKey(pos))
+                if (operation is IChunkOperation chunkOperation)
                 {
-                    var oldChunk = chunks[pos];
-                    chunks.Remove(pos);
-                    ChunkPool.Instance.ReturnChunk(oldChunk);
+                    if (!operChunks.Contains(chunkPos))
+                        continue;
+                    operChunks.Remove(chunkPos);
+
+                    if (targetChunk == null)
+                        targetChunk = GetOrCreateLatestChunk(chunkPos);
+
+                    if (clips.Count == 0)
+                    {
+                        chunkOperation.DrawOnChunk(targetChunk, chunkPos);
+                        continue;
+                    }
+
+                    List<Chunk> masks = new();
+                    foreach (var clip in clips)
+                    {
+                        var chunk = clip.ClippingMask.GetCommitedChunk(chunkPos);
+                        if (chunk == null)
+                        {
+                            //the chunked is fully masked out, no point to draw any further operations
+                            return;
+                        }
+                        masks.Add(chunk);
+                    }
+
+                    if (masks.Count == 0)
+                    {
+                        chunkOperation.DrawOnChunk(targetChunk, chunkPos);
+                    }
+                    else
+                    {
+                        tempChunk.Surface.SkiaSurface.Canvas.Clear();
+                        chunkOperation.DrawOnChunk(tempChunk, chunkPos);
+                        foreach (var mask in masks)
+                        {
+                            tempChunk.Surface.SkiaSurface.Canvas.DrawSurface(mask.Surface.SkiaSurface, 0, 0, ClippingPaint);
+                        }
+                        tempChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0));
+                    }
+                }
+                else if (operation is RasterClipOperation clipOperation)
+                {
+                    clips.Add(clipOperation);
                 }
-                chunks.Add(pos, chunk);
             }
-            uncommitedChunks.Clear();
         }
 
-        private void ProcessQueue(Vector2i chunkPos)
+        public bool CheckIfCommitedIsEmpty()
         {
-            Chunk? targetChunk = null;
-            foreach (var (operation, operChunks) in queuedOperations)
-            {
-                if (!operChunks.Contains(chunkPos))
-                    continue;
-                operChunks.Remove(chunkPos);
+            FindAndDeleteEmptyCommitedChunks();
+            return commitedChunks.Count == 0;
+        }
 
-                if (targetChunk == null)
-                    targetChunk = GetOrCreateUncommitedChunk(chunkPos);
+        private void FindAndDeleteEmptyCommitedChunks()
+        {
+            foreach (var (pos, chunk) in commitedChunks)
+            {
+                if (IsChunkEmpty(chunk))
+                    commitedChunks.Remove(pos);
+            }
+        }
 
-                operation.DrawOnChunk(targetChunk, chunkPos);
+        private unsafe bool IsChunkEmpty(Chunk chunk)
+        {
+            ulong* ptr = (ulong*)chunk.Surface.PixelBuffer;
+            for (int i = 0; i < ChunkSize * ChunkSize; i++)
+            {
+                // ptr[i] actually contains 4 16-bit floats. We only care about the first one which is alpha.
+                // An empty pixel can have alpha of 0 or -0 (not sure if -0 actually ever comes up). 0 in hex is 0x0, -0 in hex is 0x8000
+                if ((ptr[i] & 0x1111_0000_0000_0000) != 0 && (ptr[i] & 0x1111_0000_0000_0000) != 0x8000_0000_0000_0000)
+                    return false;
             }
+            return true;
         }
 
         private Chunk GetOrCreateCommitedChunk(Vector2i chunkPos)
         {
-            Chunk? targetChunk = MaybeGetChunk(chunkPos, chunks);
+            Chunk? targetChunk = MaybeGetChunk(chunkPos, commitedChunks);
             if (targetChunk != null)
                 return targetChunk;
             var newChunk = ChunkPool.Instance.BorrowChunk();
             newChunk.Surface.SkiaSurface.Canvas.Clear();
-            chunks[chunkPos] = newChunk;
+            commitedChunks[chunkPos] = newChunk;
             return newChunk;
         }
 
-        private Chunk GetOrCreateUncommitedChunk(Vector2i chunkPos)
+        private Chunk GetOrCreateLatestChunk(Vector2i chunkPos)
         {
             Chunk? targetChunk;
-            targetChunk = MaybeGetChunk(chunkPos, uncommitedChunks);
+            targetChunk = MaybeGetChunk(chunkPos, latestChunks);
             if (targetChunk == null)
             {
                 targetChunk = ChunkPool.Instance.BorrowChunk();
-                var maybeCommitedChunk = MaybeGetChunk(chunkPos, chunks);
+                var maybeCommitedChunk = MaybeGetChunk(chunkPos, commitedChunks);
 
                 if (maybeCommitedChunk != null)
                     maybeCommitedChunk.Surface.CopyTo(targetChunk.Surface);
                 else
                     targetChunk.Surface.SkiaSurface.Canvas.Clear();
 
-                uncommitedChunks[chunkPos] = targetChunk;
+                latestChunks[chunkPos] = targetChunk;
             }
             return targetChunk;
         }

+ 2 - 2
src/ChunkyImageLib/ChunkStorage.cs → src/ChunkyImageLib/CommitedChunkStorage.cs

@@ -2,11 +2,11 @@
 
 namespace ChunkyImageLib
 {
-    public class ChunkStorage : IDisposable
+    public class CommitedChunkStorage : IDisposable
     {
         private bool disposed = false;
         private List<(Vector2i, Chunk?)> savedChunks = new();
-        public ChunkStorage(ChunkyImage image, HashSet<Vector2i> commitedChunksToSave)
+        public CommitedChunkStorage(ChunkyImage image, HashSet<Vector2i> commitedChunksToSave)
         {
             foreach (var chunkPos in commitedChunksToSave)
             {

+ 1 - 1
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -4,7 +4,7 @@ namespace ChunkyImageLib
 {
     public interface IReadOnlyChunkyImage
     {
-        Chunk? GetChunk(Vector2i pos);
+        Chunk? GetLatestChunk(Vector2i pos);
         HashSet<Vector2i> FindAffectedChunks();
         HashSet<Vector2i> FindAllChunks();
     }

+ 19 - 0
src/ChunkyImageLib/Operations/ClearOperation.cs

@@ -0,0 +1,19 @@
+using ChunkyImageLib.DataHolders;
+
+namespace ChunkyImageLib.Operations
+{
+    internal record class ClearOperation : IChunkOperation
+    {
+        public void DrawOnChunk(Chunk chunk, Vector2i chunkPos)
+        {
+            chunk.Surface.SkiaSurface.Canvas.Clear();
+        }
+
+        public HashSet<Vector2i> FindAffectedChunks(IReadOnlyChunkyImage image)
+        {
+            return image.FindAllChunks();
+        }
+
+        public void Dispose() { }
+    }
+}

+ 10 - 0
src/ChunkyImageLib/Operations/IChunkOperation.cs

@@ -0,0 +1,10 @@
+using ChunkyImageLib.DataHolders;
+
+namespace ChunkyImageLib.Operations
+{
+    internal interface IChunkOperation : IOperation
+    {
+        void DrawOnChunk(Chunk chunk, Vector2i chunkPos);
+        HashSet<Vector2i> FindAffectedChunks(IReadOnlyChunkyImage image);
+    }
+}

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

@@ -1,10 +1,6 @@
-using ChunkyImageLib.DataHolders;
-
-namespace ChunkyImageLib.Operations
+namespace ChunkyImageLib.Operations
 {
     internal interface IOperation : IDisposable
     {
-        void DrawOnChunk(Chunk chunk, Vector2i chunkPos);
-        HashSet<Vector2i> FindAffectedChunks();
     }
 }

+ 2 - 2
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -3,7 +3,7 @@ using SkiaSharp;
 
 namespace ChunkyImageLib.Operations
 {
-    internal record class ImageOperation : IOperation
+    internal record class ImageOperation : IChunkOperation
     {
         private Vector2i pos;
         private Surface toPaint;
@@ -19,7 +19,7 @@ namespace ChunkyImageLib.Operations
             chunk.Surface.SkiaSurface.Canvas.DrawSurface(toPaint.SkiaSurface, pos - chunkPos * ChunkPool.ChunkSize, ReplacingPaint);
         }
 
-        public HashSet<Vector2i> FindAffectedChunks()
+        public HashSet<Vector2i> FindAffectedChunks(IReadOnlyChunkyImage image)
         {
             Vector2i start = OperationHelper.GetChunkPos(pos, ChunkPool.ChunkSize);
             Vector2i end = OperationHelper.GetChunkPos(new(pos.X + toPaint.Width - 1, pos.Y + toPaint.Height - 1), ChunkPool.ChunkSize);

+ 13 - 0
src/ChunkyImageLib/Operations/RasterClipOperation.cs

@@ -0,0 +1,13 @@
+namespace ChunkyImageLib.Operations
+{
+    internal class RasterClipOperation : IOperation
+    {
+        public ChunkyImage ClippingMask { get; }
+        public RasterClipOperation(ChunkyImage clippingMask)
+        {
+            ClippingMask = clippingMask;
+        }
+
+        public void Dispose() { }
+    }
+}

+ 2 - 2
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -3,7 +3,7 @@ using SkiaSharp;
 
 namespace ChunkyImageLib.Operations
 {
-    internal record class RectangleOperation : IOperation
+    internal record class RectangleOperation : IChunkOperation
     {
         public RectangleOperation(ShapeData rect)
         {
@@ -41,7 +41,7 @@ namespace ChunkyImageLib.Operations
             skiaSurf.Canvas.Restore();
         }
 
-        public HashSet<Vector2i> FindAffectedChunks()
+        public HashSet<Vector2i> FindAffectedChunks(IReadOnlyChunkyImage image)
         {
             if (Data.Size.X < 1 || Data.Size.Y < 1 || Data.StrokeColor.Alpha == 0 && Data.FillColor.Alpha == 0)
                 return new();

+ 1 - 1
src/ChunkyImageLib/Surface.cs

@@ -8,7 +8,7 @@ namespace ChunkyImageLib
     {
         private bool disposed;
         private int bytesPerPixel;
-        private IntPtr PixelBuffer { get; }
+        public IntPtr PixelBuffer { get; }
         public SKSurface SkiaSurface { get; }
         public int Width { get; }
         public int Height { get; }

+ 1 - 1
src/PixiEditorPrototype/MainWindow.xaml

@@ -6,7 +6,7 @@
         xmlns:local="clr-namespace:PixiEditorPrototype"
         xmlns:views="clr-namespace:PixiEditorPrototype.Views"
         mc:Ignorable="d"
-        Title="MainWindow" Height="450" Width="800">
+        Title="MainWindow" Height="576" Width="1024">
     <DockPanel>
         <views:DocumentView DataContext="{Binding Document}"></views:DocumentView>
     </DockPanel>

+ 1 - 1
src/PixiEditorPrototype/Models/DocumentStructureHelper.cs

@@ -1,5 +1,5 @@
 using ChangeableDocument;
-using ChangeableDocument.Actions;
+using ChangeableDocument.Actions.Structure;
 using PixiEditorPrototype.ViewModels;
 using System;
 using System.Collections.Generic;

+ 3 - 3
src/PixiEditorPrototype/Models/DocumentUpdater.cs

@@ -41,7 +41,7 @@ namespace PixiEditorPrototype.Models
         private void ProcessCreateStructureMember(CreateStructureMember_ChangeInfo info)
         {
             var (member, parentFolder) = doc.Tracker.Document.FindChildAndParentOrThrow(info.GuidValue);
-            var parentFolderVM = (FolderViewModel)doc.StructureHelper.FindOrThrow(parentFolder.GuidValue);
+            var parentFolderVM = (FolderViewModel)doc.StructureHelper.FindOrThrow(parentFolder.ReadOnlyGuidValue);
 
             int index = parentFolder.ReadOnlyChildren.IndexOf(member);
 
@@ -58,7 +58,7 @@ namespace PixiEditorPrototype.Models
             {
                 foreach (IReadOnlyStructureMember child in folder2.ReadOnlyChildren)
                 {
-                    ProcessCreateStructureMember(new CreateStructureMember_ChangeInfo() { GuidValue = child.GuidValue });
+                    ProcessCreateStructureMember(new CreateStructureMember_ChangeInfo() { GuidValue = child.ReadOnlyGuidValue });
                 }
             }
         }
@@ -88,7 +88,7 @@ namespace PixiEditorPrototype.Models
             var (member, targetFolder) = doc.Tracker.Document.FindChildAndParentOrThrow(info.GuidValue);
 
             int index = targetFolder.ReadOnlyChildren.IndexOf(member);
-            var targetFolderVM = (FolderViewModel)doc.StructureHelper.FindOrThrow(targetFolder.GuidValue);
+            var targetFolderVM = (FolderViewModel)doc.StructureHelper.FindOrThrow(targetFolder.ReadOnlyGuidValue);
 
             curFolderVM.Children.Remove(memberVM);
             targetFolderVM.Children.Insert(index, memberVM);

+ 8 - 0
src/PixiEditorPrototype/Models/Tool.cs

@@ -0,0 +1,8 @@
+namespace PixiEditorPrototype.Models
+{
+    enum Tool
+    {
+        Rectangle,
+        Select
+    }
+}

+ 89 - 36
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -1,6 +1,9 @@
 using ChangeableDocument;
-using ChangeableDocument.Actions;
-using ChangeableDocument.Actions.Drawing;
+using ChangeableDocument.Actions.Drawing.Rectangle;
+using ChangeableDocument.Actions.Drawing.Selection;
+using ChangeableDocument.Actions.Structure;
+using ChangeableDocument.Actions.Undo;
+using ChunkyImageLib.DataHolders;
 using PixiEditorPrototype.Models;
 using SkiaSharp;
 using System.ComponentModel;
@@ -24,6 +27,8 @@ namespace PixiEditorPrototype.ViewModels
             }
         }
 
+        private Tool activeTool = Tool.Rectangle;
+
         public event PropertyChangedEventHandler? PropertyChanged;
 
         public ActionAccumulator ActionAccumulator { get; }
@@ -35,10 +40,12 @@ namespace PixiEditorPrototype.ViewModels
         public FolderViewModel StructureRoot { get; }
         public RelayCommand? UndoCommand { get; }
         public RelayCommand? RedoCommand { get; }
+        public RelayCommand? ClearSelectionCommand { get; }
         public RelayCommand? CreateNewLayerCommand { get; }
         public RelayCommand? CreateNewFolderCommand { get; }
         public RelayCommand? DeleteStructureMemberCommand { get; }
         public RelayCommand? ChangeSelectedItemCommand { get; }
+        public RelayCommand? ChangeActiveToolCommand { get; }
 
         public RelayCommand? MouseDownCommand { get; }
         public RelayCommand? MouseMoveCommand { get; }
@@ -66,12 +73,14 @@ namespace PixiEditorPrototype.ViewModels
             ActionAccumulator = new ActionAccumulator(Tracker, Updater, this);
             StructureHelper = new DocumentStructureHelper(this);
 
-            UndoCommand = new RelayCommand(Undo, _ => true);
-            RedoCommand = new RelayCommand(Redo, _ => true);
-            CreateNewLayerCommand = new RelayCommand(_ => StructureHelper.CreateNewStructureMember(StructureMemberType.Layer), _ => true);
-            CreateNewFolderCommand = new RelayCommand(_ => StructureHelper.CreateNewStructureMember(StructureMemberType.Folder), _ => true);
-            DeleteStructureMemberCommand = new RelayCommand(DeleteStructureMember, _ => true);
-            ChangeSelectedItemCommand = new RelayCommand(ChangeSelectedItem, _ => true);
+            UndoCommand = new RelayCommand(Undo);
+            RedoCommand = new RelayCommand(Redo);
+            ClearSelectionCommand = new RelayCommand(ClearSelection);
+            CreateNewLayerCommand = new RelayCommand(_ => StructureHelper.CreateNewStructureMember(StructureMemberType.Layer));
+            CreateNewFolderCommand = new RelayCommand(_ => StructureHelper.CreateNewStructureMember(StructureMemberType.Folder));
+            DeleteStructureMemberCommand = new RelayCommand(DeleteStructureMember);
+            ChangeSelectedItemCommand = new RelayCommand(ChangeSelectedItem);
+            ChangeActiveToolCommand = new RelayCommand(ChangeActiveTool);
 
             MouseDownCommand = new RelayCommand(MouseDown);
             MouseMoveCommand = new RelayCommand(MouseMove);
@@ -83,51 +92,88 @@ namespace PixiEditorPrototype.ViewModels
                 FinalBitmap.BackBufferStride);
         }
 
-        private bool drawing = false;
-        private int mouseDownX = 0;
-        private int mouseDownY = 0;
+        private bool mouseIsDown = false;
+        private int mouseDownCanvasX = 0;
+        private int mouseDownCanvasY = 0;
+
+        private bool startedDrawingRect = false;
+        private bool startedSelectingRect = false;
+
         public void MouseDown(object? param)
         {
-            if (SelectedStructureMember != null && SelectedStructureMember is LayerViewModel)
-            {
-                drawing = true;
-                var args = (MouseButtonEventArgs)(param!);
-                var source = (System.Windows.Controls.Image)args.Source;
-                var pos = args.GetPosition(source);
-                mouseDownX = (int)(pos.X / source.Width * FinalBitmap.PixelHeight);
-                mouseDownY = (int)(pos.Y / source.Height * FinalBitmap.PixelHeight);
-            }
+            mouseIsDown = true;
+            var args = (MouseButtonEventArgs)(param!);
+            var source = (System.Windows.Controls.Image)args.Source;
+            var pos = args.GetPosition(source);
+            mouseDownCanvasX = (int)(pos.X / source.Width * FinalBitmap.PixelHeight);
+            mouseDownCanvasY = (int)(pos.Y / source.Height * FinalBitmap.PixelHeight);
         }
 
         public void MouseMove(object? param)
         {
-            if (!drawing)
+            if (!mouseIsDown)
                 return;
             var args = (MouseEventArgs)(param!);
             var source = (System.Windows.Controls.Image)args.Source;
             var pos = args.GetPosition(source);
             int curX = (int)(pos.X / source.Width * FinalBitmap.PixelHeight);
             int curY = (int)(pos.Y / source.Height * FinalBitmap.PixelHeight);
-            ActionAccumulator.AddAction
-                (
-                    new DrawRectangle_Action
-                    (
-                        SelectedStructureMember!.GuidValue,
-                        new(new(mouseDownX, mouseDownY),
-                        new(curX - mouseDownX, curY - mouseDownY),
-                        1,
-                        new SKColor(SelectedColor.R, SelectedColor.G, SelectedColor.B, SelectedColor.A),
-                        SKColors.Transparent)
-                    )
-                );
+
+            ProcessToolMouseMove(curX, curY);
+        }
+
+        private void ProcessToolMouseMove(int canvasX, int canvasY)
+        {
+            if (activeTool == Tool.Rectangle)
+            {
+                if (SelectedStructureMember == null)
+                    return;
+                startedDrawingRect = true;
+                ActionAccumulator.AddAction(new DrawRectangle_Action(
+                        SelectedStructureMember.GuidValue,
+                        new ShapeData(
+                            new(mouseDownCanvasX, mouseDownCanvasY),
+                            new(canvasX - mouseDownCanvasX, canvasY - mouseDownCanvasY),
+                            1,
+                            new SKColor(SelectedColor.R, SelectedColor.G, SelectedColor.B, SelectedColor.A),
+                            SKColors.Transparent)
+                    ));
+            }
+            else if (activeTool == Tool.Select)
+            {
+                startedSelectingRect = true;
+                ActionAccumulator.AddAction(new SelectRectangle_Action(
+                        new(mouseDownCanvasX, mouseDownCanvasY),
+                        new(canvasX - mouseDownCanvasX, canvasY - mouseDownCanvasY)
+                    ));
+            }
         }
 
         public void MouseUp(object? param)
         {
-            if (!drawing)
+            if (!mouseIsDown)
                 return;
-            ActionAccumulator.AddAction(new EndDrawRectangle_Action());
-            drawing = false;
+            mouseIsDown = false;
+            ProcessToolMouseUp();
+        }
+
+        private void ProcessToolMouseUp()
+        {
+            if (startedDrawingRect)
+            {
+                startedDrawingRect = false;
+                ActionAccumulator.AddAction(new EndDrawRectangle_Action());
+            }
+            if (startedSelectingRect)
+            {
+                startedSelectingRect = false;
+                ActionAccumulator.AddAction(new EndSelectRectangle_Action());
+            }
+        }
+
+        public void ClearSelection(object? param)
+        {
+            ActionAccumulator.AddAction(new ClearSelection_Action());
         }
 
         public void DeleteStructureMember(object? param)
@@ -150,5 +196,12 @@ namespace PixiEditorPrototype.ViewModels
         {
             SelectedStructureMember = (StructureMemberViewModel?)((RoutedPropertyChangedEventArgs<object>?)param)?.NewValue;
         }
+
+        private void ChangeActiveTool(object? param)
+        {
+            if (param == null)
+                return;
+            activeTool = (Tool)param;
+        }
     }
 }

+ 7 - 7
src/PixiEditorPrototype/ViewModels/StructureMemberViewModel.cs

@@ -1,4 +1,4 @@
-using ChangeableDocument.Actions;
+using ChangeableDocument.Actions.Properties;
 using ChangeableDocument.Changeables.Interfaces;
 using System;
 using System.ComponentModel;
@@ -13,24 +13,24 @@ namespace PixiEditorPrototype.ViewModels
 
         public string Name
         {
-            get => member.Name;
-            set => Document.ActionAccumulator.AddAction(new SetStructureMemberName_Action(value, member.GuidValue));
+            get => member.ReadOnlyName;
+            set => Document.ActionAccumulator.AddAction(new SetStructureMemberName_Action(value, member.ReadOnlyGuidValue));
         }
 
         public bool IsVisible
         {
-            get => member.IsVisible;
-            set => Document.ActionAccumulator.AddAction(new SetStructureMemberVisibility_Action(value, member.GuidValue));
+            get => member.ReadOnlyIsVisible;
+            set => Document.ActionAccumulator.AddAction(new SetStructureMemberVisibility_Action(value, member.ReadOnlyGuidValue));
         }
 
         public float Opacity
         {
-            get => member.Opacity;
+            get => member.ReadOnlyOpacity;
         }
 
         public Guid GuidValue
         {
-            get => member.GuidValue;
+            get => member.ReadOnlyGuidValue;
         }
 
         public RelayCommand MoveUpCommand { get; }

+ 14 - 6
src/PixiEditorPrototype/Views/DocumentView.xaml

@@ -4,6 +4,7 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
              xmlns:local="clr-namespace:PixiEditorPrototype.Views"
+             xmlns:models="clr-namespace:PixiEditorPrototype.Models"
              xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
              xmlns:behaviors="clr-namespace:PixiEditorPrototype.Behaviors"
              xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
@@ -11,7 +12,7 @@
              xmlns:vm="clr-namespace:PixiEditorPrototype.ViewModels"
              mc:Ignorable="d" 
              d:DataContext="{d:DesignInstance Type=vm:DocumentViewModel, IsDesignTimeCreatable=True}"
-             d:DesignHeight="450" d:DesignWidth="800">
+             d:DesignHeight="576" d:DesignWidth="1024">
     <DockPanel Background="Gray">
         <Border BorderThickness="1" Background="White" BorderBrush="Black" Width="280" DockPanel.Dock="Right" Margin="5">
             <DockPanel>
@@ -68,14 +69,21 @@
             </DockPanel>
         </Border>
         <Border BorderThickness="1" Background="White" BorderBrush="Black" DockPanel.Dock="Top" Margin="5">
-            <StackPanel Orientation="Horizontal" Margin="5" Background="White">
-                <Button Width="50" Margin="0,0,5,0" Command="{Binding UndoCommand}">Undo</Button>
-                <Button Width="50" Command="{Binding RedoCommand}">Redo</Button>
-                <colorpicker:PortableColorPicker Margin="5,0,0,0" SelectedColor="{Binding SelectedColor, Mode=TwoWay}" Width="40" Height="20"/>
+            <StackPanel Orientation="Horizontal" Background="White">
+                <Button Width="50" Margin="5" Command="{Binding UndoCommand}">Undo</Button>
+                <Button Width="50" Margin="5" Command="{Binding RedoCommand}">Redo</Button>
+                <Button Width="100" Margin="5" Command="{Binding ClearSelectionCommand}">Clear selection</Button>
+            </StackPanel>
+        </Border>
+        <Border BorderThickness="1" Background="White" BorderBrush="Black" DockPanel.Dock="Left" Margin="5">
+            <StackPanel Orientation="Vertical" Background="White">
+                <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Rectangle}">Rect</Button>
+                <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Select}">Select</Button>
+                <colorpicker:PortableColorPicker Margin="5" SelectedColor="{Binding SelectedColor, Mode=TwoWay}" Width="30" Height="30"/>
             </StackPanel>
         </Border>
         <Border BorderThickness="1" Background="Transparent" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5">
-            <Image Margin="5" Source="{Binding FinalBitmap}" Width="200" Height="200" RenderOptions.BitmapScalingMode="NearestNeighbor">
+            <Image Margin="5" Source="{Binding FinalBitmap}" Width="400" Height="400" RenderOptions.BitmapScalingMode="NearestNeighbor">
                 <i:Interaction.Triggers>
                     <i:EventTrigger EventName="MouseDown">
                         <i:InvokeCommandAction Command="{Binding MouseDownCommand}" PassEventArgsToCommand="True"/>

+ 35 - 14
src/StructureRenderer/Renderer.cs

@@ -15,6 +15,7 @@ namespace StructureRenderer
         private Surface? backSurface;
         private static SKPaint PaintToDrawChunksWith = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
         private static SKPaint BlendingPaint = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+        private static SKPaint SelectionPaint = new SKPaint() { BlendMode = SKBlendMode.SrcOver, Color = new(0xa0FFFFFF) };
         private static SKPaint ClearPaint = new SKPaint() { BlendMode = SKBlendMode.Src, Color = SKColors.Transparent };
         public Renderer(DocumentChangeTracker tracker)
         {
@@ -38,6 +39,18 @@ namespace StructureRenderer
                             throw new Exception("Chunks must not be null");
                         chunks.UnionWith(layerImageChunks.Chunks);
                         break;
+                    case Selection_ChangeInfo selection:
+                        if (tracker.Document.ReadOnlySelection.ReadOnlyIsEmptyAndInactive)
+                        {
+                            return null;
+                        }
+                        else
+                        {
+                            if (selection.Chunks == null)
+                                throw new Exception("Chunks must not be null");
+                            chunks.UnionWith(selection.Chunks);
+                        }
+                        break;
                     case CreateStructureMember_ChangeInfo:
                     case DeleteStructureMember_ChangeInfo:
                     case MoveStructureMember_ChangeInfo:
@@ -45,7 +58,7 @@ namespace StructureRenderer
                     case StructureMemberOpacity_ChangeInfo opacityChangeInfo:
                         var memberWithOpacity = tracker.Document.FindMemberOrThrow(opacityChangeInfo.GuidValue);
                         if (memberWithOpacity is IReadOnlyLayer layerWithOpacity)
-                            chunks.UnionWith(layerWithOpacity.LayerImage.FindAllChunks());
+                            chunks.UnionWith(layerWithOpacity.ReadOnlyLayerImage.FindAllChunks());
                         else
                             return null;
                         break;
@@ -54,7 +67,7 @@ namespace StructureRenderer
                             break;
                         var memberWithVisibility = tracker.Document.FindMemberOrThrow(propertiesChangeInfo.GuidValue);
                         if (memberWithVisibility is IReadOnlyLayer layerWithVisibility)
-                            chunks.UnionWith(layerWithVisibility.LayerImage.FindAllChunks());
+                            chunks.UnionWith(layerWithVisibility.ReadOnlyLayerImage.FindAllChunks());
                         else
                             return null;
                         break;
@@ -93,9 +106,7 @@ namespace StructureRenderer
                 foreach (var chunkPos in chunks!)
                 {
                     screenSurface.Canvas.DrawRect(SKRect.Create(chunkPos * ChunkyImage.ChunkSize, new(ChunkyImage.ChunkSize, ChunkyImage.ChunkSize)), ClearPaint);
-                    var renderedSurface = RenderChunkRecursively(chunkPos, 0, tracker.Document.ReadOnlyStructureRoot);
-                    if (renderedSurface != null)
-                        screenSurface.Canvas.DrawSurface(renderedSurface.SkiaSurface, chunkPos * ChunkyImage.ChunkSize, BlendingPaint);
+                    RenderChunk(chunkPos, screenSurface);
                     infos.Add(new DirtyRect_RenderInfo(
                         chunkPos * ChunkyImage.ChunkSize,
                         new(ChunkyImage.ChunkSize, ChunkyImage.ChunkSize)
@@ -118,9 +129,7 @@ namespace StructureRenderer
             {
                 for (int y = 0; y < chunksHeight; y++)
                 {
-                    var renderedSurface = RenderChunkRecursively(new(x, y), 0, structureRoot);
-                    if (renderedSurface != null)
-                        screenSurface.Canvas.DrawSurface(renderedSurface.SkiaSurface, x * ChunkyImage.ChunkSize, y * ChunkyImage.ChunkSize, BlendingPaint);
+                    RenderChunk(new(x, y), screenSurface);
                 }
             }
         }
@@ -151,24 +160,36 @@ namespace StructureRenderer
             return deepestLayer;
         }
 
+        private void RenderChunk(Vector2i chunkPos, SKSurface screenSurface)
+        {
+            var renderedSurface = RenderChunkRecursively(chunkPos, 0, tracker.Document.ReadOnlyStructureRoot);
+            if (renderedSurface != null)
+                screenSurface.Canvas.DrawSurface(renderedSurface.SkiaSurface, chunkPos * ChunkyImage.ChunkSize, BlendingPaint);
+
+            if (tracker.Document.ReadOnlySelection.ReadOnlyIsEmptyAndInactive)
+                return;
+            var selectionChunk = tracker.Document.ReadOnlySelection.ReadOnlySelectionImage.GetLatestChunk(chunkPos);
+            if (selectionChunk != null)
+                selectionChunk.DrawOnSurface(screenSurface, chunkPos * ChunkyImage.ChunkSize, SelectionPaint);
+        }
+
         private Surface? RenderChunkRecursively(Vector2i chunkPos, int depth, IReadOnlyFolder folder)
         {
             Surface? surface = temporarySurfaces.Count > depth ? temporarySurfaces[depth] : null;
             surface?.SkiaSurface.Canvas.Clear();
             foreach (var child in folder.ReadOnlyChildren)
             {
-                if (!child.IsVisible)
+                if (!child.ReadOnlyIsVisible)
                     continue;
                 if (child is IReadOnlyLayer layer)
                 {
-                    var chunk = layer.LayerImage.GetChunk(chunkPos);
+                    var chunk = layer.ReadOnlyLayerImage.GetLatestChunk(chunkPos);
                     if (chunk == null)
                         continue;
                     if (surface == null)
                         throw new Exception("Not enough surfaces have been allocated to draw the entire layer tree");
-                    using var snapshot = chunk.Snapshot();
-                    PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
-                    surface.SkiaSurface.Canvas.DrawImage(snapshot, 0, 0, PaintToDrawChunksWith);
+                    PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.ReadOnlyOpacity * 255));
+                    chunk.DrawOnSurface(surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
                 }
                 else if (child is IReadOnlyFolder innerFolder)
                 {
@@ -177,7 +198,7 @@ namespace StructureRenderer
                         continue;
                     if (surface == null)
                         throw new Exception("Not enough surfaces have been allocated to draw the entire layer tree");
-                    PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
+                    PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.ReadOnlyOpacity * 255));
                     surface.SkiaSurface.Canvas.DrawSurface(renderedSurface.SkiaSurface, 0, 0, PaintToDrawChunksWith);
                 }
             }