Browse Source

Add project files.

Equbuxu 3 years ago
parent
commit
af43d28c10
53 changed files with 1661 additions and 0 deletions
  1. 6 0
      src/ChangeableDocument/Actions/IAction.cs
  2. 9 0
      src/ChangeableDocument/Actions/MiscActions.cs
  3. 62 0
      src/ChangeableDocument/Actions/StructureActions.cs
  4. 6 0
      src/ChangeableDocument/ChangeInfos/IChangeInfo.cs
  5. 24 0
      src/ChangeableDocument/ChangeInfos/StructureChangeInfos.cs
  6. 12 0
      src/ChangeableDocument/ChangeUpdateInfos/IUpdateInfo.cs
  7. 9 0
      src/ChangeableDocument/ChangeableDocument.csproj
  8. 63 0
      src/ChangeableDocument/Changeables/Document.cs
  9. 27 0
      src/ChangeableDocument/Changeables/Folder.cs
  10. 8 0
      src/ChangeableDocument/Changeables/IChangeable.cs
  11. 11 0
      src/ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  12. 7 0
      src/ChangeableDocument/Changeables/Interfaces/IReadOnlyFolder.cs
  13. 6 0
      src/ChangeableDocument/Changeables/Interfaces/IReadOnlyLayer.cs
  14. 9 0
      src/ChangeableDocument/Changeables/Interfaces/IReadOnlyStructureMember.cs
  15. 17 0
      src/ChangeableDocument/Changeables/Layer.cs
  16. 13 0
      src/ChangeableDocument/Changeables/StructureMember.cs
  17. 48 0
      src/ChangeableDocument/Changes/Change.cs
  18. 50 0
      src/ChangeableDocument/Changes/Document_CreateStructureMember_Change.cs
  19. 41 0
      src/ChangeableDocument/Changes/Document_DeleteStructureMember_Change.cs
  20. 51 0
      src/ChangeableDocument/Changes/Document_MoveStructureMember_Change.cs
  21. 56 0
      src/ChangeableDocument/Changes/Document_UpdateStructureMemberProperties_Change.cs
  22. 12 0
      src/ChangeableDocument/Changes/IChange.cs
  23. 21 0
      src/ChangeableDocument/Changes/UpdateableChange.cs
  24. 139 0
      src/ChangeableDocument/DocumentChangeTracker.cs
  25. 8 0
      src/ChangeableDocument/StructureMemberType.cs
  26. 9 0
      src/ChunkyImage/ChunkyImage.csproj
  27. 7 0
      src/ChunkyImage/Class1.cs
  28. 26 0
      src/ChunkyImageLib/ChunkMap.cs
  29. 88 0
      src/ChunkyImageLib/ChunkyImage.cs
  30. 14 0
      src/ChunkyImageLib/ChunkyImageLib.csproj
  31. 82 0
      src/ChunkyImageLib/ImageData.cs
  32. 12 0
      src/ChunkyImageLib/Operations/IOperation.cs
  33. 18 0
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  34. 43 0
      src/PixiEditorPrototype.sln
  35. 9 0
      src/PixiEditorPrototype/App.xaml
  36. 17 0
      src/PixiEditorPrototype/App.xaml.cs
  37. 10 0
      src/PixiEditorPrototype/AssemblyInfo.cs
  38. 14 0
      src/PixiEditorPrototype/MainWindow.xaml
  39. 18 0
      src/PixiEditorPrototype/MainWindow.xaml.cs
  40. 45 0
      src/PixiEditorPrototype/Models/ActionAccumulator.cs
  41. 117 0
      src/PixiEditorPrototype/Models/DocumentStructureHelper.cs
  42. 88 0
      src/PixiEditorPrototype/Models/DocumentUpdater.cs
  43. 19 0
      src/PixiEditorPrototype/Models/IReadOnlyListEx.cs
  44. 18 0
      src/PixiEditorPrototype/PixiEditorPrototype.csproj
  45. 34 0
      src/PixiEditorPrototype/RelayCommand.cs
  46. 71 0
      src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs
  47. 15 0
      src/PixiEditorPrototype/ViewModels/FolderViewModel.cs
  48. 11 0
      src/PixiEditorPrototype/ViewModels/LayerViewModel.cs
  49. 47 0
      src/PixiEditorPrototype/ViewModels/StructureMemberViewModel.cs
  50. 66 0
      src/PixiEditorPrototype/Views/DocumentView.xaml
  51. 15 0
      src/PixiEditorPrototype/Views/DocumentView.xaml.cs
  52. 19 0
      src/StructureRenderer/Renderer.cs
  53. 14 0
      src/StructureRenderer/StructureRenderer.csproj

+ 6 - 0
src/ChangeableDocument/Actions/IAction.cs

@@ -0,0 +1,6 @@
+namespace ChangeableDocument.Actions
+{
+    public interface IAction
+    {
+    }
+}

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

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

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

@@ -0,0 +1,62 @@
+namespace ChangeableDocument.Actions;
+public record CreateStructureMemberAction : IAction
+{
+    public CreateStructureMemberAction(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; }
+}
+
+public record DeleteStructureMemberAction : IAction
+{
+    public DeleteStructureMemberAction(Guid guidValue)
+    {
+        GuidValue = guidValue;
+    }
+
+    public Guid GuidValue { get; }
+}
+
+public record MoveStructureMemberAction : IAction
+{
+    public MoveStructureMemberAction(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; }
+}
+
+public record SetStructureMemberNameAction : IAction
+{
+    public SetStructureMemberNameAction(string name, Guid guidValue)
+    {
+        Name = name;
+        GuidValue = guidValue;
+    }
+
+    public string Name { get; init; }
+    public Guid GuidValue { get; init; }
+}
+
+public record SetStructureMemberVisibilityAction : IAction
+{
+    public SetStructureMemberVisibilityAction(bool isVisible, Guid guidValue)
+    {
+        this.isVisible = isVisible;
+        GuidValue = guidValue;
+    }
+
+    public bool isVisible { get; init; }
+    public Guid GuidValue { get; init; }
+}

+ 6 - 0
src/ChangeableDocument/ChangeInfos/IChangeInfo.cs

@@ -0,0 +1,6 @@
+namespace ChangeableDocument.ChangeInfos
+{
+    public interface IChangeInfo
+    {
+    }
+}

+ 24 - 0
src/ChangeableDocument/ChangeInfos/StructureChangeInfos.cs

@@ -0,0 +1,24 @@
+namespace ChangeableDocument.ChangeInfos
+{
+    public record Document_CreateStructureMember_ChangeInfo : IChangeInfo
+    {
+        public Guid GuidValue { get; init; }
+    }
+
+    public record Document_DeleteStructureMember_ChangeInfo : IChangeInfo
+    {
+        public Guid GuidValue { get; init; }
+    }
+
+    public record Document_MoveStructureMember_ChangeInfo : IChangeInfo
+    {
+        public Guid GuidValue { get; init; }
+    }
+
+    public record Document_UpdateStructureMemberProperties_ChangeInfo : IChangeInfo
+    {
+        public Guid GuidValue { get; init; }
+        public bool IsVisibleChanged { get; init; } = false;
+        public bool NameChanged { get; init; } = false;
+    }
+}

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

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

