Browse Source

Merge pull request #705 from PixiEditor/fixes/27.12.2024

Fixes/27.12.2024
Krzysztof Krysiński 7 months ago
parent
commit
b008d8a540

+ 26 - 9
src/ChunkyImageLib/ChunkyImage.cs

@@ -76,7 +76,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
 
     public ColorSpace ProcessingColorSpace { get; set; }
-    
+
     public int CommitCounter => commitCounter;
 
     public VecI CommittedSize { get; private set; }
@@ -130,7 +130,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             [ChunkResolution.Quarter] = new(),
             [ChunkResolution.Eighth] = new(),
         };
-        
+
         ProcessingColorSpace = colorSpace;
     }
 
@@ -280,7 +280,23 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
             {
                 null => Colors.Transparent,
-                var chunk => chunk.Surface.GetSRGBPixel(posInChunk)
+                var chunk => chunk.Surface.GetSrgbPixel(posInChunk)
+            };
+        }
+    }
+
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public Color GetCommittedPixelRaw(VecI posOnImage)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
+            var posInChunk = posOnImage - chunkPos * FullChunkSize;
+            return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
+            {
+                null => Colors.Transparent,
+                var chunk => chunk.Surface.GetRawPixel(posInChunk)
             };
         }
     }
@@ -301,7 +317,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 return committedChunk switch
                 {
                     null => Colors.Transparent,
-                    _ => committedChunk.Surface.GetSRGBPixel(posInChunk)
+                    _ => committedChunk.Surface.GetSrgbPixel(posInChunk)
                 };
             }
 
@@ -312,7 +328,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 return latestChunk switch
                 {
                     null => Colors.Transparent,
-                    _ => latestChunk.Surface.GetSRGBPixel(posInChunk)
+                    _ => latestChunk.Surface.GetSrgbPixel(posInChunk)
                 };
             }
 
@@ -322,10 +338,10 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
                 Color committedColor = committedChunk is null
                     ? Colors.Transparent
-                    : committedChunk.Surface.GetSRGBPixel(posInChunk);
+                    : committedChunk.Surface.GetSrgbPixel(posInChunk);
                 Color latestColor = latestChunk is null
                     ? Colors.Transparent
-                    : latestChunk.Surface.GetSRGBPixel(posInChunk);
+                    : latestChunk.Surface.GetSrgbPixel(posInChunk);
                 // using a whole chunk just to draw 1 pixel is kinda dumb,
                 // but this should be faster than any approach that requires allocations
                 using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, ChunkResolution.Eighth);
@@ -333,7 +349,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 using Paint latestPaint = new Paint() { Color = latestColor, BlendMode = this.blendMode };
                 tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, committedPaint);
                 tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, latestPaint);
-                return tempChunk.Surface.GetSRGBPixel(VecI.Zero);
+                return tempChunk.Surface.GetSrgbPixel(VecI.Zero);
             }
         }
     }
@@ -601,7 +617,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         lock (lockObject)
         {
             ThrowIfDisposed();
-            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, antiAliased, paint);
+            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, antiAliased,
+                paint);
             EnqueueOperation(operation);
         }
     }

+ 2 - 2
src/ChunkyImageLib/DataHolders/ColorBounds.cs

