Browse Source

Merge branch 'avalonia-rewrite' into nodes-gpu

flabbet 1 year ago
parent
commit
f591fb7abd
63 changed files with 1018 additions and 121 deletions
  1. 6 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeInfoAttribute.cs
  2. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs
  3. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  4. 7 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs
  5. 8 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineSeparateColorMode.cs
  6. 1 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecD.cs
  7. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecI.cs
  8. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  9. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs
  10. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs
  11. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs
  12. 1 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs
  13. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EllipseNode.cs
  14. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EmptyImageNode.cs
  15. 25 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Evaluator/ColorEvaluatorLeftNode.cs
  16. 27 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Evaluator/ColorEvaluatorRightNode.cs
  17. 1 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  18. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorMatrixFilterNode.cs
  19. 77 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs
  20. 1 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/KernelFilterNode.cs
  21. 18 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Filters.cs
  22. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  23. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  24. 1 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs
  25. 1 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LerpColorNode.cs
  26. 1 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  27. 10 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  28. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  29. 1 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  30. 8 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  31. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  32. 1 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  33. 51 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/DistributePointsNode.cs
  34. 25 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/PointList.cs
  35. 53 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RasterizePointsNode.cs
  36. 72 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RemoveClosePointsNode.cs
  37. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  38. 1 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  39. 1 0
      src/PixiEditor.ChangeableDocument/Enums/BlendMode.cs
  40. 1 0
      src/PixiEditor.ChangeableDocument/Rendering/RenderingContext.cs
  41. 23 7
      src/PixiEditor.DrawingApi.Core/Surface.cs
  42. 33 1
      src/PixiEditor.Numerics/ColorMatrix.cs
  43. 1 1
      src/PixiEditor.Numerics/PixiEditor.Numerics.csproj
  44. 172 0
      src/PixiEditor.Numerics/VecD3.cs
  45. 10 0
      src/PixiEditor.UI.Common/Accents/Base.axaml
  46. BIN
      src/PixiEditor/Data/BetaExampleFiles/Stars.pixi
  47. 20 2
      src/PixiEditor/Data/Localization/Languages/en.json
  48. 1 0
      src/PixiEditor/Helpers/Extensions/BlendModeEx.cs
  49. 104 0
      src/PixiEditor/Helpers/Nodes/NodeAbbreviation.cs
  50. 1 0
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  51. 1 1
      src/PixiEditor/Models/AnalyticsAPI/AnalyticsClient.cs
  52. 5 0
      src/PixiEditor/Models/Commands/CommandController.cs
  53. 33 0
      src/PixiEditor/Models/Nodes/NodeTypeInfo.cs
  54. 27 0
      src/PixiEditor/Models/Serialization/Factories/PointListSerializationFactory.cs
  55. 1 1
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  56. 10 5
      src/PixiEditor/Styles/Templates/NodePicker.axaml
  57. 42 0
      src/PixiEditor/ViewModels/Nodes/Properties/VecD3PropertyViewModel.cs
  58. 1 0
      src/PixiEditor/Views/Input/BlendModeComboBox.cs
  59. 17 4
      src/PixiEditor/Views/Nodes/NodeGraphView.cs
  60. 67 16
      src/PixiEditor/Views/Nodes/NodePicker.cs
  61. 19 0
      src/PixiEditor/Views/Nodes/Properties/VecD3PropertyView.axaml
  62. 14 0
      src/PixiEditor/Views/Nodes/Properties/VecD3PropertyView.axaml.cs
  63. 2 0
      src/PixiEditor/Views/Windows/HelloTherePopup.axaml

+ 6 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeInfoAttribute.cs

@@ -4,8 +4,12 @@
 public class NodeInfoAttribute : Attribute
 {
     public string UniqueName { get; }
+    
+    public string DisplayName { get; }
+    
+    public string? PickerName { get; set; }
 
-    public NodeInfoAttribute(string uniqueName)
+    public NodeInfoAttribute(string uniqueName, string displayName)
     {
         if (!uniqueName.StartsWith("PixiEditor"))
         {
@@ -13,5 +17,6 @@ public class NodeInfoAttribute : Attribute
         }
         
         UniqueName = uniqueName;
+        DisplayName = displayName;
     }
 }

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs

@@ -3,11 +3,9 @@ using PixiEditor.DrawingApi.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
 
-[NodeInfo("Time")]
+[NodeInfo("Time", "TIME_NODE")]
 public class TimeNode : Node
 {
-    public override string DisplayName { get; set; } = "TIME_NODE";
-    
     public OutputProperty<int> ActiveFrame { get; set; }
     public OutputProperty<double> NormalizedTime { get; set; }
 

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineChannels")]
+[NodeInfo("CombineChannels", "COMBINE_CHANNELS_NODE")]
 public class CombineChannelsNode : Node
 {
     private readonly Paint _screenPaint = new() { BlendMode = BlendMode.Screen };
@@ -106,7 +106,5 @@ public class CombineChannelsNode : Node
         return final.Size;
     }
 
-    public override string DisplayName { get; set; } = "COMBINE_CHANNELS_NODE";
-
     public override Node CreateCopy() => new CombineChannelsNode();
 }

+ 7 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs

