Jelajahi Sumber

Actual BrushEngine basic logic

Krzysztof Krysiński 3 minggu lalu
induk
melakukan
f8b220398e

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 943e9abbb60b73c4965b947e987dc2696e0b08f8
+Subproject commit 214215203d18b4088730311c2023aeddae731516

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 1be85ac9f4bc6b584e6a3a5a3d0287201c6a5f03
+Subproject commit 906b76246cfd8e252fb8cf9f7659780053bd9cae

+ 16 - 4
src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushData.cs

@@ -1,14 +1,26 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Surfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Brushes;
 
 public struct BrushData
 {
-    public IReadOnlyShapeVectorData? VectorShape { get; }
+    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; }
 
-    public BrushData(IReadOnlyShapeVectorData vectorShape)
+    public bool FitToStrokeSize { get; set; } = true;
+
+    public float Pressure { get; set; } = 1f;
+    public BlendMode BlendMode { get; set; } = BlendMode.SrcOver;
+
+
+    public BrushData(IReadOnlyNodeGraph brushGraph)
     {
-        VectorShape = vectorShape;
+        BrushGraph = brushGraph;
     }
 }

+ 102 - 0
src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushEngine.cs

@@ -0,0 +1,102 @@
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Brushes;
+
+internal class BrushEngine
+{
+    public void ExecuteBrush(ChunkyImage target, BrushData brushData, VecI point, KeyFrameTime frameTime)
+    {
+        ExecuteVectorShapeBrush(target, brushData, point, frameTime);
+    }
+
+    private void ExecuteVectorShapeBrush(ChunkyImage target, BrushData brushData, VecI point, KeyFrameTime frameTime)
+    {
+        var brushNode = brushData.BrushGraph.AllNodes.FirstOrDefault(x => x is BrushOutputNode) as BrushOutputNode;
+        if (brushNode == null)
+        {
+            return;
+        }
+
+        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.CreateSrgbLinear());
+        brushData.BrushGraph.Execute(brushNode, context);
+
+        var vectorShape = brushNode.VectorShape.Value;
+        if (vectorShape == null)
+        {
+            return;
+        }
+
+        var path = vectorShape.ToPath(true);
+        if (path == null)
+        {
+            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);
+
+        if (brushData.FitToStrokeSize)
+        {
+            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));
+        }
+
+        Matrix3X3 pressureScale = Matrix3X3.CreateScale(brushData.Pressure, brushData.Pressure, (float)rect.Center.X,
+            (float)rect.Center.Y);
+        path.Transform(pressureScale);
+
+        StrokeCap strokeCap = StrokeCap.Butt;
+        PaintStyle strokeStyle = PaintStyle.Fill;
+
+        var fill = brushNode.Fill.Value;
+        var stroke = brushNode.Stroke.Value;
+
+        if (fill != null && fill.AnythingVisible)
+        {
+            strokeStyle = PaintStyle.Fill;
+        }
+        else
+        {
+            strokeStyle = PaintStyle.Stroke;
+        }
+
+        if (vectorShape is PathVectorData pathData)
+        {
+            strokeCap = pathData.StrokeLineCap;
+        }
+
+        target.EnqueueDrawPath(path, fill, vectorShape.StrokeWidth,
+            strokeCap, brushData.BlendMode, strokeStyle, brushData.AntiAliasing);
+
+        if (fill is { AnythingVisible: true } && stroke is { AnythingVisible: true })
+        {
+            strokeStyle = PaintStyle.Stroke;
+            target.EnqueueDrawPath(path, stroke, vectorShape.StrokeWidth,
+                strokeCap, brushData.BlendMode, strokeStyle, brushData.AntiAliasing);
+        }
+    }
+}

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

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