@@ -25,8 +25,8 @@ public struct ColorBounds
     {
         static (float lower, float upper) FindInclusiveBoundaryPremul(byte channel, float alpha)
         {
-            float subHalf = channel > 0 ? channel - .5f : channel;
-            float addHalf = channel < 255 ? channel + .5f : channel;
+            float subHalf = channel > 0 ? channel - 1f : channel;
+            float addHalf = channel < 255 ? channel + 1f : channel;
             
             var lower = subHalf * alpha / 255f;
             var upper = addHalf * alpha / 255f;

+ 1 - 1
src/ChunkyImageLib/Operations/PixelOperation.cs

@@ -55,7 +55,7 @@ internal class PixelOperation : IMirroredDrawOperation
         if (colorProcessor != null && getCommitedPixelFunc != null)
         {
             var pos = pixel - chunkPos * ChunkyImage.FullChunkSize;
-            pixelColor = colorProcessor(getCommitedPixelFunc(pixel), chunk.Surface.GetSRGBPixel(pos));
+            pixelColor = colorProcessor(getCommitedPixelFunc(pixel), chunk.Surface.GetSrgbPixel(pos));
         }
 
         return new Color(pixelColor.R, pixelColor.G, pixelColor.B, (byte)(pixelColor.A * chunk.Resolution.Multiplier()));

+ 14 - 1
src/ChunkyImageLib/Operations/ReplaceColorOperation.cs

@@ -2,9 +2,11 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace ChunkyImageLib.Operations;
+
 internal class ReplaceColorOperation : IDrawOperation
 {
     private readonly Color oldColor;
@@ -25,7 +27,18 @@ internal class ReplaceColorOperation : IDrawOperation
 
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
-        ReplaceColor(oldColorBounds, newColorBits, targetChunk);
+        ulong targetColorBits = newColor.ToULong();
+        ColorBounds colorBounds = new(oldColor);
+        if (targetChunk.Surface.ImageInfo.ColorSpace is { IsSrgb: false })
+        {
+            var transform = ColorSpace.CreateSrgb().GetTransformFunction();
+            targetColorBits = newColor.TransformColor(transform).ToULong();
+
+            var transformOld = targetChunk.Surface.ImageInfo.ColorSpace.GetTransformFunction();
+            colorBounds = new ColorBounds(oldColor.TransformColor(transform));
+        }
+
+        ReplaceColor(colorBounds, targetColorBits, targetChunk);
     }
 
     private static unsafe void ReplaceColor(ColorBounds oldColorBounds, ulong newColorBits, Chunk chunk)

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 1124f58f7ca5b4312aeb0ef8e22ceea7103a9bb2
+Subproject commit c90f46c8ac9050511e5d65999c72b27e8b32ebfb

+ 14 - 35
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -25,10 +25,10 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
     public override Node CreateCopy() => new FolderNode { MemberName = MemberName };
 
     public override VecD GetScenePosition(KeyFrameTime time) =>
-        documentSize / 2f; //GetTightBounds(time).GetValueOrDefault().Center;
+        documentSize / 2f; 
 
     public override VecD GetSceneSize(KeyFrameTime time) =>
-        documentSize; //GetTightBounds(time).GetValueOrDefault().Size;
+        documentSize; 
 
     protected override void OnExecute(RenderContext context)
     {
@@ -126,24 +126,31 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
-        RectI bounds = new RectI();
+        RectI? bounds = null;
         if (Content.Connection != null)
         {
             Content.Connection.Node.TraverseBackwards((n) =>
             {
-                if (n is ImageLayerNode imageLayerNode)
+                if (n is StructureNode structureNode)
                 {
-                    RectI? imageBounds = (RectI?)imageLayerNode.GetTightBounds(frameTime);
+                    RectI? imageBounds = (RectI?)structureNode.GetTightBounds(frameTime);
                     if (imageBounds != null)
                     {
-                        bounds = bounds.Union(imageBounds.Value);
+                        if (bounds == null)
+                        {
+                            bounds = imageBounds;
+                        }
+                        else
+                        {
+                            bounds = bounds.Value.Union(imageBounds.Value);
+                        }
                     }
                 }
 
                 return true;
             });
 
-            return (RectD)bounds;
+            return (RectD?)bounds ?? RectD.Empty;
         }
 
         return null;
@@ -165,32 +172,6 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
         return guids;
     }
 
-    /// <summary>
-    /// Creates a clone of the folder, its mask and all of its children
-    /// </summary>
-    /*internal override Folder Clone()
-    {
-        var builder = ImmutableList<StructureMember>.Empty.ToBuilder();
-        for (var i = 0; i < Children.Count; i++)
-        {
-            var child = Children[i];
-            builder.Add(child.Clone());
-        }
-
-        return new Folder
-        {
-            GuidValue = GuidValue,
-            IsVisible = IsVisible,
-            Name = Name,
-            Opacity = Opacity,
-            Children = builder.ToImmutable(),
-            Mask = Mask?.CloneFromCommitted(),
-            BlendMode = BlendMode,
-            ClipToMemberBelow = ClipToMemberBelow,
-            MaskIsVisible = MaskIsVisible
-        };
-    }*/
-
     public override RectD? GetPreviewBounds(int frame, string elementFor = "")
     {
         if (elementFor == nameof(EmbeddedMask))
@@ -209,8 +190,6 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
             return base.RenderPreview(renderOn, context, elementToRenderName);
         }
 
-        // TODO: Make preview better, with filters, clips and stuff
-
         if (Content.Connection != null)
         {
             var executionQueue = GraphUtils.CalculateExecutionQueue(Content.Connection.Node);

+ 8 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -154,6 +154,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
         var img = GetLayerImageAtFrame(context.FrameTime.Frame);
 
+        int cacheFrame = context.FrameTime.Frame;
         if (Guid.TryParse(elementToRenderName, out Guid guid))
         {
             var keyFrame = keyFrames.FirstOrDefault(x => x.KeyFrameGuid == guid);
@@ -161,6 +162,12 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             if (keyFrame != null)
             {
                 img = GetLayerImageByKeyFrameGuid(keyFrame.KeyFrameGuid);
+                cacheFrame = keyFrame.StartFrame;
+            }
+            else if (guid == Id)
+            {
+                img = GetLayerImageAtFrame(0);
+                cacheFrame = 0;
             }
         }
 
@@ -169,7 +176,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             return false;
         }
 
-        if (renderedSurfaceFrame == context.FrameTime.Frame)
+        if (renderedSurfaceFrame == cacheFrame)
         {
             renderOnto.Canvas.DrawSurface(fullResrenderedSurface.DrawingSurface, VecI.Zero, blendPaint);
         }

+ 23 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -33,10 +33,10 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
     protected override void OnPaint(RenderContext renderContext, DrawingSurface targetSurface)
     {
-        if (OtherNode == null)
+        if (OtherNode == null || OtherNode == default)
         {
-            FindStartNode();
-            if (OtherNode == null)
+            OtherNode = FindStartNode()?.Id ?? default;
+            if (OtherNode == null || OtherNode == default)
             {
                 return;
             }
@@ -47,6 +47,8 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         {
             return;
         }
+        
+        OtherNode = startNode.Id;
 
         if (startNode.Image.Value is not { Size: var size })
         {
@@ -106,13 +108,29 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
-        //TODO: Implement
+        var startNode = FindStartNode();
+        if (startNode != null)
+        {
+            return startNode.GetPreviewBounds(frame, elementToRenderName);
+        }
+        
         return null;
     }
 
     public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
-        //TODO: Implement
+        var startNode = FindStartNode();
+        if (drawingPaint != null && startNode != null && startNode.Image.Value != null)
+        {
+            int saved = renderOn.Canvas.SaveLayer(drawingPaint);
+            
+            renderOn.Canvas.DrawRect(0, 0, startNode.Image.Value.Size.X, startNode.Image.Value.Size.Y, drawingPaint);
+            
+            renderOn.Canvas.RestoreToCount(saved);
+            
+            return true;
+        }
+
         return false;
     }
 
@@ -130,7 +148,6 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             if (node is ModifyImageLeftNode leftNode)
             {
                 startNode = leftNode;
-                OtherNode = leftNode.Id;
                 return false;
             }
 

+ 33 - 10
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -21,7 +21,8 @@ public static class FloodFillHelper
     private static readonly VecI Left = new VecI(-1, 0);
     private static readonly VecI Right = new VecI(1, 0);
 
-    internal static FloodFillChunkCache CreateCache(HashSet<Guid> membersToFloodFill, IReadOnlyDocument document, int frame)
+    internal static FloodFillChunkCache CreateCache(HashSet<Guid> membersToFloodFill, IReadOnlyDocument document,
+        int frame)
     {
         if (membersToFloodFill.Count == 1)
         {
@@ -33,6 +34,7 @@ public static class FloodFillHelper
                 throw new InvalidOperationException("Member is not a raster layer");
             return new FloodFillChunkCache(rasterLayer.GetLayerImageAtFrame(frame));
         }
+
         return new FloodFillChunkCache(membersToFloodFill, document, frame);
     }
 
@@ -55,8 +57,9 @@ public static class FloodFillHelper
         VecI initChunkPos = OperationHelper.GetChunkPos(startingPos, chunkSize);
         VecI imageSizeInChunks = (VecI)(document.Size / (double)chunkSize).Ceiling();
         VecI initPosOnChunk = startingPos - initChunkPos * chunkSize;
-        Color colorToReplace = cache.GetChunk(initChunkPos).Match(
-            (Chunk chunk) => chunk.Surface.GetSRGBPixel(initPosOnChunk),
+        var chunkAtPos = cache.GetChunk(initChunkPos);
+        Color colorToReplace = chunkAtPos.Match(
+            (Chunk chunk) => chunk.Surface.GetRawPixel(initPosOnChunk),
             static (EmptyChunk _) => Colors.Transparent
         );
 
@@ -69,6 +72,16 @@ public static class FloodFillHelper
         // Used for faster pixel checking
         ColorBounds colorRange = new(colorToReplace, tolerance);
         ulong uLongColor = drawingColor.ToULong();
+        if (chunkAtPos.IsT0 && !chunkAtPos.AsT0.Surface.ImageInfo.ColorSpace.IsSrgb)
+        {
+            if (chunkAtPos.AsT0.Surface?.ImageInfo.ColorSpace != null)
+            {
+                var srgbTransform = ColorSpace.CreateSrgb().GetTransformFunction();
+
+                var fixedColor = drawingColor.TransformColor(srgbTransform);
+                uLongColor = fixedColor.ToULong();
+            }
+        }
 
         Dictionary<VecI, Chunk> drawingChunks = new();
         HashSet<VecI> processedEmptyChunks = new();
@@ -88,6 +101,7 @@ public static class FloodFillHelper
                 chunk.Surface.DrawingSurface.Canvas.Clear(Colors.Transparent);
                 drawingChunks[chunkPos] = chunk;
             }
+
             var drawingChunk = drawingChunks[chunkPos];
             var referenceChunk = cache.GetChunk(chunkPos);
 
@@ -108,8 +122,10 @@ public static class FloodFillHelper
                         if (chunkPos.X < imageSizeInChunks.X - 1)
                             positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
                     }
+
                     processedEmptyChunks.Add(chunkPos);
                 }
+
                 continue;
             }
 
