2
0
Эх сурвалжийг харах

Merge branch 'master' into brush-engine

Krzysztof Krysiński 1 долоо хоног өмнө
parent
commit
50b4ee251d
20 өөрчлөгдсөн 397 нэмэгдсэн , 32 устгасан
  1. 1 1
      src/Drawie
  2. 9 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  3. 166 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Image/PatternNode.cs
  4. 22 6
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  5. 16 6
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  6. 11 0
      src/PixiEditor.ChangeableDocument/Enums/FloodFillMode.cs
  7. 1 1
      src/PixiEditor.Extensions.MSBuild/PixiEditor.Extensions.MSBuild.csproj
  8. 8 6
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  9. 10 0
      src/PixiEditor/Data/Localization/Languages/en.json
  10. 24 0
      src/PixiEditor/Helpers/Converters/InlineConverter.cs
  11. 6 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/FloodFillToolExecutor.cs
  12. 7 0
      src/PixiEditor/Models/EnumTranslations.cs
  13. 4 1
      src/PixiEditor/Models/Handlers/Tools/IFloodFillToolHandler.cs
  14. 1 1
      src/PixiEditor/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs
  15. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  16. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/Image/PatternNodeViewModel.cs
  17. 3 0
      src/PixiEditor/ViewModels/Tools/Tools/FloodFillToolViewModel.cs
  18. 11 1
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  19. 88 0
      tests/PixiEditor.Tests/FloodFillTests.cs
  20. 0 2
      tests/PixiEditor.Tests/PixiEditorTest.cs

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit bc83f2b962f7e2ff4140bda8dd421aa2309f2488
+Subproject commit ecaab29256ad9ebbcf8ba3a0c19d763f64e9d0e1

+ 9 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -58,8 +58,14 @@ public class CreateImageNode : Node
         RenderOutput.ChainToPainterValue();
     }
 
