Browse Source

ChunkyImage basic implementation + rectangle operation; UpdateableChange infrastructure

Equbuxu 3 years ago
parent
commit
b422f38594
33 changed files with 582 additions and 282 deletions
  1. 9 0
      src/ChangeableDocument/Actions/IEndChangeAction.cs
  2. 9 0
      src/ChangeableDocument/Actions/IMakeChangeAction.cs
  3. 9 0
      src/ChangeableDocument/Actions/IStartChangeAction.cs
  4. 9 0
      src/ChangeableDocument/Actions/IUpdateChangeAction.cs
  5. 38 11
      src/ChangeableDocument/Actions/StructureActions.cs
  6. 0 12
      src/ChangeableDocument/ChangeUpdateInfos/IUpdateInfo.cs
  7. 4 0
      src/ChangeableDocument/ChangeableDocument.csproj
  8. 4 1
      src/ChangeableDocument/Changeables/Interfaces/IReadOnlyLayer.cs
  9. 2 0
      src/ChangeableDocument/Changeables/Layer.cs
  10. 0 48
      src/ChangeableDocument/Changes/Change.cs
  11. 5 5
      src/ChangeableDocument/Changes/CreateStructureMember_Change.cs
  12. 5 5
      src/ChangeableDocument/Changes/DeleteStructureMember_Change.cs
  13. 4 4
      src/ChangeableDocument/Changes/IChange.cs
  14. 6 0
      src/ChangeableDocument/Changes/IUpdateableChange.cs
  15. 5 5
      src/ChangeableDocument/Changes/MoveStructureMember_Change.cs
  16. 5 5
      src/ChangeableDocument/Changes/SetStructureMemberProperties_Change.cs
  17. 0 21
      src/ChangeableDocument/Changes/UpdateableChange.cs
  18. 33 70
      src/ChangeableDocument/DocumentChangeTracker.cs
  19. 0 26
      src/ChunkyImageLib/ChunkMap.cs
  20. 50 0
      src/ChunkyImageLib/ChunkPool.cs
  21. 76 46
      src/ChunkyImageLib/ChunkyImage.cs
  22. 3 1
      src/ChunkyImageLib/ImageData.cs
  23. 3 7
      src/ChunkyImageLib/Operations/IOperation.cs
  24. 12 0
      src/ChunkyImageLib/Operations/OperationHelper.cs
  25. 103 2
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  26. 27 0
      src/ChunkyImageLibTest/ChunkyImageLibTest.csproj
  27. 21 0
      src/ChunkyImageLibTest/OperationHelperTests.cs
  28. 119 0
      src/ChunkyImageLibTest/RectangleOperationTests.cs
  29. 10 4
      src/PixiEditorPrototype.sln
  30. 6 6
      src/PixiEditorPrototype/Models/DocumentStructureHelper.cs
  31. 2 0
      src/PixiEditorPrototype/PixiEditorPrototype.csproj
  32. 1 1
      src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs
  33. 2 2
      src/PixiEditorPrototype/ViewModels/StructureMemberViewModel.cs

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

@@ -0,0 +1,9 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions
+{
+    internal interface IEndChangeAction : IAction
+    {
+        bool IsChangeTypeMatching(IChange change);
+    }
+}

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

@@ -0,0 +1,9 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions
+{
+    internal interface IMakeChangeAction : IAction
+    {
+        IChange CreateCorrespondingChange();
+    }
+}

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

@@ -0,0 +1,9 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions
+{
+    internal interface IStartChangeAction : IAction
+    {
+        IUpdateableChange CreateCorrespondingChange();
+    }
+}

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

@@ -0,0 +1,9 @@
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions
+{
+    internal interface IUpdateChangeAction
+    {
+        void UpdateCorrespodingChange(IUpdateableChange change);
+    }
+}

+ 38 - 11
src/ChangeableDocument/Actions/StructureActions.cs

