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

Added TargetFullSurface and fixed some bugs

Krzysztof Krysiński 2 долоо хоног өмнө
parent
commit
e293aa8b15

+ 4 - 2
src/ChunkyImageLib/ChunkyImage.cs

@@ -191,7 +191,8 @@ 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, bool fallbackToChunkAligned = false)
+    public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full,
+        bool fallbackToChunkAligned = false)
     {
         lock (lockObject)
         {
@@ -750,7 +751,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         lock (lockObject)
         {
             ThrowIfDisposed();
-            PathOperation operation = new(path, paintable, strokeWidth, strokeCap, blendMode, style, antiAliasing, customBounds);
+            PathOperation operation = new(path, paintable, strokeWidth, strokeCap, blendMode, style, antiAliasing,
+                customBounds);
             EnqueueOperation(operation);
         }
     }

+ 2 - 1
src/ChunkyImageLib/Operations/PathOperation.cs

@@ -28,7 +28,8 @@ internal class PathOperation : IMirroredDrawOperation
         bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
     }
 
-    public PathOperation(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap cap, BlendMode blendMode, PaintStyle style, bool antiAliasing, RectI? customBounds = null)
+    public PathOperation(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap cap, BlendMode blendMode,
+        PaintStyle style, bool antiAliasing, RectI? customBounds = null)
     {
         this.antiAliasing = antiAliasing;
         this.path = new VectorPath(path);

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit a012b67aa8bdd3047593073561fc97fdfaea931d
+Subproject commit c53c9e66b01ea6ecc994b02f1fb1b8580eded004

+ 132 - 11
src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushEngine.cs

@@ -4,6 +4,7 @@ using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
@@ -20,7 +21,34 @@ namespace PixiEditor.ChangeableDocument.Changeables.Brushes;
 internal class BrushEngine
 {
     private TextureCache cache = new();
-    public void ExecuteBrush(ChunkyImage target, BrushData brushData, VecI point, KeyFrameTime frameTime, ColorSpace cs, SamplingOptions samplingOptions, PointerInfo pointerInfo, EditorData editorData)
+    private VecF lastPos;
+    private int lastAppliedPointIndex = -1;
+
+    public void ExecuteBrush(ChunkyImage target, BrushData brushData, List<VecI> points, KeyFrameTime frameTime,
+        ColorSpace cs,
+        SamplingOptions samplingOptions, PointerInfo pointerInfo, EditorData editorData)
+    {
+        float strokeWidth = brushData.StrokeWidth;
+        float spacing = brushData.Spacing;
+
+        float spacingPixels = (strokeWidth * pointerInfo.Pressure) * spacing;
+
+        for (int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
+        {
+            var point = points[i];
+            if (points.Count > 1 && VecF.Distance(lastPos, point) < spacingPixels)
+                continue;
+
+            ExecuteVectorShapeBrush(target, brushData, point, frameTime, cs, samplingOptions, pointerInfo, editorData);
+
+            lastPos = point;
+        }
+
+        lastAppliedPointIndex = points.Count - 1;
+    }
+
+    public void ExecuteBrush(ChunkyImage target, BrushData brushData, VecI point, KeyFrameTime frameTime, ColorSpace cs,
+        SamplingOptions samplingOptions, PointerInfo pointerInfo, EditorData editorData)
     {
         ExecuteVectorShapeBrush(target, brushData, point, frameTime, cs, samplingOptions, pointerInfo, editorData);
     }
@@ -33,6 +61,7 @@ internal class BrushEngine
         {
             return;
         }
+
         var brushNode = brushData.BrushGraph.AllNodes.FirstOrDefault(x => x is BrushOutputNode) as BrushOutputNode;
         if (brushNode == null)
         {
@@ -43,10 +72,28 @@ internal class BrushEngine
         var rect = new RectI(point - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
         VecI size = new VecI((int)float.Ceiling(brushData.StrokeWidth));
 
-        using var texture = Texture.ForDisplay(size);
-        var surfaceUnderRect = UpdateSurfaceUnderRect(target, rect, colorSpace);
-        BrushRenderContext context = new BrushRenderContext(texture.DrawingSurface, frameTime, ChunkResolution.Full, size, size,
-            colorSpace, samplingOptions, brushData, surfaceUnderRect) { PointerInfo = pointerInfo, EditorData = editorData };
+        bool requiresSampleTexture = GraphUsesSampleTexture(brushData.BrushGraph, brushNode);
+        bool requiresFullTexture = GraphUsesFullTexture(brushData.BrushGraph, brushNode);
+        Texture? surfaceUnderRect = null;
+        Texture? fullTexture = null;
+        Texture texture = null;
+
+        if (requiresSampleTexture)
+        {
+            surfaceUnderRect = UpdateSurfaceUnderRect(target, rect, colorSpace, brushNode.AllowSampleStacking.Value);
+        }
+
+        if (requiresFullTexture)
+        {
+            fullTexture = UpdateFullTexture(target, colorSpace, brushNode.AllowSampleStacking.Value);
+        }
+
+        BrushRenderContext context = new BrushRenderContext(texture?.DrawingSurface, frameTime, ChunkResolution.Full,
+            size, size,
+            colorSpace, samplingOptions, brushData, surfaceUnderRect, fullTexture)
+        {
+            PointerInfo = pointerInfo, EditorData = editorData
+        };
 
         brushData.BrushGraph.Execute(brushNode, context);
 
@@ -89,9 +136,9 @@ internal class BrushEngine
             var brushTexture = brushNode.ContentTexture;
             if (brushTexture != null)
             {
-                TexturePaintable brushTexturePaintable = new(brushTexture);
+                TexturePaintable brushTexturePaintable = new(new Texture(brushTexture), true);
                 target.EnqueueDrawPath(path, brushTexturePaintable, vectorShape.StrokeWidth,
-                    StrokeCap.Butt, brushData.BlendMode, PaintStyle.Fill, brushData.AntiAliasing);
+                    StrokeCap.Butt, brushData.BlendMode, PaintStyle.Fill, brushData.AntiAliasing, null);
                 return;
             }
         }
@@ -119,21 +166,95 @@ internal class BrushEngine
         }
 
         target.EnqueueDrawPath(path, paintable, vectorShape.StrokeWidth,
-            strokeCap, brushData.BlendMode, strokeStyle, brushData.AntiAliasing);
+            strokeCap, brushData.BlendMode, strokeStyle, brushData.AntiAliasing, null);
 
         if (fill is { AnythingVisible: true } && stroke is { AnythingVisible: true })
         {
             strokeStyle = PaintStyle.Stroke;
             target.EnqueueDrawPath(path, stroke, vectorShape.StrokeWidth,
-                strokeCap, brushData.BlendMode, strokeStyle, brushData.AntiAliasing);
+                strokeCap, brushData.BlendMode, strokeStyle, brushData.AntiAliasing, null);
         }
     }
 