@@ -142,6 +158,7 @@ public static class FloodFillHelper
                     positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
             }
         }
+
         return drawingChunks;
     }
 
@@ -158,9 +175,9 @@ public static class FloodFillHelper
         ColorBounds bounds,
         bool checkFirstPixel)
     {
-        if (referenceChunk.Surface.GetSRGBPixel(pos) == color || drawingChunk.Surface.GetSRGBPixel(pos) == color)
+        if (referenceChunk.Surface.GetRawPixel(pos) == color || drawingChunk.Surface.GetRawPixel(pos) == color)
             return null;
-        if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetSRGBPixel(pos)))
+        if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
             return null;
 
         byte[] pixelStates = new byte[chunkSize * chunkSize];
@@ -186,13 +203,17 @@ public static class FloodFillHelper
 
             if (curPos.X > 0 && pixelStates[pixelOffset - 1] == InSelection && bounds.IsWithinBounds(refPixel - 4))
                 toVisit.Push(new(curPos.X - 1, curPos.Y));
-            if (curPos.X < chunkSize - 1 && pixelStates[pixelOffset + 1] == InSelection && bounds.IsWithinBounds(refPixel + 4))
+            if (curPos.X < chunkSize - 1 && pixelStates[pixelOffset + 1] == InSelection &&
+                bounds.IsWithinBounds(refPixel + 4))
                 toVisit.Push(new(curPos.X + 1, curPos.Y));