@@ -1,7 +1,9 @@
-namespace ChangeableDocument.Actions;
-public record CreateStructureMemberAction : IAction
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument.Actions;
+public record CreateStructureMember_Action : IMakeChangeAction
 {
 {
-    public CreateStructureMemberAction(Guid parentGuid, int index, StructureMemberType type)
+    public CreateStructureMember_Action(Guid parentGuid, int index, StructureMemberType type)
     {
     {
         ParentGuid = parentGuid;
         ParentGuid = parentGuid;
         Index = index;
         Index = index;
@@ -11,21 +13,31 @@ public record CreateStructureMemberAction : IAction
     public Guid ParentGuid { get; init; }
     public Guid ParentGuid { get; init; }
     public int Index { get; init; }
     public int Index { get; init; }
     public StructureMemberType Type { get; init; }
     public StructureMemberType Type { get; init; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new CreateStructureMember_Change(ParentGuid, Index, Type);
+    }
 }
 }
 
 
-public record DeleteStructureMemberAction : IAction
+public record DeleteStructureMember_Action : IMakeChangeAction
 {
 {
-    public DeleteStructureMemberAction(Guid guidValue)
+    public DeleteStructureMember_Action(Guid guidValue)
     {
     {
         GuidValue = guidValue;
         GuidValue = guidValue;
     }
     }
 
 
     public Guid GuidValue { get; }
     public Guid GuidValue { get; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new DeleteStructureMember_Change(GuidValue);
+    }
 }
 }
 
 
-public record MoveStructureMemberAction : IAction
+public record MoveStructureMember_Action : IMakeChangeAction
 {
 {
-    public MoveStructureMemberAction(Guid member, Guid targetFolder, int index)
+    public MoveStructureMember_Action(Guid member, Guid targetFolder, int index)
     {
     {
         Member = member;
         Member = member;
         TargetFolder = targetFolder;
         TargetFolder = targetFolder;
@@ -35,11 +47,16 @@ public record MoveStructureMemberAction : IAction
     public Guid Member { get; init; }
     public Guid Member { get; init; }
     public Guid TargetFolder { get; init; }
     public Guid TargetFolder { get; init; }
     public int Index { get; init; }
     public int Index { get; init; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new MoveStructureMember_Change(Member, TargetFolder, Index);
+    }
 }
 }
 
 
-public record SetStructureMemberNameAction : IAction
+public record SetStructureMemberName_Action : IMakeChangeAction
 {
 {
-    public SetStructureMemberNameAction(string name, Guid guidValue)
+    public SetStructureMemberName_Action(string name, Guid guidValue)
     {
     {
         Name = name;
         Name = name;
         GuidValue = guidValue;
         GuidValue = guidValue;
@@ -47,11 +64,16 @@ public record SetStructureMemberNameAction : IAction
 
 
     public string Name { get; init; }
     public string Name { get; init; }
     public Guid GuidValue { get; init; }
     public Guid GuidValue { get; init; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new SetStructureMemberProperties_Change(GuidValue) { NewName = Name };
+    }
 }
 }
 
 
-public record SetStructureMemberVisibilityAction : IAction
+public record SetStructureMemberVisibility_Action : IMakeChangeAction
 {
 {
-    public SetStructureMemberVisibilityAction(bool isVisible, Guid guidValue)
+    public SetStructureMemberVisibility_Action(bool isVisible, Guid guidValue)
     {
     {
         this.isVisible = isVisible;
         this.isVisible = isVisible;
         GuidValue = guidValue;
         GuidValue = guidValue;
@@ -59,4 +81,9 @@ public record SetStructureMemberVisibilityAction : IAction
 
 
     public bool isVisible { get; init; }
     public bool isVisible { get; init; }
     public Guid GuidValue { get; init; }
     public Guid GuidValue { get; init; }
+
+    IChange IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new SetStructureMemberProperties_Change(GuidValue) { NewIsVisible = isVisible };
+    }
 }
 }

+ 0 - 12
src/ChangeableDocument/ChangeUpdateInfos/IUpdateInfo.cs

@@ -1,12 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace ChangeableDocument.ChangeUpdateInfos
-{
-    internal interface IUpdateInfo
-    {
-    }
-}

+ 4 - 0
src/ChangeableDocument/ChangeableDocument.csproj

@@ -6,4 +6,8 @@
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
   </PropertyGroup>
   </PropertyGroup>
 
 
+  <ItemGroup>
+    <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj" />
+  </ItemGroup>
+
 </Project>
 </Project>

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

@@ -1,6 +1,9 @@
-namespace ChangeableDocument.Changeables.Interfaces
+using ChunkyImageLib;
+
+namespace ChangeableDocument.Changeables.Interfaces
 {
 {
     public interface IReadOnlyLayer : IReadOnlyStructureMember
     public interface IReadOnlyLayer : IReadOnlyStructureMember
     {
     {
+        ChunkyImage LayerImage { get; }
     }
     }
 }
 }

+ 2 - 0
src/ChangeableDocument/Changeables/Layer.cs

@@ -1,9 +1,11 @@
 using ChangeableDocument.Changeables.Interfaces;
 using ChangeableDocument.Changeables.Interfaces;
+using ChunkyImageLib;
 
 
 namespace ChangeableDocument.Changeables
 namespace ChangeableDocument.Changeables
 {
 {
     internal class Layer : StructureMember, IReadOnlyLayer
     internal class Layer : StructureMember, IReadOnlyLayer
     {
     {
+        public ChunkyImage LayerImage { get; set; } = new();
         internal override Layer Clone()
         internal override Layer Clone()
         {
         {
             return new Layer()
             return new Layer()

+ 0 - 48
src/ChangeableDocument/Changes/Change.cs

@@ -1,48 +0,0 @@
-using ChangeableDocument.Changeables;
-using ChangeableDocument.ChangeInfos;
-
-namespace ChangeableDocument.Changes
-{
-    internal abstract class Change<TargetT> : IChange
-    {
-        protected bool Initialized { get; private set; } = false;
-        protected bool Applied { get; private set; } = false;
-        public void Initialize(IChangeable target)
-        {
-            if (Initialized)
-                throw new Exception("Already initialized");
-            if (target is not TargetT conv)
-                throw new Exception("Couldn't convert changeable");
-            Initialized = true;
-            DoInitialize(conv);
-        }
-        protected abstract void DoInitialize(TargetT target);
-
-        public IChangeInfo? Apply(IChangeable target)
-        {
-            if (!Initialized)
-                throw new Exception("Can't apply uninitialized change");
-            if (Applied)
-                throw new Exception("The change has already been applied");
-            if (target is not TargetT conv)
-                throw new Exception("Couldn't convert changeable");
-            Applied = true;
-            return DoApply(conv);
-        }
-        protected abstract IChangeInfo? DoApply(TargetT target);
-
-        public IChangeInfo? Revert(IChangeable target)
-        {
-            if (!Initialized)
-                throw new Exception("Can't revert uninitialized change");
-            if (!Applied)
-                throw new Exception("Can't revert a change that hasn't been applied");
-            if (target is not TargetT conv)
-                throw new Exception("Couldn't convert changeable");
-            Applied = false;
-            return DoRevert(conv);
-        }
-        protected abstract IChangeInfo? DoRevert(TargetT target);
-
-    };
-}

+ 5 - 5
src/ChangeableDocument/Changes/Document_CreateStructureMember_Change.cs → src/ChangeableDocument/Changes/CreateStructureMember_Change.cs

@@ -3,7 +3,7 @@ using ChangeableDocument.ChangeInfos;
 
 
 namespace ChangeableDocument.Changes
 namespace ChangeableDocument.Changes
 {
 {
-    internal class Document_CreateStructureMember_Change : Change<Document>
+    internal class CreateStructureMember_Change : IChange
     {
     {
         private Guid newMemberGuid;
         private Guid newMemberGuid;
 
 
@@ -11,19 +11,19 @@ namespace ChangeableDocument.Changes
         private int parentFolderIndex;
         private int parentFolderIndex;
         private StructureMemberType type;
         private StructureMemberType type;
 
 
-        public Document_CreateStructureMember_Change(Guid parentFolder, int parentFolderIndex, StructureMemberType type)
+        public CreateStructureMember_Change(Guid parentFolder, int parentFolderIndex, StructureMemberType type)
         {
         {
             this.parentFolderGuid = parentFolder;
             this.parentFolderGuid = parentFolder;
             this.parentFolderIndex = parentFolderIndex;
             this.parentFolderIndex = parentFolderIndex;
             this.type = type;
             this.type = type;
         }
         }
 
 
-        protected override void DoInitialize(Document target)
+        public void Initialize(Document target)
         {
         {
             newMemberGuid = Guid.NewGuid();
             newMemberGuid = Guid.NewGuid();
         }
         }
 
 
-        protected override IChangeInfo DoApply(Document document)
+        public IChangeInfo Apply(Document document)
         {
         {
             var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
             var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
 
 
@@ -39,7 +39,7 @@ namespace ChangeableDocument.Changes
             return new Document_CreateStructureMember_ChangeInfo() { GuidValue = newMemberGuid };
             return new Document_CreateStructureMember_ChangeInfo() { GuidValue = newMemberGuid };
         }
         }
 
 
-        protected override IChangeInfo DoRevert(Document document)
+        public IChangeInfo Revert(Document document)
         {
         {
             var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
             var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
             folder.Children.RemoveAt(folder.Children.FindIndex(child => child.GuidValue == newMemberGuid));
             folder.Children.RemoveAt(folder.Children.FindIndex(child => child.GuidValue == newMemberGuid));

+ 5 - 5
src/ChangeableDocument/Changes/Document_DeleteStructureMember_Change.cs → src/ChangeableDocument/Changes/DeleteStructureMember_Change.cs

@@ -3,18 +3,18 @@ using ChangeableDocument.ChangeInfos;
 
 
 namespace ChangeableDocument.Changes
 namespace ChangeableDocument.Changes
 {
 {
-    internal class Document_DeleteStructureMember_Change : Change<Document>
+    internal class DeleteStructureMember_Change : IChange
     {
     {
         private Guid memberGuid;
         private Guid memberGuid;
         private Guid parentGuid;
         private Guid parentGuid;
         private int originalIndex;
         private int originalIndex;
         private StructureMember? savedCopy;
         private StructureMember? savedCopy;
-        public Document_DeleteStructureMember_Change(Guid memberGuid)
+        public DeleteStructureMember_Change(Guid memberGuid)
         {
         {
             this.memberGuid = memberGuid;
             this.memberGuid = memberGuid;
         }
         }
 
 
-        protected override void DoInitialize(Document document)
+        public void Initialize(Document document)
         {
         {
             var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
             var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
 
 
@@ -23,14 +23,14 @@ namespace ChangeableDocument.Changes
             savedCopy = member.Clone();
             savedCopy = member.Clone();
         }
         }
 
 
-        protected override IChangeInfo DoApply(Document document)
+        public IChangeInfo Apply(Document document)
         {
         {
             var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
             var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
             parent.Children.Remove(member);
             parent.Children.Remove(member);
             return new Document_DeleteStructureMember_ChangeInfo() { GuidValue = memberGuid };
             return new Document_DeleteStructureMember_ChangeInfo() { GuidValue = memberGuid };
         }
         }
 
 
-        protected override IChangeInfo DoRevert(Document doc)
+        public IChangeInfo Revert(Document doc)
         {
         {
             var parent = (Folder)doc.FindMemberOrThrow(parentGuid);
             var parent = (Folder)doc.FindMemberOrThrow(parentGuid);
 
 

+ 4 - 4
src/ChangeableDocument/Changes/IChange.cs

@@ -5,8 +5,8 @@ namespace ChangeableDocument.Changes
 {
 {
     internal interface IChange
     internal interface IChange
     {
     {
-        void Initialize(IChangeable target);
-        IChangeInfo? Apply(IChangeable target);
-        IChangeInfo? Revert(IChangeable target);
-    }
+        void Initialize(Document target);
+        IChangeInfo? Apply(Document target);
+        IChangeInfo? Revert(Document target);
+    };
 }
 }

+ 6 - 0
src/ChangeableDocument/Changes/IUpdateableChange.cs

@@ -0,0 +1,6 @@
+namespace ChangeableDocument.Changes
+{
+    internal interface IUpdateableChange : IChange
+    {
+    }
+}

+ 5 - 5
src/ChangeableDocument/Changes/Document_MoveStructureMember_Change.cs → src/ChangeableDocument/Changes/MoveStructureMember_Change.cs

@@ -3,7 +3,7 @@ using ChangeableDocument.ChangeInfos;
 
 
 namespace ChangeableDocument.Changes
 namespace ChangeableDocument.Changes
 {
 {
-    internal class Document_MoveStructureMember_Change : Change<Document>
+    internal class MoveStructureMember_Change : IChange
     {
     {
         private Guid memberGuid;
         private Guid memberGuid;
 
 
@@ -13,14 +13,14 @@ namespace ChangeableDocument.Changes
         private Guid originalFolderGuid;
         private Guid originalFolderGuid;
         private int originalFolderIndex;
         private int originalFolderIndex;
 
 
-        public Document_MoveStructureMember_Change(Guid memberGuid, Guid targetFolder, int targetFolderIndex)
+        public MoveStructureMember_Change(Guid memberGuid, Guid targetFolder, int targetFolderIndex)
         {
         {
             this.memberGuid = memberGuid;
             this.memberGuid = memberGuid;
             this.targetFolderGuid = targetFolder;
             this.targetFolderGuid = targetFolder;
             this.targetFolderIndex = targetFolderIndex;
             this.targetFolderIndex = targetFolderIndex;
         }
         }
 
 
-        protected override void DoInitialize(Document document)
+        public void Initialize(Document document)
         {
         {
             var (member, curFolder) = document.FindChildAndParentOrThrow(memberGuid);
             var (member, curFolder) = document.FindChildAndParentOrThrow(memberGuid);
             originalFolderGuid = curFolder.GuidValue;
             originalFolderGuid = curFolder.GuidValue;
@@ -36,13 +36,13 @@ namespace ChangeableDocument.Changes
             targetFolder.Children.Insert(targetIndex, member);
             targetFolder.Children.Insert(targetIndex, member);
         }
         }
 
 
-        protected override IChangeInfo? DoApply(Document target)
+        public IChangeInfo? Apply(Document target)
         {
         {
             Move(target, memberGuid, targetFolderGuid, targetFolderIndex);
             Move(target, memberGuid, targetFolderGuid, targetFolderIndex);
             return new Document_MoveStructureMember_ChangeInfo() { GuidValue = memberGuid };
             return new Document_MoveStructureMember_ChangeInfo() { GuidValue = memberGuid };
         }
         }
 
 
-        protected override IChangeInfo? DoRevert(Document target)
+        public IChangeInfo? Revert(Document target)
         {
         {
             Move(target, memberGuid, originalFolderGuid, originalFolderIndex);
             Move(target, memberGuid, originalFolderGuid, originalFolderIndex);
             return new Document_MoveStructureMember_ChangeInfo() { GuidValue = memberGuid };
             return new Document_MoveStructureMember_ChangeInfo() { GuidValue = memberGuid };

+ 5 - 5
src/ChangeableDocument/Changes/Document_UpdateStructureMemberProperties_Change.cs → src/ChangeableDocument/Changes/SetStructureMemberProperties_Change.cs

@@ -3,7 +3,7 @@ using ChangeableDocument.ChangeInfos;
 
 
 namespace ChangeableDocument.Changes
 namespace ChangeableDocument.Changes
 {
 {
-    internal class Document_UpdateStructureMemberProperties_Change : Change<Document>
+    internal class SetStructureMemberProperties_Change : IChange
     {
     {
         private Guid memberGuid;
         private Guid memberGuid;
 
 
@@ -13,19 +13,19 @@ namespace ChangeableDocument.Changes
         private string? originalName;
         private string? originalName;
         public string? NewName { get; init; } = null;
         public string? NewName { get; init; } = null;
 
 
-        public Document_UpdateStructureMemberProperties_Change(Guid memberGuid)
+        public SetStructureMemberProperties_Change(Guid memberGuid)
         {
         {
             this.memberGuid = memberGuid;
             this.memberGuid = memberGuid;
         }
         }
 
 
-        protected override void DoInitialize(Document document)
+        public void Initialize(Document document)
         {
         {
             var member = document.FindMemberOrThrow(memberGuid);
             var member = document.FindMemberOrThrow(memberGuid);
             if (NewIsVisible != null) originalIsVisible = member.IsVisible;
             if (NewIsVisible != null) originalIsVisible = member.IsVisible;
             if (NewName != null) originalName = member.Name;
             if (NewName != null) originalName = member.Name;
         }
         }
 
 
-        protected override IChangeInfo? DoApply(Document document)
+        public IChangeInfo? Apply(Document document)
         {
         {
             var member = document.FindMemberOrThrow(memberGuid);
             var member = document.FindMemberOrThrow(memberGuid);
             if (NewIsVisible != null) member.IsVisible = NewIsVisible.Value;
             if (NewIsVisible != null) member.IsVisible = NewIsVisible.Value;
@@ -39,7 +39,7 @@ namespace ChangeableDocument.Changes
             };
             };
         }
         }
 
 
-        protected override IChangeInfo? DoRevert(Document document)
+        public IChangeInfo? Revert(Document document)
         {
         {
             var member = document.FindMemberOrThrow(memberGuid);
             var member = document.FindMemberOrThrow(memberGuid);
             if (NewIsVisible != null) member.IsVisible = originalIsVisible;
             if (NewIsVisible != null) member.IsVisible = originalIsVisible;

+ 0 - 21
src/ChangeableDocument/Changes/UpdateableChange.cs

@@ -1,21 +0,0 @@
-using ChangeableDocument.Changeables;
-using ChangeableDocument.ChangeInfos;
-using ChangeableDocument.ChangeUpdateInfos;
-
-namespace ChangeableDocument.Changes
-{
-    internal abstract class UpdateableChange<TargetT> : Change<TargetT>
-    {
-        public IChangeInfo? Update(IChangeable target, IUpdateInfo update)
-        {
-            if (!Initialized)
-                throw new Exception("Can't update uninitialized change");
-            if (Applied)
-                throw new Exception("The change has already been applied");
-            if (target is not TargetT conv)
-                throw new Exception("Couldn't convert changeable");
-            return DoUpdate(conv, update);
-        }
-        protected abstract IChangeInfo? DoUpdate(TargetT target, IUpdateInfo update);
-    }
-}

+ 33 - 70
src/ChangeableDocument/DocumentChangeTracker.cs

@@ -11,6 +11,8 @@ namespace ChangeableDocument
         private Document document;
         private Document document;
         public IReadOnlyDocument Document => document;
         public IReadOnlyDocument Document => document;
 
 
+        private IUpdateableChange? activeChange = null;
+
         private Stack<IChange> undoStack = new();
         private Stack<IChange> undoStack = new();
         private Stack<IChange> redoStack = new();
         private Stack<IChange> redoStack = new();
 
 
@@ -19,46 +21,10 @@ namespace ChangeableDocument
             document = new Document();
             document = new Document();
         }
         }
 
 
-        private IChangeInfo? InitAndApplyWithUndo(IChange change)
+        private void AddToUndo(IChange change)
         {
         {
-            change.Initialize(document);
-            var info = change.Apply(document);
             undoStack.Push(change);
             undoStack.Push(change);
             redoStack.Clear();
             redoStack.Clear();
-            return info;
-        }
-
-        private IChangeInfo? MoveStructureMember(Guid member, Guid targetFolder, int index)
-        {
-            return InitAndApplyWithUndo(new Document_MoveStructureMember_Change(member, targetFolder, index));
-        }
-
-        private IChangeInfo? CreateStructureMember(Guid parentGuid, int index, StructureMemberType type)
-        {
-            return InitAndApplyWithUndo(new Document_CreateStructureMember_Change(parentGuid, index, type));
-        }
-
-        private IChangeInfo? DeleteStructureMember(Guid member)
-        {
-            return InitAndApplyWithUndo(new Document_DeleteStructureMember_Change(member));
-        }
-
-        private IChangeInfo? SetStructureMemberVisibility(Guid guid, bool isVisible)
-        {
-            Document_UpdateStructureMemberProperties_Change change = new(guid)
-            {
-                NewIsVisible = isVisible,
-            };
-            return InitAndApplyWithUndo(change);
-        }
-
-        private IChangeInfo? SetStructureMemberName(Guid guid, string name)
-        {
-            Document_UpdateStructureMemberProperties_Change change = new(guid)
-            {
-                NewName = name,
-            };
-            return InitAndApplyWithUndo(change);
         }
         }
 
 
         private IChangeInfo? Undo()
         private IChangeInfo? Undo()
@@ -85,55 +51,52 @@ namespace ChangeableDocument
         {
         {
             List<IChangeInfo?> result = await Task.Run(() =>
             List<IChangeInfo?> result = await Task.Run(() =>
             {
             {
-                List<IChangeInfo?> changes = new();
+                List<IChangeInfo?> changeInfos = new();
                 foreach (var action in actions)
                 foreach (var action in actions)
                 {
                 {
                     switch (action)
                     switch (action)
                     {
                     {
-                        case CreateStructureMemberAction act:
-                            changes.Add(CreateStructureMember(act.ParentGuid, act.Index, act.Type));
-                            break;
-                        case MoveStructureMemberAction act:
-                            changes.Add(MoveStructureMember(act.Member, act.TargetFolder, act.Index));
+                        case IMakeChangeAction act:
+                            if (activeChange != null)
+                                throw new Exception("Can't make a change while another change is active");
+                            var change = act.CreateCorrespondingChange();
+                            change.Initialize(document);
+                            changeInfos.Add(change.Apply(document));
+                            AddToUndo(change);
                             break;
                             break;
-                        case SetStructureMemberNameAction act:
-                            changes.Add(SetStructureMemberName(act.GuidValue, act.Name));
+                        case IStartChangeAction act:
+                            if (activeChange != null)
+                                throw new Exception("Can't start a change while another change is active");
+                            activeChange = act.CreateCorrespondingChange();
+                            activeChange.Initialize(document);
                             break;
                             break;
-                        case SetStructureMemberVisibilityAction act:
-                            changes.Add(SetStructureMemberVisibility(act.GuidValue, act.isVisible));
+                        case IUpdateChangeAction act:
+                            if (activeChange == null)
+                                throw new Exception("Can't update a change: no changes are active");
+                            act.UpdateCorrespodingChange(activeChange);
+                            changeInfos.Add(activeChange.Apply(document));
                             break;
                             break;
-                        case DeleteStructureMemberAction act:
-                            changes.Add(DeleteStructureMember(act.GuidValue));
+                        case IEndChangeAction act:
+                            if (activeChange == null)
+                                throw new Exception("Can't end a change: no changes are active");
+                            if (!act.IsChangeTypeMatching(activeChange))
+                                throw new Exception($"Trying to end a change via action of type {act.GetType()} while a change of type {activeChange.GetType()} is active");
+                            AddToUndo(activeChange);
+                            activeChange = null;
                             break;
                             break;
                         case UndoAction act:
                         case UndoAction act:
-                            changes.Add(Undo());
+                            changeInfos.Add(Undo());
                             break;
                             break;
                         case RedoAction act:
                         case RedoAction act:
-                            changes.Add(Redo());
+                            changeInfos.Add(Redo());
                             break;
                             break;
+                        default:
+                            throw new Exception("Unknown action type");
                     }
                     }
                 }
                 }
-                return changes;
+                return changeInfos;
             }).ConfigureAwait(true);
             }).ConfigureAwait(true);
             return result;
             return result;
         }
         }
     }
     }
-
-    class OperationStateMachine
-    {
-        public void ExecuteSingularChange(IChange change)
-        {
-
-        }
-
-        public void StartUpdateableChange()
-        {
-
-        }
-
-        public void EndUpdateableChange()
-        {
-
-        }
-    }
 }
 }

+ 0 - 26
src/ChunkyImageLib/ChunkMap.cs

@@ -1,26 +0,0 @@
-namespace ChunkyImageLib
-{
-    internal class ChunkMap
-    {
-        private int chunkSize;
-        private Dictionary<int, Dictionary<int, int[]>> chunks = new();
-        public ChunkMap(int chunkSize)
-        {
-            this.chunkSize = chunkSize;
-        }
-        public void Get(int x, int y)
-        {
-
-        }
-
-        public void Create(int x, int y)
-        {
-
-        }
-
-        public void Delete(int x, int y)
-        {
-
-        }
-    }
-}

+ 50 - 0
src/ChunkyImageLib/ChunkPool.cs

@@ -0,0 +1,50 @@
+namespace ChunkyImageLib
+{
+    internal class ChunkPool
+    {
+        // not thread-safe!
+        public const int ChunkSize = 32;
+
+        private static ChunkPool? instance;
+        public static ChunkPool Instance => instance ??= new ChunkPool();
+
+        private List<ImageData> freeChunks = new();
+        private HashSet<ImageData> usedChunks = new();
+
+        public ImageData BorrowChunk()
+        {
+            ImageData chunk;
+            if (freeChunks.Count > 0)
+            {
+                chunk = freeChunks[^1];
+                freeChunks.RemoveAt(freeChunks.Count - 1);
+            }
+            else
+            {
+                chunk = new ImageData(ChunkSize, ChunkSize, SkiaSharp.SKColorType.RgbaF16);
+            }
+            usedChunks.Add(chunk);
+
+            return chunk;
+        }
+
+        public void ReturnChunk(ImageData chunk)
+        {
+            if (!usedChunks.Contains(chunk))
+                throw new Exception("This chunk wasn't borrowed");
+            usedChunks.Remove(chunk);
+            freeChunks.Add(chunk);
+
+            MaybeDisposeChunks();
+        }
+
+        private void MaybeDisposeChunks()
+        {
+            for (int i = freeChunks.Count - 1; i >= 100; i++)
+            {
+                freeChunks[i].Dispose();
+                freeChunks.RemoveAt(i);
+            }
+        }
+    }
+}

+ 76 - 46
src/ChunkyImageLib/ChunkyImage.cs

@@ -1,89 +1,119 @@
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
 using SkiaSharp;
 using SkiaSharp;
+using System.Runtime.CompilerServices;
 
 
+[assembly: InternalsVisibleTo("ChunkyImageLibTest")]
 namespace ChunkyImageLib
 namespace ChunkyImageLib
 {
 {
     public class ChunkyImage
     public class ChunkyImage
     {
     {
-        private bool locked = false;
-        //const int chunkSize = 32;
-        private Queue<IOperation> queuedOperations = new Queue<IOperation>();
+        private bool locked = false; //todo implement locking
 
 
-        private ImageData image;
-        private SKSurface imageSurface;
+        private Queue<(IOperation, HashSet<(int, int)>)> queuedOperations = new();
 
 
-        private ImageData pendingImage;
-        private SKSurface pendingImageSurface;
+        private Dictionary<(int, int), ImageData> chunks = new();
+        private Dictionary<(int, int), ImageData> uncommitedChunks = new();
 
 
-        public ChunkyImage(int width, int height)
+        public ImageData? GetChunk(int x, int y)
         {
         {
-            image = new ImageData(width, height, SKColorType.RgbaF16);
-            pendingImage = new ImageData(width, height, SKColorType.RgbaF16);
-            imageSurface = image.CreateSKSurface();
-            pendingImageSurface = image.CreateSKSurface();
+            if (queuedOperations.Count == 0)
+                return MaybeGetChunk(x, y, chunks);
+            ProcessQueue(x, y);
+            return MaybeGetChunk(x, y, uncommitedChunks) ?? MaybeGetChunk(x, y, chunks);
         }
         }
 
 
-        public ImageData GetCurrentImageData()
-        {
-            ProcessQueue();
-            return pendingImage;
-        }
+        private ImageData? MaybeGetChunk(int x, int y, Dictionary<(int, int), ImageData> from) => from.ContainsKey((x, y)) ? from[(x, y)] : null;
 
 
-        public void DrawRectangle(int x, int y, int width, int height)
+        public void DrawRectangle(int x, int y, int width, int height, int strokeThickness, SKColor strokeColor, SKColor fillColor)
         {
         {
-            queuedOperations.Enqueue(new RectangleOperation(x, y, width, height));
+            RectangleOperation operation = new(x, y, width, height, strokeThickness, strokeColor, fillColor);
+            queuedOperations.Enqueue((operation, operation.FindAffectedChunks(ChunkPool.ChunkSize)));
         }
         }
 
 
         public void CancelChanges()
         public void CancelChanges()
         {
         {
             queuedOperations.Clear();
             queuedOperations.Clear();
-            image.CopyTo(pendingImage);
+            foreach (var (_, chunk) in uncommitedChunks)
+            {
+                ChunkPool.Instance.ReturnChunk(chunk);
+            }
         }
         }
 
 
         public void CommitChanges()
         public void CommitChanges()
         {
         {
-            ProcessQueue();
-            pendingImage.CopyTo(image);
+            SwapUncommitedChunks();
+            ProcessQueueFinal();
         }
         }
 
 
-        private void ProcessQueue()
+        private void ProcessQueueFinal()
         {
         {
-            foreach (var operation in queuedOperations)
+            foreach (var (operation, operChunks) in queuedOperations)
             {
             {
-                if (operation is RectangleOperation rect)
+                foreach (var (x, y) in operChunks)
                 {
                 {
-                    using SKPaint black = new() { Color = SKColors.Black };
-                    pendingImageSurface.Canvas.DrawRect(rect.X, rect.Y, rect.Width, rect.Height, black);
+                    operation.DrawOnChunk(GetOrCreateCommitedChunk(x, y), x, y);
                 }
                 }
             }
             }
             queuedOperations.Clear();
             queuedOperations.Clear();
         }
         }
-        /*
-        private List<(int, int)> GetAffectedChunks(IOperation operation)
+
+        private void SwapUncommitedChunks()
         {
         {
-            if (operation is RectangleOperation rect)
-                return GetAffectedChunks(rect);
-            return new List<(int, int)>();
+            foreach (var (pos, chunk) in uncommitedChunks)
+            {
+                if (chunks.ContainsKey(pos))
+                {
+                    var oldChunk = chunks[pos];
+                    chunks.Remove(pos);
+                    ChunkPool.Instance.ReturnChunk(oldChunk);
+                }
+                chunks.Add(pos, chunk);
+            }
         }
         }
 
 
-        private List<(int, int)> GetAffectedChunks(RectangleOperation rect)
+        private void ProcessQueue(int chunkX, int chunkY)
         {
         {
-            int startX = (int)Math.Floor(rect.X / (float)chunkSize);
-            int startY = (int)Math.Floor(rect.Y / (float)chunkSize);
-            int endX = (int)Math.Floor((rect.X + rect.Width - 1) / (float)chunkSize);
-            int endY = (int)Math.Floor((rect.Y + rect.Height - 1) / (float)chunkSize);
-            List<(int, int)> chunks = new();
-            for (int i = startX; i <= endX; i++)
+            ImageData? targetChunk = null;
+            foreach (var (operation, operChunks) in queuedOperations)
             {
             {
-                chunks.Add((i, startY));
-                chunks.Add((i, endY));
+                if (!operChunks.Contains((chunkX, chunkY)))
+                    continue;
+                operChunks.Remove((chunkX, chunkY));
+
+                if (targetChunk == null)
+                    targetChunk = GetOrCreateUncommitedChunk(chunkX, chunkY);
+
+                operation.DrawOnChunk(targetChunk, chunkX, chunkY);
             }
             }
-            for (int i = startY + 1; i < endY; i++)
+            queuedOperations.Clear();
+        }
+
+        private ImageData GetOrCreateCommitedChunk(int chunkX, int chunkY)
+        {
+            ImageData? targetChunk = MaybeGetChunk(chunkX, chunkY, chunks);
+            if (targetChunk != null)
+                return targetChunk;
+            var newChunk = ChunkPool.Instance.BorrowChunk();
+            newChunk.SkiaSurface.Canvas.Clear();
+            chunks.Add((chunkX, chunkY), newChunk);
+            return newChunk;
+        }
+
+        private ImageData GetOrCreateUncommitedChunk(int chunkX, int chunkY)
+        {
+            ImageData? targetChunk;
+            targetChunk = MaybeGetChunk(chunkX, chunkY, uncommitedChunks);
+            if (targetChunk == null)
             {
             {
-                chunks.Add((startX, i));
-                chunks.Add((endX, i));
+                targetChunk = ChunkPool.Instance.BorrowChunk();
+                var maybeCommitedChunk = MaybeGetChunk(chunkX, chunkY, chunks);
+                if (maybeCommitedChunk != null)
+                    maybeCommitedChunk.CopyTo(targetChunk);
+                else
+                    targetChunk.SkiaSurface.Canvas.Clear();
             }
             }
-            return chunks;
-        }*/
+            uncommitedChunks.Add((chunkX, chunkY), targetChunk);
+            return targetChunk;
+        }
     }
     }
 }
 }

+ 3 - 1
src/ChunkyImageLib/ImageData.cs

@@ -10,6 +10,7 @@ namespace ChunkyImageLib
         private int bytesPerPixel;
         private int bytesPerPixel;
         public SKColorType ColorType { get; }
         public SKColorType ColorType { get; }
         public IntPtr PixelBuffer { get; }
         public IntPtr PixelBuffer { get; }
+        public SKSurface SkiaSurface { get; }
         public int Width { get; }
         public int Width { get; }
         public int Height { get; }
         public int Height { get; }
         public ImageData(int width, int height, SKColorType colorType)
         public ImageData(int width, int height, SKColorType colorType)
@@ -24,6 +25,7 @@ namespace ChunkyImageLib
             ColorType = colorType;
             ColorType = colorType;
             bytesPerPixel = colorType == SKColorType.RgbaF16 ? 8 : 4;
             bytesPerPixel = colorType == SKColorType.RgbaF16 ? 8 : 4;
             PixelBuffer = CreateBuffer(width, height, bytesPerPixel);
             PixelBuffer = CreateBuffer(width, height, bytesPerPixel);
+            SkiaSurface = CreateSKSurface();
         }
         }
 
 
         public unsafe void CopyTo(ImageData other)
         public unsafe void CopyTo(ImageData other)
@@ -49,7 +51,7 @@ namespace ChunkyImageLib
             }
             }
         }
         }
 
 
-        public SKSurface CreateSKSurface()
+        private SKSurface CreateSKSurface()
         {
         {
             var surface = SKSurface.Create(new SKImageInfo(Width, Height, ColorType, SKAlphaType.Premul, SKColorSpace.CreateSrgb()), PixelBuffer);
             var surface = SKSurface.Create(new SKImageInfo(Width, Height, ColorType, SKAlphaType.Premul, SKColorSpace.CreateSrgb()), PixelBuffer);
             if (surface == null)
             if (surface == null)

+ 3 - 7
src/ChunkyImageLib/Operations/IOperation.cs

@@ -1,12 +1,8 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace ChunkyImageLib.Operations
+namespace ChunkyImageLib.Operations
 {
 {
     internal interface IOperation
     internal interface IOperation
     {
     {
+        void DrawOnChunk(ImageData chunk, int chunkX, int chunkY);
+        HashSet<(int, int)> FindAffectedChunks(int chunkSize);
     }
     }
 }
 }

+ 12 - 0
src/ChunkyImageLib/Operations/OperationHelper.cs

@@ -0,0 +1,12 @@
+namespace ChunkyImageLib.Operations
+{
+    public static class OperationHelper
+    {
+        public static (int, int) GetChunkPos(int pixelX, int pixelY, int chunkSize)
+        {
+            int x = (int)Math.Floor(pixelX / (float)chunkSize);
+            int y = (int)Math.Floor(pixelY / (float)chunkSize);
+            return (x, y);
+        }
+    }
+}

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

@@ -1,18 +1,119 @@
-namespace ChunkyImageLib.Operations
+using SkiaSharp;
+
+namespace ChunkyImageLib.Operations
 {
 {
     internal record RectangleOperation : IOperation
     internal record RectangleOperation : IOperation
     {
     {
-        public RectangleOperation(int x, int y, int width, int height)
+        public RectangleOperation(int x, int y, int width, int height, int borderThickness, SKColor borderColor, SKColor fillColor)
         {
         {
+            StrokeColor = borderColor;
+            FillColor = fillColor;
+            StrokeWidth = borderThickness;
             X = x;
             X = x;
             Y = y;
             Y = y;
             Width = width;
             Width = width;
             Height = height;
             Height = height;
         }
         }
 
 
+        public SKColor StrokeColor { get; }
+        public SKColor FillColor { get; }
+        public int StrokeWidth { get; }
         public int X { get; }
         public int X { get; }
         public int Y { get; }
         public int Y { get; }
         public int Width { get; }
         public int Width { get; }
         public int Height { get; }
         public int Height { get; }
+        public int MaxX => X + Width - 1;
+        public int MaxY => Y + Height - 1;
+
+        public void DrawOnChunk(ImageData chunk, int chunkX, int chunkY)
+        {
+            // use a clipping rectangle with 2x stroke width to make sure stroke doesn't stick outside rect bounds
+            chunk.SkiaSurface.Canvas.Save();
+            var rect = SKRect.Create(X, Y, Width, Height);
+            chunk.SkiaSurface.Canvas.ClipRect(rect);
+
+            // draw fill
+            using SKPaint paint = new()
+            {
+                Color = FillColor,
+                Style = SKPaintStyle.Fill,
+            };
+
+            if (FillColor.Alpha > 0)
+                chunk.SkiaSurface.Canvas.DrawRect(rect, paint);
+
+            // draw stroke
+            paint.Color = StrokeColor;
+            paint.Style = SKPaintStyle.Stroke;
+            paint.StrokeWidth = StrokeWidth * 2;
+
+            chunk.SkiaSurface.Canvas.DrawRect(rect, paint);
+
+            // get rid of the clipping rectangle
+            chunk.SkiaSurface.Canvas.Restore();
+        }
+
+        public HashSet<(int, int)> FindAffectedChunks(int chunkSize)
+        {
+            if (Width < 1 || Height < 1 || StrokeColor.Alpha == 0 && FillColor.Alpha == 0)
+                return new();
+            if (FillColor.Alpha != 0 || Width == 1 || Height == 1)
+                return GetChunksForFilled(chunkSize);
+            return GetChunksForStroke(chunkSize);
+        }
+
+        private static (int, int)? Inset(int min, int max, int inset)
+        {
+            int insetMin = Math.Min(min + inset - 1, max);
+            int insetMax = Math.Max(max - inset + 1, min);
+            //is rectangle fully filled by the stroke
+            if (insetMin + 1 >= insetMax)
+                return null;
+            return (insetMin, insetMax);
+        }
+
+        private HashSet<(int, int)> GetChunksForStroke(int chunkSize)
+        {
+            //we need to account for wide strokes covering multiple chunks
+            //find inner stroke boudaries in pixel coords
+            var xInset = Inset(X, MaxX, StrokeWidth);
+            var yInset = Inset(Y, MaxY, StrokeWidth);
+            if (xInset == null || yInset == null)
+                return GetChunksForFilled(chunkSize);
+
+            //find two chunk rectanges, outer and inner
+            var (minX, minY) = OperationHelper.GetChunkPos(X, Y, chunkSize);
+            var (maxX, maxY) = OperationHelper.GetChunkPos(MaxX, MaxY, chunkSize);
+            var (minInsetX, minInsetY) = OperationHelper.GetChunkPos(xInset.Value.Item1, yInset.Value.Item1, chunkSize);
+            var (maxInsetX, maxInsetY) = OperationHelper.GetChunkPos(xInset.Value.Item2, yInset.Value.Item2, chunkSize);
+
+            //fill in sides
+            HashSet<(int, int)> chunks = new();
+            AddRectangle(minX, minY, maxX, minInsetY, chunks); //top
+            AddRectangle(minX, minInsetY + 1, minInsetX, maxInsetY - 1, chunks); //left
+            AddRectangle(maxInsetX, minInsetY + 1, maxX, maxInsetY - 1, chunks); //right
+            AddRectangle(minX, maxInsetY, maxX, maxY, chunks); //bottom
+            return chunks;
+        }
+
+        private HashSet<(int, int)> GetChunksForFilled(int chunkSize)
+        {
+            var (minX, minY) = OperationHelper.GetChunkPos(X, Y, chunkSize);
+            var (maxX, maxY) = OperationHelper.GetChunkPos(MaxX, MaxY, chunkSize);
+            HashSet<(int, int)> output = new();
+            AddRectangle(minX, minY, maxX, maxY, output);
+            return output;
+        }
+
+        private static void AddRectangle(int minX, int minY, int maxX, int maxY, HashSet<(int, int)> set)
+        {
+            for (int x = minX; x <= maxX; x++)
+            {
+                for (int y = minY; y <= maxY; y++)
+                {
+                    set.Add((x, y));
+                }
+            }
+        }
     }
     }
 }
 }

+ 27 - 0
src/ChunkyImageLibTest/ChunkyImageLibTest.csproj

@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <Nullable>enable</Nullable>
+
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+    <PackageReference Include="coverlet.collector" Version="3.1.0">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj" />
+  </ItemGroup>
+
+</Project>

+ 21 - 0
src/ChunkyImageLibTest/OperationHelperTests.cs

@@ -0,0 +1,21 @@
+using ChunkyImageLib.Operations;
+using Xunit;
+
+namespace ChunkyImageLibTest
+{
+    public class OperationHelperTests
+    {
+        [Theory]
+        [InlineData(0, 0, 0, 0)]
+        [InlineData(-1, -1, -1, -1)]
+        [InlineData(32, 32, 1, 1)]
+        [InlineData(-32, -32, -1, -1)]
+        [InlineData(-33, -33, -2, -2)]
+        public void GetChunkPos_32ChunkSize_ReturnsCorrectValues(int x, int y, int expX, int expY)
+        {
+            var (actX, actY) = OperationHelper.GetChunkPos(x, y, 32);
+            Assert.Equal(expX, actX);
+            Assert.Equal(expY, actY);
+        }
+    }
+}

+ 119 - 0
src/ChunkyImageLibTest/RectangleOperationTests.cs

@@ -0,0 +1,119 @@
+using ChunkyImageLib.Operations;
+using SkiaSharp;
+using System.Collections.Generic;
+using Xunit;
+
+namespace ChunkyImageLibTest
+{
+    public class RectangleOperationTests
+    {
+// to keep expected rectangles aligned
+#pragma warning disable format
+        [Fact]
+        public void FindAffectedChunks_SmallStrokeOnly_FindsCorrectChunks()
+        {
+            var (x, y, w, h) = (0, 0, 32, 32);
+            RectangleOperation operation = new(x, y, w, h, 1, SKColors.Black, SKColors.Transparent);
+
+            HashSet<(int, int)> expected = new() { (0, 0) };
+            var actual = operation.FindAffectedChunks(32);
+
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void FindAffectedChunks_2by2StrokeOnly_FindsCorrectChunks()
+        {
+            var (x, y, w, h) = (-32, -32, 64, 64);
+            RectangleOperation operation = new(x, y, w, h, 1, SKColors.Black, SKColors.Transparent);
+
+            HashSet<(int, int)> expected = new() { (-1, -1), (0, -1), (-1, 0), (0, 0) };
+            var actual = operation.FindAffectedChunks(32);
+
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void FindAffectedChunks_3x3PositiveStrokeOnly_FindsCorrectChunks()
+        {
+            var (x, y, w, h) = (48, 48, 64, 64);
+            RectangleOperation operation = new(x, y, w, h, 1, SKColors.Black, SKColors.Transparent);
+
+            HashSet<(int, int)> expected = new()
+            {
+                (1, 1), (2, 1), (3, 1), 
+                (1, 2),         (3, 2), 
+                (1, 3), (2, 3), (3, 3),
+            };
+            var actual = operation.FindAffectedChunks(32);
+
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void FindAffectedChunks_3x3NegativeStrokeOnly_FindsCorrectChunks()
+        {
+            var (x, y, w, h) = (-112, -112, 64, 64);
+            RectangleOperation operation = new(x, y, w, h, 1, SKColors.Black, SKColors.Transparent);
+
+            HashSet<(int, int)> expected = new()
+            {
+                (-4, -4), (-3, -4), (-2, -4),
+                (-4, -3),           (-2, -3),
+                (-4, -2), (-3, -2), (-2, -2),
+            };
+            var actual = operation.FindAffectedChunks(32);
+
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void FindAffectedChunks_3x3PositiveFilled_FindsCorrectChunks()
+        {
+            var (x, y, w, h) = (48, 48, 64, 64);
+            RectangleOperation operation = new(x, y, w, h, 1, SKColors.Black, SKColors.White);
+
+            HashSet<(int, int)> expected = new()
+            {
+                (1, 1), (2, 1), (3, 1), 
+                (1, 2), (2, 2), (3, 2),
+                (1, 3), (2, 3), (3, 3),
+            };
+            var actual = operation.FindAffectedChunks(32);
+
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void FindAffectedChunks_ThickPositiveStroke_FindsCorrectChunks()
+        {
+            var (x, y, w, h) = (16, 16, 128, 128);
+            RectangleOperation operation = new(x, y, w, h, 32, SKColors.Black, SKColors.Transparent);
+
+            HashSet<(int, int)> expected = new()
+            {
+                (0, 0), (1, 0), (2, 0), (3, 0), (4, 0),
+                (0, 1), (1, 1), (2, 1), (3, 1), (4, 1),
+                (0, 2), (1, 2),         (3, 2), (4, 2),
+                (0, 3), (1, 3), (2, 3), (3, 3), (4, 3),
+                (0, 4), (1, 4), (2, 4), (3, 4), (4, 4),
+            };
+            var actual = operation.FindAffectedChunks(32);
+
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void FindAffectedChunks_SmallButThick_FindsCorrectChunks()
+        {
+            var (x, y, w, h) = (16, 16, 1, 1);
+            RectangleOperation operation = new(x, y, w, h, 256, SKColors.Black, SKColors.White);
+
+            HashSet<(int, int)> expected = new() { (0, 0) };
+            var actual = operation.FindAffectedChunks(32);
+
+            Assert.Equal(expected, actual);
+        }
+#pragma warning restore format
+    }
+}

+ 10 - 4
src/PixiEditorPrototype.sln

@@ -3,13 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 17
 # Visual Studio Version 17
 VisualStudioVersion = 17.0.31912.275
 VisualStudioVersion = 17.0.31912.275
 MinimumVisualStudioVersion = 10.0.40219.1
 MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditorPrototype", "PixiEditorPrototype\PixiEditorPrototype.csproj", "{64D7EBA9-A3D0-4832-ACB7-3C519BD23755}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditorPrototype", "PixiEditorPrototype\PixiEditorPrototype.csproj", "{64D7EBA9-A3D0-4832-ACB7-3C519BD23755}"
 EndProject
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChangeableDocument", "ChangeableDocument\ChangeableDocument.csproj", "{181C9914-75B5-4BEB-AA16-F29B42A401EE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangeableDocument", "ChangeableDocument\ChangeableDocument.csproj", "{181C9914-75B5-4BEB-AA16-F29B42A401EE}"
 EndProject
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChunkyImageLib", "ChunkyImageLib\ChunkyImageLib.csproj", "{EFA4866B-F03E-4F6F-A7B8-1CA6467D5D17}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChunkyImageLib", "ChunkyImageLib\ChunkyImageLib.csproj", "{EFA4866B-F03E-4F6F-A7B8-1CA6467D5D17}"
 EndProject
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StructureRenderer", "StructureRenderer\StructureRenderer.csproj", "{2B396104-7F74-4E03-849E-0AD6EF003666}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StructureRenderer", "StructureRenderer\StructureRenderer.csproj", "{2B396104-7F74-4E03-849E-0AD6EF003666}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChunkyImageLibTest", "ChunkyImageLibTest\ChunkyImageLibTest.csproj", "{794971CA-8CD2-4D1D-BDD9-F41E2638D138}"
 EndProject
 EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -33,6 +35,10 @@ Global
 		{2B396104-7F74-4E03-849E-0AD6EF003666}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{2B396104-7F74-4E03-849E-0AD6EF003666}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{2B396104-7F74-4E03-849E-0AD6EF003666}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2B396104-7F74-4E03-849E-0AD6EF003666}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2B396104-7F74-4E03-849E-0AD6EF003666}.Release|Any CPU.Build.0 = Release|Any CPU
 		{2B396104-7F74-4E03-849E-0AD6EF003666}.Release|Any CPU.Build.0 = Release|Any CPU
+		{794971CA-8CD2-4D1D-BDD9-F41E2638D138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{794971CA-8CD2-4D1D-BDD9-F41E2638D138}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{794971CA-8CD2-4D1D-BDD9-F41E2638D138}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{794971CA-8CD2-4D1D-BDD9-F41E2638D138}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE

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

@@ -19,13 +19,13 @@ namespace PixiEditorPrototype.Models
             if (doc.SelectedStructureMember == null)
             if (doc.SelectedStructureMember == null)
             {
             {
                 //put member on top
                 //put member on top
-                doc.ActionAccumulator.AddAction(new CreateStructureMemberAction(doc.StructureRoot.GuidValue, 0, type));
+                doc.ActionAccumulator.AddAction(new CreateStructureMember_Action(doc.StructureRoot.GuidValue, 0, type));
                 return;
                 return;
             }
             }
             if (doc.SelectedStructureMember is FolderViewModel folder)
             if (doc.SelectedStructureMember is FolderViewModel folder)
             {
             {
                 //put member inside folder on top
                 //put member inside folder on top
-                doc.ActionAccumulator.AddAction(new CreateStructureMemberAction(folder.GuidValue, 0, type));
+                doc.ActionAccumulator.AddAction(new CreateStructureMember_Action(folder.GuidValue, 0, type));
                 return;
                 return;
             }
             }
             if (doc.SelectedStructureMember is LayerViewModel layer)
             if (doc.SelectedStructureMember is LayerViewModel layer)
@@ -35,7 +35,7 @@ namespace PixiEditorPrototype.Models
                 if (path.Count < 2)
                 if (path.Count < 2)
                     throw new Exception("Couldn't find a path to the selected member");
                     throw new Exception("Couldn't find a path to the selected member");
                 var parent = (FolderViewModel)path[1];
                 var parent = (FolderViewModel)path[1];
-                doc.ActionAccumulator.AddAction(new CreateStructureMemberAction(parent.GuidValue, parent.Children.IndexOf(layer), type));
+                doc.ActionAccumulator.AddAction(new CreateStructureMember_Action(parent.GuidValue, parent.Children.IndexOf(layer), type));
                 return;
                 return;
             }
             }
             throw new Exception("Unknown member type: " + type.ToString());
             throw new Exception("Unknown member type: " + type.ToString());
@@ -98,19 +98,19 @@ namespace PixiEditorPrototype.Models
                 int curIndex = doc.StructureRoot.Children.IndexOf(path[0]);
                 int curIndex = doc.StructureRoot.Children.IndexOf(path[0]);
                 if (curIndex == 0 && up || curIndex == doc.StructureRoot.Children.Count - 1 && !up)
                 if (curIndex == 0 && up || curIndex == doc.StructureRoot.Children.Count - 1 && !up)
                     return;
                     return;
-                doc.ActionAccumulator.AddAction(new MoveStructureMemberAction(guid, doc.StructureRoot.GuidValue, up ? curIndex - 1 : curIndex + 1));
+                doc.ActionAccumulator.AddAction(new MoveStructureMember_Action(guid, doc.StructureRoot.GuidValue, up ? curIndex - 1 : curIndex + 1));
                 return;
                 return;
             }
             }
             var folder = (FolderViewModel)path[1];
             var folder = (FolderViewModel)path[1];
             int index = folder.Children.IndexOf(path[0]);
             int index = folder.Children.IndexOf(path[0]);
             if (up && index > 0 || !up && index < folder.Children.Count - 1)
             if (up && index > 0 || !up && index < folder.Children.Count - 1)
             {
             {
-                doc.ActionAccumulator.AddAction(new MoveStructureMemberAction(guid, path[1].GuidValue, up ? index - 1 : index + 1));
+                doc.ActionAccumulator.AddAction(new MoveStructureMember_Action(guid, path[1].GuidValue, up ? index - 1 : index + 1));
             }
             }
             else
             else
             {
             {
                 int parentIndex = ((FolderViewModel)path[2]).Children.IndexOf(folder);
                 int parentIndex = ((FolderViewModel)path[2]).Children.IndexOf(folder);
-                doc.ActionAccumulator.AddAction(new MoveStructureMemberAction(guid, path[2].GuidValue, up ? parentIndex : parentIndex + 1));
+                doc.ActionAccumulator.AddAction(new MoveStructureMember_Action(guid, path[2].GuidValue, up ? parentIndex : parentIndex + 1));
             }
             }
         }
         }
     }
     }

+ 2 - 0
src/PixiEditorPrototype/PixiEditorPrototype.csproj

@@ -13,6 +13,8 @@
 
 
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\ChangeableDocument\ChangeableDocument.csproj" />
     <ProjectReference Include="..\ChangeableDocument\ChangeableDocument.csproj" />
+    <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj" />
+    <ProjectReference Include="..\StructureRenderer\StructureRenderer.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 
 </Project>
 </Project>

+ 1 - 1
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -46,7 +46,7 @@ namespace PixiEditorPrototype.ViewModels
         public void DeleteStructureMember(object? param)
         public void DeleteStructureMember(object? param)
         {
         {
             if (SelectedStructureMember != null)
             if (SelectedStructureMember != null)
-                ActionAccumulator.AddAction(new DeleteStructureMemberAction(SelectedStructureMember.GuidValue));
+                ActionAccumulator.AddAction(new DeleteStructureMember_Action(SelectedStructureMember.GuidValue));
         }
         }
 
 
         public void Undo(object? param)
         public void Undo(object? param)

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

@@ -14,13 +14,13 @@ namespace PixiEditorPrototype.ViewModels
         public string Name
         public string Name
         {
         {
             get => member.Name;
             get => member.Name;
-            set => Document.ActionAccumulator.AddAction(new SetStructureMemberNameAction(value, member.GuidValue));
+            set => Document.ActionAccumulator.AddAction(new SetStructureMemberName_Action(value, member.GuidValue));
         }
         }
 
 
         public bool IsVisible
         public bool IsVisible
         {
         {
             get => member.IsVisible;
             get => member.IsVisible;
-            set => Document.ActionAccumulator.AddAction(new SetStructureMemberVisibilityAction(value, member.GuidValue));
+            set => Document.ActionAccumulator.AddAction(new SetStructureMemberVisibility_Action(value, member.GuidValue));
         }
         }
 
 
         public Guid GuidValue
         public Guid GuidValue