Pārlūkot izejas kodu

Added pressure smoothing

flabbet 1 nedēļu atpakaļ
vecāks
revīzija
03d5856666

+ 87 - 30
src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushEngine.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using ChunkyImageLib.Operations;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
@@ -24,51 +25,51 @@ public class BrushEngine : IDisposable
     private TextureCache cache = new();
     private VecD lastPos;
     private VecD startPos;
+    private double lastPressure = 1.0;
     private int lastAppliedPointIndex = -1;
+    private int lastAppliedHistoryIndex = -1;
     private VecI lastCachedTexturePaintableSize = VecI.Zero;
     private TexturePaintable? lastCachedTexturePaintable = null;
+    private readonly List<RecordedPoint> pointsHistory = new();
 
     private bool drawnOnce = false;
+    
+    // Configuration: How many previous points to average.
+    // Higher = smoother but more "laggy" pressure response.
+    // 10 points is roughly 10 pixels of stroke history.
+    public int PressureSmoothingWindowSize { get; set; } = 10;
 
     public void ResetState()
     {
         lastAppliedPointIndex = -1;
         drawnOnce = false;
+        pointsHistory.Clear();
     }
 
-    public void ExecuteBrush(ChunkyImage target, BrushData brushData, List<VecD> points, KeyFrameTime frameTime,
-        ColorSpace cs, SamplingOptions samplingOptions, PointerInfo pointerInfo, KeyboardInfo keyboardInfo,
-        EditorData editorData)
+    /// <summary>
+    /// Calculates a smoothed pressure value based on the previous points in history.
+    /// This acts as a low-pass filter to remove jitter.
+    /// </summary>
+    private float GetSmoothedPressure(double targetPressure)
     {
-        if (brushData.BrushGraph == null)
-        {
-            return;
-        }
-
-        if (brushData.BrushGraph.LookupNode(brushData.TargetBrushNodeId) is not BrushOutputNode brushNode)
-        {
-            return;
-        }
+        if (pointsHistory.Count == 0)
+            return (float)targetPressure;
 
-        float strokeWidth = brushData.StrokeWidth;
-        float spacing = brushNode.Spacing.Value / 100f;
-
-        float spacingPixels = (strokeWidth * pointerInfo.Pressure) * spacing;
+        double sum = 0;
+        int count = 0;
 
-        for (int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
+        // Iterate backwards through history
+        for (int i = pointsHistory.Count - 1; i >= 0 && count < PressureSmoothingWindowSize; i--)
         {
-            var point = points[i];
-            if (VecD.Distance(lastPos, point) < spacingPixels)
-                continue;
-
-            ExecuteVectorShapeBrush(target, brushNode, brushData, point, frameTime, cs, samplingOptions, pointerInfo,
-                keyboardInfo,
-                editorData);
-
-            lastPos = point;
+            sum += pointsHistory[i].PointerInfo.Pressure;
+            count++;
         }
 
-        lastAppliedPointIndex = points.Count - 1;
+        // Add the current target to the average so we pull towards the new value
+        sum += targetPressure;
+        count++;
+
+        return (float)(sum / count);
     }
 
     public void ExecuteBrush(ChunkyImage target, BrushData brushData, List<RecordedPoint> points,
@@ -84,17 +85,72 @@ public class BrushEngine : IDisposable
         {
             return;
         }
+        
+        for (int i = lastAppliedPointIndex + 1; i < points.Count; i++)
+        {
+            RecordedPoint previousPoint = points[i];
+            if (i == 0)
+            {
+                if (pointsHistory.Count > 0)
+                {
+                    previousPoint = pointsHistory[^1];
+                }
+            }
+            else
+            {
+                previousPoint = points[i - 1];
+            }
+            
+            var currentPoint = points[i];
+            var dist = VecD.Distance(previousPoint.Position, currentPoint.Position);
+
+            if (dist > 0.5)
+            {
+                var interpolated = LineHelper.GetInterpolatedPoints(previousPoint.Position,
+                    currentPoint.Position);
+                
+                for (int j = 1; j < interpolated.Length; j++)
+                {
+                    var pt = interpolated[j];
+                    
+                    double ratio = VecD.Distance(previousPoint.Position, pt) /
+                                   VecD.Distance(previousPoint.Position, currentPoint.Position);
+                                   
+                    double linearTargetPressure = previousPoint.PointerInfo.Pressure +
+                                                  (currentPoint.PointerInfo.Pressure - previousPoint.PointerInfo.Pressure) * ratio;
+
+                    float smoothedPressure = GetSmoothedPressure(linearTargetPressure);
+                    
+                    pointsHistory.Add(new RecordedPoint(pt,
+                        currentPoint.PointerInfo with { Pressure = smoothedPressure },
+                        currentPoint.KeyboardInfo,
+                        currentPoint.EditorData));
+                }
+            }
+            else
+            {
+                float smoothedPressure = GetSmoothedPressure(currentPoint.PointerInfo.Pressure);
+                
+                pointsHistory.Add(new RecordedPoint(currentPoint.Position,
+                    currentPoint.PointerInfo with { Pressure = smoothedPressure },
+                    currentPoint.KeyboardInfo,
+                    currentPoint.EditorData));
+            }
+        }
+        
+        lastAppliedPointIndex = points.Count - 1;
 
         float strokeWidth = brushData.StrokeWidth;
         float spacing = brushNode.Spacing.Value / 100f;
 
-        for (int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
+        for (int i = Math.Max(lastAppliedHistoryIndex, 0); i < pointsHistory.Count; i++)
         {
-            var point = points[i];
+            var point = pointsHistory[i];
 
             float spacingPixels = (strokeWidth * point.PointerInfo.Pressure) * spacing;
             if (VecD.Distance(lastPos, point.Position) < spacingPixels)
                 continue;
+            
 
             ExecuteVectorShapeBrush(target, brushNode, brushData, point.Position, frameTime, cs, samplingOptions,
                 point.PointerInfo,
@@ -104,7 +160,8 @@ public class BrushEngine : IDisposable
             lastPos = point.Position;
         }
 
-        lastAppliedPointIndex = points.Count - 1;
+        lastPressure = pointsHistory.Count > 0 ? pointsHistory[^1].PointerInfo.Pressure : 1.0;
+        lastAppliedHistoryIndex = pointsHistory.Count - 1;
     }
 
 

+ 18 - 14
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/BrushOutputNode.cs

@@ -46,7 +46,9 @@ public class BrushOutputNode : Node
     public InputProperty<bool> AllowSampleStacking { get; }
     public InputProperty<bool> AlwaysClear { get; }
     public InputProperty<bool> SnapToPixels { get; }
+
     public InputProperty<string> Tags { get; }
+
     // Indicate whether stamps from this brush can be reused when drawing with the same brush again. Optimization option.
     public InputProperty<bool> CanReuseStamps { get; }
 
@@ -57,13 +59,14 @@ public class BrushOutputNode : Node
     private TextureCache cache = new();
 
     private ChunkyImage? previewChunkyImage;
-    private BrushEngine previewEngine = new BrushEngine();
+    private BrushEngine previewEngine = new BrushEngine() { PressureSmoothingWindowSize = 0};
 
     protected override bool ExecuteOnlyOnCacheChange => true;
     public Guid PersistentId { get; private set; } = Guid.NewGuid();
 
     public const string PreviewSvg =
         "M0.25 99.4606C0.25 99.4606 60.5709 79.3294 101.717 99.4606C147.825 122.019 199.75 99.4606 199.75 99.4606";
+
     public const int YOffsetInPreview = -88;
 
     private VectorPath? previewVectorPath;
@@ -116,7 +119,8 @@ public class BrushOutputNode : Node
         additionalData["PersistentId"] = PersistentId;
     }
 
-    internal override void DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
+    internal override void DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data,
+        List<IChangeInfo> infos)
     {
         base.DeserializeAdditionalData(target, data, infos);
         if (data.TryGetValue("PersistentId", out var persistentIdObj))
@@ -207,30 +211,32 @@ public class BrushOutputNode : Node
         float offset = 0;
         float pressure;
         VecD pos;
+        List<RecordedPoint> points = new();
         while (offset <= target.CommittedSize.X)
         {
             pressure = (float)Math.Sin((offset / target.CommittedSize.X) * Math.PI);
             var vec4D = previewVectorPath.GetPositionAndTangentAtDistance(offset, false);
             pos = vec4D.XY;
             pos = new VecD(pos.X, pos.Y + maxSize / 2f) + shift;
+            
+            points.Add(new RecordedPoint((VecI)pos, new PointerInfo(pos, pressure, 0, VecD.Zero, vec4D.ZW),
+                new KeyboardInfo(), new EditorData(Colors.White, Colors.Black)));
 
             previewEngine.ExecuteBrush(target,
-                new BrushData(context.Graph, Id) { StrokeWidth = maxSize, AntiAliasing = true },
-                [(VecI)pos], context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions,
-                new PointerInfo(pos, pressure, 0, VecD.Zero, vec4D.ZW),
-                new KeyboardInfo(),
-                new EditorData(Colors.White, Colors.Black));
-
+                new BrushData(context.Graph, Id) { StrokeWidth = maxSize, AntiAliasing = true }, points, context.FrameTime,
+                context.ProcessingColorSpace, context.DesiredSamplingOptions);
             offset += 1;
         }
     }
 