-            if (curPos.Y > 0 && pixelStates[pixelOffset - chunkSize] == InSelection && bounds.IsWithinBounds(refPixel - 4 * chunkSize))
+            if (curPos.Y > 0 && pixelStates[pixelOffset - chunkSize] == InSelection &&
+                bounds.IsWithinBounds(refPixel - 4 * chunkSize))
                 toVisit.Push(new(curPos.X, curPos.Y - 1));
-            if (curPos.Y < chunkSize - 1 && pixelStates[pixelOffset + chunkSize] == InSelection && bounds.IsWithinBounds(refPixel + 4 * chunkSize))
+            if (curPos.Y < chunkSize - 1 && pixelStates[pixelOffset + chunkSize] == InSelection &&
+                bounds.IsWithinBounds(refPixel + 4 * chunkSize))
                 toVisit.Push(new(curPos.X, curPos.Y + 1));
         }
+
         return pixelStates;
     }
 
@@ -215,7 +236,8 @@ public static class FloodFillHelper
     /// <summary>
     /// Use skia to set all pixels in array that are inside selection to InSelection
     /// </summary>
-    private static unsafe void DrawSelection(byte[] array, VectorPath? selection, RectI globalBounds, VecI chunkPos, int chunkSize)
+    private static unsafe void DrawSelection(byte[] array, VectorPath? selection, RectI globalBounds, VecI chunkPos,
+        int chunkSize)
     {
         if (selection is null)
         {
@@ -232,7 +254,8 @@ public static class FloodFillHelper
         fixed (byte* arr = array)
         {
             using DrawingSurface drawingSurface = DrawingSurface.Create(
-                new ImageInfo(localBounds.Right, localBounds.Bottom, ColorType.Gray8, AlphaType.Opaque), (IntPtr)arr, chunkSize);
+                new ImageInfo(localBounds.Right, localBounds.Bottom, ColorType.Gray8, AlphaType.Opaque), (IntPtr)arr,
+                chunkSize);
             drawingSurface.Canvas.ClipPath(shiftedSelection);
             drawingSurface.Canvas.Clear(new Color(InSelection, InSelection, InSelection));
             drawingSurface.Canvas.Flush();

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs

@@ -126,7 +126,7 @@ internal class MagicWandHelper
 
 
         Color colorToReplace = cache.GetChunk(initChunkPos).Match(
-            (Chunk chunk) => chunk.Surface.GetSRGBPixel(initPosOnChunk),
+            (Chunk chunk) => chunk.Surface.GetRawPixel(initPosOnChunk),
             static (EmptyChunk _) => Colors.Transparent
         );
 
@@ -257,7 +257,7 @@ internal class MagicWandHelper
         VecI pos,
         ColorBounds bounds, Lines lines)
     {
-        if (!bounds.IsWithinBounds(referenceChunk.Surface.GetSRGBPixel(pos)))
+        if (!bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
         {
             return null;
         }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs

@@ -40,7 +40,7 @@ internal class DuplicateLayer_Change : Change
 
         InputProperty<Painter?> targetInput = parent.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter) &&
-            x.Connection.Node is StructureNode) as InputProperty<Painter?>;
+            x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
 
         List<IChangeInfo> operations = new();
 

+ 1 - 0
src/PixiEditor/Models/Handlers/ICelHandler.cs

@@ -14,4 +14,5 @@ internal interface ICelHandler
     public Guid Id { get; }
     public bool IsVisible { get; }
     public IDocument Document { get; }
+    bool IsWithinRange(int frame);
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/Tools/IBrightnessToolHandler.cs

@@ -1,4 +1,5 @@
 using Avalonia.Input;
+using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Tools;
 
 namespace PixiEditor.Models.Handlers.Tools;

+ 7 - 1
src/PixiEditor/ViewModels/Document/CelViewModel.cs

@@ -16,7 +16,7 @@ internal abstract class CelViewModel : ObservableObject, ICelHandler
     private bool isVisibleBindable = true;
     private bool isSelected;
     private bool isCollapsed;
-    
+
     public bool IsCollapsed
     {
         get => isCollapsed;
@@ -24,6 +24,7 @@ internal abstract class CelViewModel : ObservableObject, ICelHandler
     }
 
     public DocumentViewModel Document { get; }
+
     protected DocumentInternalParts Internals { get; }
 
     IDocument ICelHandler.Document => Document;
@@ -134,4 +135,9 @@ internal abstract class CelViewModel : ObservableObject, ICelHandler
         isVisibleBindable = isVisible;
         OnPropertyChanged(nameof(IsVisible));
     }