-    private Texture Render(RenderContext context)
+    private Texture? Render(RenderContext context)
     {
+        var size = (VecI)(Size.Value * context.ChunkResolution.Multiplier());
+        if (size.X <= 0 || size.Y <= 0)
+        {
+            return null;
+        }
+
         int id = (Size.Value * context.ChunkResolution.Multiplier()).GetHashCode();
         var surface = textureCache.RequestTexture(id, (VecI)(Size.Value * context.ChunkResolution.Multiplier()), context.ProcessingColorSpace, false);
         surface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
@@ -81,6 +87,7 @@ public class CreateImageNode : Node
         RenderContext ctx = context.Clone();
         ctx.RenderSurface = surface.DrawingSurface.Canvas;
         ctx.RenderOutputSize = surface.Size;
+        ctx.VisibleDocumentRegion = null;
 
         float chunkMultiplier = (float)context.ChunkResolution.Multiplier();
 
@@ -96,7 +103,7 @@ public class CreateImageNode : Node
 
     private void OnPaint(RenderContext context, Canvas surface)
     {
-        if(Output.Value == null || Output.Value.IsDisposed) return;
+        if (Output.Value == null || Output.Value.IsDisposed) return;
 
         int saved = surface.Save();
         surface.Scale((float)context.ChunkResolution.InvertedMultiplier());

+ 166 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Image/PatternNode.cs

@@ -0,0 +1,166 @@
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Mesh;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Image;
+
+[NodeInfo("Pattern")]
+public class PatternNode : RenderNode
+{
+    public InputProperty<Texture?> Fill { get; }
+    public InputProperty<Matrix3X3> FillMatrix { get; }
+    public InputProperty<double> Spacing { get; }
+    public InputProperty<ShapeVectorData?> Path { get; }
+    public InputProperty<PatternAlignment> Alignment { get; }
+    public InputProperty<PatternStretching> Stretching { get; }
+
+    public PatternNode()
+    {
+        Fill = CreateInput<Texture>("Fill", "FILL", null);
+        FillMatrix = CreateInput<Matrix3X3>("FillMatrix", "MATRIX", Matrix3X3.Identity);
+        Spacing = CreateInput<double>("Spacing", "SPACING_LABEL", 0)
+            .WithRules(x => x.Min(0d));
+        Path = CreateInput<ShapeVectorData>("Path", "SHAPE", null);
+        Alignment = CreateInput<PatternAlignment>("Alignment", "ALIGNMENT", PatternAlignment.Center);
+        Stretching = CreateInput<PatternStretching>("Stretching", "STRETCHING", PatternStretching.StretchToFit);
+    }
+
+    protected override void OnPaint(RenderContext context, Canvas surface)
+    {
+        float spacing = (float)Spacing.Value;
+
+        if (Fill.Value == null || Path.Value == null)
+            return;
+
+        if (spacing == 0)
+        {
+            spacing = Fill.Value.Size.X;
+        }
+
+        float distance = 0;
+
+        using var path = Path.Value.ToPath(true);
+        if (path == null)
+            return;
+
+        using Paint tilePaint = new Paint();
+        using var snapshot = Fill.Value.DrawingSurface.Snapshot();
+        using var shader = snapshot.ToShader(TileMode.Clamp, TileMode.Clamp, FillMatrix.Value);
+        tilePaint.Shader = shader;
+
+        while (distance < path.Length)
+        {
+            if (Stretching.Value == PatternStretching.PlaceAlong)
+            {
+                PlaceAlongPath(surface, snapshot, path, distance);
+            }
+            else if (Stretching.Value == PatternStretching.StretchToFit)
+            {
+                PlaceStretchToFit(surface, path, distance, spacing, tilePaint);
+            }
+
+
+            distance += spacing;
+        }
+    }
+
+    private void PlaceAlongPath(Canvas surface, Drawie.Backend.Core.Surfaces.ImageData.Image image,
+        VectorPath path, float distance)
+    {
+        var matrix = path.GetMatrixAtDistance(distance, false, PathMeasureMatrixMode.GetPositionAndTangent);
+        if (matrix == null)
+            return;
+
+        if (Alignment.Value == PatternAlignment.Center)
+        {
+            matrix = matrix.Concat(Matrix3X3.CreateTranslation(-Fill.Value.Size.X / 2f, -Fill.Value.Size.Y / 2f));
+        }
+        else if (Alignment.Value == PatternAlignment.Outside)
+        {
+            matrix = matrix.Concat(Matrix3X3.CreateTranslation(0, -Fill.Value.Size.Y));
+        }
+
+        surface.Save();
+        surface.SetMatrix(surface.TotalMatrix.Concat(matrix));
+        surface.DrawImage(image, 0, 0);
+        surface.Restore();
+    }
+
+    private void PlaceStretchToFit(Canvas surface, VectorPath path, float distance, float spacing,
+        Paint tilePaint)
+    {
+        int texWidth = Fill.Value.Size.X;
+        int texHeight = Fill.Value.Size.Y;
+
+        // Iterate over each column of the texture (1px wide quads)
+        for (int x = 0; x < texWidth; x++)
+        {
+            float u0 = (float)x / texWidth;
+            float u1 = (float)(x + 1) / texWidth;
+
+            float d0 = distance + u0 * spacing;
+            float d1 = distance + u1 * spacing;
+
+            var startSegment = path.GetPositionAndTangentAtDistance(d0, false);
+            var endSegment = path.GetPositionAndTangentAtDistance(d1, false);
+
+            var startNormal = new VecD(-startSegment.W, startSegment.Z).Normalize();
+            var endNormal = new VecD(-endSegment.W, endSegment.Z).Normalize();
+
+            float halfHeight = texHeight / 2f;
+
+            VecD start = new VecD(startSegment.X, startSegment.Y);
+            VecD end = new VecD(endSegment.X, endSegment.Y);
+
+            if (Alignment.Value == PatternAlignment.Inside)
+            {
+                start += startNormal * halfHeight;
+                end += endNormal * halfHeight;
+            }
+            else if (Alignment.Value == PatternAlignment.Outside)
+            {
+                start -= startNormal * halfHeight;
+                end -= endNormal * halfHeight;
+            }
+
+            var v0 = start - startNormal * halfHeight;
+            var v1 = start + startNormal * halfHeight;
+            var v2 = end + endNormal * halfHeight;
+            var v3 = end - endNormal * halfHeight;
+
+            var texCoords = new VecF[] { new(x, texHeight), new(x, 0), new(x + 1, 0), new(x + 1, texHeight) };
+
+            var verts = new VecF[] { (VecF)v0, (VecF)v1, (VecF)v2, (VecF)v3 };
+            var indices = new ushort[] { 0, 1, 2, 0, 2, 3 };
+            Color[] colors = { Colors.Transparent, Colors.Transparent, Colors.Transparent, Colors.Transparent };
+
+            using var vertices = new Vertices(VertexMode.Triangles, verts, texCoords, colors, indices);
+            surface.DrawVertices(vertices, BlendMode.SrcOver, tilePaint);
+        }
+    }
+
+    public override Node CreateCopy()
+    {
+        return new PatternNode();
+    }
+}
+
+public enum PatternAlignment
+{
+    Center,
+    Outside,
+    Inside,
+}
+
+public enum PatternStretching
+{
+    PlaceAlong,
+    StretchToFit,
+}

+ 22 - 6
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -10,6 +10,8 @@ using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Enums;
+using BlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 
@@ -47,7 +49,7 @@ public static class FloodFillHelper
         VecI startingPos,
         Color drawingColor,
         float tolerance,
-        int frame, bool lockTransparency)
+        int frame, bool lockTransparency, FloodFillMode fillMode)
     {
         if (selection is not null && !selection.Contains(startingPos.X + 0.5f, startingPos.Y + 0.5f))
             return new();
@@ -78,13 +80,13 @@ public static class FloodFillHelper
             srgbSurface.DrawingSurface.Canvas.DrawPixel(0, 0, srgbPaint);
             using var processingSurface = Surface.ForProcessing(VecI.One, document.ProcessingColorSpace);
             processingSurface.DrawingSurface.Canvas.DrawSurface(srgbSurface.DrawingSurface, 0, 0);
-            var fixedColor = processingSurface.GetRawPixelPrecise(VecI.Zero);
+            var fixedColor = processingSurface.GetRawPixelPrecise(VecI.Zero).Premultiplied();
 
             uLongColor = fixedColor.ToULong();
             colorSpaceCorrectedColor = fixedColor;
         }
 
