Browse Source

Sample based brushes wip

Krzysztof Krysiński 2 weeks ago
parent
commit
5d86623074

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 3c06f52bee39798848ee1f9274a0c68c496812b6
+Subproject commit a012b67aa8bdd3047593073561fc97fdfaea931d

+ 0 - 1
src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushData.cs

@@ -10,7 +10,6 @@ public struct BrushData
 {
     public IReadOnlyNodeGraph BrushGraph { get; set; }
     public bool AntiAliasing { get; set; }
-    public float Hardness { get; set; }
     public float Spacing { get; set; }
     public float StrokeWidth { get; set; }
 

+ 29 - 4
src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushEngine.cs

@@ -7,7 +7,9 @@ using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -17,6 +19,7 @@ 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)
     {
         ExecuteVectorShapeBrush(target, brushData, point, frameTime, cs, samplingOptions, pointerInfo, editorData);
@@ -32,10 +35,14 @@ internal class BrushEngine
             return;
         }
 
+        float strokeWidth = brushData.StrokeWidth;
+        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);
-        RenderContext context = new RenderContext(texture.DrawingSurface, frameTime, ChunkResolution.Full, size, size,
-            colorSpace, samplingOptions) { PointerInfo = pointerInfo, EditorData = editorData };
+        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 };
 
         brushData.BrushGraph.Execute(brushNode, context);
 
@@ -51,8 +58,6 @@ internal class BrushEngine
             return;
         }
 
-        float strokeWidth = brushData.StrokeWidth;
-        var rect = new RectI(point - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
 
         path.Offset(vectorShape.TransformedAABB.Pos - vectorShape.GeometryAABB.Pos);
         path.Offset(rect.Center - path.Bounds.Center);
@@ -75,6 +80,18 @@ internal class BrushEngine
             (float)rect.Center.Y);
         path.Transform(pressureScale);
 
+        if (brushNode.Content.Value != null)
+        {
+            var brushTexture = brushNode.ContentTexture;
+            if (brushTexture != null)
+            {
+                TexturePaintable brushTexturePaintable = new(brushTexture);
+                target.EnqueueDrawPath(path, brushTexturePaintable, vectorShape.StrokeWidth,
+                    StrokeCap.Butt, brushData.BlendMode, PaintStyle.Fill, brushData.AntiAliasing);
+                return;
+            }
+        }
+
         StrokeCap strokeCap = StrokeCap.Butt;
         PaintStyle strokeStyle = PaintStyle.Fill;
 
@@ -107,4 +124,12 @@ internal class BrushEngine
                 strokeCap, brushData.BlendMode, strokeStyle, brushData.AntiAliasing);
         }
     }
+
+    private Texture UpdateSurfaceUnderRect(ChunkyImage target, RectI rect, ColorSpace colorSpace)
+    {
+        var surfaceUnderRect = cache.RequestTexture(0, rect.Size, colorSpace);
+
+        target.DrawCommittedRegionOn(rect, ChunkResolution.Full, surfaceUnderRect.DrawingSurface, VecI.Zero);
+        return surfaceUnderRect;
+    }
 }

+ 21 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/BrushRenderContext.cs

@@ -0,0 +1,21 @@
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Brushes;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+
+internal class BrushRenderContext : RenderContext
+{
+    public BrushData BrushData { get; }
+    public Texture TargetSampledTexture { 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)
+    {
+        BrushData = brushData;
+        TargetSampledTexture = targetSampledTexture;
+    }
+}

+ 15 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/BrushOutputNode.cs

@@ -1,4 +1,5 @@
-using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -11,15 +12,22 @@ public class BrushOutputNode : Node
     public InputProperty<ShapeVectorData> VectorShape { get; }
     public InputProperty<Paintable> Stroke { get; }
     public InputProperty<Paintable> Fill { get; }
+    public RenderInputProperty Content { get; }
     public InputProperty<float> Pressure { get; }
     public InputProperty<bool> FitToStrokeSize { get; }
 