+ 9 - 0
src/ChangeableDocument/ChangeableDocument.csproj

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

+ 63 - 0
src/ChangeableDocument/Changeables/Document.cs

@@ -0,0 +1,63 @@
+using ChangeableDocument.Changeables.Interfaces;
+
+namespace ChangeableDocument.Changeables
+{
+    internal class Document : IChangeable, IReadOnlyDocument
+    {
+        public IReadOnlyFolder ReadOnlyStructureRoot => StructureRoot;
+        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 };
+
+        public StructureMember FindMemberOrThrow(Guid guid) => FindMember(guid) ?? throw new Exception("Could not find member with guid " + guid.ToString());
+        public StructureMember? FindMember(Guid guid)
+        {
+            var list = FindMemberPath(guid);
+            return list.Count > 0 ? list[0] : null;
+        }
+
+        public (StructureMember, Folder) FindChildAndParentOrThrow(Guid childGuid)
+        {
+            var path = FindMemberPath(childGuid);
+            if (path.Count < 2)
+                throw new Exception("Couldn't find child and parent");
+            return (path[0], (Folder)path[1]);
+        }
+
+        public List<StructureMember> FindMemberPath(Guid guid)
+        {
+            var list = new List<StructureMember>();
+            if (FillMemberPath(StructureRoot, guid, list))
+                list.Add(StructureRoot);
+            return list;
+        }
+
+        private bool FillMemberPath(Folder folder, Guid guid, List<StructureMember> toFill)
+        {
+            if (folder.GuidValue == guid)
+            {
+                return true;
+            }
+            foreach (var member in folder.Children)
+            {
+                if (member is Layer childLayer && childLayer.GuidValue == guid)
+                {
+                    toFill.Add(member);
+                    return true;
+                }
+                if (member is Folder childFolder)
+                {
+                    if (FillMemberPath(childFolder, guid, toFill))
+                    {
+                        toFill.Add(childFolder);
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+}

+ 27 - 0
src/ChangeableDocument/Changeables/Folder.cs

@@ -0,0 +1,27 @@
+using ChangeableDocument.Changeables.Interfaces;
+
+namespace ChangeableDocument.Changeables
+{
+    internal class Folder : StructureMember, IReadOnlyFolder
+    {
+        internal List<StructureMember> Children { get; set; } = new();
+        public IReadOnlyList<IReadOnlyStructureMember> ReadOnlyChildren => Children;
+
+        internal override Folder Clone()
+        {
+            List<StructureMember> clonedChildren = new();
+            foreach (var child in Children)
+            {
+                clonedChildren.Add(child.Clone());
+            }
+
+            return new Folder()
+            {
+                GuidValue = GuidValue,
+                IsVisible = IsVisible,
+                Name = Name,
+                Children = clonedChildren
+            };
+        }
+    }
+}

+ 8 - 0
src/ChangeableDocument/Changeables/IChangeable.cs

@@ -0,0 +1,8 @@
+namespace ChangeableDocument.Changeables
+{
+
+    internal interface IChangeable
+    {
+
+    };
+}

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

@@ -0,0 +1,11 @@
+namespace ChangeableDocument.Changeables.Interfaces
+{
+    public interface IReadOnlyDocument
+    {
+        IReadOnlyFolder ReadOnlyStructureRoot { get; }
+        IReadOnlyStructureMember? FindMember(Guid guid);
+        IReadOnlyStructureMember FindMemberOrThrow(Guid guid);
+        (IReadOnlyStructureMember, IReadOnlyFolder) FindChildAndParentOrThrow(Guid guid);
+        IReadOnlyList<IReadOnlyStructureMember> FindMemberPath(Guid guid);
+    }
+}

+ 7 - 0
src/ChangeableDocument/Changeables/Interfaces/IReadOnlyFolder.cs

@@ -0,0 +1,7 @@
+namespace ChangeableDocument.Changeables.Interfaces
+{
+    public interface IReadOnlyFolder : IReadOnlyStructureMember
+    {
+        IReadOnlyList<IReadOnlyStructureMember> ReadOnlyChildren { get; }
+    }
+}

+ 6 - 0
src/ChangeableDocument/Changeables/Interfaces/IReadOnlyLayer.cs

@@ -0,0 +1,6 @@
+namespace ChangeableDocument.Changeables.Interfaces
+{
+    public interface IReadOnlyLayer : IReadOnlyStructureMember
+    {
+    }
+}

+ 9 - 0
src/ChangeableDocument/Changeables/Interfaces/IReadOnlyStructureMember.cs

@@ -0,0 +1,9 @@
+namespace ChangeableDocument.Changeables.Interfaces
+{
+    public interface IReadOnlyStructureMember
+    {
+        bool IsVisible { get; }
+        string Name { get; }
+        Guid GuidValue { get; }
+    }
+}

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

@@ -0,0 +1,17 @@
+using ChangeableDocument.Changeables.Interfaces;
+
+namespace ChangeableDocument.Changeables
+{
+    internal class Layer : StructureMember, IReadOnlyLayer
+    {
+        internal override Layer Clone()
+        {
+            return new Layer()
+            {
+                GuidValue = GuidValue,
+                IsVisible = IsVisible,
+                Name = Name
+            };
+        }
+    }
+}

+ 13 - 0
src/ChangeableDocument/Changeables/StructureMember.cs

@@ -0,0 +1,13 @@
+using ChangeableDocument.Changeables.Interfaces;
+
+namespace ChangeableDocument.Changeables
+{
+    internal abstract class StructureMember : IChangeable, IReadOnlyStructureMember
+    {
+        public bool IsVisible { get; set; } = true;
+        public string Name { get; set; } = "Unnamed";
+        public Guid GuidValue { get; init; }
+
+        internal abstract StructureMember Clone();
+    }
+}

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

@@ -0,0 +1,48 @@
+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);
+
+    };
+}

+ 50 - 0
src/ChangeableDocument/Changes/Document_CreateStructureMember_Change.cs

@@ -0,0 +1,50 @@
+using ChangeableDocument.Changeables;
+using ChangeableDocument.ChangeInfos;
+
+namespace ChangeableDocument.Changes
+{
+    internal class Document_CreateStructureMember_Change : Change<Document>
+    {
+        private Guid newMemberGuid;
+
+        private Guid parentFolderGuid;
+        private int parentFolderIndex;
+        private StructureMemberType type;
+
+        public Document_CreateStructureMember_Change(Guid parentFolder, int parentFolderIndex, StructureMemberType type)
+        {
+            this.parentFolderGuid = parentFolder;
+            this.parentFolderIndex = parentFolderIndex;
+            this.type = type;
+        }
+
+        protected override void DoInitialize(Document target)
+        {
+            newMemberGuid = Guid.NewGuid();
+        }
+
+        protected override IChangeInfo DoApply(Document document)
+        {
+            var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
+
+            StructureMember member = type switch
+            {
+                StructureMemberType.Layer => new Layer() { GuidValue = newMemberGuid },
+                StructureMemberType.Folder => new Folder() { GuidValue = newMemberGuid },
+                _ => throw new Exception("Cannon create member of type " + type.ToString())
+            };
+
+            folder.Children.Insert(parentFolderIndex, member);
+
+            return new Document_CreateStructureMember_ChangeInfo() { GuidValue = newMemberGuid };
+        }
+
+        protected override IChangeInfo DoRevert(Document document)
+        {
+            var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
+            folder.Children.RemoveAt(folder.Children.FindIndex(child => child.GuidValue == newMemberGuid));
+
+            return new Document_DeleteStructureMember_ChangeInfo() { GuidValue = newMemberGuid };
+        }
+    }
+}

+ 41 - 0
src/ChangeableDocument/Changes/Document_DeleteStructureMember_Change.cs

@@ -0,0 +1,41 @@
+using ChangeableDocument.Changeables;
+using ChangeableDocument.ChangeInfos;
+
+namespace ChangeableDocument.Changes
+{
+    internal class Document_DeleteStructureMember_Change : Change<Document>
+    {
+        private Guid memberGuid;
+        private Guid parentGuid;
+        private int originalIndex;
+        private StructureMember? savedCopy;
+        public Document_DeleteStructureMember_Change(Guid memberGuid)
+        {
+            this.memberGuid = memberGuid;
+        }
+
+        protected override void DoInitialize(Document document)
+        {
+            var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
+
+            originalIndex = parent.Children.IndexOf(member);
+            parentGuid = parent.GuidValue;
+            savedCopy = member.Clone();
+        }
+
+        protected override IChangeInfo DoApply(Document document)
+        {
+            var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
+            parent.Children.Remove(member);
+            return new Document_DeleteStructureMember_ChangeInfo() { GuidValue = memberGuid };
+        }
+
+        protected override IChangeInfo DoRevert(Document doc)
+        {
+            var parent = (Folder)doc.FindMemberOrThrow(parentGuid);
+
+            parent.Children.Insert(originalIndex, savedCopy!.Clone());
+            return new Document_CreateStructureMember_ChangeInfo() { GuidValue = memberGuid };
+        }
+    }
+}

+ 51 - 0
src/ChangeableDocument/Changes/Document_MoveStructureMember_Change.cs

@@ -0,0 +1,51 @@
+using ChangeableDocument.Changeables;
+using ChangeableDocument.ChangeInfos;
+
+namespace ChangeableDocument.Changes
+{
+    internal class Document_MoveStructureMember_Change : Change<Document>
+    {
+        private Guid memberGuid;
+
+        private Guid targetFolderGuid;
+        private int targetFolderIndex;
+
+        private Guid originalFolderGuid;
+        private int originalFolderIndex;
+
+        public Document_MoveStructureMember_Change(Guid memberGuid, Guid targetFolder, int targetFolderIndex)
+        {
+            this.memberGuid = memberGuid;
+            this.targetFolderGuid = targetFolder;
+            this.targetFolderIndex = targetFolderIndex;
+        }
+
+        protected override void DoInitialize(Document document)
+        {
+            var (member, curFolder) = document.FindChildAndParentOrThrow(memberGuid);
+            originalFolderGuid = curFolder.GuidValue;
+            originalFolderIndex = curFolder.Children.IndexOf(member);
+        }
+
+        private static void Move(Document document, Guid memberGuid, Guid targetFolderGuid, int targetIndex)
+        {
+            var targetFolder = (Folder)document.FindMemberOrThrow(targetFolderGuid);
+            var (member, curFolder) = document.FindChildAndParentOrThrow(memberGuid);
+
+            curFolder.Children.Remove(member);
+            targetFolder.Children.Insert(targetIndex, member);
+        }
+
+        protected override IChangeInfo? DoApply(Document target)
+        {
+            Move(target, memberGuid, targetFolderGuid, targetFolderIndex);
+            return new Document_MoveStructureMember_ChangeInfo() { GuidValue = memberGuid };
+        }
+
+        protected override IChangeInfo? DoRevert(Document target)
+        {
+            Move(target, memberGuid, originalFolderGuid, originalFolderIndex);
+            return new Document_MoveStructureMember_ChangeInfo() { GuidValue = memberGuid };
+        }
+    }
+}

+ 56 - 0
src/ChangeableDocument/Changes/Document_UpdateStructureMemberProperties_Change.cs

@@ -0,0 +1,56 @@
+using ChangeableDocument.Changeables;
+using ChangeableDocument.ChangeInfos;
+
+namespace ChangeableDocument.Changes
+{
+    internal class Document_UpdateStructureMemberProperties_Change : Change<Document>
+    {
+        private Guid memberGuid;
+
+        private bool originalIsVisible;
+        public bool? NewIsVisible { get; init; } = null;
+
+        private string? originalName;
+        public string? NewName { get; init; } = null;
+
+        public Document_UpdateStructureMemberProperties_Change(Guid memberGuid)
+        {
+            this.memberGuid = memberGuid;
+        }
+
+        protected override void DoInitialize(Document document)
+        {
+            var member = document.FindMemberOrThrow(memberGuid);
+            if (NewIsVisible != null) originalIsVisible = member.IsVisible;
+            if (NewName != null) originalName = member.Name;
+        }
+
+        protected override IChangeInfo? DoApply(Document document)
+        {
+            var member = document.FindMemberOrThrow(memberGuid);
+            if (NewIsVisible != null) member.IsVisible = NewIsVisible.Value;
+            if (NewName != null) member.Name = NewName;
+
+            return new Document_UpdateStructureMemberProperties_ChangeInfo()
+            {
+                GuidValue = member.GuidValue,
+                IsVisibleChanged = NewIsVisible != null,
+                NameChanged = NewName != null
+            };
+        }
+
+        protected override IChangeInfo? DoRevert(Document document)
+        {
+            var member = document.FindMemberOrThrow(memberGuid);
+            if (NewIsVisible != null) member.IsVisible = originalIsVisible;
+            if (NewName != null) member.Name = originalName!;
+
+            return new Document_UpdateStructureMemberProperties_ChangeInfo()
+            {
+                GuidValue = member.GuidValue,
+                IsVisibleChanged = NewIsVisible != null,
+                NameChanged = NewName != null,
+            };
+        }
+    }
+}

+ 12 - 0
src/ChangeableDocument/Changes/IChange.cs

@@ -0,0 +1,12 @@
+using ChangeableDocument.Changeables;
+using ChangeableDocument.ChangeInfos;
+
+namespace ChangeableDocument.Changes
+{
+    internal interface IChange
+    {
+        void Initialize(IChangeable target);
+        IChangeInfo? Apply(IChangeable target);
+        IChangeInfo? Revert(IChangeable target);
+    }
+}

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

@@ -0,0 +1,21 @@
+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);
+    }
+}

