Browse Source

Merge branch 'brush-engine' of https://github.com/PixiEditor/PixiEditor into brush-engine

flabbet 1 tuần trước cách đây
mục cha
commit
7da198898e

+ 1 - 1
samples/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
 <Project>
     <PropertyGroup>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.3.6</AvaloniaVersion>
+		    <AvaloniaVersion>11.3.9-cibuild0004033-alpha</AvaloniaVersion>
     </PropertyGroup>
     </PropertyGroup>
     <ItemGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

+ 15 - 0
src/ChunkyImageLib/ChunkyImage.cs

@@ -11,6 +11,7 @@ using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -935,6 +936,20 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         }
         }
     }
     }
 
 
+    /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public void EnqueueDrawPath(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap strokeCap,
+        Blender blender, PaintStyle style, bool antiAliasing, RectI? customBounds = null)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            PathOperation operation = new(path, paintable, strokeWidth, strokeCap, blender, style, antiAliasing,
+                customBounds);
+            EnqueueOperation(operation);
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawBresenhamLine(VecI from, VecI to, Paintable paintable, BlendMode blendMode)
     public void EnqueueDrawBresenhamLine(VecI from, VecI to, Paintable paintable, BlendMode blendMode)
     {
     {

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

@@ -2,6 +2,7 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
@@ -28,6 +29,15 @@ internal class PathOperation : IMirroredDrawOperation
         bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
         bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
     }
     }
 
 
+    public PathOperation(VectorPath path, Color color, float strokeWidth, StrokeCap cap, Blender blender, RectI? customBounds = null)
+    {
+        this.path = new VectorPath(path);
+        paint = new() { Color = color, Style = PaintStyle.Stroke, StrokeWidth = strokeWidth, StrokeCap = cap, Blender = blender };
+
+        RectI floatBounds = customBounds ?? (RectI)(path.TightBounds).RoundOutwards();
+        bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
+    }
+
     public PathOperation(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap cap, BlendMode blendMode,
     public PathOperation(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap cap, BlendMode blendMode,
         PaintStyle style, bool antiAliasing, RectI? customBounds = null)
         PaintStyle style, bool antiAliasing, RectI? customBounds = null)
     {
     {
@@ -39,6 +49,16 @@ internal class PathOperation : IMirroredDrawOperation
         bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
         bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
     }
     }
 
 
