Browse Source

A lot of serialization fixes

flabbet 1 year ago
parent
commit
133f1aa443
29 changed files with 352 additions and 51 deletions
  1. 8 4
      src/PixiEditor.AvaloniaUI/Helpers/DocumentViewModelBuilder.cs
  2. 6 0
      src/PixiEditor.AvaloniaUI/Helpers/SerializationUtil.cs
  3. 3 0
      src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs
  4. 29 0
      src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ColorSerializationFactory.cs
  5. 28 0
      src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/VecDSerializationFactory.cs
  6. 29 0
      src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/VecISerializationFactory.cs
  7. 19 11
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs
  8. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  9. 12 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs
  10. 14 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  11. 8 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPairNodeEnd.cs
  12. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineColorNode.cs
  13. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecD.cs
  14. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineVecI.cs
  15. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateColorNode.cs
  16. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecDNode.cs
  17. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateVecINode.cs
  18. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs
  19. 46 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LerpColorNode.cs
  20. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  21. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  22. 42 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  23. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  24. 13 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  25. 55 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  26. 6 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConversionTable.cs
  27. 4 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodePair_Change.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeserializeNodeAdditionalData_Change.cs
  29. 9 0
      src/PixiEditor.DrawingApi.Core/ColorsImpl/Color.cs

+ 8 - 4
src/PixiEditor.AvaloniaUI/Helpers/DocumentViewModelBuilder.cs

@@ -290,7 +290,7 @@ internal class NodeGraphBuilder
         public Dictionary<string, object> InputValues { get; set; }
         public KeyFrameData[] KeyFrames { get; set; }
         public Dictionary<string, object> AdditionalData { get; set; }