-    public IEnumerable<float> DrawStrokePreviewEnumerable(ChunkyImage target, RenderContext context, int maxSize, VecD shift = default)
+    public IEnumerable<float> DrawStrokePreviewEnumerable(ChunkyImage target, RenderContext context, int maxSize,
+        VecD shift = default)
     {
         if (previewVectorPath == null)
         {
             previewVectorPath = VectorPath.FromSvgPath(PreviewSvg);
         }
+        List<RecordedPoint> points = new();
 
         float offset = 0;
         float pressure;
@@ -241,14 +247,12 @@ public class BrushOutputNode : Node
             var vec4D = previewVectorPath.GetPositionAndTangentAtDistance(offset, false);
             pos = vec4D.XY;
             pos = new VecD(pos.X, pos.Y + maxSize / 2f) + shift;
+            points.Add(new RecordedPoint((VecI)pos, new PointerInfo(pos, pressure, 0, VecD.Zero, vec4D.ZW),
+                new KeyboardInfo(), new EditorData(Colors.White, Colors.Black)));
 
             previewEngine.ExecuteBrush(target,
                 new BrushData(context.Graph, Id) { StrokeWidth = maxSize, AntiAliasing = true },
-                [(VecI)pos], context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions,
-                new PointerInfo(pos, pressure, 0, VecD.Zero, vec4D.ZW),
-                new KeyboardInfo(),
-                new EditorData(Colors.White, Colors.Black));
-
+                points, context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions);
             offset += 1;
             yield return offset;
         }

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

@@ -61,15 +61,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     public void Update(VecD pos, float strokeWidth, PointerInfo pointerInfo, KeyboardInfo keyboardInfo,
         EditorData editorData, BrushData brushData)
     {
-        if (points.Count > 0)
-        {
-            var line = LineHelper.GetInterpolatedPoints(points[^1].Position, pos);
-            foreach (var linePt in line)
-            {
-                points.Add(new RecordedPoint(linePt, pointerInfo, keyboardInfo, editorData));
-            }
-        }
-
+        points.Add(new RecordedPoint(pos, pointerInfo, keyboardInfo, editorData));
         this.strokeWidth = strokeWidth;
         this.pointerInfo = pointerInfo;
         this.keyboardInfo = keyboardInfo;