-        if ((colorSpaceCorrectedColor.A == 0) || colorToReplace == colorSpaceCorrectedColor)
+        if ((colorSpaceCorrectedColor.A == 0 && fillMode == FloodFillMode.Overlay) || (colorToReplace == colorSpaceCorrectedColor && fillMode == FloodFillMode.Replace))
             return new();
 
         if (colorToReplace.A == 0 && lockTransparency)
@@ -113,7 +115,21 @@ public static class FloodFillHelper
             if (!drawingChunks.ContainsKey(chunkPos))
             {
                 var chunk = Chunk.Create(document.ProcessingColorSpace);
-                chunk.Surface.DrawingSurface.Canvas.Clear(Colors.Transparent);
+
+                if (fillMode == FloodFillMode.Replace)
+                {
+                    // For replace mode, copy original image data to avoid erasing unfilled pixels
+                    var originalChunk = cache.GetChunk(chunkPos);
+                    originalChunk.Switch(
+                        (Chunk origChunk) => chunk.Surface.DrawingSurface.Canvas.DrawSurface(origChunk.Surface.DrawingSurface, 0, 0),
+                        (EmptyChunk _) => chunk.Surface.DrawingSurface.Canvas.Clear(Colors.Transparent)
+                    );
+                }
+                else
+                {
+                    // For overlay mode, start with transparent
+                    chunk.Surface.DrawingSurface.Canvas.Clear(Colors.Transparent);
+                }
 
                 drawingChunks[chunkPos] = chunk;
             }