+    public PathOperation(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap cap, Blender blender, PaintStyle style, bool antiAliasing, RectI? customBounds)
+    {
+        this.antiAliasing = antiAliasing;
+        this.path = new VectorPath(path);
+        paint = new() { Paintable = paintable, Style = style, StrokeWidth = strokeWidth, StrokeCap = cap, Blender = blender };
+
+        RectI floatBounds = customBounds ?? (RectI)(path.Bounds).RoundOutwards();
+        bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
+    }
+
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
     {
         paint.IsAntiAliased = antiAliasing || targetChunk.Resolution != ChunkResolution.Full;
         paint.IsAntiAliased = antiAliasing || targetChunk.Resolution != ChunkResolution.Full;
@@ -68,8 +88,20 @@ internal class PathOperation : IMirroredDrawOperation
             newBounds = (RectI)newBounds.ReflectY((double)horAxisY).Round();
             newBounds = (RectI)newBounds.ReflectY((double)horAxisY).Round();
         if (paint.Paintable != null)
         if (paint.Paintable != null)
         {
         {
-            return new PathOperation(copy, paint.Paintable, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode,
-                paint.Style, antiAliasing, newBounds);
+            if( paint.Blender != null)
+            {
+                return new PathOperation(copy, paint.Paintable, paint.StrokeWidth, paint.StrokeCap, paint.Blender, paint.Style, antiAliasing, newBounds);
+            }
+            else
+            {
+                return new PathOperation(copy, paint.Paintable, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode,
+                    paint.Style, antiAliasing, newBounds);
+            }
+        }
+
+        if (paint.Blender != null)
+        {
+            return new PathOperation(copy, paint.Color, paint.StrokeWidth, paint.StrokeCap, paint.Blender, newBounds);
         }
         }
 
 
         return new PathOperation(copy, paint.Color, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode, newBounds);
         return new PathOperation(copy, paint.Color, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode, newBounds);

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit e95271836953c7a9dbc0f3b2ee02f732f670e3a5
+Subproject commit bc83f2b962f7e2ff4140bda8dd421aa2309f2488

+ 46 - 10
src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushEngine.cs

@@ -4,6 +4,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -42,6 +43,10 @@ public class BrushEngine : IDisposable
     public void ResetState()
     public void ResetState()
     {
     {
         lastAppliedPointIndex = -1;
         lastAppliedPointIndex = -1;
+        lastAppliedHistoryIndex = -1;
+        lastPos = VecD.Zero;
+        lastPressure = 1.0;
+        startPos = VecD.Zero;
         drawnOnce = false;
         drawnOnce = false;
         pointsHistory.Clear();
         pointsHistory.Clear();
     }
     }
@@ -142,21 +147,26 @@ public class BrushEngine : IDisposable
 
 
         float strokeWidth = brushData.StrokeWidth;
         float strokeWidth = brushData.StrokeWidth;
         float spacing = brushNode.Spacing.Value / 100f;
         float spacing = brushNode.Spacing.Value / 100f;
+        int startingIndex = Math.Max(lastAppliedHistoryIndex, 0);
+        float spacingPressure = pointsHistory.Count < startingIndex + 1
+            ? (float)lastPressure
+            : pointsHistory[startingIndex].PointerInfo.Pressure;
 
 
         for (int i = Math.Max(lastAppliedHistoryIndex, 0); i < pointsHistory.Count; i++)
         for (int i = Math.Max(lastAppliedHistoryIndex, 0); i < pointsHistory.Count; i++)
         {
         {
             var point = pointsHistory[i];
             var point = pointsHistory[i];
 
 
-            float spacingPixels = (strokeWidth * point.PointerInfo.Pressure) * spacing;
+            float spacingPixels = (strokeWidth * spacingPressure) * spacing;
             if (VecD.Distance(lastPos, point.Position) < spacingPixels)
             if (VecD.Distance(lastPos, point.Position) < spacingPixels)
                 continue;
                 continue;
-            
 
 
             ExecuteVectorShapeBrush(target, brushNode, brushData, point.Position, frameTime, cs, samplingOptions,
             ExecuteVectorShapeBrush(target, brushNode, brushData, point.Position, frameTime, cs, samplingOptions,
                 point.PointerInfo,
                 point.PointerInfo,
                 point.KeyboardInfo,
                 point.KeyboardInfo,
                 point.EditorData);
                 point.EditorData);
 
 
+            spacingPressure = brushNode.Pressure.Value;
+
             lastPos = point.Position;
             lastPos = point.Position;
         }
         }
 
 
@@ -315,9 +325,11 @@ public class BrushEngine : IDisposable
         var stroke = brushNode.Stroke.Value;
         var stroke = brushNode.Stroke.Value;
         bool snapToPixels = brushNode.SnapToPixels.Value;
         bool snapToPixels = brushNode.SnapToPixels.Value;
         bool canReuseStamps = brushNode.CanReuseStamps.Value;
         bool canReuseStamps = brushNode.CanReuseStamps.Value;
+        Blender? stampBlender = brushNode.UseCustomStampBlender.Value ? brushNode.LastStampBlender : null;
+        //Blender? imageBlender = brushNode.UseCustomImageBlender.Value ? brushNode.LastImageBlender : null;
 
 
         if (PaintBrush(target, autoPosition, vectorShape, rect, fitToStrokeSize, pressure, content, contentTexture,
         if (PaintBrush(target, autoPosition, vectorShape, rect, fitToStrokeSize, pressure, content, contentTexture,
-                brushNode.StampBlendMode.Value, antiAliasing, fill, stroke, snapToPixels, canReuseStamps))
+                stampBlender, brushNode.StampBlendMode.Value, antiAliasing, fill, stroke, snapToPixels, canReuseStamps))
         {
         {
             lastPos = point;
             lastPos = point;
         }
         }
@@ -325,7 +337,7 @@ public class BrushEngine : IDisposable
 
 
     public bool PaintBrush(ChunkyImage target, bool autoPosition, ShapeVectorData vectorShape,
     public bool PaintBrush(ChunkyImage target, bool autoPosition, ShapeVectorData vectorShape,
         RectD rect, bool fitToStrokeSize, float pressure, Painter? content,
         RectD rect, bool fitToStrokeSize, float pressure, Painter? content,
-        Texture? contentTexture, DrawingApiBlendMode blendMode, bool antiAliasing, Paintable fill, Paintable stroke,
+        Texture? contentTexture, Blender? blender, DrawingApiBlendMode blendMode, bool antiAliasing, Paintable fill, Paintable stroke,
         bool snapToPixels, bool canReuseStamps)
         bool snapToPixels, bool canReuseStamps)
     {
     {
         var path = vectorShape.ToPath(true);
         var path = vectorShape.ToPath(true);
@@ -358,15 +370,31 @@ public class BrushEngine : IDisposable
 
 
         if (paintable is { AnythingVisible: true })
         if (paintable is { AnythingVisible: true })
         {
         {
-            target.EnqueueDrawPath(path, paintable, vectorShape.StrokeWidth,
-                strokeCap, blendMode, strokeStyle, antiAliasing, null);
+            if (blender != null)
+            {
+                target.EnqueueDrawPath(path, paintable, vectorShape.StrokeWidth,
+                    strokeCap, blender, strokeStyle, antiAliasing, null);
+            }
+            else
+            {
+                target.EnqueueDrawPath(path, paintable, vectorShape.StrokeWidth,
+                    strokeCap, blendMode, strokeStyle, antiAliasing, null);
+            }
         }
         }
 
 
         if (fill is { AnythingVisible: true } && stroke is { AnythingVisible: true })
         if (fill is { AnythingVisible: true } && stroke is { AnythingVisible: true })
         {
         {
             strokeStyle = PaintStyle.Stroke;
             strokeStyle = PaintStyle.Stroke;
-            target.EnqueueDrawPath(path, stroke, vectorShape.StrokeWidth,
-                strokeCap, blendMode, strokeStyle, antiAliasing, null);
+            if (blender != null)
+            {
+                target.EnqueueDrawPath(path, stroke, vectorShape.StrokeWidth,
+                    strokeCap, blender, strokeStyle, antiAliasing, null);
+            }
+            else
+            {
+                target.EnqueueDrawPath(path, stroke, vectorShape.StrokeWidth,
+                    strokeCap, blendMode, strokeStyle, antiAliasing, null);
+            }
         }
         }
 
 
         if (content != null)
         if (content != null)
@@ -390,8 +418,16 @@ public class BrushEngine : IDisposable
                     brushPaintable = new TexturePaintable(new Texture(contentTexture), true);
                     brushPaintable = new TexturePaintable(new Texture(contentTexture), true);
                 }
                 }
 
 
-                target.EnqueueDrawPath(path, brushPaintable, vectorShape.StrokeWidth,
-                    StrokeCap.Butt, blendMode, PaintStyle.Fill, antiAliasing, null);
+                if (blender != null)
+                {
+                    target.EnqueueDrawPath(path, brushPaintable, vectorShape.StrokeWidth,
+                        StrokeCap.Butt, blender, PaintStyle.Fill, antiAliasing, null);
+                }
+                else
+                {
+                    target.EnqueueDrawPath(path, brushPaintable, vectorShape.StrokeWidth,
+                        StrokeCap.Butt, blendMode, PaintStyle.Fill, antiAliasing, null);
+                }
             }
             }
         }
         }
 
 