-        public Dictionary<int, (string inputPropName, string outputPropName)> InputConnections { get; set; }
+        public Dictionary<int, List<(string inputPropName, string outputPropName)>> InputConnections { get; set; }
 
         public NodeBuilder WithId(int id)
         {
@@ -330,12 +330,16 @@ internal class NodeGraphBuilder
 
         public NodeBuilder WithConnections(PropertyConnection[] nodeInputConnections)
         {
-            InputConnections = new Dictionary<int, (string, string)>();
+            InputConnections = new Dictionary<int, List<(string, string)>>();
 
             foreach (var connection in nodeInputConnections)
             {
-                InputConnections.Add(connection.OutputNodeId,
-                    (connection.InputPropertyName, connection.OutputPropertyName));
+                if (!InputConnections.ContainsKey(connection.OutputNodeId))
+                {
+                    InputConnections.Add(connection.OutputNodeId, new List<(string, string)>());
+                }
+                
+                InputConnections[connection.OutputNodeId].Add((connection.InputPropertyName, connection.OutputPropertyName)); 
             }
 
             return this;

+ 6 - 0
src/PixiEditor.AvaloniaUI/Helpers/SerializationUtil.cs

@@ -1,6 +1,7 @@
 using System.Collections;
 using PixiEditor.AvaloniaUI.Models.Serialization;
 using PixiEditor.AvaloniaUI.Models.Serialization.Factories;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 namespace PixiEditor.AvaloniaUI.Helpers;
 
@@ -14,6 +15,11 @@ public static class SerializationUtil
             return null;
         }
 
+        if (value is Delegate del)
+        {
+            value = del.DynamicInvoke(FuncContext.NoContext);
+        }
+
         var factory = allFactories.FirstOrDefault(x => x.OriginalType == value.GetType());
 
         if (factory != null)

+ 3 - 0
src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs

@@ -116,6 +116,9 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<SerializationFactory, SurfaceSerializationFactory>()
             .AddSingleton<SerializationFactory, ChunkyImageSerializationFactory>()
             .AddSingleton<SerializationFactory, KernelSerializationFactory>()
+            .AddSingleton<SerializationFactory, VecDSerializationFactory>()
+            .AddSingleton<SerializationFactory, VecISerializationFactory>()
+            .AddSingleton<SerializationFactory, ColorSerializationFactory>()
             // Palette Parsers
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<PaletteFileParser, JascFileParser>()

+ 29 - 0
src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ColorSerializationFactory.cs

@@ -0,0 +1,29 @@
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+
+namespace PixiEditor.AvaloniaUI.Models.Serialization.Factories;
+
+public class ColorSerializationFactory : SerializationFactory<byte[], Color>
+{
+    public override string DeserializationId { get; } = "PixiEditor.Color";
+    public override byte[] Serialize(Color original)
+    {
+        byte[] result = new byte[4];
+        result[0] = original.R;
+        result[1] = original.G;
+        result[2] = original.B;
+        result[3] = original.A;
+        return result; 
+    }
+
+    public override bool TryDeserialize(object serialized, out Color original)
+    {
+        if (serialized is byte[] { Length: 4 } bytes)
+        {
+            original = new Color(bytes[0], bytes[1], bytes[2], bytes[3]);
+            return true;
+        }
+
+        original = default;
+        return false; 
+    }
+}

+ 28 - 0
src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/VecDSerializationFactory.cs

@@ -0,0 +1,28 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Models.Serialization.Factories;
+
+public class VecDSerializationFactory : SerializationFactory<byte[], VecD>
+{
+    public override string DeserializationId { get; } = "PixiEditor.VecD";
+
+    public override byte[] Serialize(VecD original)
+    {
+        byte[] result = new byte[sizeof(double) * 2];
+        BitConverter.GetBytes(original.X).CopyTo(result, 0);
+        BitConverter.GetBytes(original.Y).CopyTo(result, sizeof(double));
+        return result;
+    }
+
+    public override bool TryDeserialize(object serialized, out VecD original)
+    {
+        if (serialized is byte[] { Length: sizeof(double) * 2 } bytes)
+        {
+            original = new VecD(BitConverter.ToDouble(bytes, 0), BitConverter.ToDouble(bytes, sizeof(double)));
+            return true;
+        }
+
+        original = default;
+        return false; 
+    }
+}

+ 29 - 0
src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/VecISerializationFactory.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Models.Serialization.Factories;
+
+public class VecISerializationFactory : SerializationFactory<byte[], VecI>
+{
+    public override string DeserializationId { get; } = "PixiEditor.VecI";
+
+    public override byte[] Serialize(VecI original)
+    {
+        byte[] result = new byte[8];
+        BitConverter.GetBytes(original.X).CopyTo(result, 0);
+        BitConverter.GetBytes(original.Y).CopyTo(result, 4);
+        
+        return result;
+    }
+
+    public override bool TryDeserialize(object serialized, out VecI original)
+    {
+        if (serialized is byte[] { Length: 8 } bytes)
+        {
+            original = new VecI(BitConverter.ToInt32(bytes, 0), BitConverter.ToInt32(bytes, 4));
+            return true;
+        }
+
+        original = default;
+        return false; 
+    }
+}

+ 19 - 11
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -314,14 +314,26 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             foreach (var node in graph.AllNodes)
             {
                 Guid nodeGuid = mappedNodeIds[node.Id];
+
+                var serializedNode = graph.AllNodes.First(x => x.Id == node.Id);
+
+                if (serializedNode.AdditionalData != null && serializedNode.AdditionalData.Count > 0)
+                {
+                    acc.AddActions(new DeserializeNodeAdditionalData_Action(nodeGuid,
+                        SerializationUtil.DeserializeDict(serializedNode.AdditionalData, config, allFactories)));
+                }
+
                 if (node.InputConnections != null)
                 {
-                    foreach (var connection in node.InputConnections)
+                    foreach (var connections in node.InputConnections)
                     {
-                        if (mappedNodeIds.TryGetValue(connection.Key, out Guid outputNodeId))
+                        if (mappedNodeIds.TryGetValue(connections.Key, out Guid outputNodeId))
                         {
-                            acc.AddActions(new ConnectProperties_Action(nodeGuid, outputNodeId,
-                                connection.Value.inputPropName, connection.Value.outputPropName));
+                            foreach (var connection in connections.Value)
+                            {
+                                acc.AddActions(new ConnectProperties_Action(nodeGuid, outputNodeId,
+                                    connection.inputPropName, connection.outputPropName));
+                            }
                         }
                     }
                 }
@@ -333,6 +345,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             Guid guid = Guid.NewGuid();
             mappedNodeIds.Add(id, guid);
             acc.AddActions(new CreateNodeFromName_Action(serializedNode.UniqueNodeName, guid));
+            acc.AddFinishedActions(new NodePosition_Action(guid, serializedNode.Position.ToVecD()), new EndNodePosition_Action());
 
             if (serializedNode.InputValues != null)
             {
@@ -348,7 +361,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 foreach (var keyFrame in serializedNode.KeyFrames)
                 {
                     Guid keyFrameGuid = Guid.NewGuid();
-                    mappedKeyFrameIds.Add(keyFrame.Id, keyFrameGuid);
+                    /*Add should be here I think, but it crashes while deserializing multiple layers with no frames*/
+                    mappedKeyFrameIds[keyFrame.Id] = keyFrameGuid;
                     acc.AddActions(
                         new SetKeyFrameData_Action(
                             guid,
@@ -359,12 +373,6 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 }
             }
 
-            if (serializedNode.AdditionalData != null && serializedNode.AdditionalData.Count > 0)
-            {
-                acc.AddActions(new DeserializeNodeAdditionalData_Action(guid,
-                    SerializationUtil.DeserializeDict(serializedNode.AdditionalData, config, allFactories)));
-            }
-
             if (!string.IsNullOrEmpty(serializedNode.Name))
             {
                 acc.AddActions(new SetNodeName_Action(guid, serializedNode.Name));

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs

@@ -23,5 +23,6 @@ public class FuncContext
     {
         Position = position;
         Size = size;
+        HasContext = true;
     }
 }

+ 12 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs

@@ -1,6 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
@@ -20,10 +21,20 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
         return func;
     }
 
+    protected override object FuncFactoryDelegate(Delegate delegateToCast)
+    {
+        Func<FuncContext, T> func = f =>
+        {
+            ConversionTable.TryConvert(delegateToCast.DynamicInvoke(f), typeof(T), out var result);
+            return (T)result;
+        };
+        return func;
+    }
+
     object? IFuncInputProperty.GetFuncConstantValue() => constantNonOverrideValue;
 
     void IFuncInputProperty.SetFuncConstantValue(object? value)
     {
-        constantNonOverrideValue = (T)value;
+        constantNonOverrideValue = value == null ? default : (T)value;
     }
 }

+ 14 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -53,6 +53,15 @@ public class InputProperty : IInputProperty
         return func;
     }
 
+    protected virtual object FuncFactoryDelegate(Delegate delegateToCast)
+    {
+        Func<FuncContext, object> func = f =>
+        {
+            return ConversionTable.TryConvert(delegateToCast.DynamicInvoke(f), ValueType, out object result) ? result : null;
+        };
+        return func;
+    }
+
     public Node Node { get; }
     public Type ValueType { get; } 
     internal bool CacheChanged
@@ -148,6 +157,11 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
             if(value is T tValue)
                 return tValue;
 
+            if (value is Delegate func && typeof(T).IsAssignableTo(typeof(Delegate)))
+            {
+                return (T)FuncFactoryDelegate(func); 
+            }
+
             return ConversionTable.TryConvert(value, typeof(T), out object result) ? (T)result : default;
         }
     }

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPairNodeEnd.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+public interface IPairNodeEnd
+{
+    public Node StartNode { get; set; }
+}

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

