Browse Source

Layer masks

Equbuxu 3 years ago
parent
commit
ae72e88b9c
37 changed files with 391 additions and 50 deletions
  1. 9 1
      src/ChunkyImageLib/ChunkyImage.cs
  2. 1 0
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  3. 1 2
      src/ChunkyImageLibBenchmark/Program.cs
  4. 4 2
      src/PixiEditor.ChangeableDocument/Actions/Drawing/Rectangle/DrawRectangle_Action.cs
  5. 20 0
      src/PixiEditor.ChangeableDocument/Actions/Properties/CreateStructureMemberMask_Action.cs
  6. 20 0
      src/PixiEditor.ChangeableDocument/Actions/Properties/DeleteStructureMemberMask_Action.cs
  7. 1 0
      src/PixiEditor.ChangeableDocument/Actions/Properties/EndOpacityChange_Action.cs
  8. 1 0
      src/PixiEditor.ChangeableDocument/Actions/Properties/OpacityChange_Action.cs
  9. 1 0
      src/PixiEditor.ChangeableDocument/Actions/Properties/SetStructureMemberName_Action.cs
  10. 1 0
      src/PixiEditor.ChangeableDocument/Actions/Properties/SetStructureMemberVisibility_Action.cs
  11. 2 1
      src/PixiEditor.ChangeableDocument/Actions/Root/ResizeCanvas_Action.cs
  12. 1 0
      src/PixiEditor.ChangeableDocument/Actions/Structure/CreateStructureMember_Action.cs
  13. 1 0
      src/PixiEditor.ChangeableDocument/Actions/Structure/DeleteStructureMember_Action.cs
  14. 1 0
      src/PixiEditor.ChangeableDocument/Actions/Structure/MoveStructureMember_Action.cs
  15. 10 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/MaskChunks_ChangeInfo.cs
  16. 7 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/StructureMemberMask_ChangeInfo.cs
  17. 3 1
      src/PixiEditor.ChangeableDocument/Changeables/Folder.cs
  18. 4 1
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyStructureMember.cs
  19. 3 1
      src/PixiEditor.ChangeableDocument/Changeables/Layer.cs
  20. 4 1
      src/PixiEditor.ChangeableDocument/Changeables/StructureMember.cs
  21. 65 21
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs
  22. 38 0
      src/PixiEditor.ChangeableDocument/Changes/Properties/CreateStructureMemberMask_Change.cs
  23. 52 0
      src/PixiEditor.ChangeableDocument/Changes/Properties/DeleteStructureMemberMask_Change.cs
  24. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberIsVisible_Change.cs
  25. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberName_Change.cs
  26. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberOpacity_UpdateableChange.cs
  27. 18 1
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  29. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs
  30. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs
  31. 33 3
      src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs
  32. 21 0
      src/PixiEditorPrototype/Converters/BoolToVisibilityConverter.cs
  33. 9 0
      src/PixiEditorPrototype/Models/DocumentUpdater.cs
  34. 6 0
      src/PixiEditorPrototype/Models/Rendering/WriteableBitmapUpdater.cs
  35. 24 2
      src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs
  36. 6 0
      src/PixiEditorPrototype/ViewModels/StructureMemberViewModel.cs
  37. 18 7
      src/PixiEditorPrototype/Views/MainWindow.xaml

+ 9 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -119,6 +119,14 @@ namespace ChunkyImageLib
             }
         }
 
+        public bool LatestChunkExists(Vector2i chunkPos, ChunkResolution resolution)
+        {
+            lock (lockObject)
+            {
+                return GetLatestChunk(chunkPos, resolution) is not null;
+            }
+        }
+
         internal bool DrawCommittedChunkOn(Vector2i chunkPos, ChunkResolution resolution, SKSurface surface, Vector2i pos, SKPaint? paint = null)
         {
             lock (lockObject)
@@ -486,7 +494,7 @@ namespace ChunkyImageLib
         }
 
         /// <summary>