+
+    public bool IsWithinRange(int frame)
+    {
+        return frame >= StartFrameBindable && frame < StartFrameBindable + DurationBindable;
+    }
 }

+ 23 - 14
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -698,26 +698,35 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             if (scope == DocumentScope.AllLayers)
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
-                // TODO: Implement this
-                /*return Renderer.RenderChunk(chunkPos, ChunkResolution.Full,
-                        frameTime)
-                    .Match(
-                        chunk =>
-                        {
-                            VecI posOnChunk = pos - chunkPos * ChunkyImage.FullChunkSize;
-                            var color = chunk.Surface.GetSRGBPixel(posOnChunk);
-                            chunk.Dispose();
-                            return color;
-                        },
-                        _ => Colors.Transparent);*/
+                using Texture tmpTexture = Texture.ForProcessing(SizeBindable);
+                HashSet<Guid> layers = StructureHelper.GetAllLayers().Select(x => x.Id).ToHashSet();
+                Renderer.RenderLayers(tmpTexture.DrawingSurface, layers, frameTime.Frame, ChunkResolution.Full);
+                
+                using Surface tmpSurface = new Surface(tmpTexture.Size);
+                tmpSurface.DrawingSurface.Canvas.DrawImage(tmpTexture.DrawingSurface.Snapshot(), 0, 0);
+                
+                return tmpSurface.GetSrgbPixel(pos);
             }
 
             if (SelectedStructureMember is not ILayerHandler layerVm)
                 return Colors.Transparent;
             IReadOnlyStructureNode? maybeMember = Internals.Tracker.Document.FindMember(layerVm.Id);
             if (maybeMember is not IReadOnlyImageNode layer)
