Browse Source

Clip to member below impl

Equbuxu 3 years ago
parent
commit
a4a1a216ab

+ 12 - 45
src/ChunkyImageLib/ChunkyImage.cs

@@ -149,7 +149,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             blendModePaint.BlendMode = blendMode;
             blendModePaint.BlendMode = blendMode;
             tempChunk.Surface.SkiaSurface.Canvas.DrawSurface(latestChunk.Surface.SkiaSurface, 0, 0, blendModePaint);
             tempChunk.Surface.SkiaSurface.Canvas.DrawSurface(latestChunk.Surface.SkiaSurface, 0, 0, blendModePaint);
             if (lockTransparency)
             if (lockTransparency)
-                ClampAlpha(tempChunk.Surface.SkiaSurface, committedChunk.Surface.SkiaSurface);
+                OperationHelper.ClampAlpha(tempChunk.Surface.SkiaSurface, committedChunk.Surface.SkiaSurface);
             tempChunk.DrawOnSurface(surface, pos, paint);
             tempChunk.DrawOnSurface(surface, pos, paint);
 
 
             return true;
             return true;
@@ -160,10 +160,15 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
-            return (
-                MaybeGetLatestChunk(chunkPos, ChunkResolution.Full) ??
-                MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full)
-                ) is not null;
+            if (MaybeGetLatestChunk(chunkPos, ChunkResolution.Full) is not null ||
+                MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
+                return true;
+            foreach (var operation in queuedOperations)
+            {
+                if (operation.affectedChunks.Contains(chunkPos))
+                    return true;
+            }
+            return false;
         }
         }
     }
     }
 
 
@@ -489,7 +494,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
                         using Chunk tempChunk = Chunk.Create(resolution);
                         using Chunk tempChunk = Chunk.Create(resolution);
                         tempChunk.Surface.SkiaSurface.Canvas.DrawSurface(maybeCommitted.Surface.SkiaSurface, 0, 0, ReplacingPaint);
                         tempChunk.Surface.SkiaSurface.Canvas.DrawSurface(maybeCommitted.Surface.SkiaSurface, 0, 0, ReplacingPaint);
                         maybeCommitted.Surface.SkiaSurface.Canvas.DrawSurface(chunk.Surface.SkiaSurface, 0, 0, blendModePaint);
                         maybeCommitted.Surface.SkiaSurface.Canvas.DrawSurface(chunk.Surface.SkiaSurface, 0, 0, blendModePaint);