@@ -210,7 +226,7 @@ public static class FloodFillHelper
     {
         var rawPixelRef = referenceChunk.Surface.GetRawPixelPrecise(pos);
         // color should be a fixed color
-        if ((Color)rawPixelRef == (Color)color || (Color)drawingChunk.Surface.GetRawPixelPrecise(pos) == (Color)color)
+        if ((Color)rawPixelRef == (Color)color || (Color)drawingChunk.Surface.GetRawPixelPrecise(pos).Premultiplied() == (Color)color)
             return null;
         if (checkFirstPixel && !bounds.IsWithinBounds(rawPixelRef))
             return null;
@@ -224,7 +240,7 @@ public static class FloodFillHelper
         using var refPixmap = referenceChunk.Surface.PeekPixels();
         Half* refArray = (Half*)refPixmap.GetPixels();
 
-        Surface cpuSurface = Surface.ForProcessing(new VecI(chunkSize), referenceChunk.Surface.ColorSpace);
+        using Surface cpuSurface = Surface.ForProcessing(new VecI(chunkSize), referenceChunk.Surface.ColorSpace);
         cpuSurface.DrawingSurface.Canvas.DrawSurface(drawingChunk.Surface.DrawingSurface, 0, 0);
         using var drawPixmap = cpuSurface.PeekPixels();
         Half* drawArray = (Half*)drawPixmap.GetPixels();

+ 16 - 6
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs

@@ -1,8 +1,11 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Enums;
+using BlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 
@@ -16,9 +19,10 @@ internal class FloodFill_Change : Change
     private CommittedChunkStorage? chunkStorage = null;
     private int frame;
     private float tolerance;
+    private FloodFillMode fillMode;
 
     [GenerateMakeChangeAction]
-    public FloodFill_Change(Guid memberGuid, VecI pos, Color color, bool referenceAll, float tolerance, bool drawOnMask, int frame)
+    public FloodFill_Change(Guid memberGuid, VecI pos, Color color, bool referenceAll, float tolerance, FloodFillMode fillMode, bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.pos = pos;
@@ -27,6 +31,7 @@ internal class FloodFill_Change : Change
         this.drawOnMask = drawOnMask;
         this.frame = frame;
         this.tolerance = tolerance;
+        this.fillMode = fillMode;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -48,17 +53,22 @@ internal class FloodFill_Change : Change
         else
             membersToReference.Add(memberGuid);
         bool lockTransparency = target.FindMember(memberGuid) is ImageLayerNode { LockTransparency: true };
-        var floodFilledChunks = FloodFillHelper.FloodFill(membersToReference, target, selection, pos, color, tolerance, frame, lockTransparency);
+        var floodFilledChunks = FloodFillHelper.FloodFill(membersToReference, target, selection, pos, color, tolerance, frame, lockTransparency, fillMode);
         if (floodFilledChunks.Count == 0)
         {
             ignoreInUndo = true;
             return new None();
         }
-
-        foreach (var (chunkPos, chunk) in floodFilledChunks)
+        
+        Paint paint = fillMode switch
         {
-            image.EnqueueDrawTexture(chunkPos * ChunkyImage.FullChunkSize, chunk.Surface, null, false);
-        }
+            FloodFillMode.Overlay => null,  // Default blend mode
+            FloodFillMode.Replace => new Paint() { BlendMode = BlendMode.Src }  // Replace mode
+        };
+        
+        foreach (var (chunkPos, chunk) in floodFilledChunks)
+            image.EnqueueDrawTexture(chunkPos * ChunkyImage.FullChunkSize, chunk.Surface, paint, false);
+        
         var affArea = image.FindAffectedArea();
         chunkStorage = new CommittedChunkStorage(image, affArea.Chunks);
         image.CommitChanges();

+ 11 - 0
src/PixiEditor.ChangeableDocument/Enums/FloodFillMode.cs

@@ -0,0 +1,11 @@
+using System.ComponentModel;
+
+namespace PixiEditor.ChangeableDocument.Enums;
+
+public enum FloodFillMode
+{
+    [Description("OVERLAY")]
+    Overlay,
+    [Description("REPLACE")]
+    Replace,
+}

+ 1 - 1
src/PixiEditor.Extensions.MSBuild/PixiEditor.Extensions.MSBuild.csproj

@@ -6,7 +6,7 @@
     </PropertyGroup>
 
     <ItemGroup>
-      <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.8.3" />
+      <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.8.43" />
     </ItemGroup>
 
 </Project>

+ 8 - 6
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -41,16 +41,17 @@
         },
         "Select",
         {
-            "ToolName": "MagicWand",
-            "Settings": {
-              "Spacing": 0
-            }
+          "ToolName": "MagicWand",
+          "Settings": {
+            "Spacing": 0
+          }
         },
         "Lasso",
         {
           "ToolName": "FloodFill",
           "Settings": {
-            "Tolerance": 0
+            "Tolerance": 0,
+            "FillMode": "Overlay"
           }
         },
         "RasterLine",
