Browse Source

Merge pull request #946 from PixiEditor/fixes/19.05.2025

Fixed wrong string byte serialization
Krzysztof Krysiński 2 months ago
parent
commit
e3383a00d6
24 changed files with 180 additions and 47 deletions
  1. 6 1
      src/ChunkyImageLib/ChunkyImage.cs
  2. 1 1
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  3. 1 1
      src/Drawie
  4. 27 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  5. 4 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  6. 37 8
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs
  7. 9 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayerHelper.cs
  8. 3 1
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/RightClickMode.cs
  9. 5 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/ControlDefinition.cs
  10. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs
  11. 2 1
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  12. 2 1
      src/PixiEditor/Data/Localization/Languages/en.json
  13. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs
  14. 9 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs
  15. 9 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  16. 1 1
      src/PixiEditor/Models/Handlers/Tools/IEraserToolHandler.cs
  17. 4 6
      src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs
  18. 8 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  19. 3 0
      src/PixiEditor/ViewModels/SubViewModels/ColorsViewModel.cs
  20. 17 3
      src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs
  21. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/EraserToolViewModel.cs
  22. 4 4
      src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs
  23. 4 3
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs
  24. 19 1
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

+ 6 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -191,7 +191,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// Finds the precise bounds in <paramref name="suggestedResolution"/>. If there are no chunks rendered for that resolution, full res chunks are used instead.
     /// </summary>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full)