-                return Colors.Transparent;
-            return layer.GetLayerImageAtFrame(frameTime.Frame).GetMostUpToDatePixel(pos);
+            {
+                if (maybeMember is IRasterizable rasterizable)
+                {
+                    using Texture texture = new Texture(SizeBindable);
+                    using Paint paint = new Paint();
+                    rasterizable.Rasterize(texture.DrawingSurface, paint);
+                    return texture.GetSRGBPixel(pos);
+                }
+            }
+            else
+            {
+                return layer.GetLayerImageAtFrame(frameTime.Frame).GetMostUpToDatePixel(pos);
+            }
+            
+            return Colors.Transparent;
         }
         catch (ObjectDisposedException)
         {

+ 18 - 3
src/PixiEditor/ViewModels/SubViewModels/AnimationsViewModel.cs

@@ -26,7 +26,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
     public void ChangeActiveFrame(int nudgeBy)
     {
         var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
-        if (activeDocument is null || activeDocument.TransformViewModel.TransformActive)
+        if (activeDocument is null || IsTransforming())
             return;
 
         int newFrame = activeDocument.AnimationDataViewModel.ActiveFrameBindable + nudgeBy;
@@ -138,9 +138,14 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
         if (activeDocument.AnimationDataViewModel.TryFindCels<CelGroupViewModel>(targetLayer,
                 out CelGroupViewModel groupViewModel))
         {
-            if (active == groupViewModel.StartFrameBindable + groupViewModel.DurationBindable - 1)
+            if (groupViewModel.Children.All(x => !x.IsWithinRange(active )))
             {
-                return groupViewModel.StartFrameBindable + groupViewModel.DurationBindable;
+                return active;
+            }
+            
+            if (groupViewModel.Children.All(x => !x.IsWithinRange(active + 1)))
+            {
+                return active + 1;
             }
         }
 
@@ -186,4 +191,14 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
 
         document.Operations.SetActiveFrame((int)value);
     }
+    
+    private bool IsTransforming()
+    {
+        var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (activeDocument is null)
+            return false;
+        
+        return activeDocument.TransformViewModel.TransformActive || activeDocument.LineToolOverlayViewModel.IsEnabled
+            || activeDocument.PathOverlayViewModel.IsActive;
+    }
 }