+    internal Texture ContentTexture;
+
+    private TextureCache cache = new();
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
 
     public BrushOutputNode()
     {
-        VectorShape = CreateInput<ShapeVectorData>("VectorShape", "VECTOR_SHAPE", null);
+        VectorShape = CreateInput<ShapeVectorData>("VectorShape", "SHAPE", null);
         Stroke = CreateInput<Paintable>("Stroke", "STROKE", null);
         Fill = CreateInput<Paintable>("Fill", "FILL", null);
+        Content = CreateRenderInput("Content", "CONTENT");
 
         Pressure = CreateInput<float>("Pressure", "PRESSURE", 1f);
         FitToStrokeSize = CreateInput<bool>("FitToStrokeSize", "FIT_TO_STROKE_SIZE", true);
@@ -27,7 +35,11 @@ public class BrushOutputNode : Node
 
     protected override void OnExecute(RenderContext context)
     {
-
+        if (Content.Value != null)
+        {
+            ContentTexture = cache.RequestTexture(0, context.RenderOutputSize, context.ProcessingColorSpace);
+            Content.Value.Paint(context, ContentTexture.DrawingSurface);
+        }
     }
 
     public override Node CreateCopy()

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

@@ -0,0 +1,46 @@
+using Drawie.Backend.Core;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+
+[NodeInfo("StrokeInfo")]
+public class StrokeInfoNode : Node
+{
+    public OutputProperty<float> StrokeWidth { get; }
+    public OutputProperty<float> Spacing { get; }
+    public OutputProperty<Texture> TargetSmallTexture { 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>("TargetSmallTexture", "TARGET_SMALL_TEXTURE", null);
+        TargetFullTexture = CreateOutput<Texture>("TargetFullTexture", "TARGET_FULL_TEXTURE", null);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        if (context is not BrushRenderContext brushRenderContext)
+            return;
+
+        StrokeWidth.Value = brushRenderContext.BrushData.StrokeWidth;
+        Spacing.Value = brushRenderContext.BrushData.Spacing;
+
+        if (TargetSmallTexture.Connections.Count > 0)
+        {
+            TargetSmallTexture.Value = brushRenderContext.TargetSampledTexture;
+        }
+
+        if (TargetFullTexture.Connections.Count > 0)
+        {
+            // TODO: Implement
+        }
+    }
+
+    public override Node CreateCopy()
+    {
+        return new StrokeInfoNode();
+    }
+}

+ 12 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/GradientNode.cs

@@ -61,7 +61,7 @@ public class GradientNode : Node
 
             if (!HasInputProperty("Radius"))
             {
-                Radius = CreateInput<double>("Radius", "RADIUS", 0.5);
+                Radius = CreateInput<double>("Radius", "RADIUS", 0.5).WithRules(x => x.Min(0d));
             }
         }
         else if (type == GradientType.Conical)