+    public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full, bool fallbackToChunkAligned = false)
     {
         lock (lockObject)
         {
@@ -225,6 +225,11 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 }
                 else
                 {
+                    if (fallbackToChunkAligned)
+                    {
+                        return FindChunkAlignedCommittedBounds();
+                    }
+
                     RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
                         .Intersect(new RectI(VecI.Zero, CommittedSize)).Translate(-chunkPos * FullChunkSize);
 

+ 1 - 1
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -14,7 +14,7 @@ public interface IReadOnlyChunkyImage
     bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos, Paint? paint = null);
     RectI? FindChunkAlignedMostUpToDateBounds();
     RectI? FindChunkAlignedCommittedBounds();
-    RectI? FindTightCommittedBounds(ChunkResolution precision = ChunkResolution.Full);
+    RectI? FindTightCommittedBounds(ChunkResolution precision = ChunkResolution.Full, bool fallbackToChunkAligned = false);
     Color GetCommittedPixel(VecI posOnImage);
     Color GetMostUpToDatePixel(VecI posOnImage);
     bool LatestOrCommittedChunkExists(VecI chunkPos);

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 06d7adbd41c53f0319c07b07d99faab57b376560
+Subproject commit 94048adac23b6e3aeabff09c274624580f010c7f

+ 27 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -18,6 +18,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     public const string ImageFramesKey = "Frames";
     public const string ImageLayerKey = "LayerImage";
 
+    public const int AccuratePreviewMaxSize = 2048;
+
     public override VecD GetScenePosition(KeyFrameTime time) => layerImage.CommittedSize / 2f;
     public override VecD GetSceneSize(KeyFrameTime time) => layerImage.CommittedSize;
 
@@ -49,7 +51,29 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     public override RectD? GetApproxBounds(KeyFrameTime frameTime)
     {
-        var chunkAlignedBounds = GetLayerImageAtFrame(frameTime.Frame).FindChunkAlignedCommittedBounds();
+        var layerImage = GetLayerImageAtFrame(frameTime.Frame);
+        return GetApproxBounds(layerImage);
+    }
+
+    private static RectD? GetApproxBounds(ChunkyImage layerImage)
+    {
+        if (layerImage.CommittedSize.LongestAxis <= AccuratePreviewMaxSize)
+        {
+            ChunkResolution resolution = layerImage.CommittedSize.LongestAxis switch
+            {
+                <= 256 => ChunkResolution.Full,
+                <= 512 => ChunkResolution.Half,
+                <= 1024 => ChunkResolution.Quarter,
+                _ => ChunkResolution.Eighth
+            };
+
+            // Half is efficient enough to be used even for full res chunks
+            bool fallbackToChunkAligned = (int)resolution > 2;
+
+            return (RectD?)layerImage.FindTightCommittedBounds(resolution, fallbackToChunkAligned);
+        }
+
+        var chunkAlignedBounds = layerImage.FindChunkAlignedCommittedBounds();
         if (chunkAlignedBounds == null)
         {
             return null;
@@ -145,7 +169,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                     return null;
                 }
 
-                RectI? bounds = kf.FindChunkAlignedCommittedBounds(); // Don't use tight bounds, very expensive
+                RectI? bounds = (RectI?)GetApproxBounds(kf);
                 if (bounds.HasValue)
                 {
                     return new RectD(bounds.Value.X, bounds.Value.Y,
@@ -163,7 +187,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                 return null;
             }
 
-            var bounds = kf.FindChunkAlignedCommittedBounds(); // Don't use tight bounds, very expensive
+            var bounds = GetApproxBounds(kf);
             if (bounds.HasValue)
             {
                 return new RectD(bounds.Value.X, bounds.Value.Y,

+ 4 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -59,6 +59,10 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         else if (erasing)
         {
             srcPaint.BlendMode = BlendMode.DstOut;
+            if (this.color.A == 0)
+            {
+                this.color = color.WithAlpha(255);
+            }
         }
     }
 

+ 37 - 8
src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs

@@ -14,15 +14,18 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
     private List<Guid> layerGuids;
     private VecD delta;
     private Dictionary<Guid, ShapeVectorData> originalShapes;
+    private RectD? clipRect;
+    private VectorPath? clipPath;
 
     private int frame;
 
     [GenerateUpdateableChangeActions]
-    public PreviewShiftLayers_UpdateableChange(List<Guid> layerGuids, VecD delta, int frame)
+    public PreviewShiftLayers_UpdateableChange(List<Guid> layerGuids, RectD clipRect, VecD delta, int frame)
     {
         this.delta = delta;
         this.layerGuids = layerGuids;
         this.frame = frame;
+        this.clipRect = clipRect.IsZeroOrNegativeArea ? null : clipRect;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -50,6 +53,14 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                 originalShapes[layerGuid] = transformableObject.EmbeddedShapeData;
                 transformableObject.EmbeddedShapeData = null;
             }
+            else if (layer is ImageLayerNode imgLayer)
+            {
+                var image = imgLayer.GetLayerImageAtFrame(frame);
+                if (image is null)
+                {
+                    return false;
+                }
+            }
         }
 
         return true;
@@ -68,16 +79,25 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
         {
             var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
 
-            if (layer is ImageLayerNode)
+            if (layer is ImageLayerNode imgNode)
             {
-                var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, true, (VecI)delta, frame);
+                if (clipRect.HasValue)
+                {
+                    clipPath?.Dispose();
+                    clipPath = new VectorPath();
+                    clipPath.AddRect(clipRect.Value);
+                    clipPath.Offset((VecI)delta);
+                }
+
+                var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, true, (VecI)delta, frame, clipPath);
+
                 changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
             }
             else if (layer is VectorLayerNode vectorLayer)
             {
                 StrokeJoin join = StrokeJoin.Miter;
                 StrokeCap cap = StrokeCap.Butt;
-                
+
                 (vectorLayer.EmbeddedShapeData as PathVectorData)?.Path.Dispose();
 
                 var originalShape = originalShapes[layerGuid];
@@ -90,8 +110,9 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                     cap = shape.StrokeLineCap;
                 }
 
-                VecD mappedDelta = originalShape.TransformationMatrix.Invert().MapVector((float)delta.X, (float)delta.Y);
-                
+                VecD mappedDelta = originalShape.TransformationMatrix.Invert()
+                    .MapVector((float)delta.X, (float)delta.Y);
+
                 var finalMatrix = Matrix3X3.CreateTranslation((float)mappedDelta.X, (float)mappedDelta.Y);
 
                 path.AddPath(path, finalMatrix, AddPathMode.Append);
@@ -106,9 +127,10 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                     StrokeLineJoin = join,
                     StrokeLineCap = cap
                 };
-                
+
                 vectorLayer.EmbeddedShapeData = newShapeData;
-                changes.Add(new VectorShape_ChangeInfo(layerGuid, ShiftLayer_UpdateableChange.AffectedAreaFromBounds(target, layerGuid, frame)));
+                changes.Add(new VectorShape_ChangeInfo(layerGuid,
+                    ShiftLayer_UpdateableChange.AffectedAreaFromBounds(target, layerGuid, frame)));
             }
         }
 
@@ -139,6 +161,7 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                 var image = imgLayer.GetLayerImageAtFrame(frame);
                 var affected = image.FindAffectedArea();
                 image.CancelChanges();
+                image.SetClippingPath(null);
                 changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
             }
             else if (layer is VectorLayerNode transformableObject)