+ 25 - 0
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/BrightnessToolbar.cs

@@ -0,0 +1,25 @@
+using PixiEditor.Models.Handlers.Toolbars;
+using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
+
+namespace PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
+
+internal class BrightnessToolbar : Toolbar, IToolSizeToolbar
+{
+    public double ToolSize
+    {
+        get => GetSetting<SizeSettingViewModel>(nameof(ToolSize)).Value;
+        set => GetSetting<SizeSettingViewModel>(nameof(ToolSize)).Value = value;
+    }
+
+    public override void OnLoadedSettings()
+    {
+        OnPropertyChanged(nameof(ToolSize));
+    }
+
+    public BrightnessToolbar()
+    {
+        var setting = new SizeSettingViewModel(nameof(ToolSize), "TOOL_SIZE_LABEL");
+        AddSetting(setting);
+        setting.ValueChanged += (_, _) => OnPropertyChanged(nameof(ToolSize));
+    }
+}

+ 3 - 3
src/PixiEditor/ViewModels/Tools/Tools/BrightnessToolViewModel.cs

@@ -23,7 +23,7 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
     public BrightnessToolViewModel()
     {
         ActionDisplay = defaultActionDisplay;
-        Toolbar = ToolbarFactory.Create<BrightnessToolViewModel, EmptyToolbar>(this);
+        Toolbar = ToolbarFactory.Create<BrightnessToolViewModel, BrightnessToolbar>(this);
     }
 
     public override bool IsErasable => true;
@@ -43,8 +43,8 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
         get => BrightnessMode;
     }
 
-    [Settings.Size("TOOL_SIZE_LABEL", Name = "ToolSize")]
-    public int ToolSize => GetValue<int>();
+    [Settings.Inherited]
+    public double ToolSize => GetValue<double>();
     
     [Settings.Float("STRENGTH_LABEL", 5, 0, 50)]
     public float CorrectionFactor => GetValue<float>();

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs

@@ -35,7 +35,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
         public override LocalizedString Tooltip => new LocalizedString("PEN_TOOL_TOOLTIP", Shortcut);
 
         [Settings.Inherited]
-        public double ToolSize => GetValue<int>();
+        public double ToolSize => GetValue<double>();
 
         [Settings.Bool("PIXEL_PERFECT_SETTING", Notify = nameof(PixelPerfectChanged), ExposedByDefault = false)]
         public bool PixelPerfectEnabled => GetValue<bool>();