Browse Source

Added invert filter and improved validator

Krzysztof Krysiński 5 months ago
parent
commit
870c3d8aa8

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 753d3ff0fc5164fdb28356299b747dcc4e13412e
+Subproject commit 772f1dba3edf8527753dd04057016c3a5a820d5c

+ 29 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -32,7 +32,7 @@ public class InputProperty : IInputProperty
 
 
             var connectionValue = Connection.Value;
             var connectionValue = Connection.Value;
 
 
-            if(connectionValue is null)
+            if (connectionValue is null)
             {
             {
                 return null;
                 return null;
             }
             }
@@ -77,9 +77,25 @@ public class InputProperty : IInputProperty
         get => _internalValue;
         get => _internalValue;
         set
         set
         {
         {
-            _internalValue = value;
-            NonOverridenValueChanged?.Invoke(value);
-            NonOverridenValueSet(value);
+            object evaluatedValue = value;
+            if (value != null)
+            {
+                if (!value.GetType().IsAssignableTo(ValueType))
+                {
+                    if (!ConversionTable.TryConvert(value, ValueType, out object result))
+                    {
+                        evaluatedValue = null;
+                    }
+                    else
+                    {
+                        evaluatedValue = result;
+                    }
+                }
+            }
+
+            _internalValue = evaluatedValue;
+            NonOverridenValueChanged?.Invoke(evaluatedValue);
+            NonOverridenValueSet(evaluatedValue);
         }
         }
     }
     }
 
 
@@ -89,7 +105,7 @@ public class InputProperty : IInputProperty
         {
         {
             if (validator is null)
             if (validator is null)
             {
             {
-                validator = new PropertyValidator();
+                validator = new PropertyValidator(this);
             }
             }
 
 
             return validator;
             return validator;
@@ -209,7 +225,14 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
             if (value is T tValue)
             if (value is T tValue)
                 return tValue;
                 return tValue;
 
 
-            return (T)Validator.GetClosestValidValue(value);
+            var validated = Validator.GetClosestValidValue(value);
+
+            if (!ConversionTable.TryConvert(validated, ValueType, out object result))
+            {
+                return default(T);
+            }
+
+            return (T)result;
         }
         }
     }
     }
 
 

+ 23 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs

@@ -2,7 +2,9 @@
 using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
@@ -29,10 +31,28 @@ public class ApplyFilterNode : RenderNode, IRenderInput
             return;
             return;
 
 
         _paint.SetFilters(Filter.Value);
         _paint.SetFilters(Filter.Value);
-        int layer = surface.Canvas.SaveLayer(_paint);
-        Background.Value?.Paint(context, surface);
 
 
-        surface.Canvas.RestoreToCount(layer);
+        if (!context.ProcessingColorSpace.IsSrgb)
+        {
+            var target = Texture.ForProcessing(surface, ColorSpace.CreateSrgb());
+
+            int saved = surface.Canvas.Save();
+            surface.Canvas.SetMatrix(Matrix3X3.Identity);
+
+            target.DrawingSurface.Canvas.SaveLayer(_paint);
+            Background.Value?.Paint(context, target.DrawingSurface);
+            target.DrawingSurface.Canvas.Restore();
+
+            surface.Canvas.DrawSurface(target.DrawingSurface, 0, 0);
+            surface.Canvas.RestoreToCount(saved);
+            target.Dispose();
+        }
+        else
+        {
+            int layer = surface.Canvas.SaveLayer(_paint);
+            Background.Value?.Paint(context, surface);
+            surface.Canvas.RestoreToCount(layer);
+        }
     }
     }
 
 
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs

@@ -17,7 +17,7 @@ public class ColorMatrixFilterNode : FilterNode
         Matrix = CreateInput(nameof(Matrix), "MATRIX", ColorMatrix.Identity);
         Matrix = CreateInput(nameof(Matrix), "MATRIX", ColorMatrix.Identity);
     }
     }
 
 