+ 139 - 0
src/ChangeableDocument/DocumentChangeTracker.cs

@@ -0,0 +1,139 @@
+using ChangeableDocument.Actions;
+using ChangeableDocument.Changeables;
+using ChangeableDocument.Changeables.Interfaces;
+using ChangeableDocument.ChangeInfos;
+using ChangeableDocument.Changes;
+
+namespace ChangeableDocument
+{
+    public class DocumentChangeTracker
+    {
+        private Document document;
+        public IReadOnlyDocument Document => document;
+
+        private Stack<IChange> undoStack = new();
+        private Stack<IChange> redoStack = new();
+
+        public DocumentChangeTracker()
+        {
+            document = new Document();
+        }
+
+        private IChangeInfo? InitAndApplyWithUndo(IChange change)
+        {
+            change.Initialize(document);
+            var info = change.Apply(document);
+            undoStack.Push(change);
+            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()
+        {
+            if (undoStack.Count == 0)
+                return null;
+            IChange change = undoStack.Pop();
+            var info = change.Revert(document);
+            redoStack.Push(change);
+            return info;
+        }
+
+        private IChangeInfo? Redo()
+        {
+            if (redoStack.Count == 0)
+                return null;
+            IChange change = redoStack.Pop();
+            var info = change.Apply(document);
+            undoStack.Push(change);
+            return info;
+        }
+
+        public async Task<List<IChangeInfo?>> ProcessActions(List<IAction> actions)
+        {
+            List<IChangeInfo?> result = await Task.Run(() =>
+            {
+                List<IChangeInfo?> changes = new();
+                foreach (var action in actions)
+                {
+                    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));
+                            break;
+                        case SetStructureMemberNameAction act:
+                            changes.Add(SetStructureMemberName(act.GuidValue, act.Name));
+                            break;
+                        case SetStructureMemberVisibilityAction act:
+                            changes.Add(SetStructureMemberVisibility(act.GuidValue, act.isVisible));
+                            break;
+                        case DeleteStructureMemberAction act:
+                            changes.Add(DeleteStructureMember(act.GuidValue));
+                            break;
+                        case UndoAction act:
+                            changes.Add(Undo());
+                            break;
+                        case RedoAction act:
+                            changes.Add(Redo());
+                            break;
+                    }
+                }
+                return changes;
+            }).ConfigureAwait(true);
+            return result;
+        }
+    }
+
+    class OperationStateMachine
+    {
+        public void ExecuteSingularChange(IChange change)
+        {
+
+        }
+
+        public void StartUpdateableChange()
+        {
+
+        }
+
+        public void EndUpdateableChange()
+        {
+
+        }
+    }
+}