-                        ClampAlpha(maybeCommitted.Surface.SkiaSurface, tempChunk.Surface.SkiaSurface);
+                        OperationHelper.ClampAlpha(maybeCommitted.Surface.SkiaSurface, tempChunk.Surface.SkiaSurface);
                     }
                     }
                     else
                     else
                     {
                     {
@@ -603,7 +608,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             if (lockTransparency && !chunkData.IsDeleted && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
             if (lockTransparency && !chunkData.IsDeleted && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
             {
             {
                 var committed = GetCommittedChunk(chunkPos, resolution);
                 var committed = GetCommittedChunk(chunkPos, resolution);
-                ClampAlpha(targetChunk!.Surface.SkiaSurface, committed!.Surface.SkiaSurface);
+                OperationHelper.ClampAlpha(targetChunk!.Surface.SkiaSurface, committed!.Surface.SkiaSurface);
             }
             }
 
 
             chunkData.QueueProgress = queuedOperations.Count;
             chunkData.QueueProgress = queuedOperations.Count;
@@ -646,44 +651,6 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         return intersection;
         return intersection;
     }
     }
 
 
-    /// <summary>
-    /// toModify[x,y].Alpha = Math.Min(toModify[x,y].Alpha, toGetAlphaFrom[x,y].Alpha)
-    /// </summary>
-    private unsafe void ClampAlpha(SKSurface toModify, SKSurface toGetAlphaFrom)
-    {
-        using (var map = toModify.PeekPixels())
-        {
-            using (var refMap = toGetAlphaFrom.PeekPixels())
-            {
-                long* pixels = (long*)map.GetPixels();
-                long* refPixels = (long*)refMap.GetPixels();
-                int size = map.Width * map.Height;
-                if (map.Width != refMap.Width || map.Height != refMap.Height)
-                    throw new ArgumentException("The surfaces must have the same size");
-
-                for (int i = 0; i < size; i++)
-                {
-                    long* offset = pixels + i;
-                    long* refOffset = refPixels + i;
-                    Half* alpha = (Half*)offset + 3;
-                    Half* refAlpha = (Half*)refOffset + 3;
-                    if (*refAlpha < *alpha)
-                    {
-                        float a = (float)(*alpha);
-                        float r = (float)(*((Half*)offset)) / a;
-                        float g = (float)(*((Half*)offset + 1)) / a;
-                        float b = (float)(*((Half*)offset + 2)) / a;
-                        float newA = (float)(*refAlpha);
-                        Half newR = (Half)(r * newA);
-                        Half newG = (Half)(g * newA);
-                        Half newB = (Half)(b * newA);
-                        *offset = ((long)*(ushort*)(&newR)) | ((long)*(ushort*)(&newG)) << 16 | ((long)*(ushort*)(&newB)) << 32 | ((long)*(ushort*)(refAlpha)) << 48;
-                    }
-                }
-            }
-        }
-    }
-
     /// <returns>
     /// <returns>
     /// True if the chunk was fully cleared (and should be deleted).
     /// True if the chunk was fully cleared (and should be deleted).
     /// </returns>
     /// </returns>

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

@@ -17,6 +17,44 @@ public static class OperationHelper
         return new(pixelPos.X * mult, pixelPos.Y * mult);
         return new(pixelPos.X * mult, pixelPos.Y * mult);
     }
     }
 
 