+ 29 - 18
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -7,8 +7,8 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 {
-    private ImmutableList<IReadOnlyNode>? cachedExecutionList;
-    
+    private Dictionary<IReadOnlyNode, ImmutableList<IReadOnlyNode>?> cachedExecutionList;
+
     private readonly List<Node> _nodes = new();
     public IReadOnlyCollection<Node> Nodes => _nodes;
     public IReadOnlyDictionary<Guid, Node> NodeLookup => nodeLookup;
@@ -27,7 +27,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         {
             return;
         }
-        
+
         node.ConnectionsChanged += ResetCache;
         _nodes.Add(node);
         nodeLookup[node.Id] = node;
@@ -61,10 +61,19 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     {
         return new Queue<IReadOnlyNode>(CalculateExecutionQueueInternal(outputNode));
     }
-    
+
     private ImmutableList<IReadOnlyNode> CalculateExecutionQueueInternal(IReadOnlyNode outputNode)
     {
-        return cachedExecutionList ??= GraphUtils.CalculateExecutionQueue(outputNode).ToImmutableList();
+        var cached = this.cachedExecutionList?.GetValueOrDefault(outputNode);
+        if (cached != null)
+        {
+            return cached;
+        }
+
+        var calculated = GraphUtils.CalculateExecutionQueue(outputNode).ToImmutableList();
+        cachedExecutionList ??= new Dictionary<IReadOnlyNode, ImmutableList<IReadOnlyNode>?>();
+        cachedExecutionList[outputNode] = calculated;
+        return calculated;
     }
 
     void IReadOnlyNodeGraph.AddNode(IReadOnlyNode node) => AddNode((Node)node);
@@ -81,31 +90,31 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 
     public bool TryTraverse(Action<IReadOnlyNode> action)
     {
-        if(OutputNode == null) return false;
-        
+        if (OutputNode == null) return false;
+
         var queue = CalculateExecutionQueueInternal(OutputNode);
-        
+
         foreach (var node in queue)
         {
             action(node);
         }
-        
+
         return true;
     }
 
-    public void Execute(RenderContext context)
+    public void Execute(IReadOnlyNode end, RenderContext context)
     {
-        if (OutputNode == null) return;
-        if(!CanExecute()) return;
+        if (end == null) return;
+        if (!CanExecute()) return;
+
+        var queue = CalculateExecutionQueueInternal(end);
 
-        var queue = CalculateExecutionQueueInternal(OutputNode);
-        
         foreach (var node in queue)
         {
             if (node is Node typedNode)
             {
-                if(typedNode.IsDisposed) continue;
-                
+                if (typedNode.IsDisposed) continue;
+
                 typedNode.ExecuteInternal(context);
             }
             else
@@ -114,7 +123,9 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
             }
         }
     }
-    
+
+    public void Execute(RenderContext context) => Execute(OutputNode, context);
+
     private bool CanExecute()
     {
         foreach (var node in Nodes)
@@ -127,7 +138,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 
         return true;
     }
-    
+
     private void ResetCache()
     {
         cachedExecutionList = null;

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

@@ -9,10 +9,20 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
 public class BrushOutputNode : Node
 {
     public InputProperty<ShapeVectorData> VectorShape { get; }
+    public InputProperty<Paintable> Stroke { get; }
+    public InputProperty<Paintable> Fill { get; }
+    public InputProperty<float> Pressure { get; }
+    public InputProperty<bool> FitToStrokeSize { get; }
+
 
     public BrushOutputNode()
     {
         VectorShape = CreateInput<ShapeVectorData>("VectorShape", "VECTOR_SHAPE", null);
+        Stroke = CreateInput<Paintable>("Stroke", "STROKE", null);
+        Fill = CreateInput<Paintable>("Fill", "FILL", null);
+
+        Pressure = CreateInput<float>("Pressure", "PRESSURE", 1f);
+        FitToStrokeSize = CreateInput<bool>("FitToStrokeSize", "FIT_TO_STROKE_SIZE", true);
     }
 
     protected override void OnExecute(RenderContext context)

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -30,6 +30,7 @@ public class CreateImageNode : Node, IPreviewRenderable
 
     private TextureCache textureCache = new();
 
+
     public CreateImageNode()
     {
         Output = CreateOutput<Texture>(nameof(Output), "IMAGE", null);

+ 43 - 49
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -1,4 +1,5 @@
 using ChunkyImageLib.Operations;
+using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
@@ -6,8 +7,12 @@ using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Brushes;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
@@ -21,6 +26,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     private readonly bool antiAliasing;
     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 };
@@ -80,10 +86,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         }
 
         this.strokeWidth = strokeWidth;
-        if (brushOutputNode != null)
-        {
-            brushData = new BrushData(brushOutputNode.VectorShape.Value);
-        }
+        UpdateBrushData();
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -101,10 +104,8 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         if (brushOutputGuid != Guid.Empty)
         {
             brushOutputNode = target.FindNode<BrushOutputNode>(brushOutputGuid);
-            if (brushOutputNode != null)
-            {
-                brushData = new BrushData(brushOutputNode.VectorShape.Value);
-            }
+            brushData = new BrushData(target.NodeGraph);
+            UpdateBrushData();
 
             return brushOutputNode != null;
         }
@@ -112,6 +113,20 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         return true;
     }
 
+    private void UpdateBrushData()
+    {
+        if (brushOutputNode != null)
+        {
+            brushData = new BrushData(brushData.BrushGraph)
+            {
+                StrokeWidth = strokeWidth,
+                AntiAliasing = antiAliasing,
+                Hardness = hardness,
+                Spacing = spacing,
+            };
+        }
+    }
+
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
@@ -127,10 +142,16 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
                 continue;
 
             lastPos = point;