-    protected override ColorFilter? GetColorFilter(ColorSpace colorSpace)
+    protected override ColorFilter? GetColorFilter()
     {
     {
         if (Matrix.Value.Equals(lastMatrix))
         if (Matrix.Value.Equals(lastMatrix))
         {
         {
@@ -27,7 +27,7 @@ public class ColorMatrixFilterNode : FilterNode
         lastMatrix = Matrix.Value;
         lastMatrix = Matrix.Value;
         filter?.Dispose();
         filter?.Dispose();
         
         
-        filter = ColorFilter.CreateColorMatrix(AdjustMatrixForColorSpace(Matrix.Value));
+        filter = ColorFilter.CreateColorMatrix(Matrix.Value);
         return filter;
         return filter;
     }
     }
 
 

+ 5 - 18
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/FilterNode.cs

@@ -1,9 +1,5 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
-using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
-using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
@@ -13,6 +9,9 @@ public abstract class FilterNode : Node
 
 
     public InputProperty<Filter?> Input { get; }
     public InputProperty<Filter?> Input { get; }
 
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
+    protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
+
     public FilterNode()
     public FilterNode()
     {
     {
         Output = CreateOutput<Filter>(nameof(Output), "FILTERS", null);
         Output = CreateOutput<Filter>(nameof(Output), "FILTERS", null);
@@ -21,7 +20,7 @@ public abstract class FilterNode : Node
 
 
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
-        var colorFilter = GetColorFilter(context.ProcessingColorSpace);
+        var colorFilter = GetColorFilter();
         var imageFilter = GetImageFilter();
         var imageFilter = GetImageFilter();
 
 
         if (colorFilter == null && imageFilter == null)
         if (colorFilter == null && imageFilter == null)
@@ -35,19 +34,7 @@ public abstract class FilterNode : Node
         Output.Value = filter == null ? new Filter(colorFilter, imageFilter) : filter.Add(colorFilter, imageFilter);
         Output.Value = filter == null ? new Filter(colorFilter, imageFilter) : filter.Add(colorFilter, imageFilter);
     }
     }
 
 
-    protected virtual ColorFilter? GetColorFilter(ColorSpace colorSpace) => null;
+    protected virtual ColorFilter? GetColorFilter() => null;
 
 
     protected virtual ImageFilter? GetImageFilter() => null;
     protected virtual ImageFilter? GetImageFilter() => null;
-
-    protected ColorMatrix AdjustMatrixForColorSpace(ColorMatrix matrix)
-    {
-        float[] adjusted = new float[20];
-        var transformFn = ColorSpace.CreateSrgb().GetTransformFunction();
-        for (int i = 0; i < 20; i++)
-        {
-            adjusted[i] = transformFn.Transform(matrix[i]);
-        }
-
-        return new ColorMatrix(adjusted);
-    }
 }
 }

+ 7 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs

@@ -22,26 +22,24 @@ public class GrayscaleNode : FilterNode
     private double lastFactor;
     private double lastFactor;
     private bool lastNormalize;
     private bool lastNormalize;
     private Vec3D lastCustomWeight;
     private Vec3D lastCustomWeight;
-    private ColorSpace lastColorSpace;
-    
+
     private ColorFilter? filter;
     private ColorFilter? filter;
     
     
     public GrayscaleNode()
     public GrayscaleNode()
     {
     {
         Mode = CreateInput("Mode", "MODE", GrayscaleMode.Weighted);
         Mode = CreateInput("Mode", "MODE", GrayscaleMode.Weighted);
-        // TODO: Clamp 0 - 1 in UI
-        Factor = CreateInput("Factor", "FACTOR", 1d);
+        Factor = CreateInput("Factor", "FACTOR", 1d)
+            .WithRules(rules => rules.Min(0d).Max(1d));
         Normalize = CreateInput("Normalize", "NORMALIZE", true);
         Normalize = CreateInput("Normalize", "NORMALIZE", true);
         CustomWeight = CreateInput("CustomWeight", "WEIGHT_FACTOR", new Vec3D(1, 1, 1));
         CustomWeight = CreateInput("CustomWeight", "WEIGHT_FACTOR", new Vec3D(1, 1, 1));
     }
     }
 
 