-    private Texture UpdateSurfaceUnderRect(ChunkyImage target, RectI rect, ColorSpace colorSpace)
+    private Texture UpdateFullTexture(ChunkyImage target, ColorSpace colorSpace, bool sampleLatest)
+    {
+        var texture = cache.RequestTexture(1, target.LatestSize, colorSpace);
+        if (!sampleLatest)
+        {
+            target.DrawCommittedRegionOn(new RectI(VecI.Zero, target.LatestSize), ChunkResolution.Full, texture.DrawingSurface, VecI.Zero);
+            return texture;
+        }
+
+        target.DrawMostUpToDateRegionOn(new RectI(VecI.Zero, target.LatestSize), ChunkResolution.Full, texture.DrawingSurface, VecI.Zero);
+        return texture;
+    }
+
+    private Texture UpdateSurfaceUnderRect(ChunkyImage target, RectI rect, ColorSpace colorSpace, bool sampleLatest)
     {
         var surfaceUnderRect = cache.RequestTexture(0, rect.Size, colorSpace);
 
-        target.DrawCommittedRegionOn(rect, ChunkResolution.Full, surfaceUnderRect.DrawingSurface, VecI.Zero);
+        if (sampleLatest)
+        {
+            target.DrawMostUpToDateRegionOn(rect, ChunkResolution.Full, surfaceUnderRect.DrawingSurface, VecI.Zero);
+        }
+        else
+        {
+            target.DrawCommittedRegionOn(rect, ChunkResolution.Full, surfaceUnderRect.DrawingSurface, VecI.Zero);
+        }
+
         return surfaceUnderRect;
     }