+ 74 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/BrushOutputNode.cs

@@ -3,6 +3,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -31,6 +32,21 @@ public class BrushOutputNode : Node
     public const int StrokePreviewSizeX = 200;
     public const int StrokePreviewSizeX = 200;
     public const int StrokePreviewSizeY = 50;
     public const int StrokePreviewSizeY = 50;
 
 
+    public const string DefaultBlenderCode = @"
+    vec4 main(vec4 src, vec4 dst) {
+    	return src + (1 - src.a) * dst;
+    }
+";
+
+    private string? lastStampBlenderCode = "";
+    private string? lastImageBlenderCode = "";
+
+    public Blender? LastStampBlender => cachedStampBlender;
+    public Blender? LastImageBlender => cachedImageBlender;
+
+    private Blender? cachedStampBlender = null;
+    private Blender? cachedImageBlender = null;
+
     public InputProperty<string> BrushName { get; }
     public InputProperty<string> BrushName { get; }
     public InputProperty<ShapeVectorData> VectorShape { get; }
     public InputProperty<ShapeVectorData> VectorShape { get; }
     public InputProperty<Paintable> Stroke { get; }
     public InputProperty<Paintable> Stroke { get; }
@@ -38,6 +54,8 @@ public class BrushOutputNode : Node
     public RenderInputProperty Content { get; }
     public RenderInputProperty Content { get; }
     public InputProperty<Drawie.Backend.Core.Surfaces.BlendMode> StampBlendMode { get; }
     public InputProperty<Drawie.Backend.Core.Surfaces.BlendMode> StampBlendMode { get; }
     public InputProperty<Drawie.Backend.Core.Surfaces.BlendMode> ImageBlendMode { get; }
     public InputProperty<Drawie.Backend.Core.Surfaces.BlendMode> ImageBlendMode { get; }
+    public InputProperty<bool> UseCustomStampBlender { get; }
+    public InputProperty<string> CustomStampBlenderCode { get; }
     public InputProperty<Matrix3X3> Transform { get; }
     public InputProperty<Matrix3X3> Transform { get; }
     public InputProperty<float> Pressure { get; }
     public InputProperty<float> Pressure { get; }
     public InputProperty<float> Spacing { get; }
     public InputProperty<float> Spacing { get; }
@@ -59,7 +77,7 @@ public class BrushOutputNode : Node
     private TextureCache cache = new();
     private TextureCache cache = new();
 
 
     private ChunkyImage? previewChunkyImage;
     private ChunkyImage? previewChunkyImage;
-    private BrushEngine previewEngine = new BrushEngine() { PressureSmoothingWindowSize = 0};
+    private BrushEngine previewEngine = new BrushEngine() { PressureSmoothingWindowSize = 0 };
 
 
     protected override bool ExecuteOnlyOnCacheChange => true;
     protected override bool ExecuteOnlyOnCacheChange => true;
     public Guid PersistentId { get; private set; } = Guid.NewGuid();
     public Guid PersistentId { get; private set; } = Guid.NewGuid();
@@ -68,6 +86,9 @@ public class BrushOutputNode : Node
         "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";
         "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;
     public const int YOffsetInPreview = -88;
+    public const string UseCustomStampBlenderProperty = "UseCustomStampBlender";
+    public const string CustomStampBlenderCodeProperty = "CustomStampBlender";
+    public const string StampBlendModeProperty = "StampBlendMode";
 
 
     private VectorPath? previewVectorPath;
     private VectorPath? previewVectorPath;
 
 
@@ -81,8 +102,14 @@ public class BrushOutputNode : Node
         Transform = CreateInput<Matrix3X3>("Transform", "TRANSFORM", Matrix3X3.Identity);
         Transform = CreateInput<Matrix3X3>("Transform", "TRANSFORM", Matrix3X3.Identity);
         ImageBlendMode = CreateInput<Drawie.Backend.Core.Surfaces.BlendMode>("BlendMode", "BLEND_MODE",
         ImageBlendMode = CreateInput<Drawie.Backend.Core.Surfaces.BlendMode>("BlendMode", "BLEND_MODE",
             Drawie.Backend.Core.Surfaces.BlendMode.SrcOver);
             Drawie.Backend.Core.Surfaces.BlendMode.SrcOver);
-        StampBlendMode = CreateInput<Drawie.Backend.Core.Surfaces.BlendMode>("StampBlendMode", "STAMP_BLEND_MODE",
+        StampBlendMode = CreateInput<Drawie.Backend.Core.Surfaces.BlendMode>(StampBlendModeProperty, "STAMP_BLEND_MODE",
             Drawie.Backend.Core.Surfaces.BlendMode.SrcOver);
             Drawie.Backend.Core.Surfaces.BlendMode.SrcOver);
+
+        UseCustomStampBlender = CreateInput<bool>(UseCustomStampBlenderProperty, "USE_CUSTOM_STAMP_BLENDER", false);
+
+        CustomStampBlenderCode =
+            CreateInput<string>(CustomStampBlenderCodeProperty, "CUSTOM_STAMP_BLENDER_CODE", DefaultBlenderCode)
+                .WithRules(validator => validator.Custom(ValidateBlenderCode));
         CanReuseStamps = CreateInput<bool>("CanReuseStamps", "CAN_REUSE_STAMPS", false);
         CanReuseStamps = CreateInput<bool>("CanReuseStamps", "CAN_REUSE_STAMPS", false);
 
 
         Pressure = CreateInput<float>("Pressure", "PRESSURE", 1f);
         Pressure = CreateInput<float>("Pressure", "PRESSURE", 1f);
@@ -96,6 +123,23 @@ public class BrushOutputNode : Node
         Previous = CreateInput<IReadOnlyNodeGraph>("Previous", "PREVIOUS", null);
         Previous = CreateInput<IReadOnlyNodeGraph>("Previous", "PREVIOUS", null);
     }
     }
 
 