-    protected override ColorFilter? GetColorFilter(ColorSpace colorSpace)
+    protected override ColorFilter? GetColorFilter()
     {
     {
         if (Mode.Value == lastMode 
         if (Mode.Value == lastMode 
             && Factor.Value == lastFactor 
             && Factor.Value == lastFactor 
             && Normalize.Value == lastNormalize &&
             && Normalize.Value == lastNormalize &&
-            CustomWeight.Value == lastCustomWeight
-            && colorSpace == lastColorSpace)
+            CustomWeight.Value == lastCustomWeight)
         {
         {
             return filter;
             return filter;
         }
         }
@@ -50,8 +48,7 @@ public class GrayscaleNode : FilterNode
         lastFactor = Factor.Value;
         lastFactor = Factor.Value;
         lastNormalize = Normalize.Value;
         lastNormalize = Normalize.Value;
         lastCustomWeight = CustomWeight.Value;
         lastCustomWeight = CustomWeight.Value;
-        lastColorSpace = colorSpace;
-        
+
         filter?.Dispose();
         filter?.Dispose();
         
         
         var matrix = Mode.Value switch
         var matrix = Mode.Value switch
@@ -62,8 +59,7 @@ public class GrayscaleNode : FilterNode
                                               ColorMatrix.UseAlpha)
                                               ColorMatrix.UseAlpha)
         };
         };
 
 
-        filter = ColorFilter.CreateColorMatrix(AdjustMatrixForColorSpace(matrix));
-        
+        filter = ColorFilter.CreateColorMatrix(matrix);
         return filter;
         return filter;
     }
     }
 
 

+ 37 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/InvertFilterNode.cs

@@ -0,0 +1,37 @@
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+
+[NodeInfo("InvertFilter")]
+public class InvertFilterNode : FilterNode
+{
+    public InputProperty<double> Intensity { get; }
+    private ColorFilter? filter;
+    private ColorMatrix invertedMatrix;
+
+    public InvertFilterNode()
+    {
+        Intensity = CreateInput("Intensity", "INTENSITY", 1.0)
+            .WithRules(rules => rules.Min(0d).Max(1d));
+        invertedMatrix = new ColorMatrix(new float[] { -1, 0, 0, 0, 1, 0, -1, 0, 0, 1, 0, 0, -1, 0, 1, 0, 0, 0, 1, 0 });
+
+        filter = ColorFilter.CreateColorMatrix(invertedMatrix);
+    }
+
+    protected override ColorFilter? GetColorFilter()
+    {
+        filter?.Dispose();
+
+        var lerped = ColorMatrix.Lerp(ColorMatrix.Identity, invertedMatrix, (float)Intensity.Value);
+        filter = ColorFilter.CreateColorMatrix(lerped);
+
+        return filter;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new InvertFilterNode();
+    }
+}

+ 6 - 15
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/SepiaFilterNode.cs