+    /// <summary>
+    /// toModify[x,y].Alpha = Math.Min(toModify[x,y].Alpha, toGetAlphaFrom[x,y].Alpha)
+    /// </summary>
+    public unsafe static void ClampAlpha(SKSurface toModify, SKSurface toGetAlphaFrom)
+    {
+        using (var map = toModify.PeekPixels())
+        {
+            using (var refMap = toGetAlphaFrom.PeekPixels())
+            {
+                long* pixels = (long*)map.GetPixels();
+                long* refPixels = (long*)refMap.GetPixels();
+                int size = map.Width * map.Height;
+                if (map.Width != refMap.Width || map.Height != refMap.Height)
+                    throw new ArgumentException("The surfaces must have the same size");
+
+                for (int i = 0; i < size; i++)
+                {
+                    long* offset = pixels + i;
+                    long* refOffset = refPixels + i;
+                    Half* alpha = (Half*)offset + 3;
+                    Half* refAlpha = (Half*)refOffset + 3;
+                    if (*refAlpha < *alpha)
+                    {
+                        float a = (float)(*alpha);
+                        float r = (float)(*((Half*)offset)) / a;
+                        float g = (float)(*((Half*)offset + 1)) / a;
+                        float b = (float)(*((Half*)offset + 2)) / a;
+                        float newA = (float)(*refAlpha);
+                        Half newR = (Half)(r * newA);
+                        Half newG = (Half)(g * newA);
+                        Half newB = (Half)(b * newA);
+                        *offset = ((long)*(ushort*)(&newR)) | ((long)*(ushort*)(&newG)) << 16 | ((long)*(ushort*)(&newB)) << 32 | ((long)*(ushort*)(refAlpha)) << 48;
+                    }
+                }
+            }
+        }
+    }
+
     public static ShapeCorners ConvertForResolution(ShapeCorners corners, ChunkResolution resolution)
     public static ShapeCorners ConvertForResolution(ShapeCorners corners, ChunkResolution resolution)
     {
     {
         return new ShapeCorners()
         return new ShapeCorners()

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

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

+ 5 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/StructureMemberClipToMemberBelow_ChangeInfo.cs

@@ -0,0 +1,5 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+public record class StructureMemberClipToMemberBelow_ChangeInfo : IChangeInfo
+{
+    public Guid MemberGuid { get; init; }
+}

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

@@ -6,6 +6,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 public interface IReadOnlyStructureMember
 public interface IReadOnlyStructureMember
 {
 {
     bool IsVisible { get; }
     bool IsVisible { get; }
+    bool ClipToMemberBelow { get; }
     string Name { get; }
     string Name { get; }
     Guid GuidValue { get; }
     Guid GuidValue { get; }
     float Opacity { get; }
     float Opacity { get; }

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

@@ -8,6 +8,7 @@ internal abstract class StructureMember : IChangeable, IReadOnlyStructureMember,
 {
 {
     public float Opacity { get; set; } = 1f;
     public float Opacity { get; set; } = 1f;
     public bool IsVisible { get; set; } = true;
     public bool IsVisible { get; set; } = true;
+    public bool ClipToMemberBelow { get; set; } = false;
     public string Name { get; set; } = "Unnamed";
     public string Name { get; set; } = "Unnamed";
     public BlendMode BlendMode { get; set; } = BlendMode.Normal;
     public BlendMode BlendMode { get; set; } = BlendMode.Normal;
     public Guid GuidValue { get; init; }
     public Guid GuidValue { get; init; }

+ 50 - 0
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberClipToMemberBelow_Change.cs

@@ -0,0 +1,50 @@
+using PixiEditor.ChangeableDocument.Changeables;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+
+namespace PixiEditor.ChangeableDocument.Changes.Properties;
+internal class StructureMemberClipToMemberBelow_Change : Change
+{
+    private bool originalValue;
+    private readonly bool newValue;
+    private readonly Guid memberGuid;
+
+    public StructureMemberClipToMemberBelow_Change(bool newValue, Guid memberGuid)
+    {
+        this.newValue = newValue;
+        this.memberGuid = memberGuid;
+    }
+
+    public override void Initialize(Document target)
+    {
+        var member = target.FindMemberOrThrow(memberGuid);
+        originalValue = member.ClipToMemberBelow;
+    }
+
+    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    {
+        if (originalValue == newValue)
+        {
+            ignoreInUndo = true;
+            return null;
+        }
+        var member = target.FindMemberOrThrow(memberGuid);
+        member.ClipToMemberBelow = newValue;
+        ignoreInUndo = false;
+        return new StructureMemberClipToMemberBelow_ChangeInfo() { MemberGuid = memberGuid };
+    }
+
+    public override IChangeInfo? Revert(Document target)
+    {
+        if (originalValue == newValue)
+            return null;
+        var member = target.FindMemberOrThrow(memberGuid);
+        member.ClipToMemberBelow = originalValue;
+        return new StructureMemberClipToMemberBelow_ChangeInfo() { MemberGuid = memberGuid };
+    }
+
+    public override bool IsMergeableWith(Change other)
+    {
+        return other is StructureMemberClipToMemberBelow_Change;
+    }
+}

+ 133 - 12
src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs

@@ -1,5 +1,8 @@
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using ChunkyImageLib.Operations;
+using OneOf;
+using OneOf.Types;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using SkiaSharp;
 using SkiaSharp;
@@ -48,36 +51,125 @@ public static class ChunkRenderer
 
 
     private static Chunk RenderChunkRecursively(VecI chunkPos, ChunkResolution resolution, int depth, IReadOnlyFolder folder, HashSet<Guid>? visibleLayers)
     private static Chunk RenderChunkRecursively(VecI chunkPos, ChunkResolution resolution, int depth, IReadOnlyFolder folder, HashSet<Guid>? visibleLayers)
     {
     {
+        // if you are a skilled programmer any problem can be solved with enough if/else statements
         Chunk targetChunk = Chunk.Create(resolution);
         Chunk targetChunk = Chunk.Create(resolution);
         targetChunk.Surface.SkiaSurface.Canvas.Clear();
         targetChunk.Surface.SkiaSurface.Canvas.Clear();
-        foreach (var child in folder.ReadOnlyChildren)
+
+        //<clipping Chunk; None to clip with (fully masked out); No active clip>
+        OneOf<Chunk, None, No> clippingChunk = new No();
+        for (int i = 0; i < folder.ReadOnlyChildren.Count; i++)
         {
         {
+            var child = folder.ReadOnlyChildren[i];
+
+            // next child might use clip to member below in which case we need to save the clip image
+            bool needToSaveClip =
+                i < folder.ReadOnlyChildren.Count - 1 &&
+                !child.ClipToMemberBelow &&
+                folder.ReadOnlyChildren[i + 1].ClipToMemberBelow;
+            bool clipActiveWithReference = (clippingChunk.IsT0 || clippingChunk.IsT1) && child.ClipToMemberBelow;
+            if (!child.ClipToMemberBelow && !clippingChunk.IsT2)
+            {
+                if (clippingChunk.IsT0)
+                    clippingChunk.AsT0.Dispose();
+                clippingChunk = new No();
+            }
+
             if (!child.IsVisible)
             if (!child.IsVisible)
+            {
+                if (needToSaveClip)
+                    clippingChunk = new None();
                 continue;
                 continue;
+            }
 
 
+            //// actual drawing
             // chunk fully masked out
             // chunk fully masked out
             if (child.ReadOnlyMask is not null && !child.ReadOnlyMask.LatestOrCommittedChunkExists(chunkPos))
             if (child.ReadOnlyMask is not null && !child.ReadOnlyMask.LatestOrCommittedChunkExists(chunkPos))
+            {
+                if (needToSaveClip)
+                    clippingChunk = new None();
                 continue;
                 continue;
+            }
 
 
             // layer
             // layer
             if (child is IReadOnlyLayer layer && (visibleLayers is null || visibleLayers.Contains(layer.GuidValue)))
             if (child is IReadOnlyLayer layer && (visibleLayers is null || visibleLayers.Contains(layer.GuidValue)))
             {
             {
+                // no mask
                 if (layer.ReadOnlyMask is null)
                 if (layer.ReadOnlyMask is null)
                 {
                 {
                     PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
                     PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
                     PaintToDrawChunksWith.BlendMode = GetSKBlendMode(layer.BlendMode);
                     PaintToDrawChunksWith.BlendMode = GetSKBlendMode(layer.BlendMode);
-                    layer.ReadOnlyLayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    // draw while saving clip for later
+                    if (needToSaveClip)
+                    {
+                        var clip = Chunk.Create(resolution);
+                        if (!layer.ReadOnlyLayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, clip.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
+                        {
+                            clip.Dispose();
+                            clippingChunk = new None();
+                            continue;
+                        }
+                        targetChunk.Surface.SkiaSurface.Canvas.DrawSurface(clip.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                        clippingChunk = clip;
+                    }
+                    // draw using saved clip
+                    else if (clipActiveWithReference)
+                    {
+                        if (clippingChunk.IsT1)
+                            continue;
+                        using var tempChunk = Chunk.Create();
+                        if (!layer.ReadOnlyLayerImage.DrawMostUpToDateChunkOn
+                            (chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
+                            continue;
+                        OperationHelper.ClampAlpha(tempChunk.Surface.SkiaSurface, clippingChunk.AsT0.Surface.SkiaSurface);
+                        targetChunk.Surface.SkiaSurface.Canvas.DrawSurface(tempChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    }
+                    // just draw
+                    else
+                    {
+                        layer.ReadOnlyLayerImage.DrawMostUpToDateChunkOn
+                            (chunkPos, resolution, targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    }
                 }
                 }
+                // with mask
                 else
                 else
                 {
                 {
-                    using (Chunk tempChunk = Chunk.Create(resolution))
+                    PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
+                    PaintToDrawChunksWith.BlendMode = GetSKBlendMode(layer.BlendMode);
+                    // draw while saving clip
+                    if (needToSaveClip)
                     {
                     {
+                        Chunk tempChunk = Chunk.Create(resolution);
+                        // this chunk is empty
+                        if (!layer.ReadOnlyLayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
+                        {
+                            tempChunk.Dispose();
+                            clippingChunk = new None();
+                            continue;
+                        }
+                        // this chunk is not empty
+                        layer.ReadOnlyMask.DrawMostUpToDateChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
+                        targetChunk.Surface.SkiaSurface.Canvas.DrawSurface(tempChunk.Surface.SkiaSurface, 0, 0, PaintToDrawChunksWith);
+                        clippingChunk = tempChunk;
+                    }
+                    // draw using saved clip
+                    else if (clipActiveWithReference)
+                    {
+                        if (clippingChunk.IsT1)
+                            continue;
+                        using Chunk tempChunk = Chunk.Create(resolution);
+                        if (!layer.ReadOnlyLayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
+                            continue;
+                        layer.ReadOnlyMask.DrawMostUpToDateChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
+                        OperationHelper.ClampAlpha(tempChunk.Surface.SkiaSurface, clippingChunk.AsT0.Surface.SkiaSurface);
+                        targetChunk.Surface.SkiaSurface.Canvas.DrawSurface(tempChunk.Surface.SkiaSurface, 0, 0, PaintToDrawChunksWith);
+                    }
+                    // just draw
+                    else
+                    {
+                        using Chunk tempChunk = Chunk.Create(resolution);
                         if (!layer.ReadOnlyLayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
                         if (!layer.ReadOnlyLayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
                             continue;
                             continue;
                         layer.ReadOnlyMask.DrawMostUpToDateChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
                         layer.ReadOnlyMask.DrawMostUpToDateChunkOn(chunkPos, resolution, tempChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
-
-                        PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
-                        PaintToDrawChunksWith.BlendMode = GetSKBlendMode(layer.BlendMode);
                         targetChunk.Surface.SkiaSurface.Canvas.DrawSurface(tempChunk.Surface.SkiaSurface, 0, 0, PaintToDrawChunksWith);
                         targetChunk.Surface.SkiaSurface.Canvas.DrawSurface(tempChunk.Surface.SkiaSurface, 0, 0, PaintToDrawChunksWith);
                     }
                     }
                 }
                 }
@@ -87,16 +179,45 @@ public static class ChunkRenderer
             // folder
             // folder
             if (child is IReadOnlyFolder innerFolder)
             if (child is IReadOnlyFolder innerFolder)
             {
             {
-                using Chunk renderedChunk = RenderChunkRecursively(chunkPos, resolution, depth + 1, innerFolder, visibleLayers);
-                if (innerFolder.ReadOnlyMask is not null)
-                    innerFolder.ReadOnlyMask.DrawMostUpToDateChunkOn(chunkPos, resolution, renderedChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
-
                 PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
                 PaintToDrawChunksWith.Color = new SKColor(255, 255, 255, (byte)Math.Round(child.Opacity * 255));
                 PaintToDrawChunksWith.BlendMode = GetSKBlendMode(innerFolder.BlendMode);
                 PaintToDrawChunksWith.BlendMode = GetSKBlendMode(innerFolder.BlendMode);
-                renderedChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
-                continue;
+
+                // draw while saving clip
+                if (needToSaveClip)
+                {
+                    Chunk renderedChunk = RenderChunkRecursively(chunkPos, resolution, depth + 1, innerFolder, visibleLayers);
+                    if (innerFolder.ReadOnlyMask is not null)
+                        innerFolder.ReadOnlyMask.DrawMostUpToDateChunkOn(chunkPos, resolution, renderedChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
+
+                    renderedChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    clippingChunk = renderedChunk;
+                    continue;
+                }
+                // draw using saved clip
+                else if (clipActiveWithReference)
+                {
+                    if (clippingChunk.IsT1)
+                        continue;
+                    using Chunk renderedChunk = RenderChunkRecursively(chunkPos, resolution, depth + 1, innerFolder, visibleLayers);
+                    if (innerFolder.ReadOnlyMask is not null)
+                        innerFolder.ReadOnlyMask.DrawMostUpToDateChunkOn(chunkPos, resolution, renderedChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
+                    OperationHelper.ClampAlpha(renderedChunk.Surface.SkiaSurface, clippingChunk.AsT0.Surface.SkiaSurface);
+                    renderedChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    continue;
+                }
+                // just draw
+                else
+                {
+                    using Chunk renderedChunk = RenderChunkRecursively(chunkPos, resolution, depth + 1, innerFolder, visibleLayers);
+                    if (innerFolder.ReadOnlyMask is not null)
+                        innerFolder.ReadOnlyMask.DrawMostUpToDateChunkOn(chunkPos, resolution, renderedChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
+                    renderedChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0), PaintToDrawChunksWith);
+                    continue;
+                }
             }
             }
         }
         }
+        if (clippingChunk.IsT0)
+            clippingChunk.AsT0.Dispose();
         return targetChunk;
         return targetChunk;
     }
     }
 }
 }

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

@@ -77,9 +77,18 @@ internal class DocumentUpdater
             case SymmetryAxisPosition_ChangeInfo info:
             case SymmetryAxisPosition_ChangeInfo info:
                 ProcessSymmetryPosition(info);
                 ProcessSymmetryPosition(info);
                 break;
                 break;
+            case StructureMemberClipToMemberBelow_ChangeInfo info:
+                ProcessClipToMemberBelow(info);
+                break;
         }
         }
     }
     }
 
 
+    private void ProcessClipToMemberBelow(StructureMemberClipToMemberBelow_ChangeInfo info)
+    {
+        var member = helper.StructureHelper.FindOrThrow(info.MemberGuid);
+        member.RaisePropertyChanged(nameof(member.ClipToMemberBelowEnabled));
+    }
+
     private void ProcessSymmetryPosition(SymmetryAxisPosition_ChangeInfo info)
     private void ProcessSymmetryPosition(SymmetryAxisPosition_ChangeInfo info)
     {
     {
         if (info.Direction == SymmetryAxisDirection.Horizontal)
         if (info.Direction == SymmetryAxisDirection.Horizontal)

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

@@ -86,6 +86,7 @@ internal class WriteableBitmapUpdater
                 case Size_ChangeInfo:
                 case Size_ChangeInfo:
                 case StructureMemberMask_ChangeInfo:
                 case StructureMemberMask_ChangeInfo:
                 case StructureMemberBlendMode_ChangeInfo:
                 case StructureMemberBlendMode_ChangeInfo:
+                case StructureMemberClipToMemberBelow_ChangeInfo:
                     AddAllChunks(affectedChunks);
                     AddAllChunks(affectedChunks);
                     break;
                     break;
                 case StructureMemberOpacity_ChangeInfo opacityChangeInfo:
                 case StructureMemberOpacity_ChangeInfo opacityChangeInfo:

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

@@ -71,6 +71,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
     public RelayCommand? PasteImageCommand { get; }
     public RelayCommand? PasteImageCommand { get; }
     public RelayCommand? DragSymmetryCommand { get; }
     public RelayCommand? DragSymmetryCommand { get; }
     public RelayCommand? EndDragSymmetryCommand { get; }
     public RelayCommand? EndDragSymmetryCommand { get; }
+    public RelayCommand? ClipToMemberBelowCommand { get; }
 
 
     public int Width => Helpers.Tracker.Document.Size.X;
     public int Width => Helpers.Tracker.Document.Size.X;
     public int Height => Helpers.Tracker.Document.Size.Y;
     public int Height => Helpers.Tracker.Document.Size.Y;
@@ -126,6 +127,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
         ApplyTransformCommand = new RelayCommand(ApplyTransform);
         ApplyTransformCommand = new RelayCommand(ApplyTransform);
         DragSymmetryCommand = new RelayCommand(DragSymmetry);
         DragSymmetryCommand = new RelayCommand(DragSymmetry);
         EndDragSymmetryCommand = new RelayCommand(EndDragSymmetry);
         EndDragSymmetryCommand = new RelayCommand(EndDragSymmetry);
+        ClipToMemberBelowCommand = new RelayCommand(ClipToMemberBelow);
 
 
         foreach (var bitmap in Bitmaps)
         foreach (var bitmap in Bitmaps)
         {
         {
@@ -139,6 +141,13 @@ internal class DocumentViewModel : INotifyPropertyChanged
             (new CreateStructureMember_Action(StructureRoot.GuidValue, Guid.NewGuid(), 0, StructureMemberType.Layer));
             (new CreateStructureMember_Action(StructureRoot.GuidValue, Guid.NewGuid(), 0, StructureMemberType.Layer));
     }
     }
 
 
+    private void ClipToMemberBelow(object? obj)
+    {
+        if (updateableChangeActive || SelectedStructureMember is null)
+            return;
+        SelectedStructureMember.ClipToMemberBelowEnabled = !SelectedStructureMember.ClipToMemberBelowEnabled;
+    }
+
     private bool updateableChangeActive = false;
     private bool updateableChangeActive = false;
 
 
     private bool drawingRectangle = false;
     private bool drawingRectangle = false;

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

@@ -33,6 +33,12 @@ internal abstract class StructureMemberViewModel : INotifyPropertyChanged
         set => Helpers.ActionAccumulator.AddFinishedActions(new SetStructureMemberBlendMode_Action(value, member.GuidValue));
         set => Helpers.ActionAccumulator.AddFinishedActions(new SetStructureMemberBlendMode_Action(value, member.GuidValue));
     }
     }
 
 
+    public bool ClipToMemberBelowEnabled
+    {
+        get => member.ClipToMemberBelow;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new SetStructureMemberClipToMemberBelow_Action(value, member.GuidValue));
+    }
+
     public bool IsSelected { get; set; }
     public bool IsSelected { get; set; }
     public bool ShouldDrawOnMask { get; set; }
     public bool ShouldDrawOnMask { get; set; }
 
 

+ 10 - 4
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -40,6 +40,7 @@
                 <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,5,0,0">
                 <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,5,0,0">
                     <Button Margin="5,0" Command="{Binding ActiveDocument.ToggleLockTransparencyCommand}" Width="80">Lock Alpha</Button>
                     <Button Margin="5,0" Command="{Binding ActiveDocument.ToggleLockTransparencyCommand}" Width="80">Lock Alpha</Button>
                     <Button Margin="5,0" IsEnabled="False" Width="80">Lock</Button>
                     <Button Margin="5,0" IsEnabled="False" Width="80">Lock</Button>
+                    <Button Margin="5,0" Command="{Binding ActiveDocument.ClipToMemberBelowCommand}" Width="80">Clip to below</Button>
                 </StackPanel>
                 </StackPanel>
                 <DockPanel DockPanel.Dock="Top" HorizontalAlignment="Stretch" Margin="0,5,0,5">
                 <DockPanel DockPanel.Dock="Top" HorizontalAlignment="Stretch" Margin="0,5,0,5">
                     <Button Width="80" Margin="5,0" Command="{Binding ActiveDocument.CombineCommand}">Merge</Button>
                     <Button Width="80" Margin="5,0" Command="{Binding ActiveDocument.CombineCommand}">Merge</Button>
@@ -81,13 +82,15 @@
                     <TreeView.Resources>
                     <TreeView.Resources>
                         <HierarchicalDataTemplate DataType="{x:Type vm:FolderViewModel}" ItemsSource="{Binding Children}">
                         <HierarchicalDataTemplate DataType="{x:Type vm:FolderViewModel}" ItemsSource="{Binding Children}">
                             <StackPanel Orientation="Horizontal" MinWidth="200">
                             <StackPanel Orientation="Horizontal" MinWidth="200">
+                                <CheckBox VerticalAlignment="Center" HorizontalAlignment="Center" IsChecked="{Binding IsVisible}"/>
+                                <Rectangle 
+                                    Fill="DarkRed" Width="8" Margin="3,0" 
+                                    Visibility="{Binding ClipToMemberBelowEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/>
                                 <StackPanel>
                                 <StackPanel>
                                     <Button Width="12" Command="{Binding MoveUpCommand}">^</Button>
                                     <Button Width="12" Command="{Binding MoveUpCommand}">^</Button>
                                     <Button Width="12" Command="{Binding MoveDownCommand}">v</Button>
                                     <Button Width="12" Command="{Binding MoveDownCommand}">v</Button>
                                 </StackPanel>
                                 </StackPanel>
-                                <Border BorderBrush="Black" BorderThickness="1" Background="Yellow" Width="25" Height="25" Margin="3,0,0,0">
-                                    <CheckBox VerticalAlignment="Center" HorizontalAlignment="Center" IsChecked="{Binding IsVisible}"/>
-                                </Border>
+                                <Border BorderBrush="Black" BorderThickness="1" Background="Yellow" Width="25" Height="25" Margin="3,0,0,0"/>
                                 <StackPanel VerticalAlignment="Center">
                                 <StackPanel VerticalAlignment="Center">
                                     <DockPanel Margin="3, 0, 0, 0">
                                     <DockPanel Margin="3, 0, 0, 0">
                                         <TextBlock Text="{Binding Opacity}" Width="25"/>
                                         <TextBlock Text="{Binding Opacity}" Width="25"/>
@@ -108,12 +111,15 @@
                         </HierarchicalDataTemplate>
                         </HierarchicalDataTemplate>
                         <DataTemplate DataType="{x:Type vm:LayerViewModel}">
                         <DataTemplate DataType="{x:Type vm:LayerViewModel}">
                             <StackPanel Orientation="Horizontal">
                             <StackPanel Orientation="Horizontal">
+                                <CheckBox VerticalAlignment="Center" HorizontalAlignment="Center" IsChecked="{Binding IsVisible}"/>
+                                <Rectangle 
+                                    Fill="DarkRed" Width="8" Margin="3,0" 
+                                    Visibility="{Binding ClipToMemberBelowEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/>
                                 <StackPanel>
                                 <StackPanel>
                                     <Button Width="12" Command="{Binding MoveUpCommand}">^</Button>
                                     <Button Width="12" Command="{Binding MoveUpCommand}">^</Button>
                                     <Button Width="12" Command="{Binding MoveDownCommand}">v</Button>
                                     <Button Width="12" Command="{Binding MoveDownCommand}">v</Button>
                                 </StackPanel>
                                 </StackPanel>
                                 <Border BorderBrush="Black" BorderThickness="1" Background="White" Width="25" Height="25" Margin="3,0,0,0">
                                 <Border BorderBrush="Black" BorderThickness="1" Background="White" Width="25" Height="25" Margin="3,0,0,0">
-                                    <CheckBox VerticalAlignment="Center" HorizontalAlignment="Center" IsChecked="{Binding IsVisible}"/>
                                 </Border>
                                 </Border>
                                 <StackPanel VerticalAlignment="Center">
                                 <StackPanel VerticalAlignment="Center">
                                     <DockPanel Margin="3, 0, 0, 0">
                                     <DockPanel Margin="3, 0, 0, 0">