+ 8 - 0
src/ChangeableDocument/StructureMemberType.cs

@@ -0,0 +1,8 @@
+namespace ChangeableDocument
+{
+    public enum StructureMemberType
+    {
+        Layer,
+        Folder
+    }
+}

+ 9 - 0
src/ChunkyImage/ChunkyImage.csproj

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

+ 7 - 0
src/ChunkyImage/Class1.cs

@@ -0,0 +1,7 @@
+namespace ChunkyImage
+{
+    public class Class1
+    {
+
+    }
+}

+ 26 - 0
src/ChunkyImageLib/ChunkMap.cs

@@ -0,0 +1,26 @@
+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)
+        {
+
+        }
+    }
+}

+ 88 - 0
src/ChunkyImageLib/ChunkyImage.cs

@@ -0,0 +1,88 @@
+using ChunkyImageLib.Operations;
+using SkiaSharp;
+
+namespace ChunkyImageLib
+{
+    public class ChunkyImage
+    {
+        //const int chunkSize = 32;
+        private Queue<IOperation> queuedOperations = new Queue<IOperation>();
+
+        private ImageData image;
+        private SKSurface imageSurface;
+
+        private ImageData pendingImage;
+        private SKSurface pendingImageSurface;
+
+        public ChunkyImage(int width, int height)
+        {
+            image = new ImageData(width, height, SKColorType.RgbaF16);
+            pendingImage = new ImageData(width, height, SKColorType.RgbaF16);
+            imageSurface = image.CreateSKSurface();
+            pendingImageSurface = image.CreateSKSurface();
+        }
+
+        public ImageData GetCurrentImageData()
+        {
+            ProcessQueue();
+            return pendingImage;
+        }
+
+        public void DrawRectangle(int x, int y, int width, int height)
+        {
+            queuedOperations.Enqueue(new RectangleOperation(x, y, width, height));
+        }
+
+        public void CancelChanges()
+        {
+            queuedOperations.Clear();
+            image.CopyTo(pendingImage);
+        }
+
+        public void CommitChanges()
+        {
+            ProcessQueue();
+            pendingImage.CopyTo(image);
+        }
+
+        private void ProcessQueue()
+        {
+            foreach (var operation in queuedOperations)
+            {
+                if (operation is RectangleOperation rect)
+                {
+                    using SKPaint black = new() { Color = SKColors.Black };
+                    pendingImageSurface.Canvas.DrawRect(rect.X, rect.Y, rect.Width, rect.Height, black);
+                }
+            }
+            queuedOperations.Clear();
+        }
+        /*
+        private List<(int, int)> GetAffectedChunks(IOperation operation)
+        {
+            if (operation is RectangleOperation rect)
+                return GetAffectedChunks(rect);
+            return new List<(int, int)>();
+        }
+
+        private List<(int, int)> GetAffectedChunks(RectangleOperation rect)
+        {
+            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++)
+            {
+                chunks.Add((i, startY));
+                chunks.Add((i, endY));
+            }
+            for (int i = startY + 1; i < endY; i++)
+            {
+                chunks.Add((startX, i));
+                chunks.Add((endX, i));
+            }
+            return chunks;
+        }*/
+    }
+}

+ 14 - 0
src/ChunkyImageLib/ChunkyImageLib.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="SkiaSharp" Version="2.80.3" />
+  </ItemGroup>
+
+</Project>

+ 82 - 0
src/ChunkyImageLib/ImageData.cs