+
+    private bool GraphUsesSampleTexture(IReadOnlyNodeGraph graph, IReadOnlyNode brushNode)
+    {
+        return GraphUsesInput(graph, brushNode, node => node.TargetSampleTexture.Connections);
+    }
+
+    private bool GraphUsesFullTexture(IReadOnlyNodeGraph graph, IReadOnlyNode brushNode)
+    {
+        return GraphUsesInput(graph, brushNode, node => node.TargetFullTexture.Connections);
+    }
+
+    private bool GraphUsesInput(IReadOnlyNodeGraph graph, IReadOnlyNode brushNode, Func<IBrushSampleTextureNode, IReadOnlyCollection<IInputProperty>> getConnections)
+    {
+        var sampleTextureNodes = graph.AllNodes.Where(x => x is IBrushSampleTextureNode).ToList();
+        if (sampleTextureNodes.Count == 0)
+        {
+            return false;
+        }
+
+        foreach (var node in sampleTextureNodes)
+        {
+            if (node is IBrushSampleTextureNode brushSampleTextureNode)
+            {
+                var connections = getConnections(brushSampleTextureNode);
+                if (connections.Count == 0)
+                {
+                    continue;
+                }
+                foreach (var connection in connections)
+                {
+                    bool found = false;
+                    connection.Connection.Node.TraverseForwards(x =>
+                    {
+                        if (x == brushNode)
+                        {
+                            found = true;
+                            return false;
+                        }
+
+                        return true;
+                    });
+
+                    if (found)
+                    {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
 }

+ 3 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/BrushRenderContext.cs

@@ -12,10 +12,12 @@ internal class BrushRenderContext : RenderContext
 {
     public BrushData BrushData { get; }
     public Texture TargetSampledTexture { get; }
+    public Texture TargetFullTexture { get; }
 
-    public BrushRenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution, VecI renderOutputSize, VecI documentSize, ColorSpace processingColorSpace, SamplingOptions desiredSampling, BrushData brushData, Texture targetSampledTexture, double opacity = 1) : base(renderSurface, frameTime, chunkResolution, renderOutputSize, documentSize, processingColorSpace, desiredSampling, opacity)
+    public BrushRenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution, VecI renderOutputSize, VecI documentSize, ColorSpace processingColorSpace, SamplingOptions desiredSampling, BrushData brushData, Texture? targetSampledTexture, Texture? targetFullTexture, double opacity = 1) : base(renderSurface, frameTime, chunkResolution, renderOutputSize, documentSize, processingColorSpace, desiredSampling, opacity)
     {
         BrushData = brushData;
         TargetSampledTexture = targetSampledTexture;
+        TargetFullTexture = targetFullTexture;
     }
 }

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

@@ -11,6 +11,7 @@ public interface IReadOnlyNodeGraph : ICacheable
     public void AddNode(IReadOnlyNode node);
     public void RemoveNode(IReadOnlyNode node);
     public bool TryTraverse(Action<IReadOnlyNode> action);
+    public bool TryTraverse(IReadOnlyNode end, Action<IReadOnlyNode> action);
     public void Execute(RenderContext context);
     public void Execute(IReadOnlyNode end, RenderContext context);
     Queue<IReadOnlyNode> CalculateExecutionQueue(IReadOnlyNode endNode);

+ 5 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -91,8 +91,12 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     public bool TryTraverse(Action<IReadOnlyNode> action)
     {
         if (OutputNode == null) return false;
+        return TryTraverse(OutputNode, action);
+    }
 
-        var queue = CalculateExecutionQueueInternal(OutputNode);
+    public bool TryTraverse(IReadOnlyNode end, Action<IReadOnlyNode> action)
+    {
+        var queue = CalculateExecutionQueueInternal(end);
 
         foreach (var node in queue)
         {

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/BrushOutputNode.cs

@@ -1,5 +1,6 @@
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -13,8 +14,10 @@ public class BrushOutputNode : Node
     public InputProperty<Paintable> Stroke { get; }
     public InputProperty<Paintable> Fill { get; }
     public RenderInputProperty Content { get; }
+    public InputProperty<Matrix3X3> Transform { get; }
     public InputProperty<float> Pressure { get; }
     public InputProperty<bool> FitToStrokeSize { get; }
+    public InputProperty<bool> AllowSampleStacking { get; }
 
     internal Texture ContentTexture;
 
@@ -28,9 +31,11 @@ public class BrushOutputNode : Node
         Stroke = CreateInput<Paintable>("Stroke", "STROKE", null);
         Fill = CreateInput<Paintable>("Fill", "FILL", null);
         Content = CreateRenderInput("Content", "CONTENT");
+        Transform = CreateInput<Matrix3X3>("Transform", "TRANSFORM", Matrix3X3.Identity);
 
         Pressure = CreateInput<float>("Pressure", "PRESSURE", 1f);
         FitToStrokeSize = CreateInput<bool>("FitToStrokeSize", "FIT_TO_STROKE_SIZE", true);
+        AllowSampleStacking = CreateInput<bool>("AllowSampleStacking", "ALLOW_SAMPLE_STACKING", false);
     }
 
     protected override void OnExecute(RenderContext context)
@@ -38,7 +43,10 @@ public class BrushOutputNode : Node
         if (Content.Value != null)
         {
             ContentTexture = cache.RequestTexture(0, context.RenderOutputSize, context.ProcessingColorSpace);
+            ContentTexture.DrawingSurface.Canvas.Save();
+            ContentTexture.DrawingSurface.Canvas.SetMatrix(Transform.Value);
             Content.Value.Paint(context, ContentTexture.DrawingSurface);
+            ContentTexture.DrawingSurface.Canvas.Restore();
         }
     }
 

+ 9 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/IBrushSampleTextureNode.cs

@@ -0,0 +1,9 @@
+using Drawie.Backend.Core;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+
+public interface IBrushSampleTextureNode
+{
+    public OutputProperty<Texture> TargetSampleTexture { get; }
+    public OutputProperty<Texture> TargetFullTexture { get; }
+}

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/StrokeInfoNode.cs

@@ -5,18 +5,18 @@ using PixiEditor.ChangeableDocument.Rendering;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
 
 [NodeInfo("StrokeInfo")]
-public class StrokeInfoNode : Node
+public class StrokeInfoNode : Node, IBrushSampleTextureNode
 {
     public OutputProperty<float> StrokeWidth { get; }
     public OutputProperty<float> Spacing { get; }
-    public OutputProperty<Texture> TargetSmallTexture { get; }
+    public OutputProperty<Texture> TargetSampleTexture { get; }
     public OutputProperty<Texture> TargetFullTexture { get; }
 
     public StrokeInfoNode()
     {
         StrokeWidth = CreateOutput<float>("StrokeWidth", "STROKE_WIDTH", 1f);
         Spacing = CreateOutput<float>("Spacing", "SPACING", 0.1f);
-        TargetSmallTexture = CreateOutput<Texture>("TargetSampleTexture", "TARGET_SAMPLE_TEXTURE", null);
+        TargetSampleTexture = CreateOutput<Texture>("TargetSampleTexture", "TARGET_SAMPLE_TEXTURE", null);
         TargetFullTexture = CreateOutput<Texture>("TargetFullTexture", "TARGET_FULL_TEXTURE", null);
     }
 
@@ -28,14 +28,14 @@ public class StrokeInfoNode : Node
         StrokeWidth.Value = brushRenderContext.BrushData.StrokeWidth;
         Spacing.Value = brushRenderContext.BrushData.Spacing;
 
-        if (TargetSmallTexture.Connections.Count > 0)
+        if (TargetSampleTexture.Connections.Count > 0)
         {
-            TargetSmallTexture.Value = brushRenderContext.TargetSampledTexture;
+            TargetSampleTexture.Value = brushRenderContext.TargetSampledTexture;
         }
 
         if (TargetFullTexture.Connections.Count > 0)
         {
-            // TODO: Implement
+            TargetFullTexture.Value = brushRenderContext.TargetFullTexture;
         }
     }
 

+ 9 - 71
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -32,13 +32,10 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     private BrushEngine engine = new BrushEngine();
     private float spacing = 1;
     private readonly Paint srcPaint = new Paint() { BlendMode = BlendMode.Src };
-    private Paintable? finalPaintable;
 
     private CommittedChunkStorage? storedChunks;
     private readonly List<VecI> points = new();
     private int frame;
-    private VecF lastPos;
-    private int lastAppliedPointIndex = -1;
     private BrushOutputNode? brushOutputNode;
     private PointerInfo pointerInfo;
     private EditorData editorData;
@@ -81,7 +78,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     }
 
     [UpdateChangeMethod]
-    public void Update(VecI pos, float strokeWidth, PointerInfo pointerInfo)
+    public void Update(VecI pos, float strokeWidth, float spacing, PointerInfo pointerInfo)
     {
         if (points.Count > 0)
         {
@@ -91,6 +88,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
         this.strokeWidth = strokeWidth;
         this.pointerInfo = pointerInfo;
+        this.spacing = spacing;
         UpdateBrushData();
     }
 
@@ -135,55 +133,12 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
         int opCount = image.QueueLength;
 
-        float spacingPixels = (strokeWidth * pointerInfo.Pressure) * spacing;
-
-        for (int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
-        {
-            var point = points[i];
-            if (points.Count > 1 && VecF.Distance(lastPos, point) < spacingPixels)
-                continue;
-
-            lastPos = point;
-            finalPaintable = color;
-
-            brushData.AntiAliasing = antiAliasing;
-            brushData.Spacing = spacing;
-            brushData.StrokeWidth = strokeWidth;
-
-            engine.ExecuteBrush(image, brushData, point, frame, target.ProcessingColorSpace, SamplingOptions.Default,
-                pointerInfo, editorData);
-
-            /*if (brushData.VectorShape == null)
-            {
-                if (antiAliasing)
-                {
-                    finalPaintable = ApplySoftnessGradient((VecD)point);
-                }
-
-                image.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing, srcPaint);
-            }
-            else
-            {
-                BlendMode blendMode = srcPaint.BlendMode;
-                /*ShapeData shapeData = new ShapeData(rect.Center, rect.Size, 0, 0, 0, finalPaintable, finalPaintable,
-                    blendMode);
-                image.EnqueueDrawRectangle(shapeData);#1#
-
-                var path = brushData.VectorShape.ToPath(true);
-                path.Offset(brushData.VectorShape.TransformedAABB.Pos - brushData.VectorShape.GeometryAABB.Pos);
-                path.Offset(rect.Center - path.Bounds.Center);
-                /*VecD scale = new VecD(rect.Size.X / (float)path.Bounds.Width, rect.Size.Y / (float)path.Bounds.Height);
-                if (scale.IsNaNOrInfinity())
-                {
-                    scale = VecD.Zero;
-                }
-                VecD uniformScale = new VecD(Math.Min(scale.X, scale.Y));
-                path.Transform(Matrix3X3.CreateScale((float)uniformScale.X, (float)uniformScale.Y, (float)rect.Center.X, (float)rect.Center.Y));#1#
-                image.EnqueueDrawPath(path, finalPaintable, 1, StrokeCap.Butt, blendMode, PaintStyle.StrokeAndFill, true);
-            }*/
-        }
+        brushData.AntiAliasing = antiAliasing;
+        brushData.Spacing = spacing;
+        brushData.StrokeWidth = strokeWidth;
 
-        lastAppliedPointIndex = points.Count - 1;
+        engine.ExecuteBrush(image, brushData, points, frame, target.ProcessingColorSpace, SamplingOptions.Default,
+            pointerInfo, editorData);
 
         var affChunks = image.FindAffectedArea(opCount);
 
@@ -206,31 +161,14 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
         if (points.Count == 1)
         {
-            var rect = new RectI(points[0] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
-            finalPaintable = color;
-
             engine.ExecuteBrush(targetImage, brushData, points[0], frameTime, targetImage.ProcessingColorSpace,
                 SamplingOptions.Default, pointerInfo, editorData);
 
             return;
         }
 
-        VecF lastPos = points[0];
-
-        float spacingInPixels = (strokeWidth * pointerInfo.Pressure) * this.spacing;
-
-        for (int i = 0; i < points.Count; i++)
-        {
-            if (i > 0 && VecF.Distance(lastPos, points[i]) < spacingInPixels)
-                continue;
-
-            lastPos = points[i];
-            var rect = new RectI(points[i] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
-            finalPaintable = color;
-
-            engine.ExecuteBrush(targetImage, brushData, points[i], frameTime, targetImage.ProcessingColorSpace,
-                SamplingOptions.Default, pointerInfo, editorData);
-        }
+        engine.ExecuteBrush(targetImage, brushData, points, frameTime, targetImage.ProcessingColorSpace,
+            SamplingOptions.Default, pointerInfo, editorData);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,

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

@@ -1132,5 +1132,6 @@
   "MOVEMENT_DIRECTION": "Movement Direction",
   "TWIST": "Twist",
     "TILT": "Tilt",
-  "POINTER_INFO_NODE": "Pointer Info"
+  "POINTER_INFO_NODE": "Pointer Info",
+  "ALLOW_SAMPLE_STACKING": "Allow sample stacking"
 }

+ 4 - 5
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -26,12 +26,12 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     private Color color;
     public double ToolSize => penToolbar.ToolSize;
     public bool SquareBrush => penToolbar.PaintShape == PaintBrushShape.Square;
+    public float Spacing => penToolbar.Spacing;
     public override bool BlocksOtherActions => controller.LeftMousePressed;
 
     private bool drawOnMask;
     private bool pixelPerfect;
     private bool antiAliasing;
-    private float spacing = 1;
     private bool transparentErase;
 
     private Guid brushOutputGuid = Guid.Empty;
@@ -60,7 +60,6 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         color = colorsHandler.PrimaryColor;
         pixelPerfect = penTool.PixelPerfectEnabled;
         antiAliasing = toolbar.AntiAliasing;
-        spacing = toolbar.Spacing;
 
         if (color.A > 0)
         {
@@ -84,7 +83,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
             IAction? action = pixelPerfect switch
             {
                 false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize,
-                    transparentErase, antiAliasing, spacing, brushOutputGuid, drawOnMask,
+                    transparentErase, antiAliasing, Spacing, brushOutputGuid, drawOnMask,
                     document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData),
                 true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask,
                     document!.AnimationHandler.ActiveFrameBindable)
@@ -103,7 +102,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         IAction? action = pixelPerfect switch
         {
             false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize,
-                transparentErase, antiAliasing, spacing, brushOutputGuid, drawOnMask,
+                transparentErase, antiAliasing, Spacing, brushOutputGuid, drawOnMask,
                 document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData),
             true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask,
                 document!.AnimationHandler.ActiveFrameBindable)
@@ -129,7 +128,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
             IAction? action = pixelPerfect switch
             {
                 false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, transparentErase, antiAliasing,
-                    spacing, brushOutputGuid, drawOnMask, document!.AnimationHandler.ActiveFrameBindable,
+                    Spacing, brushOutputGuid, drawOnMask, document!.AnimationHandler.ActiveFrameBindable,
                     controller.LastPointerInfo, controller.EditorData),
                 true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask,
                     document!.AnimationHandler.ActiveFrameBindable)