@@ -22,7 +22,7 @@ public class CombineColorNode : Node
 
     public CombineColorNode()
     {
-        Color = CreateFieldOutput(nameof(Color), "COLOR", GetColor);
+        Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
 
         R = CreateFuncInput("R", "R", 0d);
         G = CreateFuncInput("G", "G", 0d);

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

@@ -20,7 +20,7 @@ public class CombineVecD : Node
 
     public CombineVecD()
     {
-        Vector = CreateFieldOutput(nameof(Vector), "VECTOR", GetVector);
+        Vector = CreateFuncOutput(nameof(Vector), "VECTOR", GetVector);
 
         X = CreateFuncInput(nameof(X), "X", 0d);
         Y = CreateFuncInput(nameof(Y), "Y", 0d);

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

@@ -18,7 +18,7 @@ public class CombineVecI : Node
 
     public CombineVecI()
     {
-        Vector = CreateFieldOutput(nameof(Vector), "VECTOR", GetVector);
+        Vector = CreateFuncOutput(nameof(Vector), "VECTOR", GetVector);
 
         X = CreateFuncInput(nameof(X), "X", 0);
         Y = CreateFuncInput(nameof(Y), "Y", 0);

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

@@ -22,10 +22,10 @@ public class SeparateColorNode : Node
     public SeparateColorNode()
     {
         Color = CreateFuncInput(nameof(Color), "COLOR", new Color());
-        R = CreateFieldOutput(nameof(R), "R", ctx => Color.Value(ctx).R / 255d);
-        G = CreateFieldOutput(nameof(G), "G", ctx => Color.Value(ctx).G / 255d);
-        B = CreateFieldOutput(nameof(B), "B", ctx => Color.Value(ctx).B / 255d);
-        A = CreateFieldOutput(nameof(A), "A", ctx => Color.Value(ctx).A / 255d);
+        R = CreateFuncOutput(nameof(R), "R", ctx => Color.Value(ctx).R / 255d);
+        G = CreateFuncOutput(nameof(G), "G", ctx => Color.Value(ctx).G / 255d);
+        B = CreateFuncOutput(nameof(B), "B", ctx => Color.Value(ctx).B / 255d);
+        A = CreateFuncOutput(nameof(A), "A", ctx => Color.Value(ctx).A / 255d);
     }
 
     protected override Surface? OnExecute(RenderingContext context)

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

@@ -17,8 +17,8 @@ public class SeparateVecDNode : Node
 
     public SeparateVecDNode()
     {
-        X = CreateFieldOutput("X", "X", ctx => Vector.Value(ctx).X);
-        Y = CreateFieldOutput("Y", "Y", ctx => Vector.Value(ctx).Y);
+        X = CreateFuncOutput("X", "X", ctx => Vector.Value(ctx).X);
+        Y = CreateFuncOutput("Y", "Y", ctx => Vector.Value(ctx).Y);
         Vector = CreateFuncInput("Vector", "VECTOR", new VecD(0, 0));
     }
 

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

@@ -17,8 +17,8 @@ public class SeparateVecINode : Node
 
     public SeparateVecINode()
     {
-        X = CreateFieldOutput("X", "X", ctx => Vector.Value(ctx).X);
-        Y = CreateFieldOutput("Y", "Y", ctx => Vector.Value(ctx).Y);
+        X = CreateFuncOutput("X", "X", ctx => Vector.Value(ctx).X);
+        Y = CreateFuncOutput("Y", "Y", ctx => Vector.Value(ctx).Y);
         Vector = CreateFuncInput("Vector", "VECTOR", new VecI(0, 0));
     }
 

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

@@ -16,8 +16,8 @@ public class ImageSpaceNode : Node
     public override string DisplayName { get; set; } = "IMAGE_SPACE_NODE";
     public ImageSpaceNode()
     {
-        SpacePosition = CreateFieldOutput(nameof(SpacePosition), "PIXEL_COORDINATE", ctx => ctx.Position);
-        Size = CreateFieldOutput(nameof(Size), "SIZE", ctx => ctx.Size);
+        SpacePosition = CreateFuncOutput(nameof(SpacePosition), "UV", ctx => ctx.Position);
+        Size = CreateFuncOutput(nameof(Size), "SIZE", ctx => ctx.Size);
     }
 
 

+ 46 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LerpColorNode.cs

@@ -0,0 +1,46 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("Lerp")]
+public class LerpColorNode : Node // TODO: ILerpable as inputs? 
+{
+    public FuncOutputProperty<Color> Result { get; } 
+    public FuncInputProperty<Color> From { get; }
+    public FuncInputProperty<Color> To { get; }
+    public FuncInputProperty<double> Time { get; }
+    
+    public override string DisplayName { get; set; } = "LERP_NODE";
+    
+    
+    public LerpColorNode()
+    {
+        Result = CreateFuncOutput("Result", "RESULT", Lerp);
+        From = CreateFuncInput("From", "FROM", Colors.Black);
+        To = CreateFuncInput("To", "TO", Colors.White);
+        Time = CreateFuncInput("Time", "TIME", 0.5);
+    }
+
+    private Color Lerp(FuncContext arg)
+    {
+        var from = From.Value(arg);
+        var to = To.Value(arg);
+        var time = Time.Value(arg);
+        
+        return Color.Lerp(from, to, time); 
+    }
+
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        return null;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new LerpColorNode();
+    }
+}

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

@@ -25,7 +25,7 @@ public class MathNode : Node
     
     public MathNode()
     {
-        Result = CreateFieldOutput(nameof(Result), "RESULT", Calculate);
+        Result = CreateFuncOutput(nameof(Result), "RESULT", Calculate);
         Mode = CreateInput(nameof(Mode), "MATH_MODE", MathNodeMode.Add);
         Clamp = CreateInput(nameof(Clamp), "CLAMP", false);
         X = CreateFuncInput(nameof(X), "X", 0d);

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

@@ -26,8 +26,8 @@ public class ModifyImageLeftNode : Node
     public ModifyImageLeftNode()
     {
         Image = CreateInput<Surface>(nameof(Surface), "IMAGE", null);
-        Coordinate = CreateFieldOutput(nameof(Coordinate), "COORDINATE", ctx => ctx.Position);
-        Color = CreateFieldOutput(nameof(Color), "COLOR", GetColor);
+        Coordinate = CreateFuncOutput(nameof(Coordinate), "UV", ctx => ctx.Position);
+        Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
     }
 
     private Color GetColor(FuncContext context)

+ 42 - 13
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -1,6 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
@@ -12,39 +13,50 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("ModifyImageRight")]
 [PairNode(typeof(ModifyImageLeftNode), "ModifyImageZone")]
-public class ModifyImageRightNode : Node
+public class ModifyImageRightNode : Node, IPairNodeEnd
 {
-    private ModifyImageLeftNode startNode;
-    
+    public Node StartNode { get; set; }
+
     private Paint drawingPaint = new Paint() { BlendMode = BlendMode.Src };
-    
+
+    public FuncInputProperty<VecD> Coordinate { get; }
     public FuncInputProperty<Color> Color { get; }
-    
+
     public OutputProperty<Surface> Output { get; }
 
     public override string DisplayName { get; set; } = "MODIFY_IMAGE_RIGHT_NODE";
 
-    public ModifyImageRightNode(ModifyImageLeftNode startNode)
+    public ModifyImageRightNode()
     {
-        this.startNode = startNode;
+        Coordinate = CreateFuncInput(nameof(Coordinate), "UV", new VecD());
         Color = CreateFuncInput(nameof(Color), "COLOR", new Color());
         Output = CreateOutput<Surface>(nameof(Output), "OUTPUT", null);
     }
 
     protected override Surface? OnExecute(RenderingContext renderingContext)
     {
+        if (StartNode == null)
+        {
+            FindStartNode();
+            if (StartNode == null)
+            {
+                return null;
+            }
+        }
+
+        var startNode = StartNode as ModifyImageLeftNode;
         if (startNode.Image.Value is not { Size: var size })
         {
             return null;
         }
-        
+
         startNode.PreparePixmap();
-        
+
         var width = size.X;
         var height = size.Y;
 
         var surface = new Surface(size);
-        
+
         var context = new FuncContext();
 
         for (int y = 0; y < height; y++)
@@ -53,9 +65,12 @@ public class ModifyImageRightNode : Node
             {
                 context.UpdateContext(new VecD((double)x / width, (double)y / height), new VecI(width, height));
                 var color = Color.Value(context);
-
+                var uv = Coordinate.Value(context);
+                var position = new VecD(uv.X * width, uv.Y * height);
+                
                 drawingPaint.Color = color;
-                surface.DrawingSurface.Canvas.DrawPixel(x, y, drawingPaint);
+
+                surface.DrawingSurface.Canvas.DrawPixel((int)position.X, (int)position.Y, drawingPaint);
             }
         }
 
@@ -64,5 +79,19 @@ public class ModifyImageRightNode : Node
         return Output.Value;
     }
 
-    public override Node CreateCopy() => throw new NotImplementedException();
+    private void FindStartNode()
+    {
+        TraverseBackwards(node =>
+        {
+            if (node is ModifyImageLeftNode leftNode)
+            {
+                StartNode = leftNode;
+                return false;
+            }
+
+            return true;
+        });
+    }
+
+    public override Node CreateCopy() => new ModifyImageRightNode();
 }

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