+    private ValidatorResult ValidateBlenderCode(object? value)
+    {
+        if (value is string code)
+        {
+            Blender? blender = Blender.CreateFromString(code, out string? error);
+            if (blender != null)
+            {
+                blender.Dispose();
+                return new ValidatorResult(true, null);
+            }
+
+            return new ValidatorResult(false, error);
+        }
+
+        return new ValidatorResult(false, "Blender code must be a string.");
+    }
+
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
         if (Content.Value != null)
         if (Content.Value != null)
@@ -110,6 +154,22 @@ public class BrushOutputNode : Node
             }
             }
         }
         }
 
 
+        if (UseCustomStampBlender.Value)
+        {
+            if (CustomStampBlenderCode.Value != lastStampBlenderCode || cachedStampBlender == null)
+            {
+                cachedStampBlender?.Dispose();
+                cachedStampBlender = Blender.CreateFromString(CustomStampBlenderCode.Value, out _);
+                lastStampBlenderCode = CustomStampBlenderCode.Value;
+            }
+        }
+        else
+        {
+            cachedStampBlender?.Dispose();
+            cachedStampBlender = null;
+            lastStampBlenderCode = "";
+        }
+
         RenderPreviews(context.GetPreviewTexturesForNode(Id), context);
         RenderPreviews(context.GetPreviewTexturesForNode(Id), context);
     }
     }
 
 
@@ -180,6 +240,8 @@ public class BrushOutputNode : Node
         const int spacing = 10;
         const int spacing = 10;
         const int marginEdges = 30;
         const int marginEdges = 30;
         VecD pos = VecD.Zero;
         VecD pos = VecD.Zero;
+        previewEngine.ResetState();
+
         for (var i = 0; i < sizes.Length; i++)
         for (var i = 0; i < sizes.Length; i++)
         {
         {
             var size = sizes[i];
             var size = sizes[i];
@@ -193,6 +255,7 @@ public class BrushOutputNode : Node
                 new KeyboardInfo(),
                 new KeyboardInfo(),
                 new EditorData(Colors.White, Colors.Black));
                 new EditorData(Colors.White, Colors.Black));
         }
         }
+        previewChunkyImage.CommitChanges();
 
 
         DrawStrokePreview(previewChunkyImage, context, maxSize);
         DrawStrokePreview(previewChunkyImage, context, maxSize);
 
 
@@ -212,18 +275,21 @@ public class BrushOutputNode : Node
         float pressure;
         float pressure;
         VecD pos;
         VecD pos;
         List<RecordedPoint> points = new();
         List<RecordedPoint> points = new();
+        previewEngine.ResetState();
+
         while (offset <= target.CommittedSize.X)
         while (offset <= target.CommittedSize.X)
         {
         {
             pressure = (float)Math.Sin((offset / target.CommittedSize.X) * Math.PI);
             pressure = (float)Math.Sin((offset / target.CommittedSize.X) * Math.PI);
             var vec4D = previewVectorPath.GetPositionAndTangentAtDistance(offset, false);
             var vec4D = previewVectorPath.GetPositionAndTangentAtDistance(offset, false);
             pos = vec4D.XY;
             pos = vec4D.XY;
             pos = new VecD(pos.X, pos.Y + maxSize / 2f) + shift;
             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),
             points.Add(new RecordedPoint((VecI)pos, new PointerInfo(pos, pressure, 0, VecD.Zero, vec4D.ZW),
                 new KeyboardInfo(), new EditorData(Colors.White, Colors.Black)));
                 new KeyboardInfo(), new EditorData(Colors.White, Colors.Black)));
 
 
             previewEngine.ExecuteBrush(target,
             previewEngine.ExecuteBrush(target,
-                new BrushData(context.Graph, Id) { StrokeWidth = maxSize, AntiAliasing = true }, points, context.FrameTime,
+                new BrushData(context.Graph, Id) { StrokeWidth = maxSize, AntiAliasing = true }, points,
+                context.FrameTime,
                 context.ProcessingColorSpace, context.DesiredSamplingOptions);
                 context.ProcessingColorSpace, context.DesiredSamplingOptions);
             offset += 1;
             offset += 1;
         }
         }
@@ -236,11 +302,14 @@ public class BrushOutputNode : Node
         {
         {
             previewVectorPath = VectorPath.FromSvgPath(PreviewSvg);
             previewVectorPath = VectorPath.FromSvgPath(PreviewSvg);
         }
         }
+
         List<RecordedPoint> points = new();
         List<RecordedPoint> points = new();
 
 
         float offset = 0;
         float offset = 0;
         float pressure;
         float pressure;
         VecD pos;
         VecD pos;