@@ -6,24 +6,25 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineColor")]
+[NodeInfo("CombineColor", "COMBINE_COLOR_NODE")]
 public class CombineColorNode : Node
 {
     public FuncOutputProperty<Half4> Color { get; }
 
-    public FuncInputProperty<Float1> R { get; }
+    public InputProperty<CombineSeparateColorMode> Mode { get; }
 
-    public FuncInputProperty<Float1> G { get; }
+    public FuncInputProperty<Float1> RH { get; }
 
-    public FuncInputProperty<Float1> B { get; }
+    public FuncInputProperty<Float1> GS { get; }
 
-    public FuncInputProperty<Float1> A { get; }
+    public FuncInputProperty<Float1> BVL { get; }
 
-    public override string DisplayName { get; set; } = "COMBINE_COLOR_NODE";
+    public FuncInputProperty<Float1> A { get; }
 
     public CombineColorNode()
     {
         Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
+        Mode = CreateInput("Mode", "MODE", CombineSeparateColorMode.RGB);
 
         R = CreateFuncInput<Float1>("R", "R", 0d);
         G = CreateFuncInput<Float1>("G", "G", 0d);

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineSeparateColorMode.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+
+public enum CombineSeparateColorMode
+{
+    RGB,
+    HSV,
+    HSL
+}

+ 1 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecD.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineVecD")]
+[NodeInfo("CombineVecD", "COMBINE_VECD_NODE")]
 public class CombineVecD : Node
 {
     public FuncOutputProperty<Float2> Vector { get; }
@@ -16,9 +16,6 @@ public class CombineVecD : Node
     
     public FuncInputProperty<Float1> Y { get; }
     
-    
-    public override string DisplayName { get; set; } = "COMBINE_VECD_NODE";
-
     public CombineVecD()
     {
         Vector = CreateFuncOutput(nameof(Vector), "VECTOR", GetVector);

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecI.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("CombineVecI")]
+[NodeInfo("CombineVecI", "COMBINE_VECI_NODE")]
 public class CombineVecI : Node
 {
     public FuncOutputProperty<Int2> Vector { get; }
@@ -15,8 +15,6 @@ public class CombineVecI : Node
     
     public FuncInputProperty<Int1> Y { get; }
 
-    public override string DisplayName { get; set; } = "COMBINE_VECI_NODE";
-
     public CombineVecI()
     {
         Vector = CreateFuncOutput(nameof(Vector), "VECTOR", GetVector);

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateChannels")]
+[NodeInfo("SeparateChannels", "SEPARATE_CHANNELS_NODE")]
 public class SeparateChannelsNode : Node
 {
     private readonly Paint _paint = new();
@@ -42,8 +42,6 @@ public class SeparateChannelsNode : Node
         Image = CreateInput<Texture>(nameof(Image), "IMAGE", null);
         Grayscale = CreateInput(nameof(Grayscale), "GRAYSCALE", false);
     }
-
-    public override string DisplayName { get; set; } = "SEPARATE_CHANNELS_NODE";
     
     protected override Texture? OnExecute(RenderingContext context)
     {

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateColor")]
+[NodeInfo("SeparateColor", "SEPARATE_COLOR_NODE")]
 public class SeparateColorNode : Node
 {
     public FuncInputProperty<Half4> Color { get; }
@@ -19,7 +19,6 @@ public class SeparateColorNode : Node
     
     public FuncOutputProperty<Float1> A { get; }
     
-    public override string DisplayName { get; set; } = "SEPARATE_COLOR_NODE";
     
     private FuncContext lastContext;
     private Half4 lastColor;
@@ -55,6 +54,5 @@ public class SeparateColorNode : Node
         return lastColor;
     }
 
-
     public override Node CreateCopy() => new SeparateColorNode();
 }

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs

@@ -6,7 +6,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateVecD")]
+[NodeInfo("SeparateVecD", "SEPARATE_VECD_NODE")]
 public class SeparateVecDNode : Node
 {
     public FuncInputProperty<Float2> Vector { get; }
@@ -15,8 +15,6 @@ public class SeparateVecDNode : Node
     
     public FuncOutputProperty<Float1> Y { get; }
     
-    public override string DisplayName { get; set; } = "SEPARATE_VECD_NODE";
-
     public SeparateVecDNode()
     {
         X = CreateFuncOutput("X", "X", ctx => Vector.Value(ctx).X);

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
-[NodeInfo("SeparateVecI")]
+[NodeInfo("SeparateVecI", "SEPARATE_VECI_NODE")]
 public class SeparateVecINode : Node
 {
     public FuncInputProperty<Int2> Vector { get; }
@@ -14,8 +14,6 @@ public class SeparateVecINode : Node
     
     public FuncOutputProperty<Int1> Y { get; }
     
-    public override string DisplayName { get; set; } = "SEPARATE_VECI_NODE";
-
     public SeparateVecINode()
     {
         X = CreateFuncOutput<Int1>("X", "X", ctx => Vector.Value(ctx).X);

+ 1 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 // TODO: Add based on debug mode, not debug build.
-[NodeInfo("DebugBlendMode")]
+[NodeInfo("DebugBlendMode", "Debug Blend Mode")]
 public class DebugBlendModeNode : Node
 {
     private Paint _paint = new();
@@ -20,7 +20,6 @@ public class DebugBlendModeNode : Node
 
     public OutputProperty<Texture> Result { get; }
 
-    public override string DisplayName { get; set; } = "Debug Blend Mode";
     public DebugBlendModeNode()
     {
         Dst = CreateInput<Texture?>(nameof(Dst), "Dst", null);

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EllipseNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Ellipse")]
+[NodeInfo("Ellipse", "ELLIPSE_NODE")]
 public class EllipseNode : Node
 {
     public InputProperty<VecI> Radius { get; }
@@ -75,7 +75,5 @@ public class EllipseNode : Node
         return targetSurface;
     }
 
-    public override string DisplayName { get; set; } = "ELLIPSE_NODE";
-
     public override Node CreateCopy() => new EllipseNode();
 }

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EmptyImageNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("CreateImage")]
+[NodeInfo("CreateImage", "CREATE_IMAGE_NODE")]
 public class CreateImageNode : Node
 {
     private Paint _paint = new();
@@ -42,7 +42,5 @@ public class CreateImageNode : Node
         return Output.Value;
     }
  
-    public override string DisplayName { get; set; } = "CREATE_IMAGE_NODE";
-
     public override Node CreateCopy() => new CreateImageNode();
 }

+ 25 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Evaluator/ColorEvaluatorLeftNode.cs

@@ -0,0 +1,25 @@
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Evaluator;
+
+[NodeInfo("ColorEvaluatorLeft", "BEGIN_COLOR_EVALUATOR", PickerName = "COLOR_EVALUATOR_NODE_PAIR")]
+[PairNode(typeof(ColorEvaluatorRightNode), "ColorEvaluatorZone", true)]
+public class ColorEvaluatorLeftNode : Node
+{
+    public FuncOutputProperty<VecD> Position { get; }
+
+    public ColorEvaluatorLeftNode()
+    {
+        Position = CreateFuncOutput("Position", "UV", c => c.Position);
+    }
+    
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        return null;
+    }
+
+    public override Node CreateCopy() => new ColorEvaluatorLeftNode();
+}

+ 27 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Evaluator/ColorEvaluatorRightNode.cs

@@ -0,0 +1,27 @@
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Evaluator;
+
+[NodeInfo("ColorEvaluatorRight", "FINISH_COLOR_EVALUATOR", PickerName = "")]
+[PairNode(typeof(ColorEvaluatorLeftNode), "ColorEvaluatorZone")]
+public class ColorEvaluatorRightNode : Node
+{
+    public FuncOutputProperty<Color> Output { get; }
+
+    public FuncInputProperty<Color> Input { get; }
+
+    public ColorEvaluatorRightNode()
+    {
+        Output = CreateFuncOutput("Output", "COLOR", c => Input.Value(c));
+        Input = CreateFuncInput("Input", "COLOR", Colors.Black);
+    }
+    
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        return null;
+    }
+
+    public override Node CreateCopy() => new ColorEvaluatorRightNode();
+}

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

@@ -5,12 +5,11 @@ using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("ApplyFilter")]
+[NodeInfo("ApplyFilter", "APPLY_FILTER_NODE")]
 public class ApplyFilterNode : Node
 {
     private Paint _paint = new();
     
-    public override string DisplayName { get; set; } = "APPLY_FILTER_NODE";
     
     public OutputProperty<Texture?> Output { get; }
 

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

@@ -3,13 +3,11 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("ColorMatrixFilter")]
+[NodeInfo("ColorMatrixFilter", "COLOR_MATRIX_TRANSFORM_FILTER_NODE")]
 public class ColorMatrixFilterNode : FilterNode
 {
     public InputProperty<ColorMatrix> Matrix { get; }
 
-    public override string DisplayName { get; set; } = "COLOR_MATRIX_FILTER_NODE";
-    
     public ColorMatrixFilterNode()
     {
         Matrix = CreateInput(nameof(Matrix), "MATRIX", ColorMatrix.Identity);

+ 77 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/GrayscaleNode.cs

@@ -0,0 +1,77 @@
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+
+[NodeInfo("GrayscaleFilter", "GRAYSCALE_FILTER_NODE")]
+public class GrayscaleNode : FilterNode
+{
+    private static readonly ColorMatrix WeightedMatrix = ColorMatrix.WeightedWavelengthGrayscale + ColorMatrix.UseAlpha;
+    private static readonly ColorMatrix AverageMatrix = ColorMatrix.AverageGrayscale + ColorMatrix.UseAlpha;
+    
+    public InputProperty<GrayscaleMode> Mode { get; }
+    
+    public InputProperty<double> Factor { get; }
+    
+    public InputProperty<bool> Normalize { get; }
+
+    // TODO: Hide when Mode != Custom
+    public InputProperty<VecD3> CustomWeight { get; }
+    
+    public GrayscaleNode()
+    {
+        Mode = CreateInput("Mode", "MODE", GrayscaleMode.Weighted);
+        // TODO: Clamp 0 - 1 in UI
+        Factor = CreateInput("Factor", "FACTOR", 1d);
+        Normalize = CreateInput("Normalize", "NORMALIZE", true);
+        CustomWeight = CreateInput("CustomWeight", "WEIGHT_FACTOR", new VecD3(1, 1, 1));
+    }
+
+    protected override ColorFilter GetColorFilter() => ColorFilter.CreateColorMatrix(Mode.Value switch
+    {
+        GrayscaleMode.Weighted => UseFactor(WeightedMatrix),
+        GrayscaleMode.Average => UseFactor(AverageMatrix),
+        GrayscaleMode.Custom => UseFactor(ColorMatrix.WeightedGrayscale(GetAdjustedCustomWeight()) + ColorMatrix.UseAlpha)
+    });
+
+    private ColorMatrix UseFactor(ColorMatrix target)
+    {
+        var factor = Factor.Value;
+
+        return factor switch
+        {
+            0 => ColorMatrix.Identity,
+            1 => target,
+            _ => ColorMatrix.Lerp(ColorMatrix.Identity, target, (float)factor)
+        };
+    }
+
+    private VecD3 GetAdjustedCustomWeight()
+    {
+        var weight = CustomWeight.Value;
+        var normalize = Normalize.Value;
+
+        if (!normalize)
+        {
+            return weight;
+        }
+
+        var sum = weight.Sum();
+
+        if (sum == 0)
+        {
+            return VecD3.Zero;
+        }
+            
+        return weight / weight.Sum();
+    }
+
+    public override Node CreateCopy() => new GrayscaleNode();
+
+    public enum GrayscaleMode
+    {
+        Weighted,
+        Average,
+        Custom
+    }
+}

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

@@ -4,7 +4,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
-[NodeInfo("KernelFilter")]
+[NodeInfo("KernelFilter", "KERNEL_FILTER_NODE")]
 public class KernelFilterNode : FilterNode
 {
     private readonly Paint _paint = new();
@@ -19,7 +19,6 @@ public class KernelFilterNode : FilterNode
 
     public InputProperty<bool> OnAlpha { get; }
 
-    public override string DisplayName { get; set; } = "KERNEL_FILTER_NODE";
     public KernelFilterNode()
     {
         Kernel = CreateInput(nameof(Kernel), "KERNEL", Numerics.Kernel.Identity(3, 3));

+ 18 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Filters.cs

@@ -5,18 +5,36 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 public static class Filters
 {
+    /// <summary>
+    /// Maps red to the red, green and blue channels. Sets alpha to 1
+    /// </summary>
     public static readonly ColorFilter RedGrayscaleFilter =
         ColorFilter.CreateColorMatrix(
             ColorMatrix.UseRed + ColorMatrix.MapRedToGreenBlue + ColorMatrix.OpaqueAlphaOffset);
 
+    /// <summary>
+    /// Maps green to the red, green and blue channels. Sets alpha to 1
+    /// </summary>
     public static readonly ColorFilter GreenGrayscaleFilter =
         ColorFilter.CreateColorMatrix(ColorMatrix.UseGreen + ColorMatrix.MapGreenToRedBlue +
                                       ColorMatrix.OpaqueAlphaOffset);
 
+    /// <summary>
+    /// Maps blue to the red, green and blue channels. Sets alpha to 1
+    /// </summary>
     public static readonly ColorFilter BlueGrayscaleFilter =
         ColorFilter.CreateColorMatrix(ColorMatrix.UseBlue + ColorMatrix.MapBlueToRedGreen +
                                       ColorMatrix.OpaqueAlphaOffset);
 
+    /// <summary>
+    /// Maps alpha to the red, green and blue channels. Sets alpha to 1
+    /// </summary>
     public static readonly ColorFilter AlphaGrayscaleFilter =
         ColorFilter.CreateColorMatrix(ColorMatrix.MapAlphaToRedGreenBlue + ColorMatrix.OpaqueAlphaOffset);
+    
+    /// <summary>
+    /// The rgb values become averaged into a grayscale image. Sets alpha to 1 <br/>
+    /// </summary>
+    public static readonly ColorFilter AverageGrayscaleFilter =
+        ColorFilter.CreateColorMatrix(ColorMatrix.AverageGrayscale + ColorMatrix.OpaqueAlphaOffset);
 }

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

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Folder")]
+[NodeInfo("Folder", "FOLDER_NODE")]
 public class FolderNode : StructureNode, IReadOnlyFolderNode
 {
     public InputProperty<Texture?> Content { get; }

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

@@ -10,7 +10,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ImageLayer")]
+[NodeInfo("ImageLayer", "IMAGE_LAYER_NODE")]
 public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 {
     public const string ImageFramesKey = "Frames";

+ 1 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs

@@ -6,14 +6,13 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ImageSpace")]
+[NodeInfo("ImageSpace", "IMAGE_SPACE_NODE")]
 public class ImageSpaceNode : Node
 {
     public FuncOutputProperty<VecD> SpacePosition { get; }
     
     public FuncOutputProperty<VecI> Size { get; }
 
-    public override string DisplayName { get; set; } = "IMAGE_SPACE_NODE";
     public ImageSpaceNode()
     {
         // TODO: Implement this

+ 1 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LerpColorNode.cs

@@ -7,7 +7,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Lerp")]
+[NodeInfo("Lerp", "LERP_NODE")]
 public class LerpColorNode : Node // TODO: ILerpable as inputs? 
 {
     public FuncOutputProperty<Half4> Result { get; } 
@@ -15,9 +15,6 @@ public class LerpColorNode : Node // TODO: ILerpable as inputs?
     public FuncInputProperty<Half4> To { get; }
     public FuncInputProperty<Float1> Time { get; }
     
-    public override string DisplayName { get; set; } = "LERP_NODE";
-    
-    
     public LerpColorNode()
     {
         Result = CreateFuncOutput<Half4>("Result", "RESULT", Lerp);

+ 1 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs

@@ -8,7 +8,7 @@ using PixiEditor.DrawingApi.Core.Shaders.Generation.Expressions;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Math")]
+[NodeInfo("Math", "MATH_NODE")]
 public class MathNode : Node
 {
     public FuncOutputProperty<Float1> Result { get; }
@@ -21,9 +21,6 @@ public class MathNode : Node
     
     public FuncInputProperty<Float1> Y { get; }
     
-    
-    public override string DisplayName { get; set; } = "MATH_NODE";
-    
     public MathNode()
     {
         Result = CreateFuncOutput<Float1>(nameof(Result), "RESULT", Calculate);

+ 10 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -1,27 +1,31 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Merge")]
+[NodeInfo("Merge", "MERGE_NODE")]
 public class MergeNode : Node, IBackgroundInput
 {
+    private Paint _paint = new();
+
+    public InputProperty<BlendMode> BlendMode { get; }
     public InputProperty<Texture?> Top { get; }
     public InputProperty<Texture?> Bottom { get; }
     public OutputProperty<Texture?> Output { get; }
     
     public MergeNode() 
     {
+        BlendMode = CreateInput("BlendMode", "BlendMode", Enums.BlendMode.Normal);
         Top = CreateInput<Texture?>("Top", "TOP", null);
         Bottom = CreateInput<Texture?>("Bottom", "BOTTOM", null);
         Output = CreateOutput<Texture?>("Output", "OUTPUT", null);
     }
 
-    public override string DisplayName { get; set; } = "MERGE_NODE";
-
     public override Node CreateCopy()
     {
         return new MergeNode();
@@ -45,10 +49,11 @@ public class MergeNode : Node, IBackgroundInput
         {
             workingSurface.DrawingSurface.Canvas.DrawSurface(Bottom.Value.DrawingSurface, 0, 0);
         }
-        
+
         if(Top.Value != null)
         {
-            workingSurface.DrawingSurface.Canvas.DrawSurface(Top.Value.DrawingSurface, 0, 0);
+            _paint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
+            workingSurface.DrawingSurface.Canvas.DrawSurface(Top.Value.DrawingSurface, 0, 0, _paint);
         }
 
         Output.Value = workingSurface;

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -13,7 +13,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ModifyImageLeft")]
+[NodeInfo("ModifyImageLeft", "MODIFY_IMAGE_LEFT_NODE", PickerName = "MODIFY_IMAGE_PAIR_NODE")]
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
 public class ModifyImageLeftNode : Node
 {
@@ -23,8 +23,6 @@ public class ModifyImageLeftNode : Node
     
     public FuncOutputProperty<Half4> Color { get; }
 
-    public override string DisplayName { get; set; } = "MODIFY_IMAGE_LEFT_NODE";
-    
     private ConcurrentDictionary<RenderingContext, Pixmap> pixmapCache = new();
 
     public ModifyImageLeftNode()

+ 1 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -14,7 +14,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("ModifyImageRight")]
+[NodeInfo("ModifyImageRight", "MODIFY_IMAGE_RIGHT_NODE", PickerName = "")]
 [PairNode(typeof(ModifyImageLeftNode), "ModifyImageZone")]
 public class ModifyImageRightNode : Node, IPairNodeEnd
 {
@@ -27,7 +27,6 @@ public class ModifyImageRightNode : Node, IPairNodeEnd
 
     public OutputProperty<Texture> Output { get; }
 
-    public override string DisplayName { get; set; } = "MODIFY_IMAGE_RIGHT_NODE";
 
     private string _lastSksl;
     

+ 8 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -14,6 +14,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [DebuggerDisplay("Type = {GetType().Name}")]
 public abstract class Node : IReadOnlyNode, IDisposable
 {
+    private string displayName;
     private List<InputProperty> inputs = new();
     private List<OutputProperty> outputs = new();
     protected List<KeyFrameData> keyFrames = new();
@@ -44,13 +45,19 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected Node()
     {
+        displayName = GetType().GetCustomAttribute<NodeInfoAttribute>().DisplayName;
     }
 
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
     IReadOnlyList<IReadOnlyKeyFrameData> IReadOnlyNode.KeyFrames => keyFrames;
     public VecD Position { get; set; }
-    public abstract string DisplayName { get; set; }
+
+    public virtual string DisplayName
+    {
+        get => displayName;
+        set => displayName = value;
+    }
 
     private KeyFrameTime _lastFrameTime = new KeyFrameTime(-1, 0);
     private ChunkResolution? _lastResolution;

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Noise")]
+[NodeInfo("Noise", "NOISE_NODE")]
 public class NoiseNode : Node
 {
     private double previousScale = double.NaN;
@@ -107,8 +107,6 @@ public class NoiseNode : Node
         return shader;
     }
 
-    public override string DisplayName { get; set; } = "NOISE_NODE";
-
     public override Node CreateCopy() => new NoiseNode();
 }
 

+ 1 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs

@@ -5,12 +5,11 @@ using PixiEditor.DrawingApi.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("Output")]
+[NodeInfo("Output", "OUTPUT_NODE", PickerName = "")]
 public class OutputNode : Node, IBackgroundInput
 {
     public const string InputPropertyName = "Background";
 
-    public override string DisplayName { get; set; } = "OUTPUT_NODE";
     public InputProperty<Texture?> Input { get; } 
     public OutputNode()
     {

+ 51 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/DistributePointsNode.cs

@@ -0,0 +1,51 @@
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
+
+[NodeInfo("DistributePoints", "DISTRIBUTE_POINTS")]
+public class DistributePointsNode : Node
+{
+    public OutputProperty<PointList> Points { get; }
+
+    public InputProperty<int> MaxPointCount { get; }
+
+    public InputProperty<int> Seed { get; }
+
+    public DistributePointsNode()
+    {
+        Points = CreateOutput(nameof(Points), "POINTS", PointList.Empty);
+
+        MaxPointCount = CreateInput("MaxPointCount", "MAX_POINTS", 10);
+        Seed = CreateInput("Seed", "SEED", 0);
+    }
+
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        Points.Value = GetPointsRandomly();
+        
+        return null;
+    }
+
+    private PointList GetPointsRandomly()
+    {
+        var seed = Seed.Value;
+        var random = new Random(seed);
+        var pointCount = MaxPointCount.Value;
+        var finalPoints = new PointList(pointCount)
+        {
+            HashValue = HashCode.Combine(pointCount, seed)
+        };
+
+        for (int i = 0; i < pointCount; i++)
+        {
+            finalPoints.Add(new VecD(random.NextDouble(), random.NextDouble()));
+        }
+        
+        return finalPoints;
+    }
+
+    public override Node CreateCopy() => new DistributePointsNode();
+}

+ 25 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/PointList.cs

@@ -0,0 +1,25 @@
+using PixiEditor.Common;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
+
+public class PointList : List<VecD>, ICacheable
+{
+    public required int HashValue { get; set; }
+
+    public PointList()
+    {
+    }
+
+    public PointList(IEnumerable<VecD> collection) : base(collection)
+    {
+    }
+
+    public PointList(int capacity) : base(capacity)
+    {
+    }
+
+    public static PointList Empty { get; } = new(0) { HashValue = 0 };
+
+    public int GetCacheHash() => HashValue;
+}

+ 53 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RasterizePointsNode.cs

@@ -0,0 +1,53 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
+
+[NodeInfo("RasterizePoints", "RASTERIZE_POINTS")]
+public class RasterizePointsNode : Node
+{
+    private Paint _paint = new();
+
+    public OutputProperty<Surface> Image { get; }
+
+    public InputProperty<PointList> Points { get; }
+
+    public FuncInputProperty<Color> Color { get; }
+
+    public RasterizePointsNode()
+    {
+        Image = CreateOutput<Surface>("Image", "IMAGE", null);
+        Points = CreateInput("Points", "POINTS", PointList.Empty);
+        Color = CreateFuncInput("Color", "COLOR", Colors.White);
+    }
+
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        var points = Points.Value;
+
+        if (points.Count == 0)
+            return null;
+
+        var size = context.DocumentSize;
+        var image = new Surface(size);
+
+        var colorFunc = Color.Value;
+        var funcContext = new FuncContext();
+        foreach (var point in points)
+        {
+            funcContext.UpdateContext(point, context.DocumentSize);
+            _paint.Color = colorFunc(funcContext);
+            image.DrawingSurface.Canvas.DrawPixel((VecI)point.Multiply(size), _paint);
+        }
+
+        Image.Value = image;
+        
+        return image;
+    }
+
+    public override Node CreateCopy() => new RasterizePointsNode();
+}

+ 72 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/RemoveClosePointsNode.cs

@@ -0,0 +1,72 @@
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
+
+[NodeInfo("RemoveClosePoints", "REMOVE_CLOSE_POINTS")]
+public class RemoveClosePointsNode : Node
+{
+    public OutputProperty<PointList> Output { get; }
+    
+    public InputProperty<PointList> Input { get; }
+    
+    public InputProperty<double> MinDistance { get; }
+
+    public InputProperty<int> Seed { get; }
+
+    public RemoveClosePointsNode()
+    {
+        Output = CreateOutput("Output", "POINTS", PointList.Empty);
+        Input = CreateInput("Input", "POINTS", PointList.Empty);
+        MinDistance = CreateInput("MinDistance", "MIN_DISTANCE", 0d);
+        Seed = CreateInput("Seed", "SEED", 0);
+    }
+    
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        var distance = MinDistance.Value;
+
+        if (distance == 0)
+        {
+            Output.Value = Input.Value;
+            return null;
+        }
+
+        var availablePoints = Input.Value.Distinct().ToList();
+        var newPoints = new PointList(availablePoints.Count) { HashValue = HashCode.Combine(Input.Value.HashValue, MinDistance.Value, Seed.Value) };
+
+        var minDistance = MinDistance.Value;
+        var documentSize = context.DocumentSize;
+
+        var random = new Random(Seed.Value);
+        while (availablePoints.Count > 1)
+        {
+            var index = random.Next(availablePoints.Count);
+            var point = availablePoints[index];
+
+            newPoints.Add(point);
+            availablePoints.RemoveAt(index);
+
+            foreach (var remove in availablePoints.Where(InRange).ToList())
+            {
+                availablePoints.Remove(remove);
+            }
+
+            continue;
+            bool InRange(VecD other) => (other.Multiply(documentSize) - point.Multiply(documentSize)).Length <= minDistance;
+        }
+
+        if (availablePoints.Count == 1)
+        {
+            newPoints.Add(availablePoints[0]);
+        }
+        
+        Output.Value = newPoints;
+        
+        return null;
+
+    }
+
+    public override Node CreateCopy() => new RemoveClosePointsNode();
+}

+ 1 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-[NodeInfo("SampleImage")]
+[NodeInfo("SampleImage", "SAMPLE_IMAGE")]
 public class SampleImageNode : Node
 {
     public InputProperty<Texture?> Image { get; }
@@ -17,8 +17,6 @@ public class SampleImageNode : Node
 
     public FuncOutputProperty<Half4> Color { get; }
 
-    public override string DisplayName { get; set; } = "SAMPLE_IMAGE";
-
     public SampleImageNode()
     {
         Image = CreateInput<Texture>(nameof(Texture), "IMAGE", null);

+ 1 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -28,11 +28,7 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
 
     public string MemberName { get; set; } = "New Element"; // would be good to add localization here, it is set if node is created via node graph
     
-    public override string DisplayName
-    {
-        get => MemberName;
-        set => MemberName = value;
-    }
+    public string DisplayName => MemberName;
 
     protected Dictionary<(ChunkResolution, int), Texture> workingSurfaces = new Dictionary<(ChunkResolution, int), Texture>();
     private Paint maskPaint = new Paint() { BlendMode = DrawingApi.Core.Surfaces.BlendMode.DstIn };

+ 1 - 0
src/PixiEditor.ChangeableDocument/Enums/BlendMode.cs

@@ -18,4 +18,5 @@ public enum BlendMode
     Saturation,
     Luminosity,
     Color,
+    Erase
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Rendering/RenderingContext.cs

@@ -35,6 +35,7 @@ public class RenderingContext : IDisposable
         return blendMode switch
         {
             BlendMode.Normal => DrawingApiBlendMode.SrcOver,
+            BlendMode.Erase => DrawingApiBlendMode.DstOut,
             BlendMode.Darken => DrawingApiBlendMode.Darken,
             BlendMode.Multiply => DrawingApiBlendMode.Multiply,
             BlendMode.ColorBurn => DrawingApiBlendMode.ColorBurn,

+ 23 - 7
src/PixiEditor.DrawingApi.Core/Surface.cs

@@ -21,28 +21,44 @@ public class Surface : IDisposable, ICloneable, IPixelsMap
     
     public bool IsDisposed => disposed;
 
+    private static ImageInfo DefaultImageInfo => new(0, 0, ColorType.RgbaF16, AlphaType.Premul, ColorSpace.CreateSrgb());
+
     public event SurfaceChangedEventHandler? Changed;
 
     private Paint drawingPaint = new Paint() { BlendMode = BlendMode.Src };
     private Paint nearestNeighborReplacingPaint = new() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.None };
 
-    public Surface(VecI size)
+    private Surface(ImageInfo info)
     {
+        var size = info.Size;
+        
         if (size.X < 1 || size.Y < 1)
             throw new ArgumentException("Width and height must be >=1");
 
         Size = size;
 
-        BytesPerPixel = 8;
+        BytesPerPixel = info.BytesPerPixel;
         PixelBuffer = CreateBuffer(size.X, size.Y, BytesPerPixel);
-        DrawingSurface = CreateDrawingSurface();
+        DrawingSurface = CreateDrawingSurface(info);
     }
 
-    public Surface(Surface original) : this((VecI)original.Size)
+    public Surface(VecI size) : this(DefaultImageInfo.WithSize(size))
+    {
+    }
+
+    public Surface(Surface original) : this(original.Size)
     {
         DrawingSurface.Canvas.DrawSurface(original.DrawingSurface, 0, 0);
     }
-    
+
+    public static Surface UsingColorType(VecI size, ColorType type = ColorType.RgbaF16)
+    {
+        if (type == ColorType.Unknown)
+            throw new ArgumentException("Can't use unknown color type for surface", nameof(type));
+
+        return new Surface(DefaultImageInfo.WithSize(size).WithColorType(type));
+    }
+
     public static Surface Combine(int width, int height, List<(Image img, VecI offset)> images)
     {
         Surface surface = new Surface(new VecI(width, height));
@@ -187,9 +203,9 @@ public class Surface : IDisposable, ICloneable, IPixelsMap
     }
 #endif
 
-    private DrawingSurface CreateDrawingSurface()
+    private DrawingSurface CreateDrawingSurface(ImageInfo info)
     {
-        var surface = DrawingSurface.Create(new ImageInfo(Size.X, Size.Y, ColorType.RgbaF16, AlphaType.Premul, ColorSpace.CreateSrgb()), PixelBuffer);
+        var surface = DrawingSurface.Create(info, PixelBuffer);
         surface.Changed += DrawingSurfaceChanged;
         if (surface is null)
             throw new InvalidOperationException($"Could not create surface (Size:{Size})");

+ 33 - 1
src/PixiEditor.Numerics/ColorMatrix.cs

@@ -121,7 +121,7 @@ public record struct ColorMatrix
         (0, 0, 1, 0, 0),
         (0, 0, 0, 0, 0)
     );
-    
+
     /// <summary>
     /// The alpha value will stay the alpha value <br/>
     /// (_, _, _, w) => (0, 0, 0, w)
@@ -138,6 +138,38 @@ public record struct ColorMatrix
     /// (_, _, _, w) => (0, 0, 0, w + 1)
     /// </summary>
     public static ColorMatrix OpaqueAlphaOffset => Offset(0, 0, 0, 1);
+
+    /// <summary>
+    /// The rgb values become averaged into a grayscale image. Alpha becomes zero <br/>
+    /// (r, g, b, _) => (r, g, b, 0) / 3
+    /// </summary>
+    public static ColorMatrix AverageGrayscale { get; } = WeightedGrayscale(1 / 3f, 1 / 3f, 1 / 3f, 0);
+
+    public static ColorMatrix WeightedWavelengthGrayscale { get; } = WeightedGrayscale(0.299f, 0.587f, 0.114f, 0);
+
+    /// <summary>
+    /// The rgb values become grayscale according to the weights image. Alpha becomes zero <br/>
+    /// (r, g, b, a) => (rgb: r * rWeight + g * gWeight + b * bWeight + a * aWeight, 0)
+    /// </summary>
+    public static ColorMatrix WeightedGrayscale(float rWeight, float gWeight, float bWeight, float aWeight) => new(
+        (rWeight, gWeight, bWeight, aWeight, 0),
+        (rWeight, gWeight, bWeight, aWeight, 0),
+        (rWeight, gWeight, bWeight, aWeight, 0),
+        (0, 0, 0, 0, 0)
+    );
+
+    /// <summary>
+    /// The rgb values become grayscale according to the weights image. Alpha becomes zero <br/>
+    /// (r, g, b, a) => (rgb: r * rWeight + g * gWeight + b * bWeight + a * aWeight, 0)
+    /// </summary>
+    public static ColorMatrix WeightedGrayscale(VecD3 vector) =>
+        WeightedGrayscale((float)vector.X, (float)vector.Y, (float)vector.Z, 0);
+    
+    public static ColorMatrix Lerp(ColorMatrix from, ColorMatrix to, float amount) => new(float.Lerp(from.M11, to.M11, amount),
+        float.Lerp(from.M12, to.M12, amount), float.Lerp(from.M13, to.M13, amount), float.Lerp(from.M14, to.M14, amount), float.Lerp(from.M15, to.M15, amount), float.Lerp(from.M21, to.M21, amount),
+        float.Lerp(from.M22, to.M22, amount), float.Lerp(from.M23, to.M23, amount), float.Lerp(from.M24, to.M24, amount), float.Lerp(from.M25, to.M25, amount), float.Lerp(from.M31, to.M31, amount),
+        float.Lerp(from.M32, to.M32, amount), float.Lerp(from.M33, to.M33, amount), float.Lerp(from.M34, to.M34, amount), float.Lerp(from.M35, to.M35, amount), float.Lerp(from.M41, to.M41, amount),
+        float.Lerp(from.M42, to.M42, amount), float.Lerp(from.M43, to.M43, amount), float.Lerp(from.M44, to.M44, amount), float.Lerp(from.M45, to.M45, amount));
     
     public static ColorMatrix operator +(ColorMatrix left, ColorMatrix right) => new(left.M11 + right.M11,
         left.M12 + right.M12, left.M13 + right.M13, left.M14 + right.M14, left.M15 + right.M15, left.M21 + right.M21,

+ 1 - 1
src/PixiEditor.Numerics/PixiEditor.Numerics.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>netstandard2.1</TargetFramework>
+        <TargetFramework>net8.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <LangVersion>default</LangVersion>

+ 172 - 0
src/PixiEditor.Numerics/VecD3.cs

@@ -0,0 +1,172 @@
+namespace PixiEditor.Numerics;
+
+public struct VecD3
+{
+    public double X { get; set; }
+    public double Y { get; set; }
+    public double Z { get; set; }
+
+    public VecD XY => new(X, Y);
+
+    public double TaxicabLength => Math.Abs(X) + Math.Abs(Y) + Math.Abs(Z);
+    public double Length => Math.Sqrt(LengthSquared);
+    public double LengthSquared => X * X + Y * Y + Z * Z;
+
+    public static VecD3 Zero { get; } = new(0, 0, 0);
+
+    public VecD3(double x, double y, double z)
+    {
+        X = x;
+        Y = y;
+        Z = z;
+    }
+
+    public VecD3(VecD xy, double z) : this(xy.X, xy.Y, z)
+    {
+    }
+
+    public VecD3(double bothAxesValue) : this(bothAxesValue, bothAxesValue, bothAxesValue)
+    {
+    }
+    
+    public VecD3 Round()
+    {
+        return new VecD3(Math.Round(X), Math.Round(Y), Math.Round(Z));
+    }
+    public VecD3 Ceiling()
+    {
+        return new VecD3(Math.Ceiling(X), Math.Ceiling(Y), Math.Ceiling(Z));
+    }
+    public VecD3 Floor()
+    {
+        return new VecD3(Math.Floor(X), Math.Floor(Y), Math.Floor(Z));
+    }
+    
+    public VecD3 Lerp(VecD3 other, double factor)
+    {
+        return (other - this) * factor + this;
+    }
+    
+    public VecD3 Normalize()
+    {
+        return new VecD3(X / Length, Y / Length, Z / Length);
+    }
+    
+    public VecD3 Abs()
+    {
+        return new VecD3(Math.Abs(X), Math.Abs(Y), Math.Abs(Z));
+    }
+    
+    public VecD3 Signs()
+    {
+        return new VecD3(X >= 0 ? 1 : -1, Y >= 0 ? 1 : -1, Z >= 0 ? 1 : -1);
+    }
+    
+    public double Dot(VecD3 other) => (X * other.X) + (Y * other.Y) + (Z * other.Z);
+    
+    public VecD3 Multiply(VecD3 other)
+    {
+        return new VecD3(X * other.X, Y * other.Y, Z * other.Z);
+    }
+    
+    public VecD3 Divide(VecD3 other)
+    {
+        return new VecD3(X / other.X, Y / other.Y, Z / other.Z);
+    }
+    
+    public static VecD3 operator +(VecD3 a, VecD3 b)
+    {
+        return new VecD3(a.X * b.X, a.Y * b.Y, a.Z * b.Z);
+    }
+    
+    public static VecD3 operator -(VecD3 a, VecD3 b)
+    {
+        return new VecD3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
+    }
+
+    public static VecD3 operator -(VecD3 a)
+    {
+        return new VecD3(-a.X, -a.Y, -a.Z);
+    }
+    
+    public static VecD3 operator *(double b, VecD3 a)
+    {
+        return new VecD3(a.X * b, a.Y * b, a.Z * b);
+    }
+
+    public static double operator *(VecD3 a, VecD3 b) => a.Dot(b);
+    
+    public static VecD3 operator *(VecD3 a, double b)
+    {
+        return new VecD3(a.X * b, a.Y * b, a.Z * b);
+    }
+    
+    public static VecD3 operator /(VecD3 a, double b)
+    {
+        return new VecD3(a.X / b, a.Y / b, a.Z / b);
+    }
+    
+    public static VecD3 operator %(VecD3 a, double b)
+    {
+        return new VecD3(a.X % b, a.Y % b, a.Z % b);
+    }
+
+    public static bool operator ==(VecD3 a, VecD3 b)
+    {
+        return a.X == b.X && a.Y == b.Y && a.Z == b.Z;
+    }
+    public static bool operator !=(VecD3 a, VecD3 b)
+    {
+        return !(a.X == b.X && a.Y == b.Y && a.Z == b.Z);
+    }
+
+    public double Sum() => X + Y + Z;
+
+    public static implicit operator VecD3((double, double, double, double) tuple)
+    {
+        return new VecD3(tuple.Item1, tuple.Item2, tuple.Item3);
+    }
+    
+    public void Deconstruct(out double x, out double y, out double z)
+    {
+        x = X;
+        y = Y;
+        z = Z;
+    }
+    
+    public bool IsNaNOrInfinity()
+    {
+        return double.IsNaN(X) || double.IsNaN(Y) || double.IsInfinity(X) || double.IsInfinity(Y) || double.IsNaN(Z) || double.IsInfinity(Z);
+    }
+
+    public override string ToString()
+    {
+        return $"({X}; {Y}; {Z})";
+    }
+
+    public override bool Equals(object? obj)
+    {
+        if (obj is not VecD3 item)
+            return false;
+        
+        return this == (VecD3?)item;
+    }
+
+    public override int GetHashCode()
+    {
+        return HashCode.Combine(X, Y, Z);
+    }
+
+    public bool Equals(VecD3 other)
+    {
+        return other.X == X && other.Y == Y && other.Z == Z;
+    }
+
+    public bool AlmostEquals(VecD3 other, double axisEpsilon = 0.001)
+    {
+        double dX = Math.Abs(X - other.X);
+        double dY = Math.Abs(Y - other.Y);
+        double dZ = Math.Abs(Z - other.Z);
+        return dX < axisEpsilon && dY < axisEpsilon && dZ < axisEpsilon;
+    }
+}

+ 10 - 0
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -47,10 +47,14 @@
             <Color x:Key="VecDSocketColor">#c984ca</Color>
             <Color x:Key="VecISocketColor">#c9b4ca</Color>
             <Color x:Key="IntSocketColor">#4C64B1</Color>
+            <Color x:Key="PointListSocketColor">#7f5280</Color>
             
             <Color x:Key="PixiEditorModifyImageBorderColor">#68abdf</Color>
             <Color x:Key="PixiEditorModifyImageNodeBackgroundColor">#4068abdf</Color>
 
+            <Color x:Key="PixiEditorColorEvaluatorBorderColor">#99e4aa</Color>
+            <Color x:Key="PixiEditorColorEvaluatorNodeBackgroundColor">#4099e4aa</Color>
+
             <Color x:Key="NodeFrameBorderColor">#101010</Color>
             <Color x:Key="NodeFrameBackgroundColor">#40101010</Color>
 
@@ -103,12 +107,18 @@
             <SolidColorBrush x:Key="Int2SocketBrush" Color="{StaticResource VecISocketColor}"/>
             <SolidColorBrush x:Key="Int32SocketBrush" Color="{StaticResource IntSocketColor}"/>
             <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}"/>
+            <SolidColorBrush x:Key="PointListSocketBrush" Color="{StaticResource PointListSocketColor}"/>
             
             <SolidColorBrush x:Key="PixiEditorModifyImageLeftBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
             <SolidColorBrush x:Key="PixiEditorModifyImageRightBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
             <SolidColorBrush x:Key="PixiEditorModifyImageZoneBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
             <SolidColorBrush x:Key="PixiEditorModifyImageZoneBackgroundBrush" Color="{StaticResource PixiEditorModifyImageNodeBackgroundColor}"/>
             
+            <SolidColorBrush x:Key="PixiEditorColorEvaluatorLeftBorderBrush" Color="{StaticResource PixiEditorColorEvaluatorBorderColor}"/>
+            <SolidColorBrush x:Key="PixiEditorColorEvaluatorRightBorderBrush" Color="{StaticResource PixiEditorColorEvaluatorBorderColor}"/>
+            <SolidColorBrush x:Key="PixiEditorColorEvaluatorZoneBorderBrush" Color="{StaticResource PixiEditorColorEvaluatorBorderColor}"/>
+            <SolidColorBrush x:Key="PixiEditorColorEvaluatorZoneBackgroundBrush" Color="{StaticResource PixiEditorColorEvaluatorNodeBackgroundColor}"/>
+
             <SolidColorBrush x:Key="NodeFrameBorderBrush" Color="{StaticResource NodeFrameBorderColor}"/>
             <SolidColorBrush x:Key="NodeFrameBackgroundBrush" Color="{StaticResource NodeFrameBackgroundColor}"/>
 

BIN
src/PixiEditor/Data/BetaExampleFiles/Stars.pixi


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

@@ -382,6 +382,7 @@
   "LAYERS_TITLE": "Layers",
   "NAVIGATION_TITLE": "Navigation",
   "NORMAL_BLEND_MODE": "Normal",
+  "ERASE_BLEND_MODE": "Erase",
   "DARKEN_BLEND_MODE": "Darken",
   "MULTIPLY_BLEND_MODE": "Multiply",
   "COLOR_BURN_BLEND_MODE": "Color burn",
@@ -651,10 +652,11 @@
   "IMAGE_SPACE_NODE": "Image Space",
   "KERNEL_FILTER_NODE": "Kernel Filter",
   "MATH_NODE": "Math",
-  "MATRIX_TRANSFORM_NODE": "Matrix Transform",
+  "COLOR_MATRIX_TRANSFORM_FILTER_NODE": "Matrix Transform Filter",
   "MERGE_NODE": "Merge",
   "MODIFY_IMAGE_LEFT_NODE": "Begin Modify Image",
   "MODIFY_IMAGE_RIGHT_NODE": "End Modify Image",
+  "MODIFY_IMAGE_PAIR_NODE": "Modify Image",
   "COMBINE_CHANNELS_NODE": "Combine Channels",
   "COMBINE_COLOR_NODE": "Combine Color",
   "COMBINE_VECD_NODE": "Combine Vector",
@@ -685,6 +687,7 @@
   "APPLY_FILTER_NODE": "Apply Filter",
   "FILTER": "Filter",
   "LERP_NODE": "Lerp",
+  "GRAYSCALE_FILTER_NODE": "Grayscale Filter",
   "FROM": "From",
   "TO": "To",
   "TIME": "Time",
@@ -694,5 +697,20 @@
   "FINISHED": "Finished",
   "GENERATING_SPRITE_SHEET": "Generating Sprite Sheet",
   "RENDERING_IMAGE": "Rendering Image",
-  "PROGRESS_POPUP_TITLE": "Progress"
+  "PROGRESS_POPUP_TITLE": "Progress",
+  "POINTS": "Points",
+  "MIN_DISTANCE": "Min. Distance",
+  "MAX_POINTS": "Max. Points",
+  "PROBABILITY": "Probability",
+  "DISTRIBUTE_POINTS": "Distribute points",
+  "REMOVE_CLOSE_POINTS": "Remove close points",
+  "RASTERIZE_POINTS": "Rasterize Points",
+  "BEGIN_COLOR_EVALUATOR_NODE": "Begin evaluating color",
+  "FINISH_COLOR_EVALUATOR_NODE": "Finish evaluating color",
+  "COLOR_EVALUATOR_NODE_PAIR": "Color Evaluator",
+  "MODE": "Mode",
+  "Factor": "Factor",
+  "NORMALIZE": "Normalize",
+  "WEIGHT_FACTOR": "Weight",
+  "STARS_EXAMPLE": "Stars"
 }

+ 1 - 0
src/PixiEditor/Helpers/Extensions/BlendModeEx.cs

@@ -8,6 +8,7 @@ internal static class BlendModeEx
         return mode switch
         {
             BlendMode.Normal => "NORMAL_BLEND_MODE",
+            BlendMode.Erase => "ERASE_BLEND_MODE",
             BlendMode.Darken => "DARKEN_BLEND_MODE",
             BlendMode.Multiply => "MULTIPLY_BLEND_MODE",
             BlendMode.ColorBurn => "COLOR_BURN_BLEND_MODE",

+ 104 - 0
src/PixiEditor/Helpers/Nodes/NodeAbbreviation.cs

@@ -0,0 +1,104 @@
+using System.Buffers;
+using PixiEditor.Models.Nodes;
+
+namespace PixiEditor.Helpers.Nodes;
+
+public static class NodeAbbreviation
+{
+    private static readonly SearchValues<char> SearchFor = SearchValues.Create(['.']);
+
+    
+    public static bool IsAbbreviation(string value, out string? lastValue)
+    {
+        var span = value.AsSpan();
+
+        int i = span.LastIndexOfAny(SearchFor);
+
+        if (i == -1)
+        {
+            lastValue = null;
+            return false;
+        }
+
+        lastValue = span[(i + 1)..].ToString();
+        return true;
+    }
+    
+    public static List<NodeTypeInfo>? FromString(string value, ICollection<NodeTypeInfo> allNodes)
+    {
+        var span = value.AsSpan();
+
+        if (!span.ContainsAny(SearchFor))
+        {
+            return null;
+        }
+        
+        var list = new List<NodeTypeInfo>();
+
+        var enumerator = new PartEnumerator(span, SearchFor);
+
+        foreach (var name in enumerator)
+        {
+            var lookFor = name.Name.ToString();
+            var node = allNodes.First(SearchComparer);
+
+            list.Add(node);
+
+            continue;
+
+            bool SearchComparer(NodeTypeInfo x) =>
+                x.FinalPickerName.Value.Replace(" ", "").Contains(lookFor.Replace(" ", ""), StringComparison.OrdinalIgnoreCase);
+        }
+
+        return list;
+    }
+
+    private readonly ref struct PartResult(ReadOnlySpan<char> name, char? separator)
+    {
+        public ReadOnlySpan<char> Name { get; } = name;
+
+        public char? Separator { get; } = separator;
+    }
+
+    private ref struct PartEnumerator(ReadOnlySpan<char> name, SearchValues<char> searchFor)
+    {
+        private bool isDone;
+        private ReadOnlySpan<char> _remaining = name;
+        private PartResult _current;
+
+        public PartResult Current => _current;
+
+        /// <summary>
+        /// Returns this instance as an enumerator.
+        /// </summary>
+        public PartEnumerator GetEnumerator() => this;
+
+        public bool MoveNext()
+        {
+            if (isDone)
+                return false;
+
+            int i = _remaining.IndexOfAny(searchFor);
+
+            if (i == -1)
+            {
+                isDone = true;
+
+                if (_remaining.Length == 0)
+                {
+                    return false;
+                }
+
+                _current = new PartResult(_remaining, null);
+                return true;
+            }
+
+            _current = new PartResult(_remaining[..i], _remaining[i]);
+
+            i++;
+            _remaining = _remaining[i..];
+
+            return true;
+        }
+    }
+}

+ 1 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -123,6 +123,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<SerializationFactory, VecISerializationFactory>()
             .AddSingleton<SerializationFactory, ColorSerializationFactory>()
             .AddSingleton<SerializationFactory, ColorMatrixSerializationFactory>()
+            .AddSingleton<SerializationFactory, PointListSerializationFactory>()
             // Palette Parsers
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<PaletteFileParser, JascFileParser>()

+ 1 - 1
src/PixiEditor/Models/AnalyticsAPI/AnalyticsClient.cs

@@ -110,7 +110,7 @@ public class AnalyticsClient
         public override void Write(Utf8JsonWriter writer, VecD value, JsonSerializerOptions options)
         {
             var invariant = CultureInfo.InvariantCulture;
-            writer.WriteStringValue($"{value.X.ToString("F2", invariant)}; {value.X.ToString("F2", invariant)}");
+            writer.WriteStringValue($"{value.X.ToString("F2", invariant)}; {value.Y.ToString("F2", invariant)}");
         }
     }
     

+ 5 - 0
src/PixiEditor/Models/Commands/CommandController.cs

@@ -422,8 +422,13 @@ internal class CommandController
     {
         object CastParameter(object input, Type target)
         {
+            var commandExecutionType = typeof(CommandExecutionContext);
+            if (input is CommandExecutionContext context && !target.IsAssignableTo(commandExecutionType))
+                input = context.Parameter;
+
             if (target == typeof(object) || target == input?.GetType())
                 return input;
+            
             return Convert.ChangeType(input, target);
         }
 

+ 33 - 0
src/PixiEditor/Models/Nodes/NodeTypeInfo.cs

@@ -0,0 +1,33 @@
+using System.Reflection;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.Models.Nodes;
+
+public class NodeTypeInfo
+{
+    public string UniqueName { get; }
+    
+    public string DisplayName { get; }
+    
+    public string? PickerName { get; }
+
+    public LocalizedString FinalPickerName { get; }
+
+    public bool Hidden => PickerName is { Length: 0 };
+    
+    public Type NodeType { get; }
+
+    public NodeTypeInfo(Type type)
+    {
+        NodeType = type;
+
+        var attribute = type.GetCustomAttribute<NodeInfoAttribute>();
+
+        UniqueName = attribute.UniqueName;
+        DisplayName = attribute.DisplayName;
+        PickerName = attribute.PickerName;
+
+        FinalPickerName = PickerName ?? DisplayName;
+    }
+}

+ 27 - 0
src/PixiEditor/Models/Serialization/Factories/PointListSerializationFactory.cs

@@ -0,0 +1,27 @@
+using MessagePack;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
+
+namespace PixiEditor.Models.Serialization.Factories;
+
+public class PointListSerializationFactory : SerializationFactory<byte[], PointList>
+{
+    public override string DeserializationId { get; } = "PixiEditor.PointList";
+
+    public override byte[] Serialize(PointList original)
+    {
+        return MessagePackSerializer.Serialize(original);
+    }
+
+    public override bool TryDeserialize(object serialized, out PointList? original)
+    {
+        if (serialized is not byte[] buffer)
+        {
+            original = null;
+            return false;
+        }
+        
+        original = MessagePackSerializer.Deserialize<PointList>(buffer);
+
+        return true;
+    }
+}

+ 1 - 1
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -10,7 +10,7 @@
                     <Grid.ContextFlyout>
                         <Flyout>
                             <nodes:NodePicker
-                                AllNodeTypes="{Binding AllNodeTypes, RelativeSource={RelativeSource TemplatedParent}}"
+                                AllNodeTypeInfos="{Binding AllNodeTypeInfos, RelativeSource={RelativeSource TemplatedParent}}"
                                 SearchQuery="{Binding SearchQuery, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                                 SelectNodeCommand="{Binding CreateNodeFromContextCommand, RelativeSource={RelativeSource TemplatedParent}}"
                                 />

+ 10 - 5
src/PixiEditor/Styles/Templates/NodePicker.axaml

@@ -2,7 +2,9 @@
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:nodes="clr-namespace:PixiEditor.Views.Nodes"
                     xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
-                    xmlns:input="clr-namespace:PixiEditor.Views.Input">
+                    xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                    xmlns:input="clr-namespace:PixiEditor.Views.Input"
+                    xmlns:nodeModels="clr-namespace:PixiEditor.Models.Nodes">
     <ControlTheme TargetType="nodes:NodePicker" x:Key="{x:Type nodes:NodePicker}">
         <Setter Property="Template">
             <ControlTemplate>
@@ -13,13 +15,16 @@
                     </Grid.RowDefinitions>
 
                     <input:InputBox
-                        Text="{Binding SearchQuery, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
-                    <ItemsControl MinHeight="200" Grid.Row="1" ItemsSource="{TemplateBinding FilteredNodeTypes}">
+                        Text="{Binding SearchQuery, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                        Name="PART_InputBox" />
+                    <ItemsControl MinHeight="200" Grid.Row="1" ItemsSource="{TemplateBinding FilteredNodeTypeInfos}">
                         <ItemsControl.ItemTemplate>
-                            <DataTemplate>
+                            <DataTemplate DataType="nodeModels:NodeTypeInfo">
                                 <Button Command="{Binding SelectNodeCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodePicker}}"
                                         CommandParameter="{Binding}"
-                                        Content="{Binding Name}" />
+                                        Margin="0,1"
+                                        IsVisible="{Binding !Hidden}"
+                                        ui:Translator.LocalizedString="{Binding FinalPickerName}" />
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>
                     </ItemsControl>

+ 42 - 0
src/PixiEditor/ViewModels/Nodes/Properties/VecD3PropertyViewModel.cs

@@ -0,0 +1,42 @@
+using System.ComponentModel;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ViewModels.Nodes.Properties;
+
+internal class VecD3PropertyViewModel : NodePropertyViewModel<VecD3>
+{
+    public VecD3PropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+        PropertyChanged += OnPropertyChanged;
+    }
+    
+    private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName != nameof(Value))
+        {
+            return;
+        }
+        
+        OnPropertyChanged(nameof(XValue));
+        OnPropertyChanged(nameof(YValue));
+        OnPropertyChanged(nameof(ZValue));
+    }
+
+    public double XValue
+    {
+        get => Value.X;
+        set => Value = new VecD3(value, Value.Y, Value.Z);
+    }
+    
+    public double YValue
+    {
+        get => Value.Y;
+        set => Value = new VecD3(Value.X, value, Value.Z);
+    }
+    
+    public double ZValue
+    {
+        get => Value.Z;
+        set => Value = new VecD3(Value.X, Value.Y, value);
+    }
+}

+ 1 - 0
src/PixiEditor/Views/Input/BlendModeComboBox.cs

@@ -74,6 +74,7 @@ internal class BlendModeComboBox : ComboBox
         var items = new List<AvaloniaObject>()
         {
             new ComboBoxItem() { Content = BlendMode.Normal.LocalizedKeys(), Tag = BlendMode.Normal },
+            new ComboBoxItem() { Content = BlendMode.Erase.LocalizedKeys(), Tag = BlendMode.Erase },
             new Separator(),
             new ComboBoxItem() { Content = BlendMode.Darken.LocalizedKeys(), Tag = BlendMode.Darken },
             new ComboBoxItem() { Content = BlendMode.Multiply.LocalizedKeys(), Tag = BlendMode.Multiply },

+ 17 - 4
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -14,6 +14,7 @@ using PixiEditor.ViewModels.Document;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.Models.Handlers;
+using PixiEditor.Models.Nodes;
 using PixiEditor.Numerics;
 using PixiEditor.ViewModels.Nodes;
 using PixiEditor.Views.Nodes.Properties;
@@ -59,6 +60,10 @@ internal class NodeGraphView : Zoombox.Zoombox
         AvaloniaProperty.Register<NodeGraphView, ObservableCollection<Type>>(
             nameof(AllNodeTypes));
 
+    public static readonly StyledProperty<ObservableCollection<NodeTypeInfo>> AllNodeTypeInfosProperty =
+        AvaloniaProperty.Register<NodeGraphView, ObservableCollection<NodeTypeInfo>>(
+            nameof(AllNodeTypeInfos));
+
     public static readonly StyledProperty<ICommand> SocketDropCommandProperty =
         AvaloniaProperty.Register<NodeGraphView, ICommand>(
             nameof(SocketDropCommand));
@@ -90,6 +95,12 @@ internal class NodeGraphView : Zoombox.Zoombox
         get => GetValue(AllNodeTypesProperty);
         set => SetValue(AllNodeTypesProperty, value);
     }
+    
+    public ObservableCollection<NodeTypeInfo> AllNodeTypeInfos
+    {
+        get => GetValue(AllNodeTypeInfosProperty);
+        set => SetValue(AllNodeTypeInfosProperty, value);
+    }
 
     public string SearchQuery
     {
@@ -180,16 +191,18 @@ internal class NodeGraphView : Zoombox.Zoombox
         DraggedCommand = new RelayCommand<PointerEventArgs>(Dragged);
         EndDragCommand = new RelayCommand<PointerCaptureLostEventArgs>(EndDrag);
         SocketDropCommand = new RelayCommand<NodeSocket>(SocketDrop);
-        CreateNodeFromContextCommand = new RelayCommand<Type>(CreateNodeType);
+        CreateNodeFromContextCommand = new RelayCommand<NodeTypeInfo>(CreateNodeType);
 
         AllNodeTypes = new ObservableCollection<Type>(GatherAssemblyTypes<Node>());
+        AllNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(AllNodeTypes.Select(x => new NodeTypeInfo(x)));
     }
     
-    private void CreateNodeType(Type nodeType)
+    private void CreateNodeType(NodeTypeInfo nodeType)
     {
-        if (CreateNodeCommand != null && CreateNodeCommand.CanExecute(nodeType))
+        var type = nodeType.NodeType;
+        if (CreateNodeCommand != null && CreateNodeCommand.CanExecute(type))
         {
-            CreateNodeCommand.Execute((nodeType, _lastMouseClickPos));
+            CreateNodeCommand.Execute((type, _lastMouseClickPos));
             ((Control)this.GetVisualDescendants().FirstOrDefault()).ContextFlyout.Hide();
         }
     }

+ 67 - 16
src/PixiEditor/Views/Nodes/NodePicker.cs

@@ -2,14 +2,20 @@
 using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
+using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
 using CommunityToolkit.Mvvm.Input;
+using PixiEditor.Helpers.Nodes;
+using PixiEditor.Models.Nodes;
 using PixiEditor.Numerics;
+using PixiEditor.Views.Input;
 
 namespace PixiEditor.Views.Nodes;
 
+[TemplatePart("PART_InputBox", typeof(InputBox))]
 public partial class NodePicker : TemplatedControl
 {
     public static readonly StyledProperty<string> SearchQueryProperty = AvaloniaProperty.Register<NodePicker, string>(
@@ -21,23 +27,23 @@ public partial class NodePicker : TemplatedControl
         set => SetValue(SearchQueryProperty, value);
     }
 
-    public static readonly StyledProperty<ObservableCollection<Type>> AllNodeTypesProperty =
-        AvaloniaProperty.Register<NodePicker, ObservableCollection<Type>>(
+    public static readonly StyledProperty<ObservableCollection<NodeTypeInfo>> AllNodeTypeInfosProperty =
+        AvaloniaProperty.Register<NodePicker, ObservableCollection<NodeTypeInfo>>(
             "AllNodeTypes");
 
-    public static readonly StyledProperty<ObservableCollection<Type>> FilteredNodeTypesProperty =
-        AvaloniaProperty.Register<NodePicker, ObservableCollection<Type>>(nameof(FilteredNodeTypes));
+    public static readonly StyledProperty<ObservableCollection<NodeTypeInfo>> FilteredNodeTypeInfosProperty =
+        AvaloniaProperty.Register<NodePicker, ObservableCollection<NodeTypeInfo>>(nameof(FilteredNodeTypeInfos));
 
-    public ObservableCollection<Type> AllNodeTypes
+    public ObservableCollection<NodeTypeInfo> AllNodeTypeInfos
     {
-        get => GetValue(AllNodeTypesProperty);
-        set => SetValue(AllNodeTypesProperty, value);
+        get => GetValue(AllNodeTypeInfosProperty);
+        set => SetValue(AllNodeTypeInfosProperty, value);
     }
 
-    public ObservableCollection<Type> FilteredNodeTypes
+    public ObservableCollection<NodeTypeInfo> FilteredNodeTypeInfos
     {
-        get { return (ObservableCollection<Type>)GetValue(FilteredNodeTypesProperty); }
-        set { SetValue(FilteredNodeTypesProperty, value); }
+        get => GetValue(FilteredNodeTypeInfosProperty);
+        set => SetValue(FilteredNodeTypeInfosProperty, value);
     }
 
     public static readonly StyledProperty<ICommand> SelectNodeCommandProperty = AvaloniaProperty.Register<NodePicker, ICommand>(
@@ -48,27 +54,72 @@ public partial class NodePicker : TemplatedControl
         get => GetValue(SelectNodeCommandProperty);
         set => SetValue(SelectNodeCommandProperty, value);
     }
-    
+
     static NodePicker()
     {
         SearchQueryProperty.Changed.Subscribe(OnSearchQueryChanged);
-        AllNodeTypesProperty.Changed.Subscribe(OnAllNodeTypesChanged);
+        AllNodeTypeInfosProperty.Changed.Subscribe(OnAllNodeTypesChanged);
+    }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        var inputBox = e.NameScope.Find<InputBox>("PART_InputBox");
+        
+        inputBox.KeyDown += OnInputBoxKeyDown;
     }
 
     private static void OnSearchQueryChanged(AvaloniaPropertyChangedEventArgs e)
     {
-        if (e.Sender is NodePicker nodePicker)
+        if (e.Sender is not NodePicker nodePicker)
         {
-            nodePicker.FilteredNodeTypes = new ObservableCollection<Type>(nodePicker.AllNodeTypes
-                .Where(x => x.Name.ToLower().Contains(nodePicker.SearchQuery.ToLower())));
+            return;
         }
+
+        if (NodeAbbreviation.IsAbbreviation(nodePicker.SearchQuery, out var abbreviationName))
+        {
+            nodePicker.FilteredNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos
+                .Where(x => SearchComparer(x, abbreviationName)));
+        }
+        else
+        {
+            nodePicker.FilteredNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos
+                .Where(x => SearchComparer(x, nodePicker.SearchQuery)));
+        }
+
+        return;
+
+        bool SearchComparer(NodeTypeInfo x, string lookFor) =>
+            x.FinalPickerName.Value.Replace(" ", "")
+                .Contains(lookFor.Replace(" ", ""), StringComparison.OrdinalIgnoreCase);
     }
     
+    private void OnInputBoxKeyDown(object? sender, KeyEventArgs e)
+    {
+        if (e.Key != Key.Enter)
+        {
+            return;
+        }
+
+        var nodes = NodeAbbreviation.FromString(SearchQuery, AllNodeTypeInfos);
+
+        if (nodes == null && FilteredNodeTypeInfos.Count > 0)
+        {
+            SelectNodeCommand.Execute(FilteredNodeTypeInfos[0]);
+        }
+        else
+        {
+            foreach (var node in nodes)
+            {
+                SelectNodeCommand.Execute(node);
+            }
+        }
+    }
+
     private static void OnAllNodeTypesChanged(AvaloniaPropertyChangedEventArgs e)
     {
         if (e.Sender is NodePicker nodePicker)
         {
-            nodePicker.FilteredNodeTypes = new ObservableCollection<Type>(nodePicker.AllNodeTypes);
+            nodePicker.FilteredNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos);
         }
     }
 }

+ 19 - 0
src/PixiEditor/Views/Nodes/Properties/VecD3PropertyView.axaml

@@ -0,0 +1,19 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.Views.Nodes.Properties"
+                             xmlns:input="clr-namespace:PixiEditor.Views.Input"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.Views.Nodes.Properties.VecD3PropertyView">
+    <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
+        <StackPanel IsVisible="{Binding ShowInputField}">
+            <input:NumberInput EnableScrollChange="False" MinWidth="100" Value="{Binding XValue, Mode=TwoWay}" Margin="0,2" />
+            <input:NumberInput EnableScrollChange="False" MinWidth="100" Value="{Binding YValue, Mode=TwoWay}" Margin="0,2" />
+            <input:NumberInput EnableScrollChange="False" MinWidth="100" Value="{Binding ZValue, Mode=TwoWay}" Margin="0,2" />
+        </StackPanel>
+    </StackPanel>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor/Views/Nodes/Properties/VecD3PropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.Views.Nodes.Properties;
+
+public partial class VecD3PropertyView : NodePropertyView
+{
+    public VecD3PropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 2 - 0
src/PixiEditor/Views/Windows/HelloTherePopup.axaml

@@ -84,6 +84,7 @@
                         <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                             <windows:BetaExampleButton FileName="Pond.pixi" DisplayName="POND_EXAMPLE" CloseCommand="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=windows:HelloTherePopup}}" />
                             <windows:BetaExampleButton FileName="Tree.pixi" DisplayName="TREE_EXAMPLE" CloseCommand="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=windows:HelloTherePopup}}" />
+                            <windows:BetaExampleButton FileName="Stars.pixi" DisplayName="STARS_EXAMPLE" CloseCommand="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=windows:HelloTherePopup}}" />
                             
                             <StackPanel>
                                 <Button Margin="0,10,0,0" HorizontalAlignment="Center"
@@ -339,6 +340,7 @@
                         <StackPanel Orientation="Horizontal">
                             <windows:BetaExampleButton FileName="Pond.pixi" DisplayName="POND_EXAMPLE" CloseCommand="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=windows:HelloTherePopup}}" />
                             <windows:BetaExampleButton FileName="Tree.pixi" DisplayName="TREE_EXAMPLE" CloseCommand="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=windows:HelloTherePopup}}" />
+                            <windows:BetaExampleButton FileName="Stars.pixi" DisplayName="STARS_EXAMPLE" CloseCommand="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=windows:HelloTherePopup}}" />
                             <windows:BetaExampleButton FileName="Outline.pixi" DisplayName="OUTLINE_EXAMPLE" CloseCommand="{Binding CloseCommand, RelativeSource={RelativeSource AncestorType=windows:HelloTherePopup}}" />
                         </StackPanel>
                     </ScrollViewer>