@@ -111,7 +112,8 @@
         {
           "ToolName": "FloodFill",
           "Settings": {
-            "ExposeTolerance": true
+            "ExposeTolerance": true,
+            "ExposeFillMode": true
           }
         },
         {

+ 10 - 0
src/PixiEditor/Data/Localization/Languages/en.json

@@ -321,6 +321,8 @@
   "ELLIPSE_TOOL": "Ellipse",
   "ERASER_TOOL": "Eraser",
   "FLOOD_FILL_TOOL": "Flood Fill",
+  "FLOOD_FILL_MODE_LABEL" : "Fill mode",
+  "OVERLAY" : "Overlay",
   "LASSO_TOOL": "Lasso",
   "LINE_TOOL": "Line",
   "MAGIC_WAND_TOOL": "Magic Wand",
@@ -1165,6 +1167,14 @@
   "LEVELS": "Levels",
   "RGB_POSTERIZATION_MODE": "RGB",
   "LUMINANCE_POSTERIZATION_MODE": "Luminance",
+  "ALIGNMENT": "Alignment",
+  "STRETCHING": "Stretching",
+  "STRETCH_TO_FIT_PATTERN_STRETCHING": "Stretch to fit",
+  "PLACE_ALONG_PATTERN_STRETCHING": "Place along",
+  "CENTER_PATTERN_ALIGNMENT": "Center",
+  "OUTSIDE_PATTERN_ALIGNMENT": "Outside",
+  "INSIDE_PATTERN_ALIGNMENT": "Inside",
+  "PATTERN_NODE": "Pattern on Path",
   "DECIMAL_NUMBER": "Decimal Number",
   "WHOLE_NUMBER": "Whole Number",
   "BOOLEAN": "Boolean (true/false)",

+ 24 - 0
src/PixiEditor/Helpers/Converters/InlineConverter.cs

@@ -0,0 +1,24 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace PixiEditor.Helpers.Converters;
+
+public class InlineConverter : IValueConverter
+{
+    Func<object?, bool>? simpleConvert;
+
+    public InlineConverter(Func<object, bool> simpleConvert)
+    {
+        this.simpleConvert = simpleConvert;
+    }
+
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        return simpleConvert?.Invoke(value);
+    }
+
+    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        throw new NotImplementedException();
+    }
+}

+ 6 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/FloodFillToolExecutor.cs

@@ -7,6 +7,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Controllers.InputDevice;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
@@ -18,6 +19,7 @@ internal class FloodFillToolExecutor : UpdateableChangeExecutor
     private Guid memberGuid;
     private Color color;
     private float tolerance;