-            var rect = new RectI(point - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             finalPaintable = color;
 
-            if (brushData.VectorShape == null)
+            brushData.AntiAliasing = antiAliasing;
+            brushData.Hardness = hardness;
+            brushData.Spacing = spacing;
+            brushData.StrokeWidth = strokeWidth;
+
+            engine.ExecuteBrush(image, brushData, point, frame);
+
+            /*if (brushData.VectorShape == null)
             {
                 if (antiAliasing)
                 {
@@ -144,7 +165,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
                 BlendMode blendMode = srcPaint.BlendMode;
                 /*ShapeData shapeData = new ShapeData(rect.Center, rect.Size, 0, 0, 0, finalPaintable, finalPaintable,
                     blendMode);
-                image.EnqueueDrawRectangle(shapeData);*/
+                image.EnqueueDrawRectangle(shapeData);#1#
 
                 var path = brushData.VectorShape.ToPath(true);
                 path.Offset(brushData.VectorShape.TransformedAABB.Pos - brushData.VectorShape.GeometryAABB.Pos);
@@ -155,9 +176,9 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
                     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));*/
+                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);
-            }
+            }*/
         }
 
         lastAppliedPointIndex = points.Count - 1;
@@ -167,30 +188,19 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affChunks, drawOnMask);
     }
 
-    private void FastforwardEnqueueDrawLines(ChunkyImage targetImage)
+    private void FastforwardEnqueueDrawLines(ChunkyImage targetImage, KeyFrameTime frameTime)
     {
+        brushData.AntiAliasing = antiAliasing;
+        brushData.Hardness = hardness;
+        brushData.Spacing = spacing;
+        brushData.StrokeWidth = strokeWidth;
+
         if (points.Count == 1)
         {
             var rect = new RectI(points[0] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             finalPaintable = color;
 
-            if (brushData.VectorShape == null)
-            {
-                if (antiAliasing)
-                {
-                    finalPaintable = ApplySoftnessGradient(points[0]);
-                }
-
-                targetImage.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);
-                targetImage.EnqueueDrawRectangle(shapeData);
-            }
+            engine.ExecuteBrush(targetImage, brushData, points[0], frameTime);
 
             return;
         }
@@ -208,23 +218,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             var rect = new RectI(points[i] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
             finalPaintable = color;
 
-            if (brushData.VectorShape == null)
-            {
-                if (antiAliasing)
-                {
-                    finalPaintable = ApplySoftnessGradient(points[i]);
-                }
-
-                targetImage.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);
-                targetImage.EnqueueDrawRectangle(shapeData);
-            }
+            engine.ExecuteBrush(targetImage, brushData, points[i], frameTime);
         }
     }
 
@@ -267,7 +261,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
                 image.SetBlendMode(BlendMode.SrcOver);
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
 
-            FastforwardEnqueueDrawLines(image);
+            FastforwardEnqueueDrawLines(image, frame);
             var affArea = image.FindAffectedArea();
             storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectProperties_Change.cs

@@ -227,6 +227,11 @@ internal class ConnectProperties_Change : Change
                 return true;
             }
 
+            if (outputValue == null)
+            {
+                return ConversionTable.CanConvertType(input.ValueType, output.ValueType);
+            }
+
             return false;
         }
 

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

@@ -1,9 +1,11 @@
 using System.Numerics;
 using System.Reflection;
+using Drawie.Backend.Core;
 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.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
@@ -68,6 +70,11 @@ public static class ConversionTable
                     (typeof(VecI), new TypeConverter<Vec3D, VecI>(v => new VecI((int)v.X, (int)v.Y))),
                     (typeof(Color), new TypeConverter<Vec3D, Color>(v => new Color((byte)Math.Clamp(v.X, 0, 255), (byte)Math.Clamp(v.Y, 0, 255), (byte)Math.Clamp(v.Z, 0, 255)))),
                 ]
+            },
+            {
+                typeof(Texture), [
+                    (typeof(Paintable), new TypeConverter<Texture, Paintable>(img => new TexturePaintable(img))),
+                ]
             }
         };
 
@@ -192,6 +199,27 @@ public static class ConversionTable
     {
         return (int)vec.X;
     }
+
+    public static bool CanConvertType(Type inputValueType, Type outputValueType)
+    {
+        if (inputValueType == outputValueType)
+        {
+            return true;
+        }
+
+        if (_conversionTable.TryGetValue(outputValueType, out var converters))
+        {
+            foreach (var (outType, _) in converters)
+            {
+                if (outType == inputValueType)
+                {
+                    return true;
+                }
+            }
+        }
+
+        return outputValueType.IsAssignableFrom(inputValueType);
+    }
 }
 
 interface ITypeConverter