@@ -150,4 +173,10 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
 
         return changes;
     }
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        clipPath?.Dispose();
+    }
 }

+ 9 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayerHelper.cs

@@ -1,18 +1,26 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 internal static class ShiftLayerHelper
 {
-    public static AffectedArea DrawShiftedLayer(Document target, Guid layerGuid, bool keepOriginal, VecI delta, int frame)
+    public static AffectedArea DrawShiftedLayer(Document target, Guid layerGuid, bool keepOriginal, VecI delta,
+        int frame, VectorPath? clipPath = null)
     {
         var targetImage = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
         var prevArea = targetImage.FindAffectedArea();
         targetImage.CancelChanges();
         if (!keepOriginal)
             targetImage.EnqueueClear();
+
+        if (clipPath != null)
+        {
+            targetImage.SetClippingPath(clipPath);
+        }
+
         targetImage.EnqueueDrawCommitedChunkyImage(delta, targetImage, false, false);
         var curArea = targetImage.FindAffectedArea();
 

+ 3 - 1
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/RightClickMode.cs

@@ -9,5 +9,7 @@ public enum RightClickMode
     [Description("SHOW_CONTEXT_MENU")]
     ContextMenu,
     [Description("ERASE")]
-    Erase
+    Erase,
+    [Description("COLOR_PICKER")]
+    ColorPicker,
 }

+ 5 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/ControlDefinition.cs

@@ -122,7 +122,11 @@ public class ControlDefinition
             result.Add(ByteMap.GetTypeByteId(property.type));
             if (property.type == typeof(string))
             {
-                result.AddRange(BitConverter.GetBytes(property.value is string s ? s.Length : 0));
+                if (property.value is string str)
+                {
+                    int stringLengthBytes = Encoding.UTF8.GetByteCount(str);
+                    result.AddRange(BitConverter.GetBytes(stringLengthBytes));
+                }
             }
 
             result.AddRange(property.value switch

+ 3 - 3
src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs

@@ -61,10 +61,10 @@ public class LayoutBuilder
             Type type = ByteMap.GetTypeFromByteId((byte)propertyType);
             if (type == typeof(string))
             {
-                int stringLength = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
+                int stringBytesLength = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
                 offset += int32Size;
-                string value = Encoding.UTF8.GetString(layoutSpan[offset..(offset + stringLength)]);
-                offset += stringLength;
+                string value = Encoding.UTF8.GetString(layoutSpan[offset..(offset + stringBytesLength)]);
+                offset += stringBytesLength;
                 properties.Add(value);
             }
             else if (type == typeof(byte[]))

+ 2 - 1
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -60,7 +60,8 @@
           "ExposeHardness": true,
           "ExposeSpacing": true,
           "BrushShapeSetting": "CircleSmooth",
-          "PaintShape": "Circle"
+          "PaintShape": "Circle",
+          "PixelPerfectEnabled": false
         }
       },
       "Select",

+ 2 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1046,5 +1046,6 @@
   "IS_DEFAULT_EXPORT": "Is Default Export",
   "EXPORT_OUTPUT": "Export Output",
   "RENDER_OUTPUT_SIZE": "Render Output Size",
-  "RENDER_OUTPUT_CENTER": "Render Output Center"
+  "RENDER_OUTPUT_CENTER": "Render Output Center",
+  "COLOR_PICKER": "Color Picker"
 }

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs

@@ -50,7 +50,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
         spacing = toolbar.Spacing;
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
-        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, (float)toolSize, true,
+        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, (float)eraserTool.ToolSize, true,
             antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
 

+ 9 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -22,6 +22,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     private bool antiAliasing;
     private float hardness;
     private float spacing = 1;
+    private bool transparentErase;
 
     private IPenToolbar penToolbar;
 
@@ -47,10 +48,15 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         hardness = toolbar.Hardness;
         spacing = toolbar.Spacing;
 
-        colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
+        if (color.A > 0)
+        {
+            colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
+        }
+
+        transparentErase = color.A == 0;
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize, false, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize, transparentErase, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);
@@ -62,7 +68,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     {
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, false, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, transparentErase, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);

+ 9 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -294,7 +294,9 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
                 to.X - from.X,
                 to.Y - from.Y);
 