@@ -10,10 +10,7 @@ public class SepiaFilterNode : FilterNode
 {
 {
     public InputProperty<double> Intensity { get; }
     public InputProperty<double> Intensity { get; }
 
 
-    private ColorMatrix srgbSepiaMatrix;
-    private ColorMatrix linearSepiaMatrix;
-    private ColorFilter linearSepiaFilter;
-    private ColorFilter sepiaColorFilter;
+    private ColorMatrix sepiaMatrix;
 
 
     protected override bool ExecuteOnlyOnCacheChange => true;
     protected override bool ExecuteOnlyOnCacheChange => true;
     protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
     protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
@@ -22,9 +19,10 @@ public class SepiaFilterNode : FilterNode
 
 
     public SepiaFilterNode()
     public SepiaFilterNode()
     {
     {
-        Intensity = CreateInput("Intensity", "INTENSITY", 1d);
+        Intensity = CreateInput("Intensity", "INTENSITY", 1d)
+            .WithRules(rules => rules.Min(0d).Max(1d));
 
 
-        srgbSepiaMatrix = new ColorMatrix(
+        sepiaMatrix = new ColorMatrix(
             [
             [
                 0.393f, 0.769f, 0.189f, 0.0f, 0.0f,
                 0.393f, 0.769f, 0.189f, 0.0f, 0.0f,
                 0.349f, 0.686f, 0.168f, 0.0f, 0.0f,
                 0.349f, 0.686f, 0.168f, 0.0f, 0.0f,
@@ -32,20 +30,13 @@ public class SepiaFilterNode : FilterNode
                 0.0f, 0.0f, 0.0f, 1.0f, 0.0f
                 0.0f, 0.0f, 0.0f, 1.0f, 0.0f
             ]
             ]
         );
         );
-
-        sepiaColorFilter = ColorFilter.CreateColorMatrix(srgbSepiaMatrix);
-
-        linearSepiaMatrix = AdjustMatrixForColorSpace(srgbSepiaMatrix);
-        linearSepiaFilter = ColorFilter.CreateColorMatrix(linearSepiaMatrix);
     }
     }
 
 
-    protected override ColorFilter? GetColorFilter(ColorSpace colorSpace)
+    protected override ColorFilter? GetColorFilter()
     {
     {
-        var targetMatrix = colorSpace.IsSrgb ? srgbSepiaMatrix : linearSepiaMatrix;
-
         lastFilter?.Dispose();
         lastFilter?.Dispose();
 
 
-        var lerped = ColorMatrix.Lerp(ColorMatrix.Identity, targetMatrix, (float)Intensity.Value);
+        var lerped = ColorMatrix.Lerp(ColorMatrix.Identity, sepiaMatrix, (float)Intensity.Value);
         lastFilter = ColorFilter.CreateColorMatrix(lerped);
         lastFilter = ColorFilter.CreateColorMatrix(lerped);
 
 
         return lastFilter;
         return lastFilter;

+ 4 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -52,7 +52,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return (GetFrameWithImage(ctx.FrameTime).Data as ChunkyImage).LatestSize;
         return (GetFrameWithImage(ctx.FrameTime).Data as ChunkyImage).LatestSize;
     }
     }
 
 
-    protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx,
+        DrawingSurface workingSurface,
         bool useFilters = true)
         bool useFilters = true)
     {
     {
         int scaled = workingSurface.Canvas.Save();
         int scaled = workingSurface.Canvas.Save();
@@ -63,7 +64,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         workingSurface.Canvas.RestoreToCount(scaled);
         workingSurface.Canvas.RestoreToCount(scaled);
     }
     }
 
 
-    protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx,
+        DrawingSurface workingSurface,
         bool useFilters)
         bool useFilters)
     {
     {
         int scaled = workingSurface.Canvas.Save();
         int scaled = workingSurface.Canvas.Save();

+ 33 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -3,6 +3,7 @@ using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 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;
@@ -23,7 +24,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
     {
     {
         if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
         if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
         {
         {
-            Output.Value = Background.Value; 
+            Output.Value = Background.Value;
             return;
             return;
         }
         }
 
 
@@ -49,7 +50,8 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             return;
             return;
         }
         }
 
 
-        var outputWorkingSurface = TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 1);
+        var outputWorkingSurface =
+            TryInitWorkingSurface(size, context.ChunkResolution, context.ProcessingColorSpace, 1);
         outputWorkingSurface.DrawingSurface.Canvas.Clear();
         outputWorkingSurface.DrawingSurface.Canvas.Clear();
 
 
         DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface, useFilters);
         DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface, useFilters);
@@ -69,10 +71,11 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         }
         }
 
 
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
-        DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, context.ChunkResolution, size);
+        DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, context.ChunkResolution);
     }
     }
 
 
-    protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx,
+        DrawingSurface workingSurface,
         bool useFilters)
         bool useFilters)
     {
     {
         int scaled = workingSurface.Canvas.Save();
         int scaled = workingSurface.Canvas.Save();
@@ -83,7 +86,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         workingSurface.Canvas.RestoreToCount(scaled);
         workingSurface.Canvas.RestoreToCount(scaled);
     }
     }
 
 