+        previewEngine.ResetState();
+
         while (offset <= target.CommittedSize.X)
         while (offset <= target.CommittedSize.X)
         {
         {
             pressure = (float)Math.Sin((offset / target.CommittedSize.X) * Math.PI);
             pressure = (float)Math.Sin((offset / target.CommittedSize.X) * Math.PI);
@@ -260,6 +329,7 @@ public class BrushOutputNode : Node
 
 
     public void DrawPointPreview(ChunkyImage img, RenderContext context, int size, VecD pos)
     public void DrawPointPreview(ChunkyImage img, RenderContext context, int size, VecD pos)
     {
     {
+        previewEngine.ResetState();
         previewEngine.ExecuteBrush(img,
         previewEngine.ExecuteBrush(img,
             new BrushData(context.Graph, Id) { StrokeWidth = size, AntiAliasing = true },
             new BrushData(context.Graph, Id) { StrokeWidth = size, AntiAliasing = true },
             pos, context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions,
             pos, context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions,

BIN
src/PixiEditor/Data/Brushes/BasicFlowOpacity.pixi


BIN
src/PixiEditor/Data/Brushes/Texture.pixi


+ 17 - 5
src/PixiEditor/Data/Localization/Languages/en.json

@@ -988,7 +988,6 @@
   "DOWNLOAD_UPDATE": "Download",
   "DOWNLOAD_UPDATE": "Download",
   "DOWNLOADING_UPDATE": "Downloading update...",
   "DOWNLOADING_UPDATE": "Downloading update...",
   "CHECKING_FOR_UPDATES": "Checking for updates...",
   "CHECKING_FOR_UPDATES": "Checking for updates...",
-  "PAINT_SHAPE_SETTING": "Brush shape",
   "BOOL_OPERATION_NODE": "Boolean Operation",
   "BOOL_OPERATION_NODE": "Boolean Operation",
   "FIRST_SHAPE": "First shape",
   "FIRST_SHAPE": "First shape",
   "SECOND_SHAPE": "Second shape",
   "SECOND_SHAPE": "Second shape",
@@ -1150,8 +1149,6 @@
   "TOGGLE_TINTING_SELECTION": "Toggle selection tinting",
   "TOGGLE_TINTING_SELECTION": "Toggle selection tinting",
   "TOGGLE_TINTING_SELECTION_DESCRIPTIVE": "Toggle selection tinting",
   "TOGGLE_TINTING_SELECTION_DESCRIPTIVE": "Toggle selection tinting",
   "TINT_SELECTION": "Selection tinting",
   "TINT_SELECTION": "Selection tinting",
-  "PAINT_BRUSH_SHAPE_CIRCLE": "Circle",
-  "PAINT_BRUSH_SHAPE_SQUARE": "Square",
   "BRIGHTNESS_MODE_DEFAULT": "Default",
   "BRIGHTNESS_MODE_DEFAULT": "Default",
   "BRIGHTNESS_MODE_REPEAT": "Repeat",
   "BRIGHTNESS_MODE_REPEAT": "Repeat",
   "ROUND_STROKE_CAP": "Round",
   "ROUND_STROKE_CAP": "Round",
@@ -1207,7 +1204,6 @@
   "BRUSH": "Brush",
   "BRUSH": "Brush",
   "UNLINK": "Unlink",
   "UNLINK": "Unlink",
   "UNLINK_DESCRIPTIVE": "Unlink this layer from the original document",
   "UNLINK_DESCRIPTIVE": "Unlink this layer from the original document",
-  "CANNOT_SAVE_NESTED_DOCUMENT_PARENT_CLOSED": "Cannot save nested document because the parent document is not open.",
   "INPUTS": "Inputs",
   "INPUTS": "Inputs",
   "BRUSHES": "Brushes",
   "BRUSHES": "Brushes",
   "EDITOR": "Editor",
   "EDITOR": "Editor",
@@ -1266,5 +1262,21 @@
   "TEXTURE": "Texture",
   "TEXTURE": "Texture",
   "PAINTER": "Painter",
   "PAINTER": "Painter",
   "VECTOR_PATH": "Vector Path",
   "VECTOR_PATH": "Vector Path",
-  "CAN_REUSE_STAMPS": "Can Reuse Stamps"
+  "CAN_REUSE_STAMPS": "Can Reuse Stamps",
+  "USE_CUSTOM_STAMP_BLENDER": "Use Custom Stamp Blender",
+  "CUSTOM_STAMP_BLENDER_CODE": "Stamp Blender",
+  "CLEAR_BLEND_MODE": "Clear",
+  "REPLACE_BLEND_MODE": "Replace",
+  "BEHIND_BLEND_MODE": "Behind",
+  "MASK_INSIDE_BLEND_MODE": "Mask Inside",
+  "KEEP_INSIDE_BLEND_MODE": "Keep Inside",
+  "OUTSIDE_BLEND_MODE": "Outside",
+  "ON_TOP_OF_BLEND_MODE": "On Top Of",
+  "KEEP_UNDER_BLEND_MODE": "Keep Under",
+  "XOR_BLEND_MODE": "XOR",
+  "MODULATE_BLEND_MODE": "Modulate",
+  "TARGET_BLEND_MODE": "Target",
+  "LAST_APPLIED_POINT": "Last Applied Point",
+  "VIEWPORT_INFO_NODE": "Viewport Info",
+  "EQUALS_NODE": "Equals"
 }
 }

+ 14 - 0
src/PixiEditor/Helpers/LocalizedKeyAttribute.cs

@@ -0,0 +1,14 @@
+namespace PixiEditor.Helpers;
+
+public interface ILocalizedKeyInfo
+{
+    public string LocalizationKey { get; }
+    public string Location { get; }
+}
+
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+public class LocalizedKeyAttribute(string key, string whereItIsUsed) : Attribute, ILocalizedKeyInfo
+{
+    public string LocalizationKey { get; } = key;
+    public string Location { get; } = whereItIsUsed;
+}

+ 1 - 1
src/PixiEditor/Helpers/SurfaceHelpers.cs

@@ -40,7 +40,7 @@ public static class SurfaceHelpers
 
 
     public static unsafe byte[] ToByteArray(this Surface surface, ColorType colorType = ColorType.Bgra8888, AlphaType alphaType = AlphaType.Premul, ColorSpace colorSpace = null)
     public static unsafe byte[] ToByteArray(this Surface surface, ColorType colorType = ColorType.Bgra8888, AlphaType alphaType = AlphaType.Premul, ColorSpace colorSpace = null)
     {
     {
-        using var ctx = IDrawieInteropContext.Current.EnsureContext();
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         int width = surface.Size.X;
         int width = surface.Size.X;
         int height = surface.Size.Y;
         int height = surface.Size.Y;
         var imageInfo = new ImageInfo(width, height, colorType, alphaType, colorSpace == null ? surface.ImageInfo.ColorSpace : colorSpace);
         var imageInfo = new ImageInfo(width, height, colorType, alphaType, colorSpace == null ? surface.ImageInfo.ColorSpace : colorSpace);

+ 4 - 0
src/PixiEditor/Models/MarkedLocalizationKeys.cs

@@ -0,0 +1,4 @@
+using PixiEditor.Helpers;
+
+[assembly:LocalizedKey("HARDNESS_SETTING", "Used as a blackboard variable within brush inside built-in .pixi files.")]
+[assembly:LocalizedKey("SPACING_SETTING", "Used as a blackboard variable within brush inside built-in .pixi files.")]

+ 71 - 0
src/PixiEditor/Models/Serialization/Factories/Paintables/TexturePaintableSerializationFactory.cs

@@ -0,0 +1,71 @@
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Silk.NET.OpenGL;
+
+namespace PixiEditor.Models.Serialization.Factories.Paintables;
+
+public class TexturePaintableSerializationFactory : SerializationFactory<byte[], TexturePaintable>,
+    IPaintableSerializationFactory
+{
+    public override string DeserializationId { get; } = "PixiEditor.TexturePaintable";
+
+    private readonly TextureSerializationFactory textureFactory = new TextureSerializationFactory();
+
+    public override byte[] Serialize(TexturePaintable original)
+    {
+        ByteBuilder builder = new();
+        Serialize(original, builder);
+
+        return builder.Build();
+    }
+
+    public override bool TryDeserialize(object serialized, out TexturePaintable original,
+        (string serializerName, string serializerVersion) serializerData)
+    {
+        if (serialized is not byte[] bytes)
+        {
+            original = null!;
+            return false;
+        }
+
+        ByteExtractor extractor = new(bytes);
+        original = TryDeserialize(extractor) as TexturePaintable;
+
+        return true;
+    }
+
+    public Paintable TryDeserialize(ByteExtractor extractor)
+    {
+        return TryDeserialize(extractor, default);
+    }
+
+    public Paintable TryDeserialize(ByteExtractor extractor,
+        (string serializerName, string serializerVersion) serializerData)
+    {
+        textureFactory.Config = Config;
+        textureFactory.ResourceLocator = ResourceLocator;
+
+        int length = extractor.GetInt();
+        var textureData = extractor.GetByteSpan(length).ToArray();
+        if (textureFactory.TryDeserialize(textureData, out var tex, serializerData))
+        {
+            return new TexturePaintable(tex);
+        }
+
+        return null!;
+    }
+
+    public void Serialize(Paintable paintable, ByteBuilder builder)
+    {
+        if (paintable is not TexturePaintable texturePaintable)
+        {
+            throw new ArgumentException("Paintable is not a TexturePaintable", nameof(paintable));
+        }
+
+        textureFactory.Config = Config;
+        textureFactory.ResourceLocator = ResourceLocator;
+
+        var array = textureFactory.Serialize(texturePaintable.Image);
+        builder.AddInt(array.Length);
+        builder.AddByteArray(array);
+    }
+}

+ 17 - 1
src/PixiEditor/ViewModels/Document/Nodes/Brushes/BrushOutputNodeViewModel.cs

@@ -1,5 +1,9 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+using Drawie.Backend.Core.Bridge;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+using PixiEditor.Models.Events;
+using PixiEditor.Models.Handlers;
 using PixiEditor.ViewModels.Nodes;
 using PixiEditor.ViewModels.Nodes;
+using PixiEditor.ViewModels.Nodes.Properties;
 
 
 namespace PixiEditor.ViewModels.Document.Nodes.Brushes;
 namespace PixiEditor.ViewModels.Document.Nodes.Brushes;
 
 
@@ -10,5 +14,17 @@ internal class BrushOutputNodeViewModel : NodeViewModel<BrushOutputNode>
     {
     {
         InputPropertyMap[BrushOutputNode.BrushNameProperty].SocketEnabled = false;
         InputPropertyMap[BrushOutputNode.BrushNameProperty].SocketEnabled = false;
         InputPropertyMap[BrushOutputNode.FitToStrokeSizeProperty].SocketEnabled = false;
         InputPropertyMap[BrushOutputNode.FitToStrokeSizeProperty].SocketEnabled = false;
+        InputPropertyMap[BrushOutputNode.UseCustomStampBlenderProperty].ValueChanged += OnValueChanged;
+        if(InputPropertyMap[BrushOutputNode.CustomStampBlenderCodeProperty] is StringPropertyViewModel codeProperty)
+        {
+            codeProperty.IsVisible = (bool)InputPropertyMap[BrushOutputNode.UseCustomStampBlenderProperty].Value;
+            codeProperty.Kind = DrawingBackendApi.Current.ShaderImplementation.ShaderLanguageExtension;
+        }
+    }
+
+    private void OnValueChanged(INodePropertyHandler property, NodePropertyValueChangedArgs args)
+    {
+        InputPropertyMap[BrushOutputNode.CustomStampBlenderCodeProperty].IsVisible = (bool)args.NewValue;
+        InputPropertyMap[BrushOutputNode.StampBlendModeProperty].IsVisible = !(bool)args.NewValue;
     }
     }
 }
 }

+ 6 - 0
src/PixiEditor/ViewModels/Tools/Tools/BrushBasedToolViewModel.cs

@@ -148,6 +148,12 @@ internal class BrushBasedToolViewModel : ToolViewModel, IBrushToolHandler
         }
         }
     }
     }
 
 