-            internals.ActionAccumulator.AddActions(new PreviewShiftLayers_Action(selectedMembers, delta,
+            RectD clipRect = lastCorners.AABBBounds;
+            internals.ActionAccumulator.AddActions(new PreviewShiftLayers_Action(
+                selectedMembers, clipRect, delta,
                 document!.AnimationHandler.ActiveFrameBindable));
         }
     }
@@ -395,6 +397,12 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
         actions.Add(new ShiftLayer_Action(newLayerGuids, delta, document!.AnimationHandler.ActiveFrameBindable));
 
+        if (original is { IsEmpty: false })
+        {
+            original.Offset((VecI)delta);
+            actions.Add(new SetSelection_Action(original));
+        }
+
         internals!.ActionAccumulator.AddFinishedActions(actions.ToArray());
 
         actions.Clear();

+ 1 - 1
src/PixiEditor/Models/Handlers/Tools/IEraserToolHandler.cs

@@ -2,5 +2,5 @@
 
 internal interface IEraserToolHandler : IToolHandler
 {
-
+    public double ToolSize { get; }
 }

+ 4 - 6
src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs

@@ -1,4 +1,5 @@
-using Drawie.Backend.Core.ColorsImpl;
+using System.Text;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
 
@@ -69,11 +70,8 @@ public class ByteBuilder
 
     public void AddString(string str)
     {
-        AddInt(str.Length);
-        foreach (var c in str)
-        {
-            AddInt(c);
-        }
+        AddInt(Encoding.UTF8.GetByteCount(str));
+        _data.AddRange(Encoding.UTF8.GetBytes(str));
     }
 
     public void AddFloat(float value)

+ 8 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -183,7 +183,14 @@ internal partial class DocumentViewModel
             transform = transform.PostConcat(Matrix3X3.CreateScale((float)resizeFactor.X, (float)resizeFactor.Y));
             primitive.Transform.Unit = new SvgTransformUnit?(new SvgTransformUnit(transform));
 
-            primitive.Fill.Unit = new SvgPaintServerUnit(data.FillPaintable);
+            Paintable finalFill = data.Fill ? data.FillPaintable : new ColorPaintable(Colors.Transparent);
+            primitive.Fill.Unit = new SvgPaintServerUnit(finalFill);
+
+            if (finalFill is ColorPaintable colorPaintable)
+            {
+                primitive.FillOpacity.Unit = new SvgNumericUnit(colorPaintable.Color.A / 255f, "");
+            }
+
             primitive.Stroke.Unit = new SvgPaintServerUnit(data.Stroke);
 
             primitive.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(data.StrokeWidth);

+ 3 - 0
src/PixiEditor/ViewModels/SubViewModels/ColorsViewModel.cs

@@ -404,6 +404,9 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
     [Commands_Command.Internal("PixiEditor.Colors.SelectColor")]
     public void SelectColor(PaletteColor color)
     {
+        if (color == null)
+            return;
+
         PrimaryColor = color.ToColor();
     }
 

+ 17 - 3
src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs

@@ -256,6 +256,9 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
 
                 Owner.ColorsSubViewModel.SwapColors(true);
                 return true;
+            case RightClickMode.ColorPicker when tools.ActiveTool is not ColorPickerToolViewModel:
+                HandleRightMouseColorPickerDown(tools);
+                return true;
             case RightClickMode.Erase when tools.ActiveTool is ColorPickerToolViewModel:
                 Owner.ColorsSubViewModel.SwapColors(true);
                 return true;
@@ -305,7 +308,7 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
             var toolSize = eraserTool.Toolbar.Settings.First(x => x.Name == "ToolSize");
             previousEraseSize = (double)toolSize.Value;
             toolSize.Value = tools.ActiveTool is PenToolViewModel { PixelPerfectEnabled: true }
-                ? 1
+                ? 1d
                 : currentToolSize.Value;
         }
         else
@@ -316,6 +319,17 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         tools.SetActiveTool<EraserToolViewModel>(true);
     }
 