@@ -83,19 +83,25 @@ public class GradientNode : Node
 
     private void RegenerateStops()
     {
-        foreach (var (colorInput, positionInput) in ColorStops)
+        if (StopsCount.Value < ColorStops.Count)
         {
-            RemoveInputProperty(colorInput);
-            RemoveInputProperty(positionInput);
+            int diff = ColorStops.Count - StopsCount.Value;
+            var keysToRemove = ColorStops.Keys.TakeLast(diff).ToList();
+            foreach (var key in keysToRemove)
+            {
+                RemoveInputProperty(key);
+                RemoveInputProperty(ColorStops[key]);
+                ColorStops.Remove(key);
+            }
         }
 
-        ColorStops.Clear();
         GenerateStops();
     }
 
     private void GenerateStops()
     {
-        for (int i = 0; i < StopsCount.Value; i++)
+        int startIndex = ColorStops.Count;
+        for (int i = startIndex; i < StopsCount.Value; i++)
         {
             var colorInput = CreateInput<Color>($"ColorStop{i + 1}Color", $"COLOR_STOP_COLOR",
                 Drawie.Backend.Core.ColorsImpl.Colors.White);

+ 3 - 25
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -30,7 +30,6 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     private Guid brushOutputGuid;
     private BrushData brushData;
     private BrushEngine engine = new BrushEngine();
-    private float hardness;
     private float spacing = 1;
     private readonly Paint srcPaint = new Paint() { BlendMode = BlendMode.Src };
     private Paintable? finalPaintable;
@@ -47,7 +46,6 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     [GenerateUpdateableChangeActions]
     public LineBasedPen_UpdateableChange(Guid memberGuid, Color color, VecI pos, float strokeWidth, bool erasing,
         bool antiAliasing,
-        float hardness,
         float spacing,
         Guid brushOutputGuid,
         bool drawOnMask, int frame, PointerInfo pointerInfo, EditorData editorData)
@@ -59,7 +57,6 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         this.antiAliasing = antiAliasing;
         this.drawOnMask = drawOnMask;
         this.brushOutputGuid = brushOutputGuid;
-        this.hardness = hardness;
         this.spacing = spacing;
         points.Add(pos);
         this.frame = frame;
@@ -127,7 +124,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         {
             brushData = new BrushData(brushData.BrushGraph)
             {
-                StrokeWidth = strokeWidth, AntiAliasing = antiAliasing, Hardness = hardness, Spacing = spacing,
+                StrokeWidth = strokeWidth, AntiAliasing = antiAliasing, Spacing = spacing,
             };
         }
     }
@@ -138,7 +135,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
         int opCount = image.QueueLength;
 
-        float spacingPixels = strokeWidth * spacing;
+        float spacingPixels = (strokeWidth * pointerInfo.Pressure) * spacing;
 
         for (int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
         {
@@ -150,7 +147,6 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             finalPaintable = color;
 
             brushData.AntiAliasing = antiAliasing;
-            brushData.Hardness = hardness;
             brushData.Spacing = spacing;
             brushData.StrokeWidth = strokeWidth;
 
@@ -205,7 +201,6 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     private void FastforwardEnqueueDrawLines(ChunkyImage targetImage, KeyFrameTime frameTime)
     {
         brushData.AntiAliasing = antiAliasing;
-        brushData.Hardness = hardness;
         brushData.Spacing = spacing;
         brushData.StrokeWidth = strokeWidth;
 
@@ -222,7 +217,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
         VecF lastPos = points[0];
 
-        float spacingInPixels = strokeWidth * this.spacing;
+        float spacingInPixels = (strokeWidth * pointerInfo.Pressure) * this.spacing;
 
         for (int i = 0; i < points.Count; i++)
         {
@@ -238,23 +233,6 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         }
     }
 
-    private Paintable? ApplySoftnessGradient(VecD pos)
-    {
-        srcPaint.Paintable?.Dispose();
-        if (hardness >= 1)
-        {
-            return new ColorPaintable(color);
-        }
-
-        float radius = strokeWidth / 2f;
-        radius = MathF.Max(1, radius);
-        return new RadialGradientPaintable(pos, radius,
-        [
-            new GradientStop(color, Math.Max(hardness - 0.05f, 0)),
-            new GradientStop(color.WithAlpha(0), 0.95f)
-        ]) { AbsoluteValues = true };
-    }
-
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
     {

+ 3 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConversionTable.cs

@@ -5,10 +5,12 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using Drawie.Backend.Core.Shaders.Generation;
+using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
 
@@ -76,6 +78,7 @@ public static class ConversionTable
             {
                 typeof(Texture), [
                     (typeof(Paintable), new TypeConverter<Texture, Paintable>(img => new TexturePaintable(img))),
+                    (typeof(Painter), new TypeConverter<Texture, Painter>(img => new Painter((c, s) => s.Canvas.DrawSurface(img.DrawingSurface, 0, 0)))),
                 ]
             }
         };

+ 2 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs

@@ -47,12 +47,11 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
         color = GetHandler<IColorsHandler>().PrimaryColor;
         toolSize = toolbar.ToolSize;
         antiAliasing = toolbar.AntiAliasing;
-        hardness = toolbar.Hardness;
         spacing = toolbar.Spacing;
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, (float)eraserTool.ToolSize, true,
-            antiAliasing, hardness, spacing, Guid.Empty, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData);
+            antiAliasing, spacing, Guid.Empty, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData);
         internals!.ActionAccumulator.AddActions(action);
 
         return ExecutionState.Success;
@@ -60,7 +59,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
     public override void OnPixelPositionChange(VecI pos, MouseOnCanvasEventArgs args)
     {
-        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, pos, (float)toolSize, true, antiAliasing, hardness, spacing, Guid.Empty, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData);
+        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, pos, (float)toolSize, true, antiAliasing, spacing, Guid.Empty, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, controller.LastPointerInfo, controller.EditorData);
         internals!.ActionAccumulator.AddActions(action);
     }
 

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