@@ -128,7 +128,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             {
                 if (inputProperty.Connection != null)
                 {
-                    queueNodes.Enqueue(inputProperty.Node);
+                    queueNodes.Enqueue(inputProperty.Connection.Node);
                 }
             }
         }
@@ -229,7 +229,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         return property;
     }
 
-    protected FuncOutputProperty<T> CreateFieldOutput<T>(string propName, string displayName,
+    protected FuncOutputProperty<T> CreateFuncOutput<T>(string propName, string displayName,
         Func<FuncContext, T> defaultFunc)
     {
         var property = new FuncOutputProperty<T>(this, propName, displayName, defaultFunc);
@@ -363,7 +363,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
     }
 
-    internal virtual void DeserializeData(IReadOnlyDictionary<string, object> data)
+    internal virtual void DeserializeData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data)
     {
     }
 }

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

@@ -43,12 +43,18 @@ public class NoiseNode : Node
 
     protected override Surface OnExecute(RenderingContext context)
     {
-        if (Math.Abs(previousScale - Scale.Value) > 0.000001 
+        if (Math.Abs(previousScale - Scale.Value) > 0.000001
             || previousSeed != Seed.Value
             || previousOctaves != Octaves.Value
             || previousNoiseType != NoiseType.Value
             || double.IsNaN(previousScale))
         {
+            if(Scale.Value < 0.000001)
+            {
+                Noise.Value = null;
+                return null;
+            }
+            
             var shader = SelectShader();
             if (shader == null)
             {
@@ -69,6 +75,12 @@ public class NoiseNode : Node
         
         var size = Size.Value;
         
+        if (size.X < 1 || size.Y < 1)
+        {
+            Noise.Value = null;
+            return null;
+        }
+        
         var workingSurface = new Surface(size);
        
         workingSurface.DrawingSurface.Canvas.DrawPaint(paint);

+ 55 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs

@@ -0,0 +1,55 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("SampleImage")]
+public class SampleImageNode : Node
+{
+    private Pixmap? pixmap;
+
+    public InputProperty<Surface?> Image { get; }
+
+    public FuncOutputProperty<VecD> Coordinate { get; }
+
+    public FuncOutputProperty<Color> Color { get; }
+
+    public override string DisplayName { get; set; } = "SAMPLE_IMAGE";
+
+    public SampleImageNode()
+    {
+        Image = CreateInput<Surface>(nameof(Surface), "IMAGE", null);
+        Coordinate = CreateFuncOutput(nameof(Coordinate), "UV", ctx => ctx.Position);
+        Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
+    }
+
+    private Color GetColor(FuncContext context)
+    {
+        context.ThrowOnMissingContext();
+
+        if (pixmap == null)
+            return new Color();
+
+        var x = context.Position.X * context.Size.X;
+        var y = context.Position.Y * context.Size.Y;
+
+        return pixmap.GetPixelColor((int)x, (int)y);
+    }
+
+    internal void PreparePixmap()
+    {
+        pixmap = Image.Value?.PeekPixels();
+    }
+
+    protected override Surface? OnExecute(RenderingContext context)
+    {
+        PreparePixmap();
+        return Image.Value;
+    }
+
+    public override Node CreateCopy() => new SampleImageNode();
+}

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

@@ -58,6 +58,12 @@ public static class ConversionTable
                 return false;
             }
         }
+
+        if (targetType.IsAssignableTo(typeof(Delegate)))
+        {
+            result = arg;
+            return true;
+        }
         
         if (_conversionTable.TryGetValue(arg.GetType(), out var converters))
         {

+ 4 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodePair_Change.cs

@@ -40,7 +40,10 @@ internal class CreateNodePair_Change : Change
         Type endingType = attribute.IsStartingType ? attribute.OtherType : nodeType;
         
         var start = NodeOperations.CreateNode(startingType, target);
-        var end = NodeOperations.CreateNode(endingType, target, start);
+        var end = NodeOperations.CreateNode(endingType, target);
+        
+        if(end is IPairNodeEnd pairEnd)
+            pairEnd.StartNode = start;
 
         start.Id = startId;
         end.Id = endId;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeserializeNodeAdditionalData_Change.cs

@@ -23,7 +23,7 @@ internal class DeserializeNodeAdditionalData_Change : Change
     {
         Node node = target.FindNode<Node>(nodeId);
         
-        node.DeserializeData(data);
+        node.DeserializeData(target, data);
         ignoreInUndo = false;
         return new None();
     }

+ 9 - 0
src/PixiEditor.DrawingApi.Core/ColorsImpl/Color.cs

@@ -217,5 +217,14 @@ namespace PixiEditor.DrawingApi.Core.ColorsImpl
     {
         return this == Empty ? null : $"{this._colorValue:X8}";
     }
+
+    public static Color Lerp(Color from, Color to, double time)
+    {
+        byte r = (byte)(from.R + (to.R - from.R) * time);
+        byte g = (byte)(from.G + (to.G - from.G) * time);
+        byte b = (byte)(from.B + (to.B - from.B) * time);
+        byte a = (byte)(from.A + (to.A - from.A) * time);
+        return new Color(r, g, b, a);
+    }
   }
 }