+    private void HandleRightMouseColorPickerDown(IToolsHandler tools)
+    {
+        ColorPickerToolViewModel? colorPickerTool = tools.GetTool<ColorPickerToolViewModel>();
+        if (colorPickerTool == null)
+        {
+            return;
+        }
+
+        tools.SetActiveTool<ColorPickerToolViewModel>(true);
+    }
+
     private void OnMiddleMouseButton()
     {
         Owner.ToolsSubViewModel.SetActiveTool<MoveViewportToolViewModel>(true);
@@ -344,7 +358,7 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         var tools = Owner.ToolsSubViewModel;
 
         var rightCanUp = (button == MouseButton.Right) &&
-                         tools.RightClickMode is RightClickMode.Erase or RightClickMode.SecondaryColor;
+                         tools.RightClickMode is RightClickMode.Erase or RightClickMode.SecondaryColor or RightClickMode.ColorPicker;
 
         if (button == MouseButton.Left || rightCanUp)
         {
@@ -383,7 +397,7 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
                 }
 
                 break;
-            case MouseButton.Right when tools.RightClickMode == RightClickMode.Erase:
+            case MouseButton.Right when tools.RightClickMode is RightClickMode.Erase or RightClickMode.ColorPicker:
                 HandleRightMouseEraseUp(tools);
                 break;
         }

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

@@ -22,7 +22,7 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
         Toolbar = ToolbarFactory.Create<EraserToolViewModel, PenToolbar>(this);
     }
 
-    [Settings.Inherited] public double ToolSize => GetValue<int>();
+    [Settings.Inherited] public double ToolSize => GetValue<double>();
 
     public override bool IsErasable => true;
 

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

@@ -18,7 +18,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
     [Command.Tool(Key = Key.B)]
     internal class PenToolViewModel : ShapeTool, IPenToolHandler
     {
-        private double actualToolSize;
+        private double actualToolSize = 1;
 
         public override string ToolNameLocalizationKey => "PEN_TOOL";
 
@@ -31,7 +31,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
         {
             Cursor = Cursors.PreciseCursor;
             Toolbar = ToolbarFactory.Create<PenToolViewModel, PenToolbar>(this);
-            
+
             ViewModelMain.Current.ToolsSubViewModel.SelectedToolChanged += SelectedToolChanged;
         }
 
@@ -82,7 +82,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
                 var setting = toolbar.Settings.FirstOrDefault(x => x.Name == nameof(toolbar.ToolSize));
                 if (setting is SizeSettingViewModel sizeSetting)
                 {
-                    sizeSetting.Value = 1;
+                    sizeSetting.Value = 1d;
                 }
             }
             
@@ -139,7 +139,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
                 if (PixelPerfectEnabled)
                 {
                     actualToolSize = ToolSize;
-                    sizeSettingViewModel.Value = 1;
+                    sizeSettingViewModel.Value = 1d;
                 }
                 else
                 {

+ 4 - 3
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -663,10 +663,11 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         }
 
         var useContextMenu = vm.Owner.Owner.ToolsSubViewModel.RightClickMode == RightClickMode.ContextMenu;
-        var usesErase = tools.RightClickMode == RightClickMode.Erase && tools.ActiveTool.IsErasable;
-        var usesSecondaryColor = tools.RightClickMode == RightClickMode.SecondaryColor && tools.ActiveTool.UsesColor;
+        var usesErase = tools is { RightClickMode: RightClickMode.Erase, ActiveTool.IsErasable: true };
+        bool usesColorPicker = vm.Owner.Owner.ToolsSubViewModel.RightClickMode == RightClickMode.ColorPicker;
+        var usesSecondaryColor = tools is { RightClickMode: RightClickMode.SecondaryColor, ActiveTool.UsesColor: true };
 
-        if (!useContextMenu && (usesErase || usesSecondaryColor))
+        if (!useContextMenu && (usesErase || usesSecondaryColor || usesColorPicker))
         {
             return;
         }

+ 19 - 1
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -140,7 +140,7 @@ internal class ViewportOverlays
 
     private void BindSelectionOverlay()
     {
-        Binding showFillBinding = new()
+        Binding toolIsSelectionBinding = new()
         {
             Source = ViewModelMain.Current,
             Path = "ToolsSubViewModel.ActiveTool",
@@ -148,6 +148,24 @@ internal class ViewportOverlays
             Mode = BindingMode.OneWay
         };
 
+        Binding isTransformingBinding = new()
+        {
+            Source = Viewport,
+            Path = "!Document.TransformViewModel.TransformActive",
+            Mode = BindingMode.OneWay
+        };
+
+        MultiBinding showFillBinding = new()
+        {
+            Converter = new AllTrueConverter(),
+            Mode = BindingMode.OneWay,
+            Bindings = new List<IBinding>()
+            {
+                toolIsSelectionBinding,
+                isTransformingBinding
+            }
+        };
+
         Binding pathBinding = new()
         {
             Source = Viewport, Path = "Document.SelectionPathBindable", Mode = BindingMode.OneWay