+    protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
+    {
+        OnDeselecting(false);
+        OnToolSelected(false);
+    }
+
     private void AddBrushShapeSettings()
     private void AddBrushShapeSettings()
     {
     {
         foreach (var setting in brushShapeSettings)
         foreach (var setting in brushShapeSettings)

+ 6 - 2
src/PixiEditor/Views/Input/BrushItem.axaml.cs

@@ -4,6 +4,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Metadata;
 using Avalonia.Input;
 using Avalonia.Input;
 using Avalonia.Markup.Xaml;
 using Avalonia.Markup.Xaml;
+using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
 using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
@@ -11,6 +12,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -128,7 +130,8 @@ internal partial class BrushItem : UserControl
         DrawingStrokeTexture = previewTexture;
         DrawingStrokeTexture = previewTexture;
 
 
         previewTexture.DrawingSurface.Canvas.Clear();
         previewTexture.DrawingSurface.Canvas.Clear();
-        previewImage.CancelChanges();
+        previewImage.EnqueueClear();
+        previewImage.CommitChanges();
         enumerator = brushNode.DrawStrokePreviewEnumerable(previewImage, CreateContext(),
         enumerator = brushNode.DrawStrokePreviewEnumerable(previewImage, CreateContext(),
             BrushOutputNode.StrokePreviewSizeY / 2,
             BrushOutputNode.StrokePreviewSizeY / 2,
             new VecD(0, BrushOutputNode.YOffsetInPreview)).GetEnumerator();
             new VecD(0, BrushOutputNode.YOffsetInPreview)).GetEnumerator();
@@ -158,11 +161,12 @@ internal partial class BrushItem : UserControl
                     return false;
                     return false;
                 }
                 }
 
 