@@ -0,0 +1,82 @@
+using SkiaSharp;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace ChunkyImageLib
+{
+    public class ImageData : IDisposable
+    {
+        private bool disposed;
+        private int bytesPerPixel;
+        public SKColorType ColorType { get; }
+        public IntPtr PixelBuffer { get; }
+        public int Width { get; }
+        public int Height { get; }
+        public ImageData(int width, int height, SKColorType colorType)
+        {
+            if (colorType is not SKColorType.RgbaF16 or SKColorType.Bgra8888)
+                throw new ArgumentException("Unsupported color type");
+            if (width < 1 || height < 1)
+                throw new ArgumentException("Width and height must be >1");
+            if (width > 10000 || height > 1000)
+                throw new ArgumentException("Width and height must be <=10000");
+
+            ColorType = colorType;
+            bytesPerPixel = colorType == SKColorType.RgbaF16 ? 8 : 4;
+            PixelBuffer = CreateBuffer(width, height, bytesPerPixel);
+        }
+
+        public unsafe void CopyTo(ImageData other)
+        {
+            if (other.Width != Width || other.Height != Height || other.ColorType != ColorType)
+                throw new ArgumentException("Target ImageData must have the same format");
+            int bytesC = Width * Height * bytesPerPixel;
+            Buffer.MemoryCopy((void*)PixelBuffer, (void*)other.PixelBuffer, bytesC, bytesC);
+        }
+
+        public unsafe SKColor GetSRGBPixel(int x, int y)
+        {
+            if (ColorType == SKColorType.RgbaF16)
+            {
+                Half* ptr = (Half*)(PixelBuffer + (x + y * Width) * bytesPerPixel);
+                float a = (float)ptr[3];
+                return (SKColor)new SKColorF((float)ptr[0] / a, (float)ptr[1] / a, (float)ptr[2] / a, (float)ptr[3]);
+            }
+            else
+            {
+                // todo later
+                throw new NotImplementedException();
+            }
+        }
+
+        public SKSurface CreateSKSurface()
+        {
+            var surface = SKSurface.Create(new SKImageInfo(Width, Height, ColorType, SKAlphaType.Premul, SKColorSpace.CreateSrgb()));
+            if (surface == null)
+                throw new Exception("Could not create surface");
+            return surface;
+        }
+
+        private unsafe static IntPtr CreateBuffer(int width, int height, int bytesPerPixel)
+        {
+            int byteC = width * height * bytesPerPixel;
+            var buffer = Marshal.AllocHGlobal(byteC);
+            Unsafe.InitBlockUnaligned((byte*)buffer, 0, (uint)byteC);
+            return buffer;
+        }
+
+        public void Dispose()
+        {
+            if (disposed)
+                return;
+            disposed = true;
+            Marshal.FreeHGlobal(PixelBuffer);
+            GC.SuppressFinalize(this);
+        }
+
+        ~ImageData()
+        {
+            Marshal.FreeHGlobal(PixelBuffer);
+        }
+    }
+}

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

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ChunkyImageLib.Operations
+{
+    internal interface IOperation
+    {
+    }
+}

+ 18 - 0
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -0,0 +1,18 @@
+namespace ChunkyImageLib.Operations
+{
+    internal record RectangleOperation : IOperation
+    {
+        public RectangleOperation(int x, int y, int width, int height)
+        {
+            X = x;
+            Y = y;
+            Width = width;
+            Height = height;
+        }
+
+        public int X { get; }
+        public int Y { get; }
+        public int Width { get; }
+        public int Height { get; }
+    }
+}

+ 43 - 0
src/PixiEditorPrototype.sln