@@ -31,7 +31,6 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     private bool drawOnMask;
     private bool pixelPerfect;
     private bool antiAliasing;
-    private float hardness;
     private float spacing = 1;
     private bool transparentErase;
 
@@ -61,7 +60,6 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         color = colorsHandler.PrimaryColor;
         pixelPerfect = penTool.PixelPerfectEnabled;
         antiAliasing = toolbar.AntiAliasing;
-        hardness = toolbar.Hardness;
         spacing = toolbar.Spacing;
 
         if (color.A > 0)
@@ -86,7 +84,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
             IAction? action = pixelPerfect switch
             {
                 false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize,
-                    transparentErase, antiAliasing, hardness, 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)
@@ -105,7 +103,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         IAction? action = pixelPerfect switch
         {
             false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize,
-                transparentErase, antiAliasing, hardness, 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)
@@ -131,7 +129,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
             IAction? action = pixelPerfect switch
             {
                 false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, transparentErase, antiAliasing,
-                    hardness, 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)

+ 0 - 1
src/PixiEditor/Models/Handlers/Toolbars/IPenToolbar.cs

@@ -5,7 +5,6 @@ namespace PixiEditor.Models.Handlers.Toolbars;
 internal interface IPenToolbar : IToolbar, IToolSizeToolbar
 {
     public bool AntiAliasing { get; set; }
-    public float Hardness { get; set; }
     public float Spacing { get; set; }
     public PaintBrushShape PaintShape { get; set; }
 }

+ 10 - 0
src/PixiEditor/ViewModels/Document/Nodes/Brushes/StrokeInfoNodeViewModel.cs

@@ -0,0 +1,10 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Brushes;
+
+[NodeViewModel("STROKE_INFO_NODE", "BRUSHES", null)]
+internal class StrokeInfoNodeViewModel : NodeViewModel<StrokeInfoNode>
+{
+
+}

+ 0 - 7
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/PenToolbar.cs

@@ -12,12 +12,6 @@ internal class PenToolbar : Toolbar, IPenToolbar
         set => GetSetting<BoolSettingViewModel>(nameof(AntiAliasing)).Value = value;
     }
 
-    public float Hardness
-    {
-        get => GetSetting<PercentSettingViewModel>(nameof(Hardness)).Value;
-        set => GetSetting<PercentSettingViewModel>(nameof(Hardness)).Value = value;
-    }
-
     public float Spacing
     {
         get => GetSetting<PercentSettingViewModel>(nameof(Spacing)).Value;
@@ -44,7 +38,6 @@ internal class PenToolbar : Toolbar, IPenToolbar
     public PenToolbar()
     {
         AddSetting(new BoolSettingViewModel(nameof(AntiAliasing), "ANTI_ALIASING_SETTING") { IsExposed = false });
-        AddSetting(new PercentSettingViewModel(nameof(Hardness), 0.8f, "HARDNESS_SETTING") { IsExposed = false });
         AddSetting(new PercentSettingViewModel(nameof(Spacing), 0.15f, "SPACING_SETTING") { IsExposed = false });
         var setting = new SizeSettingViewModel(nameof(ToolSize), "TOOL_SIZE_LABEL");
         setting.ValueChanged += (_, _) => OnPropertyChanged(nameof(ToolSize));