+                using Paint srcOver = new() { BlendMode = BlendMode.Src, Style = PaintStyle.Fill };
                 previewImage.DrawMostUpToDateRegionOn(
                 previewImage.DrawMostUpToDateRegionOn(
                     new RectI(0, 0, previewImage.CommittedSize.X, previewImage.CommittedSize.Y),
                     new RectI(0, 0, previewImage.CommittedSize.X, previewImage.CommittedSize.Y),
                     ChunkResolution.Full,
                     ChunkResolution.Full,
                     previewTexture.DrawingSurface.Canvas,
                     previewTexture.DrawingSurface.Canvas,
-                    VecI.Zero, null, SamplingOptions.Bilinear);
+                    VecI.Zero, srcOver);
 
 
                 StrokePreviewControl.QueueNextFrame();
                 StrokePreviewControl.QueueNextFrame();
             }
             }

+ 1 - 1
tests/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -21,7 +21,7 @@ public class ChunkyImageTests : PixiEditorTest
         image.EnqueueDrawRectangle(new(new(5, 5), new(80, 80), 0, 0, 2, Colors.AliceBlue, Colors.Snow));
         image.EnqueueDrawRectangle(new(new(5, 5), new(80, 80), 0, 0, 2, Colors.AliceBlue, Colors.Snow));
         using (Chunk target = Chunk.Create(ColorSpace.CreateSrgb()))
         using (Chunk target = Chunk.Create(ColorSpace.CreateSrgb()))
         {
         {
-            image.DrawMostUpToDateChunkOn(new(0, 0), ChunkResolution.Full, target.Surface.DrawingSurface, VecI.Zero);
+            image.DrawMostUpToDateChunkOn(new(0, 0), ChunkResolution.Full, target.Surface.DrawingSurface.Canvas, VecI.Zero);
             image.CancelChanges();
             image.CancelChanges();
             image.EnqueueResize(new(ChunkyImage.FullChunkSize * 4, ChunkyImage.FullChunkSize * 4));
             image.EnqueueResize(new(ChunkyImage.FullChunkSize * 4, ChunkyImage.FullChunkSize * 4));
             image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 0, 2, Colors.AliceBlue, Colors.Snow,
             image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 0, 2, Colors.AliceBlue, Colors.Snow,

+ 1 - 1
tests/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
 <Project>
     <PropertyGroup>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		<AvaloniaVersion>11.3.6</AvaloniaVersion>
+		<AvaloniaVersion>11.3.9-cibuild0004033-alpha</AvaloniaVersion>
     </PropertyGroup>
     </PropertyGroup>
     <ItemGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

+ 22 - 0
tests/PixiEditor.Backend.Tests/MockDocument.cs

@@ -1,5 +1,6 @@
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -74,6 +75,7 @@ public class MockDocument : IReadOnlyDocument
 
 
     public IReadOnlyReferenceLayer? ReferenceLayer { get; }
     public IReadOnlyReferenceLayer? ReferenceLayer { get; }
     public DocumentRenderer Renderer { get; }
     public DocumentRenderer Renderer { get; }
+    public IReadOnlyBlackboard Blackboard { get; }
     public ColorSpace ProcessingColorSpace { get; }
     public ColorSpace ProcessingColorSpace { get; }
     public void InitProcessingColorSpace(ColorSpace processingColorSpace)
     public void InitProcessingColorSpace(ColorSpace processingColorSpace)
     {
     {
@@ -89,4 +91,24 @@ public class MockDocument : IReadOnlyDocument
     {
     {
         throw new NotImplementedException();
         throw new NotImplementedException();
     }
     }
+
+    public ICrossDocumentPipe<IReadOnlyNodeGraph> CreateGraphPipe()
+    {
+        throw new NotImplementedException();
+    }
+
+    public IReadOnlyDocument Clone(bool preserveDocumentId = false)
+    {
+        throw new NotImplementedException();
+    }
+
+    public IReadOnlyStructureNode[] GetStructureTreeInOrder()
+    {
+        throw new NotImplementedException();
+    }
+
+    public object Clone()
+    {
+        throw new NotImplementedException();
+    }
 }
 }

+ 3 - 2
tests/PixiEditor.Backend.Tests/NodeSystemTests.cs

@@ -24,7 +24,8 @@ public class NodeSystemTests : PixiEditorTest
     private Type[] knownNonSerializableTypes = new[]
     private Type[] knownNonSerializableTypes = new[]
     {
     {
         typeof(Filter),
         typeof(Filter),
-        typeof(Painter)
+        typeof(Painter),
+        typeof(Object) // Objects are assumed to be only passed from other node and not serialized directly
     };
     };
 
 
     public NodeSystemTests(ITestOutputHelper output)
     public NodeSystemTests(ITestOutputHelper output)
@@ -98,7 +99,7 @@ public class NodeSystemTests : PixiEditorTest
             Assert.NotNull(node);
             Assert.NotNull(node);
 
 
             Dictionary<string, object> data = new Dictionary<string, object>();
             Dictionary<string, object> data = new Dictionary<string, object>();
-            node.SerializeAdditionalData(data);
+            node.SerializeAdditionalData(target, data);
             Assert.NotNull(data);
             Assert.NotNull(data);
         }
         }
     }
     }