+    private FloodFillMode fillMode;
 
     public override ExecutionState Start()
     {
@@ -40,15 +42,16 @@ internal class FloodFillToolExecutor : UpdateableChangeExecutor
         color = colorsVM.PrimaryColor;
         var pos = controller!.LastPixelPosition;
         tolerance = fillTool.Tolerance;
-
-        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, tolerance, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
+        fillMode = fillTool.FillMode;
+        
+        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, tolerance, fillMode, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
 
         return ExecutionState.Success;
     }
 
     public override void OnPixelPositionChange(VecI pos, MouseOnCanvasEventArgs args)
     {
-        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, tolerance, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
+        internals!.ActionAccumulator.AddActions(new FloodFill_Action(memberGuid, pos, color, considerAllLayers, tolerance, fillMode, drawOnMask, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)

+ 7 - 0
src/PixiEditor/Models/EnumTranslations.cs

@@ -9,6 +9,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Effects;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Image;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Handlers.Toolbars;
@@ -127,6 +128,12 @@ using DrawingBlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
 [assembly: LocalizeEnum<BlendMode>(BlendMode.Erase, "ERASE_BLEND_MODE")]
 [assembly: LocalizeEnum<BlendMode>(BlendMode.LinearDodge, "LINEAR_DODGE_BLEND_MODE")]
 
+[assembly: LocalizeEnum<PatternAlignment>(PatternAlignment.Center, "CENTER_PATTERN_ALIGNMENT")]
+[assembly: LocalizeEnum<PatternAlignment>(PatternAlignment.Outside, "OUTSIDE_PATTERN_ALIGNMENT")]
+[assembly: LocalizeEnum<PatternAlignment>(PatternAlignment.Inside, "INSIDE_PATTERN_ALIGNMENT")]
+
+[assembly: LocalizeEnum<PatternStretching>(PatternStretching.StretchToFit, "STRETCH_TO_FIT_PATTERN_STRETCHING")]
+[assembly: LocalizeEnum<PatternStretching>(PatternStretching.PlaceAlong, "PLACE_ALONG_PATTERN_STRETCHING")]
 [assembly: LocalizeEnum<GradientType>(GradientType.Linear, "LINEAR_GRADIENT_TYPE")]
 [assembly: LocalizeEnum<GradientType>(GradientType.Radial, "RADIAL_GRADIENT_TYPE")]
 [assembly: LocalizeEnum<GradientType>(GradientType.Conical, "CONICAL_GRADIENT_TYPE")]

+ 4 - 1
src/PixiEditor/Models/Handlers/Tools/IFloodFillToolHandler.cs

@@ -1,7 +1,10 @@
-namespace PixiEditor.Models.Handlers.Tools;
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditor.Models.Handlers.Tools;
 
 internal interface IFloodFillToolHandler : IToolHandler
 {
     public bool ConsiderAllLayers { get; }
     public float Tolerance { get; }
+    FloodFillMode FillMode { get; }
 }

+ 1 - 1
src/PixiEditor/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs

@@ -36,7 +36,7 @@ public class ChunkyImageSerializationFactory : SerializationFactory<byte[], Chun
     {
         SurfaceSerializationFactory surfaceFactory = new();
         surfaceFactory.Config = Config;
-        if (IsFilePreVersion(serializerData, new Version(2, 0, 1, 19)) || serializerData == default)
+        if (IsFilePreVersion(serializerData, new Version(2, 1, 0, 0)) || serializerData == default)
         {
             if (serialized is byte[] imgBytes)
             {

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -43,5 +43,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.1.19")]
-[assembly: AssemblyFileVersion("2.0.1.19")]
+[assembly: AssemblyVersion("2.1.0.0")]
+[assembly: AssemblyFileVersion("2.1.0.0")]

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/Image/PatternNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Image;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Image;
+
+[NodeViewModel("PATTERN_NODE", "IMAGE", PixiPerfectIcons.Stamp)]
+internal class PatternNodeViewModel : NodeViewModel<PatternNode>;

+ 3 - 0
src/PixiEditor/ViewModels/Tools/Tools/FloodFillToolViewModel.cs

@@ -6,6 +6,7 @@ using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.UI.Common.Localization;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
@@ -33,6 +34,8 @@ internal class FloodFillToolViewModel : ToolViewModel, IFloodFillToolHandler
 
     [Settings.Percent("TOLERANCE_LABEL", ExposedByDefault = false)]
     public float Tolerance => GetValue<float>();
+    [Settings.Enum("FLOOD_FILL_MODE_LABEL", FloodFillMode.Overlay, ExposedByDefault = false)]
+    public FloodFillMode FillMode => GetValue<FloodFillMode>();
 
     public override string DefaultIcon => PixiPerfectIcons.Bucket;
 

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

@@ -6,6 +6,7 @@ using Avalonia.Input;
 using PixiEditor.Views.Visuals;
 using PixiEditor.Helpers.Converters;
 using PixiEditor.Models.Commands.XAML;
+using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels.Document.TransformOverlays;
 using PixiEditor.Views.Overlays;
@@ -461,6 +462,14 @@ internal class ViewportOverlays
             Source = ViewModelMain.Current.ToolsSubViewModel, Path = "ActiveBrushToolbar.LastBrushData", Mode = BindingMode.OneWay
         };
 
+        Binding isBrushToolActiveBinding = new()
+        {
+            Source = ViewModelMain.Current,
+            Path = "ToolsSubViewModel.ActiveTool",
+            Converter = new InlineConverter(obj => obj is IBrushToolHandler),
+            Mode = BindingMode.OneWay
+        };
+
         MultiBinding isVisibleMultiBinding = new()
         {
             Converter = new AllTrueConverter(),
@@ -468,7 +477,8 @@ internal class ViewportOverlays
             Bindings = new List<IBinding>()
             {
                 isTransformingBinding,
-                isOverCanvasBinding
+                isOverCanvasBinding,
+                isBrushToolActiveBinding
             }
         };
 

+ 88 - 0
tests/PixiEditor.Tests/FloodFillTests.cs

@@ -0,0 +1,88 @@
+using Avalonia.Headless.XUnit;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Tests;
+using PixiEditor.ViewModels.Document;
+
+namespace PixiEditor.Backend.Tests;
+
+public class FloodFillTests : FullPixiEditorTest
+{
+    [AvaloniaTheory]
+    [InlineData(0, 64)]
+    [InlineData(130, 64)]
+    [InlineData(255, 64)]
+    [InlineData(0, 512)]
+    [InlineData(130, 512)]
+    [InlineData(255, 512)]
+    [InlineData(0, 2048)]
+    [InlineData(130, 2048)]
+    [InlineData(255, 2048)]
+    public void TestThatFloodFillHelperFinishesLinearCs(byte alpha, int imgSize)
+    {
+        var doc = DocumentViewModel.Build(b => b.WithSize(imgSize, imgSize)
+            .WithGraph(g =>
+                g.WithImageLayerNode(
+                        "layer", new VecI(imgSize), ColorSpace.CreateSrgbLinear(), out var id)
+                    .WithOutputNode(id, "Output")));
+
+        var color = Color.FromHsv(0f, 59.3f, 82.6f, alpha);
+
+        var dict = FloodFillHelper.FloodFill([doc.NodeGraph.StructureTree.Members[0].Id],
+            doc.AccessInternalReadOnlyDocument(),
+            null, VecI.Zero, color, 0, 0, false, FloodFillMode.Replace);
+
+        Assert.NotNull(dict);
+        foreach (var kvp in dict)
+        {
+            Assert.NotNull(kvp.Value);
+            var srgbPixel = kvp.Value.Surface.GetSrgbPixel(VecI.Zero);
+            if (alpha == 0)
+                Assert.Equal(Color.FromRgba(0, 0, 0, 0), srgbPixel);
+            else
+                Assert.Equal(color, srgbPixel);
+        }
+    }
+
+    [AvaloniaTheory]
+    [InlineData(0, 64)]
+    [InlineData(130, 64)]
+    [InlineData(255, 64)]
+    [InlineData(0, 512)]
+    [InlineData(130, 512)]
+    [InlineData(255, 512)]
+    [InlineData(0, 2048)]
+    [InlineData(130, 2048)]
+    [InlineData(255, 2048)]
+    public void TestThatFloodFillHelperFinishesSrgbCs(byte alpha, int imgSize)
+    {
+        var doc = DocumentViewModel.Build(b => b.WithSize(imgSize, imgSize)
+            .WithGraph(g =>
+                g.WithImageLayerNode(
+                    "layer", new VecI(imgSize), ColorSpace.CreateSrgb(), out var id).WithOutputNode(id, "Output")));
+
+        FloodFillHelper.FloodFill([doc.NodeGraph.StructureTree.Members[0].Id], doc.AccessInternalReadOnlyDocument(),
+            null, VecI.Zero,
+            Color.FromHsv(0f, 59.3f, 82.6f, alpha), 0, 0, false, FloodFillMode.Replace);
+
+        var color = Color.FromHsv(0f, 59.3f, 82.6f, alpha);
+
+        var dict = FloodFillHelper.FloodFill([doc.NodeGraph.StructureTree.Members[0].Id],
+            doc.AccessInternalReadOnlyDocument(),
+            null, VecI.Zero, color, 0, 0, false, FloodFillMode.Replace);
+
+        Assert.NotNull(dict);
+        foreach (var kvp in dict)
+        {
+            Assert.NotNull(kvp.Value);
+            var srgbPixel = kvp.Value.Surface.GetSrgbPixel(VecI.Zero);
+            if (alpha == 0)
+                Assert.Equal(Color.FromRgba(0, 0, 0, 0), srgbPixel);
+            else
+                Assert.Equal(color, srgbPixel);
+        }
+    }
+}

+ 0 - 2
tests/PixiEditor.Tests/PixiEditorTest.cs

@@ -1,6 +1,5 @@
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
-using Drawie.Interop.Avalonia.Core;
 using Drawie.Numerics;
 using Drawie.RenderApi;
 using Drawie.RenderApi.OpenGL;
@@ -9,7 +8,6 @@ using Drawie.Silk;
 using Drawie.Skia;
 using Drawie.Windowing;
 using DrawiEngine;
-using DrawiEngine.Desktop;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.Helpers;