@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31912.275
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditorPrototype", "PixiEditorPrototype\PixiEditorPrototype.csproj", "{64D7EBA9-A3D0-4832-ACB7-3C519BD23755}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChangeableDocument", "ChangeableDocument\ChangeableDocument.csproj", "{181C9914-75B5-4BEB-AA16-F29B42A401EE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChunkyImageLib", "ChunkyImageLib\ChunkyImageLib.csproj", "{EFA4866B-F03E-4F6F-A7B8-1CA6467D5D17}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StructureRenderer", "StructureRenderer\StructureRenderer.csproj", "{2B396104-7F74-4E03-849E-0AD6EF003666}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{64D7EBA9-A3D0-4832-ACB7-3C519BD23755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{64D7EBA9-A3D0-4832-ACB7-3C519BD23755}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{64D7EBA9-A3D0-4832-ACB7-3C519BD23755}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{64D7EBA9-A3D0-4832-ACB7-3C519BD23755}.Release|Any CPU.Build.0 = Release|Any CPU
+		{181C9914-75B5-4BEB-AA16-F29B42A401EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{181C9914-75B5-4BEB-AA16-F29B42A401EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{181C9914-75B5-4BEB-AA16-F29B42A401EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{181C9914-75B5-4BEB-AA16-F29B42A401EE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{EFA4866B-F03E-4F6F-A7B8-1CA6467D5D17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EFA4866B-F03E-4F6F-A7B8-1CA6467D5D17}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EFA4866B-F03E-4F6F-A7B8-1CA6467D5D17}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{EFA4866B-F03E-4F6F-A7B8-1CA6467D5D17}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2B396104-7F74-4E03-849E-0AD6EF003666}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {90FC4DB9-E9C3-4E6F-9564-0B1011A1B521}
+	EndGlobalSection
+EndGlobal

+ 9 - 0
src/PixiEditorPrototype/App.xaml

@@ -0,0 +1,9 @@
+<Application x:Class="PixiEditorPrototype.App"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:PixiEditorPrototype"
+             StartupUri="MainWindow.xaml">
+    <Application.Resources>
+         
+    </Application.Resources>
+</Application>

+ 17 - 0
src/PixiEditorPrototype/App.xaml.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PixiEditorPrototype
+{
+    /// <summary>
+    /// Interaction logic for App.xaml
+    /// </summary>
+    public partial class App : Application
+    {
+    }
+}

+ 10 - 0
src/PixiEditorPrototype/AssemblyInfo.cs

@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+                                     //(used if a resource is not found in the page,
+                                     // or application resource dictionaries)
+    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+                                              //(used if a resource is not found in the page,
+                                              // app, or any theme specific resource dictionaries)
+)]

+ 14 - 0
src/PixiEditorPrototype/MainWindow.xaml

@@ -0,0 +1,14 @@
+<Window x:Class="PixiEditorPrototype.MainWindow" x:ClassModifier="internal"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:PixiEditorPrototype"
+        xmlns:views="clr-namespace:PixiEditorPrototype.Views"
+        mc:Ignorable="d"
+        Title="MainWindow" Height="450" Width="800">
+    <DockPanel>
+        <Button DockPanel.Dock="Top">Create a new document</Button>
+        <views:DocumentView DataContext="{Binding Document}"></views:DocumentView>
+    </DockPanel>
+</Window>

+ 18 - 0
src/PixiEditorPrototype/MainWindow.xaml.cs

@@ -0,0 +1,18 @@
+using PixiEditorPrototype.ViewModels;
+using System.Windows;
+
+namespace PixiEditorPrototype
+{
+    /// <summary>
+    /// Interaction logic for MainWindow.xaml
+    /// </summary>
+    internal partial class MainWindow : Window
+    {
+        public DocumentViewModel Document { get; set; } = new();
+        public MainWindow()
+        {
+            InitializeComponent();
+            DataContext = this;
+        }
+    }
+}

+ 45 - 0
src/PixiEditorPrototype/Models/ActionAccumulator.cs

@@ -0,0 +1,45 @@
+using ChangeableDocument;
+using ChangeableDocument.Actions;
+using ChangeableDocument.ChangeInfos;
+using System.Collections.Generic;
+
+namespace PixiEditorPrototype.Models
+{
+    internal class ActionAccumulator
+    {
+        private bool executing = false;
+        private List<IAction> queuedActions = new();
+        private DocumentChangeTracker tracker;
+        private DocumentUpdater documentUpdater;
+
+        public ActionAccumulator(DocumentChangeTracker tracker, DocumentUpdater updater)
+        {
+            this.tracker = tracker;
+            this.documentUpdater = updater;
+        }
+
+        public void AddAction(IAction action)
+        {
+            queuedActions.Add(action);
+            TryExecuteAccumulatedActions();
+        }
+
+        public async void TryExecuteAccumulatedActions()
+        {
+            if (executing)
+                return;
+            executing = true;
+            var toExecute = queuedActions;
+            queuedActions = new List<IAction>();
+
+            var result = await tracker.ProcessActions(toExecute);
+            foreach (IChangeInfo? info in result)
+            {
+                documentUpdater.ApplyChangeFromChangeInfo(info);
+            }
+
+
+            executing = false;
+        }
+    }
+}

+ 117 - 0
src/PixiEditorPrototype/Models/DocumentStructureHelper.cs

@@ -0,0 +1,117 @@
+using ChangeableDocument;
+using ChangeableDocument.Actions;
+using PixiEditorPrototype.ViewModels;
+using System;
+using System.Collections.Generic;
+
+namespace PixiEditorPrototype.Models
+{
+    internal class DocumentStructureHelper
+    {
+        private DocumentViewModel doc;
+        public DocumentStructureHelper(DocumentViewModel doc)
+        {
+            this.doc = doc;
+        }
+
+        public void CreateNewStructureMember(StructureMemberType type)
+        {
+            if (doc.SelectedStructureMember == null)
+            {
+                //put member on top
+                doc.ActionAccumulator.AddAction(new CreateStructureMemberAction(doc.StructureRoot.GuidValue, 0, type));
+                return;
+            }
+            if (doc.SelectedStructureMember is FolderViewModel folder)
+            {
+                //put member inside folder on top
+                doc.ActionAccumulator.AddAction(new CreateStructureMemberAction(folder.GuidValue, 0, type));
+                return;
+            }
+            if (doc.SelectedStructureMember is LayerViewModel layer)
+            {
+                //put member above the layer
+                var path = FindPath(layer.GuidValue);
+                if (path.Count < 2)
+                    throw new Exception("Couldn't find a path to the selected member");
+                var parent = (FolderViewModel)path[1];
+                doc.ActionAccumulator.AddAction(new CreateStructureMemberAction(parent.GuidValue, parent.Children.IndexOf(layer), type));
+                return;
+            }
+            throw new Exception("Unknown member type: " + type.ToString());
+        }
+
+        public StructureMemberViewModel FindOrThrow(Guid guid) => Find(guid) ?? throw new Exception("Could not find member with guid " + guid.ToString());
+        public StructureMemberViewModel? Find(Guid guid)
+        {
+            var list = FindPath(guid);
+            return list.Count > 0 ? list[0] : null;
+        }
+
+        public (StructureMemberViewModel, FolderViewModel) FindChildAndParentOrThrow(Guid childGuid)
+        {
+            var path = FindPath(childGuid);
+            if (path.Count < 2)
+                throw new Exception("Couldn't find child and parent");
+            return (path[0], (FolderViewModel)path[1]);
+        }
+        public List<StructureMemberViewModel> FindPath(Guid guid)
+        {
+            var list = new List<StructureMemberViewModel>();
+            if (FillPath(doc.StructureRoot, guid, list))
+                list.Add(doc.StructureRoot);
+            return list;
+        }
+
+        private bool FillPath(FolderViewModel folder, Guid guid, List<StructureMemberViewModel> toFill)
+        {
+            if (folder.GuidValue == guid)
+            {
+                return true;
+            }
+            foreach (var member in folder.Children)
+            {
+                if (member is LayerViewModel childLayer && childLayer.GuidValue == guid)
+                {
+                    toFill.Add(member);
+                    return true;
+                }
+                if (member is FolderViewModel childFolder)
+                {
+                    if (FillPath(childFolder, guid, toFill))
+                    {
+                        toFill.Add(childFolder);
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        public void MoveStructureMember(Guid guid, bool up)
+        {
+            var path = FindPath(guid);
+            if (path.Count < 2)
+                throw new Exception("Couldn't find the member to be moved");
+            if (path.Count == 2)
+            {
+                int curIndex = doc.StructureRoot.Children.IndexOf(path[0]);
+                if (curIndex == 0 && up || curIndex == doc.StructureRoot.Children.Count - 1 && !up)
+                    return;
+                doc.ActionAccumulator.AddAction(new MoveStructureMemberAction(guid, doc.StructureRoot.GuidValue, up ? curIndex - 1 : curIndex + 1));
+                return;
+            }
+            var folder = (FolderViewModel)path[1];
+            int index = folder.Children.IndexOf(path[0]);
+            if (up && index > 0 || !up && index < folder.Children.Count - 1)
+            {
+                doc.ActionAccumulator.AddAction(new MoveStructureMemberAction(guid, path[1].GuidValue, up ? index - 1 : index + 1));
+            }
+            else
+            {
+                int parentIndex = ((FolderViewModel)path[2]).Children.IndexOf(folder);
+                doc.ActionAccumulator.AddAction(new MoveStructureMemberAction(guid, path[2].GuidValue, up ? parentIndex : parentIndex + 1));
+            }
+        }
+    }
+}

+ 88 - 0
src/PixiEditorPrototype/Models/DocumentUpdater.cs

@@ -0,0 +1,88 @@
+using ChangeableDocument.Changeables.Interfaces;
+using ChangeableDocument.ChangeInfos;
+using PixiEditorPrototype.ViewModels;
+using System;
+
+namespace PixiEditorPrototype.Models
+{
+    internal class DocumentUpdater
+    {
+        private DocumentViewModel doc;
+        public DocumentUpdater(DocumentViewModel doc)
+        {
+            this.doc = doc;
+        }
+
+        public void ApplyChangeFromChangeInfo(IChangeInfo? arbitraryInfo)
+        {
+            if (arbitraryInfo == null)
+                return;
+
+            switch (arbitraryInfo)
+            {
+                case Document_CreateStructureMember_ChangeInfo info:
+                    ProcessCreateStructureMember(info);
+                    break;
+                case Document_DeleteStructureMember_ChangeInfo info:
+                    ProcessDeleteStructureMember(info);
+                    break;
+                case Document_UpdateStructureMemberProperties_ChangeInfo info:
+                    ProcessUpdateStructureMemberProperties(info);
+                    break;
+                case Document_MoveStructureMember_ChangeInfo info:
+                    ProcessMoveStructureMember(info);
+                    break;
+            }
+        }
+
+        private void ProcessCreateStructureMember(Document_CreateStructureMember_ChangeInfo info)
+        {
+            var (member, parentFolder) = doc.Tracker.Document.FindChildAndParentOrThrow(info.GuidValue);
+            var parentFolderVM = (FolderViewModel)doc.StructureHelper.FindOrThrow(parentFolder.GuidValue);
+
+            int index = parentFolder.ReadOnlyChildren.IndexOf(member);
+
+            StructureMemberViewModel memberVM = member switch
+            {
+                IReadOnlyLayer layer => new LayerViewModel(doc, layer),
+                IReadOnlyFolder folder => new FolderViewModel(doc, folder),
+                _ => throw new Exception("Unsupposed member type")
+            };
+
+            parentFolderVM.Children.Insert(index, memberVM);
+
+            if (member is IReadOnlyFolder folder2)
+            {
+                foreach (IReadOnlyStructureMember child in folder2.ReadOnlyChildren)
+                {
+                    ProcessCreateStructureMember(new Document_CreateStructureMember_ChangeInfo() { GuidValue = child.GuidValue });
+                }
+            }
+        }
+
+        private void ProcessDeleteStructureMember(Document_DeleteStructureMember_ChangeInfo info)
+        {
+            var (memberVM, folderVM) = doc.StructureHelper.FindChildAndParentOrThrow(info.GuidValue);
+            folderVM.Children.Remove(memberVM);
+        }
+
+        private void ProcessUpdateStructureMemberProperties(Document_UpdateStructureMemberProperties_ChangeInfo info)
+        {
+            var memberVM = doc.StructureHelper.FindOrThrow(info.GuidValue);
+            if (info.NameChanged) memberVM.RaisePropertyChanged(nameof(memberVM.Name));
+            if (info.IsVisibleChanged) memberVM.RaisePropertyChanged(nameof(memberVM.IsVisible));
+        }
+
+        private void ProcessMoveStructureMember(Document_MoveStructureMember_ChangeInfo info)
+        {
+            var (memberVM, curFolderVM) = doc.StructureHelper.FindChildAndParentOrThrow(info.GuidValue);
+            var (member, targetFolder) = doc.Tracker.Document.FindChildAndParentOrThrow(info.GuidValue);
+
+            int index = targetFolder.ReadOnlyChildren.IndexOf(member);
+            var targetFolderVM = (FolderViewModel)doc.StructureHelper.FindOrThrow(targetFolder.GuidValue);
+
+            curFolderVM.Children.Remove(memberVM);
+            targetFolderVM.Children.Insert(index, memberVM);
+        }
+    }
+}

+ 19 - 0
src/PixiEditorPrototype/Models/IReadOnlyListEx.cs

@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+namespace PixiEditorPrototype.Models
+{
+    internal static class IReadOnlyListEx
+    {
+        public static int IndexOf<T>(this IReadOnlyList<T> self, T elementToFind)
+        {
+            int i = 0;
+            foreach (T element in self)
+            {
+                if (Equals(element, elementToFind))
+                    return i;
+                i++;
+            }
+            return -1;
+        }
+    }
+}

+ 18 - 0
src/PixiEditorPrototype/PixiEditorPrototype.csproj

@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net6.0-windows</TargetFramework>
+    <Nullable>enable</Nullable>
+    <UseWPF>true</UseWPF>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\ChangeableDocument\ChangeableDocument.csproj" />
+  </ItemGroup>
+
+</Project>

+ 34 - 0
src/PixiEditorPrototype/RelayCommand.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Windows.Input;
+
+namespace PixiEditorPrototype
+{
+    internal class RelayCommand : ICommand
+    {
+        public event EventHandler? CanExecuteChanged;
+
+        private Action<object?> execute;
+        private Func<object?, bool> canExecute;
+
+        public RelayCommand(Action<object?> execute, Func<object?, bool> canExecute)
+        {
+            this.execute = execute;
+            this.canExecute = canExecute;
+        }
+
+        public void RaiseCanExecuteChanged()
+        {
+            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+        }
+
+        public bool CanExecute(object? parameter)
+        {
+            return canExecute?.Invoke(parameter) ?? true;
+        }
+
+        public void Execute(object? parameter)
+        {
+            execute?.Invoke(parameter);
+        }
+    }
+}

+ 71 - 0
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -0,0 +1,71 @@
+using ChangeableDocument;
+using ChangeableDocument.Actions;
+using PixiEditorPrototype.Models;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditorPrototype.ViewModels
+{
+    internal class DocumentViewModel : INotifyPropertyChanged
+    {
+        public StructureMemberViewModel? SelectedStructureMember { get; private set; }
+
+        public event PropertyChangedEventHandler? PropertyChanged;
+
+        public ActionAccumulator ActionAccumulator { get; }
+        public DocumentChangeTracker Tracker { get; }
+        public DocumentStructureHelper StructureHelper { get; }
+        private DocumentUpdater Updater { get; }
+
+
+        public FolderViewModel StructureRoot { get; }
+        public RelayCommand? UndoCommand { get; }
+        public RelayCommand? RedoCommand { get; }
+        public RelayCommand? CreateNewLayerCommand { get; }
+        public RelayCommand? CreateNewFolderCommand { get; }
+        public RelayCommand? DeleteStructureMemberCommand { get; }
+        public RelayCommand? ChangeSelectedItemCommand { get; }
+
+        public WriteableBitmap FinalBitmap
+        {
+        }
+
+        public DocumentViewModel()
+        {
+            Tracker = new DocumentChangeTracker();
+            Updater = new DocumentUpdater(this);
+            StructureRoot = new FolderViewModel(this, Tracker.Document.ReadOnlyStructureRoot);
+            ActionAccumulator = new ActionAccumulator(Tracker, Updater);
+            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);
+        }
+
+        public void DeleteStructureMember(object? param)
+        {
+            if (SelectedStructureMember != null)
+                ActionAccumulator.AddAction(new DeleteStructureMemberAction(SelectedStructureMember.GuidValue));
+        }
+
+        public void Undo(object? param)
+        {
+            ActionAccumulator.AddAction(new UndoAction());
+        }
+
+        public void Redo(object? param)
+        {
+            ActionAccumulator.AddAction(new RedoAction());
+        }
+
+        private void ChangeSelectedItem(object? param)
+        {
+            SelectedStructureMember = (StructureMemberViewModel?)((RoutedPropertyChangedEventArgs<object>?)param)?.NewValue;
+        }
+    }
+}

+ 15 - 0
src/PixiEditorPrototype/ViewModels/FolderViewModel.cs

@@ -0,0 +1,15 @@
+using ChangeableDocument.Changeables.Interfaces;
+using System.Collections.ObjectModel;
+
+namespace PixiEditorPrototype.ViewModels
+{
+    internal class FolderViewModel : StructureMemberViewModel
+    {
+        public ObservableCollection<StructureMemberViewModel> Children { get; } = new();
+        public FolderViewModel(DocumentViewModel doc, IReadOnlyFolder member) : base(doc, member)
+        {
+        }
+
+
+    }
+}

+ 11 - 0
src/PixiEditorPrototype/ViewModels/LayerViewModel.cs

@@ -0,0 +1,11 @@
+using ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditorPrototype.ViewModels
+{
+    internal class LayerViewModel : StructureMemberViewModel
+    {
+        public LayerViewModel(DocumentViewModel doc, IReadOnlyLayer layer) : base(doc, layer)
+        {
+        }
+    }
+}

+ 47 - 0
src/PixiEditorPrototype/ViewModels/StructureMemberViewModel.cs

@@ -0,0 +1,47 @@
+using ChangeableDocument.Actions;
+using ChangeableDocument.Changeables.Interfaces;
+using System;
+using System.ComponentModel;
+
+namespace PixiEditorPrototype.ViewModels
+{
+    internal abstract class StructureMemberViewModel : INotifyPropertyChanged
+    {
+        private IReadOnlyStructureMember member;
+        public event PropertyChangedEventHandler? PropertyChanged;
+        public DocumentViewModel Document { get; }
+
+        public string Name
+        {
+            get => member.Name;
+            set => Document.ActionAccumulator.AddAction(new SetStructureMemberNameAction(value, member.GuidValue));
+        }
+
+        public bool IsVisible
+        {
+            get => member.IsVisible;
+            set => Document.ActionAccumulator.AddAction(new SetStructureMemberVisibilityAction(value, member.GuidValue));
+        }
+
+        public Guid GuidValue
+        {
+            get => member.GuidValue;
+        }
+
+        public RelayCommand MoveUpCommand { get; }
+        public RelayCommand MoveDownCommand { get; }
+
+        public void RaisePropertyChanged(string name)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+        }
+
+        public StructureMemberViewModel(DocumentViewModel doc, IReadOnlyStructureMember member)
+        {
+            this.member = member;
+            Document = doc;
+            MoveUpCommand = new(_ => Document.StructureHelper.MoveStructureMember(GuidValue, true), _ => true);
+            MoveDownCommand = new(_ => Document.StructureHelper.MoveStructureMember(GuidValue, false), _ => true);
+        }
+    }
+}

+ 66 - 0
src/PixiEditorPrototype/Views/DocumentView.xaml

@@ -0,0 +1,66 @@
+<UserControl x:Class="PixiEditorPrototype.Views.DocumentView" x:ClassModifier="internal"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             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:i="http://schemas.microsoft.com/xaml/behaviors"
+             xmlns:cmd="mvvm"
+             xmlns:vm="clr-namespace:PixiEditorPrototype.ViewModels"
+             mc:Ignorable="d" 
+             d:DataContext="{d:DesignInstance Type=vm:DocumentViewModel, IsDesignTimeCreatable=True}"
+             d:DesignHeight="450" d:DesignWidth="800">
+    <DockPanel Background="Gray">
+        <Border BorderThickness="1" Background="White" BorderBrush="Black" Width="280" DockPanel.Dock="Right" Margin="5">
+            <DockPanel>
+                <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
+                    <Button Margin="5" Command="{Binding CreateNewLayerCommand}" Width="80">New Layer</Button>
+                    <Button Margin="5" Command="{Binding CreateNewFolderCommand}" Width="80">New Folder</Button>
+                    <Button Margin="5" Command="{Binding DeleteStructureMemberCommand}" Width="80">Delete</Button>
+                </StackPanel>
+                <TreeView ItemsSource="{Binding StructureRoot.Children}">
+                    <i:Interaction.Triggers>
+                        <i:EventTrigger EventName="SelectedItemChanged">
+                            <i:InvokeCommandAction Command="{Binding ChangeSelectedItemCommand}" PassEventArgsToCommand="True"/>
+                        </i:EventTrigger>
+                    </i:Interaction.Triggers>
+                    <TreeView.Resources>
+                        <HierarchicalDataTemplate DataType="{x:Type vm:FolderViewModel}" ItemsSource="{Binding Children}">
+                            <StackPanel Orientation="Horizontal" Width="150">
+                                <StackPanel>
+                                    <Button Width="18" Command="{Binding MoveUpCommand}">^</Button>
+                                    <Button Width="18" Command="{Binding MoveDownCommand}">v</Button>
+                                </StackPanel>
+                                <Border BorderBrush="Black" BorderThickness="1" Background="Yellow" Margin="5">
+                                    <CheckBox VerticalAlignment="Center" Margin="5" IsChecked="{Binding IsVisible}"/>
+                                </Border>
+                                <TextBox Text="{Binding Name}" Margin="5" Height="20"/>
+                            </StackPanel>
+                        </HierarchicalDataTemplate>
+                        <DataTemplate DataType="{x:Type vm:LayerViewModel}">
+                            <StackPanel Orientation="Horizontal" Width="150">
+                                <StackPanel>
+                                    <Button Width="18" Command="{Binding MoveUpCommand}">^</Button>
+                                    <Button Width="18" Command="{Binding MoveDownCommand}">v</Button>
+                                </StackPanel>
+                                <Border BorderBrush="Black" BorderThickness="1" Margin="5">
+                                    <CheckBox VerticalAlignment="Center" Margin="5" IsChecked="{Binding IsVisible}"/>
+                                </Border>
+                                <TextBox Text="{Binding Name}" Margin="5" Height="20"/>
+                            </StackPanel>
+                        </DataTemplate>
+                    </TreeView.Resources>
+                </TreeView>
+            </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>
+            </StackPanel>
+        </Border>
+        <Border BorderThickness="1" Background="Transparent" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5">
+            <Image Margin="5"/>
+        </Border>
+    </DockPanel>
+</UserControl>

+ 15 - 0
src/PixiEditorPrototype/Views/DocumentView.xaml.cs

@@ -0,0 +1,15 @@
+using System.Windows.Controls;
+
+namespace PixiEditorPrototype.Views
+{
+    /// <summary>
+    /// Interaction logic for DocumentView.xaml
+    /// </summary>
+    internal partial class DocumentView : UserControl
+    {
+        public DocumentView()
+        {
+            InitializeComponent();
+        }
+    }
+}

+ 19 - 0
src/StructureRenderer/Renderer.cs

@@ -0,0 +1,19 @@
+using ChangeableDocument;
+using ChangeableDocument.ChangeInfos;
+
+namespace StructureRenderer
+{
+    public class Renderer
+    {
+        private DocumentChangeTracker tracker;
+        public Renderer(DocumentChangeTracker tracker)
+        {
+            this.tracker = tracker;
+        }
+
+        public async Task<List<IChangeInfo>> ProcessChanges(IReadOnlyList<IChangeInfo> changes)
+        {
+
+        }
+    }
+}

+ 14 - 0
src/StructureRenderer/StructureRenderer.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\ChangeableDocument\ChangeableDocument.csproj" />
+    <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj" />
+  </ItemGroup>
+
+</Project>