-        /// Note: this function modifies the internal state, it is not thread safe! (same as all other functions that change the image in some way)
+        /// Note: this function modifies the internal state, it is not thread safe! (same as all the other functions that change the image in some way)
         /// </summary>
         public bool CheckIfCommittedIsEmpty()
         {

+ 1 - 0
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -6,6 +6,7 @@ namespace ChunkyImageLib
     public interface IReadOnlyChunkyImage
     {
         bool DrawLatestChunkOn(Vector2i chunkPos, ChunkResolution resolution, SKSurface surface, Vector2i pos, SKPaint? paint = null);
+        bool LatestChunkExists(Vector2i chunkPos, ChunkResolution resolution);
         HashSet<Vector2i> FindAffectedChunks();
         HashSet<Vector2i> FindAllChunks();
     }

+ 1 - 2
src/ChunkyImageLibBenchmark/Program.cs

@@ -1,5 +1,4 @@
 using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
 using SkiaSharp;
 using System.Diagnostics;
 
@@ -24,7 +23,7 @@ Console.ReadKey();
 
 (double first, double second) Benchmark()
 {
-    using ChunkyImage image = new(new(1024, 1024), ColorType.RgbaF16);
+    using ChunkyImage image = new(new(1024, 1024));
     image.DrawRectangle(new(new(0, 0), new(1024, 1024), 10, SKColors.Black, SKColors.Bisque));
 
     Stopwatch sw = Stopwatch.StartNew();

+ 4 - 2
src/PixiEditor.ChangeableDocument/Actions/Drawing/Rectangle/DrawRectangle_Action.cs

@@ -6,13 +6,15 @@ namespace PixiEditor.ChangeableDocument.Actions.Drawing.Rectangle
 {
     public record class DrawRectangle_Action : IStartOrUpdateChangeAction
     {
-        public DrawRectangle_Action(Guid layerGuid, ShapeData rectangle)
+        public DrawRectangle_Action(Guid layerGuid, ShapeData rectangle, bool drawOnMask)
         {
             LayerGuid = layerGuid;
             Rectangle = rectangle;
+            DrawOnMask = drawOnMask;
         }
 
         public Guid LayerGuid { get; }
+        public bool DrawOnMask { get; }
         public ShapeData Rectangle { get; }
 
         void IStartOrUpdateChangeAction.UpdateCorrespodingChange(UpdateableChange change)
@@ -22,7 +24,7 @@ namespace PixiEditor.ChangeableDocument.Actions.Drawing.Rectangle
 
         UpdateableChange IStartOrUpdateChangeAction.CreateCorrespondingChange()
         {
-            return new DrawRectangle_UpdateableChange(LayerGuid, Rectangle);
+            return new DrawRectangle_UpdateableChange(LayerGuid, Rectangle, DrawOnMask);
         }
     }
 }

+ 20 - 0
src/PixiEditor.ChangeableDocument/Actions/Properties/CreateStructureMemberMask_Action.cs

@@ -0,0 +1,20 @@
+using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Properties;
+
+namespace PixiEditor.ChangeableDocument.Actions.Properties
+{
+    public record class CreateStructureMemberMask_Action : IMakeChangeAction
+    {
+        public Guid GuidValue { get; }
+
+        public CreateStructureMemberMask_Action(Guid guidValue)
+        {
+            GuidValue = guidValue;
+        }
+
+        Change IMakeChangeAction.CreateCorrespondingChange()
+        {
+            return new CreateStructureMemberMask_Change(GuidValue);
+        }
+    }
+}

+ 20 - 0
src/PixiEditor.ChangeableDocument/Actions/Properties/DeleteStructureMemberMask_Action.cs

@@ -0,0 +1,20 @@
+using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Properties;
+
+namespace PixiEditor.ChangeableDocument.Actions.Properties
+{
+    public record class DeleteStructureMemberMask_Action : IMakeChangeAction
+    {
+        public Guid GuidValue { get; }
+
+        public DeleteStructureMemberMask_Action(Guid guidValue)
+        {
+            GuidValue = guidValue;
+        }
+
+        Change IMakeChangeAction.CreateCorrespondingChange()
+        {
+            return new DeleteStructureMemberMask_Change(GuidValue);
+        }
+    }
+}

+ 1 - 0
src/PixiEditor.ChangeableDocument/Actions/Properties/EndOpacityChange_Action.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Properties;
 
 namespace PixiEditor.ChangeableDocument.Actions.Properties
 {

+ 1 - 0
src/PixiEditor.ChangeableDocument/Actions/Properties/OpacityChange_Action.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Properties;
 
 namespace PixiEditor.ChangeableDocument.Actions.Properties
 {

+ 1 - 0
src/PixiEditor.ChangeableDocument/Actions/Properties/SetStructureMemberName_Action.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Properties;
 
 namespace PixiEditor.ChangeableDocument.Actions.Properties;
 

+ 1 - 0
src/PixiEditor.ChangeableDocument/Actions/Properties/SetStructureMemberVisibility_Action.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Properties;
 
 namespace PixiEditor.ChangeableDocument.Actions.Properties;
 

+ 2 - 1
src/PixiEditor.ChangeableDocument/Actions/Document/ResizeCanvas_Action.cs → src/PixiEditor.ChangeableDocument/Actions/Root/ResizeCanvas_Action.cs

@@ -1,7 +1,8 @@
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Root;
 
-namespace PixiEditor.ChangeableDocument.Actions.Document
+namespace PixiEditor.ChangeableDocument.Actions.Root
 {
     public record class ResizeCanvas_Action : IMakeChangeAction
     {

+ 1 - 0
src/PixiEditor.ChangeableDocument/Actions/Structure/CreateStructureMember_Action.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Structure;
 
 namespace PixiEditor.ChangeableDocument.Actions.Structure;
 

+ 1 - 0
src/PixiEditor.ChangeableDocument/Actions/Structure/DeleteStructureMember_Action.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Structure;
 
 namespace PixiEditor.ChangeableDocument.Actions.Structure;
 

+ 1 - 0
src/PixiEditor.ChangeableDocument/Actions/Structure/MoveStructureMember_Action.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Structure;
 
 namespace PixiEditor.ChangeableDocument.Actions.Structure;
 

+ 10 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/MaskChunks_ChangeInfo.cs

@@ -0,0 +1,10 @@
+using ChunkyImageLib.DataHolders;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos
+{
+    public record class MaskChunks_ChangeInfo : IChangeInfo
+    {
+        public Guid MemberGuid { get; init; }
+        public HashSet<Vector2i>? Chunks { get; init; }
+    }
+}

+ 7 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/StructureMemberMask_ChangeInfo.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos
+{
+    public record class StructureMemberMask_ChangeInfo : IChangeInfo
+    {
+        public Guid GuidValue { get; init; }
+    }
+}

+ 3 - 1
src/PixiEditor.ChangeableDocument/Changeables/Folder.cs

@@ -21,7 +21,8 @@ namespace PixiEditor.ChangeableDocument.Changeables
                 IsVisible = IsVisible,
                 Name = Name,
                 Opacity = Opacity,
-                Children = clonedChildren
+                Children = clonedChildren,
+                Mask = Mask?.CloneFromLatest()
             };
         }
 
@@ -31,6 +32,7 @@ namespace PixiEditor.ChangeableDocument.Changeables
             {
                 child.Dispose();
             }
+            Mask?.Dispose();
         }
     }
 }

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

@@ -1,4 +1,6 @@
-namespace PixiEditor.ChangeableDocument.Changeables.Interfaces
+using ChunkyImageLib;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Interfaces
 {
     public interface IReadOnlyStructureMember
     {
@@ -6,5 +8,6 @@
         string Name { get; }
         Guid GuidValue { get; }
         float Opacity { get; }
+        IReadOnlyChunkyImage? ReadOnlyMask { get; }
     }
 }

+ 3 - 1
src/PixiEditor.ChangeableDocument/Changeables/Layer.cs

@@ -22,6 +22,7 @@ namespace PixiEditor.ChangeableDocument.Changeables
         public override void Dispose()
         {
             LayerImage.Dispose();
+            Mask?.Dispose();
         }
 
         internal override Layer Clone()
@@ -31,7 +32,8 @@ namespace PixiEditor.ChangeableDocument.Changeables
                 GuidValue = GuidValue,
                 IsVisible = IsVisible,
                 Name = Name,
-                Opacity = Opacity
+                Opacity = Opacity,
+                Mask = Mask?.CloneFromLatest(),
             };
         }
     }

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

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using ChunkyImageLib;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 namespace PixiEditor.ChangeableDocument.Changeables
 {
@@ -8,6 +9,8 @@ namespace PixiEditor.ChangeableDocument.Changeables
         public bool IsVisible { get; set; } = true;
         public string Name { get; set; } = "Unnamed";
         public Guid GuidValue { get; init; }
+        public ChunkyImage? Mask { get; set; } = null;
+        public IReadOnlyChunkyImage? ReadOnlyMask => Mask;
 
         internal abstract StructureMember Clone();
         public abstract void Dispose();

+ 65 - 21
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs

@@ -7,13 +7,15 @@ namespace PixiEditor.ChangeableDocument.Changes.Drawing
 {
     internal class DrawRectangle_UpdateableChange : UpdateableChange
     {
-        private Guid layerGuid;
+        private readonly Guid memberGuid;
         private ShapeData rect;
+        private readonly bool drawOnMask;
         private CommittedChunkStorage? storedChunks;
-        public DrawRectangle_UpdateableChange(Guid layerGuid, ShapeData rectangle)
+        public DrawRectangle_UpdateableChange(Guid memberGuid, ShapeData rectangle, bool drawOnMask)
         {
-            this.layerGuid = layerGuid;
+            this.memberGuid = memberGuid;
             this.rect = rectangle;
+            this.drawOnMask = drawOnMask;
         }
 
         public override void Initialize(Document target) { }
@@ -23,29 +25,63 @@ namespace PixiEditor.ChangeableDocument.Changes.Drawing
             rect = updatedRectangle;
         }
 
+        private ChunkyImage GetTargetImage(Document target)
+        {
+            var member = target.FindMemberOrThrow(memberGuid);
+            if (drawOnMask)
+            {
+                if (member.Mask is null)
+                    throw new InvalidOperationException("Trying to draw on a mask that doesn't exist");
+                return member.Mask;
+            }
+            else if (member is Folder)
+            {
+                throw new InvalidOperationException("Trying to draw on a folder");
+            }
+            return ((Layer)member).LayerImage;
+        }
+
         public override IChangeInfo? ApplyTemporarily(Document target)
         {
-            Layer layer = (Layer)target.FindMemberOrThrow(layerGuid);
-            var oldChunks = layer.LayerImage.FindAffectedChunks();
-            layer.LayerImage.CancelChanges();
+            ChunkyImage targetImage = GetTargetImage(target);
+
+            var oldChunks = targetImage.FindAffectedChunks();
+            targetImage.CancelChanges();
             if (!target.Selection.IsEmptyAndInactive)
-                layer.LayerImage.ApplyRasterClip(target.Selection.SelectionImage);
-            layer.LayerImage.DrawRectangle(rect);
-            var newChunks = layer.LayerImage.FindAffectedChunks();
+                targetImage.ApplyRasterClip(target.Selection.SelectionImage);
+            targetImage.DrawRectangle(rect);
+            var newChunks = targetImage.FindAffectedChunks();
             newChunks.UnionWith(oldChunks);
-            return new LayerImageChunks_ChangeInfo()
+
+            return drawOnMask switch
             {
-                Chunks = newChunks,
-                LayerGuid = layerGuid
+                false => new LayerImageChunks_ChangeInfo()
+                {
+                    Chunks = newChunks,
+                    LayerGuid = memberGuid
+                },
+                true => new MaskChunks_ChangeInfo()
+                {
+                    Chunks = newChunks,
+                    MemberGuid = memberGuid
+                },
             };
         }
 
         public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
         {
-            Layer layer = (Layer)target.FindMemberOrThrow(layerGuid);
+            ChunkyImage targetImage = GetTargetImage(target);
             var changes = ApplyTemporarily(target);
-            storedChunks = new CommittedChunkStorage(layer.LayerImage, ((LayerImageChunks_ChangeInfo)changes!).Chunks!);
-            layer.LayerImage.CommitChanges();
+
+            var changedChunks = changes! switch
+            {
+                LayerImageChunks_ChangeInfo info => info.Chunks,
+                MaskChunks_ChangeInfo info => info.Chunks,
+                _ => throw new InvalidOperationException("Unknown chunk type"),
+            };
+
+            storedChunks = new CommittedChunkStorage(targetImage, changedChunks!);
+            targetImage.CommitChanges();
 
             ignoreInUndo = false;
             return changes;
@@ -53,16 +89,24 @@ namespace PixiEditor.ChangeableDocument.Changes.Drawing
 
         public override IChangeInfo? Revert(Document target)
         {
-            Layer layer = (Layer)target.FindMemberOrThrow(layerGuid);
-            storedChunks!.ApplyChunksToImage(layer.LayerImage);
+            ChunkyImage targetImage = GetTargetImage(target);
+            storedChunks!.ApplyChunksToImage(targetImage);
             storedChunks.Dispose();
             storedChunks = null;
-            var changes = new LayerImageChunks_ChangeInfo()
+            IChangeInfo changes = drawOnMask switch
             {
-                Chunks = layer.LayerImage.FindAffectedChunks(),
-                LayerGuid = layerGuid,
+                false => new LayerImageChunks_ChangeInfo()
+                {
+                    Chunks = targetImage.FindAffectedChunks(),
+                    LayerGuid = memberGuid,
+                },
+                true => new MaskChunks_ChangeInfo()
+                {
+                    Chunks = targetImage.FindAffectedChunks(),
+                    MemberGuid = memberGuid,
+                },
             };
-            layer.LayerImage.CommitChanges();
+            targetImage.CommitChanges();
             return changes;
         }
 

+ 38 - 0
src/PixiEditor.ChangeableDocument/Changes/Properties/CreateStructureMemberMask_Change.cs

@@ -0,0 +1,38 @@
+using ChunkyImageLib;
+using PixiEditor.ChangeableDocument.Changeables;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+
+namespace PixiEditor.ChangeableDocument.Changes.Properties
+{
+    internal class CreateStructureMemberMask_Change : Change
+    {
+        private Guid targetMember;
+        public CreateStructureMemberMask_Change(Guid memberGuid)
+        {
+            targetMember = memberGuid;
+        }
+
+        public override void Initialize(Document target) { }
+
+        public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+        {
+            var member = target.FindMemberOrThrow(targetMember);
+            if (member.Mask is not null)
+                throw new InvalidOperationException("Cannot create a mask; the target member already has one");
+            member.Mask = new ChunkyImage(target.Size);
+
+            ignoreInUndo = false;
+            return new StructureMemberMask_ChangeInfo() { GuidValue = targetMember };
+        }
+
+        public override IChangeInfo? Revert(Document target)
+        {
+            var member = target.FindMemberOrThrow(targetMember);
+            if (member.Mask is null)
+                throw new InvalidOperationException("Cannot delete the mask; the target member has no mask");
+            member.Mask.Dispose();
+            member.Mask = null;
+            return new StructureMemberMask_ChangeInfo() { GuidValue = targetMember };
+        }
+    }
+}

+ 52 - 0
src/PixiEditor.ChangeableDocument/Changes/Properties/DeleteStructureMemberMask_Change.cs

@@ -0,0 +1,52 @@
+using ChunkyImageLib;
+using PixiEditor.ChangeableDocument.Changeables;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+
+namespace PixiEditor.ChangeableDocument.Changes.Properties
+{
+    internal class DeleteStructureMemberMask_Change : Change
+    {
+        private readonly Guid memberGuid;
+        private ChunkyImage? storedMask;
+
+        public DeleteStructureMemberMask_Change(Guid memberGuid)
+        {
+            this.memberGuid = memberGuid;
+        }
+
+        public override void Initialize(Document target)
+        {
+            var member = target.FindMemberOrThrow(memberGuid);
+            if (member.Mask is null)
+                throw new InvalidOperationException("Cannot delete the mask; Target member has no mask");
+            storedMask = member.Mask.CloneFromLatest();
+        }
+
+        public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+        {
+            var member = target.FindMemberOrThrow(memberGuid);
+            if (member.Mask is null)
+                throw new InvalidOperationException("Cannot delete the mask; Target member has no mask");
+            member.Mask.Dispose();
+            member.Mask = null;
+
+            ignoreInUndo = false;
+            return new StructureMemberMask_ChangeInfo() { GuidValue = memberGuid };
+        }
+
+        public override IChangeInfo? Revert(Document target)
+        {
+            var member = target.FindMemberOrThrow(memberGuid);
+            if (member.Mask is not null)
+                throw new InvalidOperationException("Cannot revert mask deletion; The target member already has a mask");
+            member.Mask = storedMask!.CloneFromLatest();
+
+            return new StructureMemberMask_ChangeInfo() { GuidValue = memberGuid };
+        }
+
+        public override void Dispose()
+        {
+            storedMask?.Dispose();
+        }
+    }
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/StructureMemberIsVisible_Change.cs → src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberIsVisible_Change.cs

@@ -1,7 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 
-namespace PixiEditor.ChangeableDocument.Changes
+namespace PixiEditor.ChangeableDocument.Changes.Properties
 {
     internal class StructureMemberIsVisible_Change : Change
     {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/StructureMemberName_Change.cs → src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberName_Change.cs

@@ -1,7 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 
-namespace PixiEditor.ChangeableDocument.Changes
+namespace PixiEditor.ChangeableDocument.Changes.Properties
 {
     internal class StructureMemberName_Change : Change
     {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/StructureMemberOpacity_UpdateableChange.cs → src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberOpacity_UpdateableChange.cs

@@ -1,7 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 
-namespace PixiEditor.ChangeableDocument.Changes
+namespace PixiEditor.ChangeableDocument.Changes.Properties
 {
     internal class StructureMemberOpacity_UpdateableChange : UpdateableChange
     {

+ 18 - 1
src/PixiEditor.ChangeableDocument/Changes/ResizeCanvas_Change.cs → src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs

@@ -3,12 +3,13 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 
-namespace PixiEditor.ChangeableDocument.Changes
+namespace PixiEditor.ChangeableDocument.Changes.Root
 {
     internal class ResizeCanvas_Change : Change
     {
         private Vector2i originalSize;
         private Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
+        private Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
         private CommittedChunkStorage? selectionChunkStorage;
         private Vector2i newSize;
         public ResizeCanvas_Change(Vector2i size)
@@ -48,6 +49,13 @@ namespace PixiEditor.ChangeableDocument.Changes
                 layer.LayerImage.Resize(newSize);
                 deletedChunks.Add(layer.GuidValue, new CommittedChunkStorage(layer.LayerImage, layer.LayerImage.FindAffectedChunks()));
                 layer.LayerImage.CommitChanges();
+
+                if (layer.Mask is null)
+                    return;
+
+                layer.Mask.Resize(newSize);
+                deletedMaskChunks.Add(layer.GuidValue, new CommittedChunkStorage(layer.Mask, layer.Mask.FindAffectedChunks()));
+                layer.Mask.CommitChanges();
             });
 
             target.Selection.SelectionImage.Resize(newSize);
@@ -69,6 +77,13 @@ namespace PixiEditor.ChangeableDocument.Changes
                 layer.LayerImage.Resize(originalSize);
                 deletedChunks[layer.GuidValue].ApplyChunksToImage(layer.LayerImage);
                 layer.LayerImage.CommitChanges();
+
+                if (layer.Mask is null)
+                    return;
+
+                layer.Mask.Resize(originalSize);
+                deletedMaskChunks[layer.GuidValue].ApplyChunksToImage(layer.Mask);
+                layer.Mask.CommitChanges();
             });
 
             target.Selection.SelectionImage.Resize(originalSize);
@@ -88,6 +103,8 @@ namespace PixiEditor.ChangeableDocument.Changes
         {
             foreach (var layer in deletedChunks)
                 layer.Value.Dispose();
+            foreach (var mask in deletedMaskChunks)
+                mask.Value.Dispose();
             selectionChunkStorage?.Dispose();
         }
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/CreateStructureMember_Change.cs → src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -1,7 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 
-namespace PixiEditor.ChangeableDocument.Changes
+namespace PixiEditor.ChangeableDocument.Changes.Structure
 {
     internal class CreateStructureMember_Change : Change
     {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/DeleteStructureMember_Change.cs → src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs

@@ -1,7 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 
-namespace PixiEditor.ChangeableDocument.Changes
+namespace PixiEditor.ChangeableDocument.Changes.Structure
 {
     internal class DeleteStructureMember_Change : Change
     {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/MoveStructureMember_Change.cs → src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

@@ -1,7 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 
-namespace PixiEditor.ChangeableDocument.Changes
+namespace PixiEditor.ChangeableDocument.Changes.Structure
 {
     internal class MoveStructureMember_Change : Change
     {

+ 33 - 3
src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs

@@ -8,6 +8,8 @@ namespace PixiEditor.ChangeableDocument.Rendering
     public static class ChunkRenderer
     {
         private static SKPaint PaintToDrawChunksWith = new SKPaint() { BlendMode = SKBlendMode.SrcOver };
+        private static SKPaint ReplacingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
+        private static SKPaint ClippingPaint = new SKPaint() { BlendMode = SKBlendMode.DstIn };
         public static Chunk RenderWholeStructure(Vector2i pos, ChunkResolution resolution, IReadOnlyFolder root)
         {
             return RenderChunkRecursively(pos, resolution, 0, root, null);
@@ -26,16 +28,44 @@ namespace PixiEditor.ChangeableDocument.Rendering
             {
                 if (!child.IsVisible)
                     continue;
+
+                // chunk fully masked out
+                if (child.ReadOnlyMask is not null && !child.ReadOnlyMask.LatestChunkExists(chunkPos, resolution))
+                    continue;
+
+                // layer
                 if (child is IReadOnlyLayer layer && (visibleLayers is null || visibleLayers.Contains(layer.GuidValue)))
                 {
-                    PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
-                    layer.ReadOnlyLayerImage.DrawLatestChunkOn(chunkPos, resolution, targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    if (layer.ReadOnlyMask is null)
+                    {
+                        PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
+                        layer.ReadOnlyLayerImage.DrawLatestChunkOn(chunkPos, resolution, targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    }
+                    else
+                    {
+                        using (Chunk tempChunk = Chunk.Create(resolution))
+                        {
+                            if (!layer.ReadOnlyLayerImage.DrawLatestChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
+                                continue;
+                            layer.ReadOnlyMask.DrawLatestChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
+
+                            PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
+                            targetChunk.Surface.SkiaSurface.Canvas.DrawSurface(tempChunk.Surface.SkiaSurface, 0, 0, PaintToDrawChunksWith);
+                        }
+                    }
+                    continue;
                 }
-                else if (child is IReadOnlyFolder innerFolder)
+
+                // folder
+                if (child is IReadOnlyFolder innerFolder)
                 {
                     using Chunk renderedChunk = RenderChunkRecursively(chunkPos, resolution, depth + 1, innerFolder, visibleLayers);
+                    if (innerFolder.ReadOnlyMask is not null)
+                        innerFolder.ReadOnlyMask.DrawLatestChunkOn(chunkPos, resolution, renderedChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
+
                     PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
                     renderedChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    continue;
                 }
             }
             return targetChunk;

+ 21 - 0
src/PixiEditorPrototype/Converters/BoolToVisibilityConverter.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace PixiEditorPrototype.Converters
+{
+    internal class BoolToVisibilityConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            bool boolean = (bool)value;
+            return boolean ? Visibility.Visible : Visibility.Collapsed;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

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

@@ -50,9 +50,18 @@ namespace PixiEditorPrototype.Models
                 case MoveViewport_PassthroughAction info:
                     ProcessMoveViewport(info);
                     break;
+                case StructureMemberMask_ChangeInfo info:
+                    ProcessStructureMemberMask(info);
+                    break;
             }
         }
 
+        private void ProcessStructureMemberMask(StructureMemberMask_ChangeInfo info)
+        {
+            var memberVm = helper.StructureHelper.FindOrThrow(info.GuidValue);
+            memberVm.RaisePropertyChanged(nameof(memberVm.HasMask));
+        }
+
         private void ProcessMoveViewport(MoveViewport_PassthroughAction info)
         {
             var oldResolution = doc.RenderResolution;

+ 6 - 0
src/PixiEditorPrototype/Models/Rendering/WriteableBitmapUpdater.cs

@@ -51,6 +51,11 @@ namespace PixiEditorPrototype.Models.Rendering
             {
                 switch (change)
                 {
+                    case MaskChunks_ChangeInfo maskChunks:
+                        if (maskChunks.Chunks is null)
+                            throw new InvalidOperationException("Chunks must not be null");
+                        affectedChunks.UnionWith(maskChunks.Chunks);
+                        break;
                     case LayerImageChunks_ChangeInfo layerImageChunks:
                         if (layerImageChunks.Chunks is null)
                             throw new InvalidOperationException("Chunks must not be null");
@@ -72,6 +77,7 @@ namespace PixiEditorPrototype.Models.Rendering
                     case DeleteStructureMember_ChangeInfo:
                     case MoveStructureMember_ChangeInfo:
                     case Size_ChangeInfo:
+                    case StructureMemberMask_ChangeInfo:
                         AddAllChunks(affectedChunks);
                         break;
                     case StructureMemberOpacity_ChangeInfo opacityChangeInfo:

+ 24 - 2
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -1,10 +1,10 @@
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument;
-using PixiEditor.ChangeableDocument.Actions.Document;
 using PixiEditor.ChangeableDocument.Actions.Drawing;
 using PixiEditor.ChangeableDocument.Actions.Drawing.Rectangle;
 using PixiEditor.ChangeableDocument.Actions.Drawing.Selection;
 using PixiEditor.ChangeableDocument.Actions.Properties;
+using PixiEditor.ChangeableDocument.Actions.Root;
 using PixiEditor.ChangeableDocument.Actions.Structure;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.Zoombox;
@@ -52,6 +52,9 @@ namespace PixiEditorPrototype.ViewModels
         public RelayCommand? CombineCommand { get; }
         public RelayCommand? ClearHistoryCommand { get; }
         public RelayCommand? MoveViewportCommand { get; }
+        public RelayCommand? CreateMaskCommand { get; }
+        public RelayCommand? DeleteMaskCommand { get; }
+
 
 
         public SKSurface SurfaceFull { get; set; }
@@ -130,6 +133,8 @@ namespace PixiEditorPrototype.ViewModels
             CombineCommand = new RelayCommand(Combine);
             ClearHistoryCommand = new RelayCommand(ClearHistory);
             MoveViewportCommand = new RelayCommand(MoveViewport);
+            CreateMaskCommand = new RelayCommand(CreateMask);
+            DeleteMaskCommand = new RelayCommand(DeleteMask);
 
             SurfaceFull = SKSurface.Create(
                 new SKImageInfo(BitmapFull.PixelWidth, BitmapFull.PixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul, SKColorSpace.CreateSrgb()),
@@ -142,8 +147,11 @@ namespace PixiEditorPrototype.ViewModels
         {
             if (SelectedStructureMember is null)
                 return;
+            bool drawOnMask = SelectedStructureMember.HasMask && SelectedStructureMember.ShouldDrawOnMask;
+            if (SelectedStructureMember is not LayerViewModel && !drawOnMask)
+                return;
             startedRectangle = true;
-            Helpers.ActionAccumulator.AddAction(new DrawRectangle_Action(SelectedStructureMember.GuidValue, data));
+            Helpers.ActionAccumulator.AddAction(new DrawRectangle_Action(SelectedStructureMember.GuidValue, data, drawOnMask));
         }
 
         public void EndRectangle()
@@ -207,6 +215,20 @@ namespace PixiEditorPrototype.ViewModels
             SelectedStructureMember = (StructureMemberViewModel?)((RoutedPropertyChangedEventArgs<object>?)param)?.NewValue;
         }
 
+        private void CreateMask(object? param)
+        {
+            if (SelectedStructureMember is null || SelectedStructureMember.HasMask)
+                return;
+            Helpers.ActionAccumulator.AddAction(new CreateStructureMemberMask_Action(SelectedStructureMember.GuidValue));
+        }
+
+        private void DeleteMask(object? param)
+        {
+            if (SelectedStructureMember is null || !SelectedStructureMember.HasMask)
+                return;
+            Helpers.ActionAccumulator.AddAction(new DeleteStructureMemberMask_Action(SelectedStructureMember.GuidValue));
+        }
+
         private void Combine(object? param)
         {
             if (SelectedStructureMember is null)

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

@@ -26,6 +26,7 @@ namespace PixiEditorPrototype.ViewModels
         }
 
         public bool IsSelected { get; set; }
+        public bool ShouldDrawOnMask { get; set; }
 
         public float Opacity
         {
@@ -37,6 +38,11 @@ namespace PixiEditorPrototype.ViewModels
             get => member.GuidValue;
         }
 
+        public bool HasMask
+        {
+            get => member.ReadOnlyMask is not null;
+        }
+
         public RelayCommand MoveUpCommand { get; }
         public RelayCommand MoveDownCommand { get; }
 

+ 18 - 7
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -18,16 +18,21 @@
     </Window.DataContext>
     <Window.Resources>
         <conv:IndexToChunkResolutionConverter x:Key="IndexToChunkResolutionConverter"/>
+        <conv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
     </Window.Resources>
     <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 ActiveDocument.CreateNewLayerCommand}" Width="80">New Layer</Button>
-                    <Button Margin="5" Command="{Binding ActiveDocument.CreateNewFolderCommand}" Width="80">New Folder</Button>
-                    <Button Margin="5" Command="{Binding ActiveDocument.DeleteStructureMemberCommand}" Width="80">Delete</Button>
+                <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,5,0,0">
+                    <Button Margin="5,0" Command="{Binding ActiveDocument.CreateNewLayerCommand}" Width="80">New Layer</Button>
+                    <Button Margin="5,0" Command="{Binding ActiveDocument.CreateNewFolderCommand}" Width="80">New Folder</Button>
+                    <Button Margin="5,0" Command="{Binding ActiveDocument.DeleteStructureMemberCommand}" Width="80">Delete</Button>
                 </StackPanel>
-                <DockPanel DockPanel.Dock="Top" HorizontalAlignment="Stretch" Margin="0,0,0,5">
+                <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,5,0,0">
+                    <Button Margin="5,0" Command="{Binding ActiveDocument.CreateMaskCommand}" Width="80">Create Mask</Button>
+                    <Button Margin="5,0" Command="{Binding ActiveDocument.DeleteMaskCommand}" Width="80">Delete Mask</Button>
+                </StackPanel>
+                <DockPanel DockPanel.Dock="Top" HorizontalAlignment="Stretch" Margin="0,5,0,5">
                     <Button Width="80" Margin="5,0" Command="{Binding ActiveDocument.CombineCommand}">Merge</Button>
                     <TextBlock Text="{Binding ActiveDocument.SelectedStructureMember.Opacity, StringFormat=N2}" 
                            Margin="5,0" DockPanel.Dock="Right" VerticalAlignment="Center" TextAlignment="Center" d:Text="1.00" Width="30"/>
@@ -66,7 +71,7 @@
                     </i:Interaction.Triggers>
                     <TreeView.Resources>
                         <HierarchicalDataTemplate DataType="{x:Type vm:FolderViewModel}" ItemsSource="{Binding Children}">
-                            <StackPanel Orientation="Horizontal" Width="150">
+                            <StackPanel Orientation="Horizontal" Width="200">
                                 <CheckBox VerticalAlignment="Center" Margin="3" IsChecked="{Binding IsSelected}"/>
                                 <StackPanel>
                                     <Button Width="18" Command="{Binding MoveUpCommand}">^</Button>
@@ -76,10 +81,13 @@
                                     <CheckBox VerticalAlignment="Center" Margin="5" IsChecked="{Binding IsVisible}"/>
                                 </Border>
                                 <TextBox Text="{Binding Name}" Margin="5" Height="20"/>
+                                <CheckBox VerticalAlignment="Center" 
+                                          Visibility="{Binding HasMask, Converter={StaticResource BoolToVisibilityConverter}}"
+                                          IsChecked="{Binding ShouldDrawOnMask}">Mask</CheckBox>
                             </StackPanel>
                         </HierarchicalDataTemplate>
                         <DataTemplate DataType="{x:Type vm:LayerViewModel}">
-                            <StackPanel Orientation="Horizontal" Width="150">
+                            <StackPanel Orientation="Horizontal" Width="200">
                                 <CheckBox VerticalAlignment="Center" Margin="3" IsChecked="{Binding IsSelected}"/>
                                 <StackPanel>
                                     <Button Width="18" Command="{Binding MoveUpCommand}">^</Button>
@@ -89,6 +97,9 @@
                                     <CheckBox VerticalAlignment="Center" Margin="5" IsChecked="{Binding IsVisible}"/>
                                 </Border>
                                 <TextBox Text="{Binding Name}" Margin="5" Height="20"/>
+                                <CheckBox VerticalAlignment="Center" 
+                                          Visibility="{Binding HasMask, Converter={StaticResource BoolToVisibilityConverter}}"
+                                          IsChecked="{Binding ShouldDrawOnMask}">Mask</CheckBox>
                             </StackPanel>
                         </DataTemplate>
                     </TreeView.Resources>