-    private void DrawWithResolution(DrawingSurface source, DrawingSurface target, ChunkResolution resolution, VecI size)
+    private void DrawWithResolution(DrawingSurface source, DrawingSurface target, ChunkResolution resolution)
     {
     {
         int scaled = target.Canvas.Save();
         int scaled = target.Canvas.Save();
         float multiplier = (float)resolution.InvertedMultiplier();
         float multiplier = (float)resolution.InvertedMultiplier();
@@ -96,20 +99,42 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
 
     protected abstract VecI GetTargetSize(RenderContext ctx);
     protected abstract VecI GetTargetSize(RenderContext ctx);
 
 
-    protected internal virtual void DrawLayerInScene(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected internal virtual void DrawLayerInScene(SceneObjectRenderContext ctx,
+        DrawingSurface workingSurface,
         bool useFilters = true)
         bool useFilters = true)
     {
     {
         DrawLayerOnto(ctx, workingSurface, useFilters);
         DrawLayerOnto(ctx, workingSurface, useFilters);
     }
     }
 
 
-    protected void DrawLayerOnto(SceneObjectRenderContext ctx, DrawingSurface workingSurface, bool useFilters)
+    protected void DrawLayerOnto(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+        bool useFilters)
     {
     {
         blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
         blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
 
 
         if (useFilters && Filters.Value != null)
         if (useFilters && Filters.Value != null)
         {
         {
             blendPaint.SetFilters(Filters.Value);
             blendPaint.SetFilters(Filters.Value);
-            DrawWithFilters(ctx, workingSurface, blendPaint);
+
+            var targetSurface = workingSurface;
+            Texture? tex = null;
+            int saved = -1;
+            if (!ctx.ProcessingColorSpace.IsSrgb)
+            {
+                saved = workingSurface.Canvas.Save();
+
+                tex = Texture.ForProcessing(workingSurface, ColorSpace.CreateSrgb()); // filters are meant to be applied in sRGB
+                targetSurface = tex.DrawingSurface;
+                workingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+            }
+
+            DrawWithFilters(ctx, targetSurface, blendPaint);
+
+            if(targetSurface != workingSurface)
+            {
+                workingSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+                tex.Dispose();
+                workingSurface.Canvas.RestoreToCount(saved);
+            }
         }
         }
         else
         else
         {
         {

+ 41 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/PropertyValidator.cs

@@ -6,34 +6,70 @@ public delegate ValidatorResult ValidateProperty(object? value);
 
 
 public class PropertyValidator
 public class PropertyValidator
 {
 {
+    public InputProperty ForProperty { get; }
     public List<ValidateProperty> Rules { get; } = new();
     public List<ValidateProperty> Rules { get; } = new();
 
 
+    public PropertyValidator(InputProperty forProperty)
+    {
+        ForProperty = forProperty;
+    }
+
     public PropertyValidator Min(VecI min)
     public PropertyValidator Min(VecI min)
     {
     {
-       return Min(min, v => new VecI(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y))); 
+        return Min(min, v => new VecI(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y)));
     }
     }
-    
+
     public PropertyValidator Min(VecD min)
     public PropertyValidator Min(VecD min)
     {
     {
-        return Min(min, v => new VecD(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y))); 
+        return Min(min, v => new VecD(Math.Max(v.X, min.X), Math.Max(v.Y, min.Y)));
     }
     }
 
 
     public PropertyValidator Min<T>(T min, Func<T, T>? adjust = null) where T : IComparable<T>
     public PropertyValidator Min<T>(T min, Func<T, T>? adjust = null) where T : IComparable<T>
     {
     {
+        if (!typeof(T).IsAssignableTo(ForProperty.ValueType))
+        {
+            throw new ArgumentException($"Type mismatch. Expected {ForProperty.ValueType}, got {typeof(T)}");
+        }
+
         Rules.Add((value) =>
         Rules.Add((value) =>
         {
         {
             if (value is T val)
             if (value is T val)
             {
             {
                 bool isValid = val.CompareTo(min) >= 0;
                 bool isValid = val.CompareTo(min) >= 0;
-                return new (isValid, isValid ? val : GetReturnValue(val, min, adjust));
+                return new(isValid, isValid ? val : GetReturnValue(val, min, adjust));
             }
             }
 
 
-            return new (false, GetReturnValue(min, min, adjust));
+            return new(false, GetReturnValue(min, min, adjust));
         });
         });
 
 
         return this;
         return this;
     }
     }
 
 
+
+    public void Max(VecI max)
+    {
+        Max(max, v => new VecI(Math.Min(v.X, max.X), Math.Min(v.Y, max.Y)));
+    }
+
+    public void Max(VecD max)
+    {
+        Max(max, v => new VecD(Math.Min(v.X, max.X), Math.Min(v.Y, max.Y)));
+    }
+
+    public void Max<T>(T max, Func<T, T>? adjust = null) where T : IComparable<T>
+    {
+        Rules.Add((value) =>
+        {
+            if (value is T val)
+            {
+                bool isValid = val.CompareTo(max) <= 0;
+                return new(isValid, isValid ? val : GetReturnValue(val, max, adjust));
+            }
+
+            return new(false, GetReturnValue(max, max, adjust));
+        });
+    }
+
     public PropertyValidator Custom(ValidateProperty rule)
     public PropertyValidator Custom(ValidateProperty rule)
     {
     {
         Rules.Add(rule);
         Rules.Add(rule);

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

@@ -891,5 +891,6 @@
   "DOCUMENT_INFO_NODE": "Document Info",
   "DOCUMENT_INFO_NODE": "Document Info",
   "MASK_NODE": "Mask",
   "MASK_NODE": "Mask",
   "SEPIA_FILTER_NODE": "Sepia Filter",
   "SEPIA_FILTER_NODE": "Sepia Filter",
-  "INTENSITY": "Intensity"
+  "INTENSITY": "Intensity",
+  "INVERT_FILTER_NODE": "Invert Filter"
 }
 }

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/InvertFilterNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
+
+[NodeViewModel("INVERT_FILTER_NODE", "FILTERS", PixiPerfectIcons.Invert)]
+internal class InvertFilterNodeViewModel : NodeViewModel<InvertFilterNode>;

+ 14 - 4
src/PixiEditor/ViewModels/Document/TransformOverlays/TextOverlayViewModel.cs

@@ -57,19 +57,19 @@ internal class TextOverlayViewModel : ObservableObject, ITextOverlayHandler
         get => requestEditTextTrigger;
         get => requestEditTextTrigger;
         set => SetProperty(ref requestEditTextTrigger, value);
         set => SetProperty(ref requestEditTextTrigger, value);
     }
     }
-    
+
     public Matrix3X3 Matrix
     public Matrix3X3 Matrix
     {
     {
         get => matrix;
         get => matrix;
         set => SetProperty(ref matrix, value);
         set => SetProperty(ref matrix, value);
     }
     }
-    
+
     public double? Spacing
     public double? Spacing
     {
     {
         get => spacing;
         get => spacing;
         set => SetProperty(ref spacing, value);
         set => SetProperty(ref spacing, value);
     }
     }
-    
+
     public int CursorPosition
     public int CursorPosition
     {
     {
         get => cursorPosition;
         get => cursorPosition;
@@ -86,11 +86,21 @@ internal class TextOverlayViewModel : ObservableObject, ITextOverlayHandler
     {
     {
         VecD mapped = Matrix.Invert().MapPoint(closestToPosition);
         VecD mapped = Matrix.Invert().MapPoint(closestToPosition);
         RichText richText = new(Text);
         RichText richText = new(Text);
+        if (Font == null)
+        {
+            return;
+        }
+
         var positions = richText.GetGlyphPositions(Font);
         var positions = richText.GetGlyphPositions(Font);
+        if (positions == null || positions.Length == 0)
+        {
+            return;
+        }
+
         int indexOfClosest = positions.Select((pos, index) => (pos, index))
         int indexOfClosest = positions.Select((pos, index) => (pos, index))
             .OrderBy(pos => ((pos.pos + Position - new VecD(0, Font.Size / 2f)) - mapped).LengthSquared)
             .OrderBy(pos => ((pos.pos + Position - new VecD(0, Font.Size / 2f)) - mapped).LengthSquared)
             .First().index;
             .First().index;
-        
+
         CursorPosition = indexOfClosest;
         CursorPosition = indexOfClosest;
         SelectionEnd = indexOfClosest;
         SelectionEnd = indexOfClosest;
     }
     }