+ 1 - 1
tests/PixiEditor.Tests/BlendingTests.cs

@@ -75,7 +75,7 @@ public class BlendingTests : PixiEditorTest
         secondImageLayer.BlendMode.NonOverridenValue = blendMode;
         secondImageLayer.BlendMode.NonOverridenValue = blendMode;
 
 
         Surface output = Surface.ForProcessing(VecI.One, ColorSpace.CreateSrgbLinear());
         Surface output = Surface.ForProcessing(VecI.One, ColorSpace.CreateSrgbLinear());
-        graph.Execute(new RenderContext(output.DrawingSurface, 0, ChunkResolution.Full, VecI.One, VecI.One, ColorSpace.CreateSrgbLinear(), SamplingOptions.Default, 1));
+        graph.Execute(new RenderContext(output.DrawingSurface.Canvas, 0, ChunkResolution.Full, VecI.One, VecI.One, ColorSpace.CreateSrgbLinear(), SamplingOptions.Default, new NodeGraph(), 1));
 
 
         Color result = output.GetSrgbPixel(VecI.Zero);
         Color result = output.GetSrgbPixel(VecI.Zero);
         Assert.Equal(expectedColor, result.ToRgbHex());
         Assert.Equal(expectedColor, result.ToRgbHex());

+ 1 - 1
tests/PixiEditor.Tests/PixiEditor.Tests.csproj

@@ -12,7 +12,7 @@
 
 
     <ItemGroup>
     <ItemGroup>
         <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
         <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
-        <PackageReference Include="Avalonia.Headless.XUnit" Version="$(AvaloniaVersion)" />
+        <PackageReference Include="Avalonia.Headless.XUnit" Version="11.3.8" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
         <PackageReference Include="xunit" Version="2.9.2"/>
         <PackageReference Include="xunit" Version="2.9.2"/>
         <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
         <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">

+ 7 - 2
tests/PixiEditor.Tests/RenderTests.cs

@@ -8,6 +8,7 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering.ContextData;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Position;
 using Xunit.Abstractions;
 using Xunit.Abstractions;
@@ -96,9 +97,13 @@ public class RenderTests : FullPixiEditorTest
             0,
             0,
             document.SizeBindable / 2f,
             document.SizeBindable / 2f,
             document.SizeBindable,
             document.SizeBindable,
-            Matrix3X3.Identity, null, "DEFAULT", SamplingOptions.Default, document.SizeBindable, ChunkResolution.Half,
+            new ViewportData(),
+            new PointerInfo(),
+            new KeyboardInfo(),
+            new EditorData(),
+            null, "DEFAULT", SamplingOptions.Default, document.SizeBindable, ChunkResolution.Half,
             Guid.NewGuid(), false, false, () => { });
             Guid.NewGuid(), false, false, () => { });
-        using var output = document.SceneRenderer.RenderScene(info, new AffectedArea(), null);
+        using var output = document.SceneRenderer.RenderScene(info, new AffectedArea());
 
 
         Color expectedColor = Colors.Yellow;
         Color expectedColor = Colors.Yellow;
 
 

+ 31 - 0
tests/PixiEditor.Tests/SerializationTests.cs

@@ -1,7 +1,11 @@
 using Avalonia.Headless.XUnit;
 using Avalonia.Headless.XUnit;
+using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Bridge;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
 using Drawie.Skia;
 using Drawie.Skia;
 using DrawiEngine;
 using DrawiEngine;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -11,6 +15,7 @@ using PixiEditor.Models.Serialization;
 using PixiEditor.Models.Serialization.Factories;
 using PixiEditor.Models.Serialization.Factories;
 using PixiEditor.Models.Serialization.Factories.Paintables;
 using PixiEditor.Models.Serialization.Factories.Paintables;
 using PixiEditor.Parser.Skia.Encoders;
 using PixiEditor.Parser.Skia.Encoders;
+using PixiEditor.ViewModels.Document;
 
 
 namespace PixiEditor.Tests;
 namespace PixiEditor.Tests;
 
 
@@ -50,6 +55,32 @@ public class SerializationTests : PixiEditorTest
         }
         }
     }
     }
 
 
+    [Fact]
+    public void TestTexturePaintableFactory()
+    {
+        Texture texture = new Texture(new VecI(32, 32));
+        texture.DrawingSurface.Canvas.DrawCircle(16, 16, 16, new Paint() { Color = Colors.Red, BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src });
+        TexturePaintable paintable = new TexturePaintable(texture);
+        TexturePaintableSerializationFactory factory = new TexturePaintableSerializationFactory();
+        factory.Config = new SerializationConfig(new QoiEncoder(), ColorSpace.CreateSrgbLinear());
+        var serialized = factory.Serialize(paintable);
+        var deserialized = (TexturePaintable)factory.Deserialize(serialized, default);
+
+        Assert.NotNull(deserialized);
+        var deserializedImage = deserialized.Image;
+        Assert.NotNull(deserializedImage);
+        Assert.Equal(texture.Size, deserializedImage.Size);
+        for (int y = 0; y < texture.Size.Y; y++)
+        {
+            for (int x = 0; x < texture.Size.X; x++)
+            {
+                Color originalPixel = texture.GetSrgbPixel(new VecI(x, y));
+                Color deserializedPixel = deserializedImage.GetSrgbPixel(new VecI(x, y));
+                Assert.Equal(originalPixel, deserializedPixel);
+            }
+        }
+    }
+
     [AvaloniaTheory]
     [AvaloniaTheory]
     [InlineData("Fibi")]
     [InlineData("Fibi")]
     [InlineData("Pond")]
     [InlineData("Pond")]