Browse Source

Added dynamic node inputs/outputs

Krzysztof Krysiński 5 months ago
parent
commit
8f13783b24

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 848a8491a1ef9ccc47a6426d84902331b271fa53
+Subproject commit e31b43ced0fd8e89707ff2226de7f7b886f66b4a

+ 22 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/NodeInputsChanged_ChangeInfo.cs

@@ -0,0 +1,22 @@
+using System.Collections.Immutable;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record NodeInputsChanged_ChangeInfo(Guid NodeId, ImmutableArray<NodePropertyInfo> Inputs) : IChangeInfo
+{
+    public static NodeInputsChanged_ChangeInfo FromNode(Node node)
+    {
+        var infos = CreateNode_ChangeInfo.CreatePropertyInfos(node.InputProperties, true, node.Id);
+        return new NodeInputsChanged_ChangeInfo(node.Id, infos);
+    }
+}
+
+public record NodeOutputsChanged_ChangeInfo(Guid NodeId, ImmutableArray<NodePropertyInfo> Outputs) : IChangeInfo
+{
+    public static NodeOutputsChanged_ChangeInfo FromNode(Node node)
+    {
+        var infos = CreateNode_ChangeInfo.CreatePropertyInfos(node.OutputProperties, false, node.Id);
+        return new NodeOutputsChanged_ChangeInfo(node.Id, infos);
+    }
+}

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

@@ -16,6 +16,7 @@ public class InputProperty : IInputProperty
     private IOutputProperty? connection;
 
     public event Action ConnectionChanged;
+    public event Action<object> NonOverridenValueChanged;
 
     public string InternalPropertyName { get; }
     public string DisplayName { get; }
@@ -51,6 +52,7 @@ public class InputProperty : IInputProperty
         set
         {
             _internalValue = value;
+            NonOverridenValueChanged?.Invoke(value);
             NonOverridenValueSet(value);
         }
     }
@@ -124,6 +126,7 @@ public class InputProperty : IInputProperty
 
     protected virtual void NonOverridenValueSet(object value)
     {
+
     }
 
     internal virtual void UpdateCache()
@@ -232,4 +235,10 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
         rules(Validator);
         return this;
     }
+
+    public InputProperty<T> NonOverridenChanged(Action<T> callback)
+    {
+        NonOverridenValueChanged += value => callback((T)value);
+        return this;
+    }
 }

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

@@ -350,6 +350,14 @@ public abstract class Node : IReadOnlyNode, IDisposable
         return property;
     }
 
+    protected void RemoveInputProperty(InputProperty property)
+    {
+        if(inputs.Remove(property))
+        {
+            property.ConnectionChanged -= InvokeConnectionsChanged;
+        }
+    }
+
     protected void AddOutputProperty(OutputProperty property)
     {
         outputs.Add(property);

+ 125 - 29
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs

@@ -21,6 +21,9 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     private Paint paint;
 
     private VecI lastDocumentSize;
+    private List<Shader> lastCustomImageShaders = new();
+
+    private Dictionary<string, InputProperty> uniformInputs = new();
 
     protected override bool ExecuteOnlyOnCacheChange => true;
     protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.All;
@@ -29,7 +32,8 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     {
         Background = CreateRenderInput("Background", "BACKGROUND");
         ShaderCode = CreateInput("ShaderCode", "SHADER_CODE", "")
-            .WithRules(validator => validator.Custom(ValidateShaderCode));
+            .WithRules(validator => validator.Custom(ValidateShaderCode))
+            .NonOverridenChanged(RegenerateUniformInputs);
 
         paint = new Paint();
         Output.FirstInChain = null;
@@ -43,35 +47,35 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
 
         if (lastShaderCode != ShaderCode.Value)
         {
-            Uniforms uniforms = null;
+            GenerateShader(context);
+        }
+        else if (shader != null)
+        {
+            Uniforms uniforms = GenerateUniforms(context);
+            shader = shader.WithUpdatedUniforms(uniforms);
+        }
 
-            uniforms = GenerateUniforms(context);
+        paint.Shader = shader;
+    }
 
-            shader?.Dispose();
+    private void GenerateShader(RenderContext context)
+    {
+        Uniforms uniforms = null;
 
-            if (uniforms != null)
-            {
-                shader = Shader.CreateFromString(ShaderCode.Value, uniforms, out _);
-            }
-            else
-            {
-                shader = Shader.CreateFromString(ShaderCode.Value, out _);
-            }
+        uniforms = GenerateUniforms(context);
 
-            lastShaderCode = ShaderCode.Value;
+        shader?.Dispose();
 
-            if (shader == null)
-            {
-                return;
-            }
+        if (uniforms != null)
+        {
+            shader = Shader.Create(ShaderCode.Value, uniforms, out _);
         }
-        else if (shader != null)
+        else
         {
-            Uniforms uniforms = GenerateUniforms(context);
-            shader = shader.WithUpdatedUniforms(uniforms);
+            shader = Shader.Create(ShaderCode.Value, out _);
         }
 
-        paint.Shader = shader;
+        lastShaderCode = ShaderCode.Value;
     }
 
     private Uniforms GenerateUniforms(RenderContext context)
@@ -83,6 +87,8 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         uniforms.Add("iNormalizedTime", new Uniform("iNormalizedTime", (float)context.FrameTime.NormalizedTime));
         uniforms.Add("iFrame", new Uniform("iFrame", context.FrameTime.Frame));
 
+        AddCustomUniforms(uniforms);
+
         if (Background.Value == null)
         {
             lastImageShader?.Dispose();
@@ -111,13 +117,6 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
             return;
         }
 
-        /*if (Background.Value != null)
-        {
-            int saved = surface.Canvas.SaveLayer(paint);
-            Background.Value.Paint(context, surface);
-            surface.Canvas.RestoreToCount(saved);
-        }*/
-
         surface.Canvas.DrawRect(0, 0, context.DocumentSize.X, context.DocumentSize.Y, paint);
     }
 
@@ -143,11 +142,108 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         return new ShaderNode();
     }
 
+    private void RegenerateUniformInputs(string newShaderCode)
+    {
+        UniformDeclaration[]? declarations = Shader.GetUniformDeclarations(newShaderCode);
+        if(declarations == null) return;
+
+        if(declarations.Length == 0)
+        {
+            foreach (var input in uniformInputs)
+            {
+                RemoveInputProperty(input.Value);
+            }
+
+            uniformInputs.Clear();
+            return;
+        }
+
+        var uniforms = declarations;
+
+        var nonExistingUniforms = uniformInputs.Keys.Where(x => uniforms.All(y => y.Name != x)).ToList();
+        foreach (var nonExistingUniform in nonExistingUniforms)
+        {
+            RemoveInputProperty(uniformInputs[nonExistingUniform]);
+            uniformInputs.Remove(nonExistingUniform);
+        }
+
+        foreach (var uniform in uniforms)
+        {
+            if(IsBuiltInUniform(uniform.Name))
+            {
+                continue;
+            }
+
+            if (!uniformInputs.ContainsKey(uniform.Name))
+            {
+                InputProperty input;
+                if (uniform.DataType == UniformValueType.Float)
+                {
+                    input = CreateInput(uniform.Name, uniform.Name, 0f);
+                }
+                else if (uniform.DataType == UniformValueType.Shader)
+                {
+                    input = CreateInput<Texture>(uniform.Name, uniform.Name, null);
+                }
+                //TODO
+                /*else if (uniform.DataType == UniformValueType.FloatArray)
+                {
+                    input = CreateFuncInput<Kernel>(uniform.Name, uniform.Name, null);
+                }*/
+                else
+                {
+                    continue;
+                }
+
+                uniformInputs.Add(uniform.Name, input);
+            }
+        }
+    }
+
+    private void AddCustomUniforms(Uniforms uniforms)
+    {
+        foreach (var imgShader in lastCustomImageShaders)
+        {
+            imgShader.Dispose();
+        }
+
+        lastCustomImageShaders.Clear();
+
+        foreach (var input in uniformInputs)
+        {
+            if (input.Value.Value is float floatValue)
+            {
+                uniforms.Add(input.Key, new Uniform(input.Key, floatValue));
+            }
+            else if (input.Value.Value is double doubleValue)
+            {
+                uniforms.Add(input.Key, new Uniform(input.Key, (float)doubleValue));
+            }
+            else if (input.Value.Value is int intValue)
+            {
+                uniforms.Add(input.Key, new Uniform(input.Key, intValue));
+            }
+            else if (input.Value.Value is Texture texture)
+            {
+                var snapshot = texture.DrawingSurface.Snapshot();
+                Shader snapshotShader = snapshot.ToShader();
+                lastCustomImageShaders.Add(snapshotShader);
+                uniforms.Add(input.Key, new Uniform(input.Key, snapshotShader));
+                snapshot.Dispose();
+            }
+        }
+    }
+
+    private bool IsBuiltInUniform(string name)
+    {
+        return name is "iResolution" or "iNormalizedTime" or "iFrame" or "iImage";
+    }
+
     private ValidatorResult ValidateShaderCode(object? value)
     {
         if (value is string code)
         {
-            var result = Shader.CreateFromString(code, out string errors);
+            var result = Shader.Create(code, out string errors);
             result?.Dispose();
             return new (string.IsNullOrWhiteSpace(errors), errors);
         }

+ 63 - 3
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/UpdateProperty_Change.cs

@@ -35,7 +35,8 @@ internal class UpdatePropertyValue_Change : Change
         var node = target.NodeGraph.Nodes.First(x => x.Id == _nodeId);
         var property = node.GetInputProperty(_propertyName);
 
-        List<IChangeInfo> changes = new();
+        int inputsHash = CalculateInputsHash(node);
+        int outputsHash = CalculateOutputsHash(node);
 
         previousValue = GetValue(property);
         string errors = string.Empty;
@@ -59,16 +60,53 @@ internal class UpdatePropertyValue_Change : Change
             ignoreInUndo = false;
         }
 
-        return new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, _value) { Errors = errors };
+        List<IChangeInfo> changes = new();
+        changes.Add(new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, _value) { Errors = errors });
+
+        int newInputsHash = CalculateInputsHash(node);
+        int newOutputsHash = CalculateOutputsHash(node);
+
+        if (inputsHash != newInputsHash)
+        {
+            changes.Add(NodeInputsChanged_ChangeInfo.FromNode(node));
+        }
+
+        if (outputsHash != newOutputsHash)
+        {
+            changes.Add(NodeOutputsChanged_ChangeInfo.FromNode(node));
+        }
+
+        return changes;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var node = target.NodeGraph.Nodes.First(x => x.Id == _nodeId);
         var property = node.GetInputProperty(_propertyName);
+
+        int inputsHash = CalculateInputsHash(node);
+        int outputsHash = CalculateOutputsHash(node);
+
         SetValue(property, previousValue);
 
-        return new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, previousValue);
+        List<IChangeInfo> changes = new();
+
+        changes.Add(new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, previousValue));
+
+        int newInputsHash = CalculateInputsHash(node);
+        int newOutputsHash = CalculateOutputsHash(node);
+
+        if (inputsHash != newInputsHash)
+        {
+            changes.Add(NodeInputsChanged_ChangeInfo.FromNode(node));
+        }
+
+        if (outputsHash != newOutputsHash)
+        {
+            changes.Add(NodeOutputsChanged_ChangeInfo.FromNode(node));
+        }
+
+        return changes;
     }
 
     private static object SetValue(InputProperty property, object? value)
@@ -100,4 +138,26 @@ internal class UpdatePropertyValue_Change : Change
 
         return property.NonOverridenValue;
     }
+
+    private static int CalculateInputsHash(Node node)
+    {
+        HashCode hash = new();
+        foreach (var input in node.InputProperties)
+        {
+            hash.Add(input.InternalPropertyName);
+        }
+
+        return hash.ToHashCode();
+    }
+
+    private static int CalculateOutputsHash(Node node)
+    {
+        HashCode hash = new();
+        foreach (var output in node.OutputProperties)
+        {
+            hash.Add(output.InternalPropertyName);
+        }
+
+        return hash.ToHashCode();
+    }
 }

+ 65 - 20
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -187,6 +187,12 @@ internal class DocumentUpdater
             case ConnectProperty_ChangeInfo info:
                 ProcessConnectProperty(info);
                 break;
+            case NodeInputsChanged_ChangeInfo info:
+                ProcessInputsChanged(info);
+                break;
+            case NodeOutputsChanged_ChangeInfo info:
+                ProcessOutputsChanged(info);
+                break;
             case NodePosition_ChangeInfo info:
                 ProcessNodePosition(info);
                 break;
@@ -370,7 +376,7 @@ internal class DocumentUpdater
             memberVM = doc.NodeGraphHandler.AllNodes.FirstOrDefault(x => x.Id == info.Id) as ILayerHandler;
             if (memberVM is ITransparencyLockableMember transparencyLockableMember)
             {
-                transparencyLockableMember.SetLockTransparency(layerInfo.LockTransparency);        
+                transparencyLockableMember.SetLockTransparency(layerInfo.LockTransparency);
             }
         }
         else if (info is CreateFolder_ChangeInfo)
@@ -425,7 +431,7 @@ internal class DocumentUpdater
                 closestMember.Selection = StructureMemberSelectionType.Hard;
             }
 
-            
+
             doc.SetSelectedMember(closestMember);
         }
 
@@ -437,7 +443,6 @@ internal class DocumentUpdater
     {
         IStructureMemberHandler? memberVM = doc.StructureHelper.FindOrThrow(info.Id);
         memberVM.SetIsVisible(info.IsVisible);
-        
     }
 
     private void ProcessUpdateStructureMemberName(StructureMemberName_ChangeInfo info)
@@ -472,7 +477,7 @@ internal class DocumentUpdater
         var vm = new IRasterCelViewModel(info.TargetLayerGuid, info.Frame, 1,
             info.KeyFrameId,
             (DocumentViewModel)doc, helper);
-        
+
         doc.AnimationHandler.AddKeyFrame(vm);
     }
 
@@ -514,7 +519,7 @@ internal class DocumentUpdater
     private void ProcessCreateNode(CreateNode_ChangeInfo info)
     {
         var nodeType = info.Metadata.NodeType;
-        
+
         var ns = nodeType.Namespace.Replace("ChangeableDocument.Changeables.Graph.", "ViewModels.Document.");
         var name = nodeType.Name.Replace("Node", "NodeViewModel");
         var fullViewModelName = $"{ns}.{name}";
@@ -522,19 +527,59 @@ internal class DocumentUpdater
 
         if (nodeViewModelType == null)
             throw new NullReferenceException($"No ViewModel found for {nodeType}. Looking for '{fullViewModelName}'");
-        
+
         var viewModel = (NodeViewModel)Activator.CreateInstance(nodeViewModelType);
 
         InitializeNodeViewModel(info, viewModel);
     }
 
+    private void ProcessInputsChanged(NodeInputsChanged_ChangeInfo info)
+    {
+        NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
+
+        List<INodePropertyHandler> removedInputs =
+            node.Inputs.Where(x => !info.Inputs.Any(y => y.PropertyName == x.PropertyName)).ToList();
+
+        foreach (var input in removedInputs)
+        {
+            node.Inputs.Remove(input);
+            doc.NodeGraphHandler.RemoveConnection(input.Node.Id, input.PropertyName);
+        }
+
+        List<NodePropertyInfo> newInputs =
+            info.Inputs.Where(x => node.Inputs.All(y => y.PropertyName != x.PropertyName)).ToList();
+
+        List<INodePropertyHandler> inputs = CreateProperties([..newInputs], node, true);
+        node.Inputs.AddRange(inputs);
+    }
+
+    private void ProcessOutputsChanged(NodeOutputsChanged_ChangeInfo info)
+    {
+        NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
+
+        List<INodePropertyHandler> removedOutputs =
+            node.Outputs.Where(x => !info.Outputs.Any(y => y.PropertyName == x.PropertyName)).ToList();
+
+        foreach (var output in removedOutputs)
+        {
+            node.Outputs.Remove(output);
+            doc.NodeGraphHandler.RemoveConnection(output.Node.Id, output.PropertyName);
+        }
+
+        List<NodePropertyInfo> newOutputs =
+            info.Outputs.Where(x => node.Outputs.All(y => y.PropertyName != x.PropertyName)).ToList();
+
+        List<INodePropertyHandler> outputs = CreateProperties([..newOutputs], node, false);
+        node.Outputs.AddRange(outputs);
+    }
+
     private void InitializeNodeViewModel(CreateNode_ChangeInfo info, NodeViewModel viewModel)
     {
         viewModel.Initialize(info.Id, info.InternalName, (DocumentViewModel)doc, helper);
-        
+
         viewModel.SetName(info.NodeName);
         viewModel.SetPosition(info.Position);
-        
+
         var inputs = CreateProperties(info.Inputs, viewModel, true);
         var outputs = CreateProperties(info.Outputs, viewModel, false);
         viewModel.Inputs.AddRange(inputs);
@@ -544,7 +589,7 @@ internal class DocumentUpdater
         viewModel.Metadata = info.Metadata;
 
         AddZoneIfNeeded(info, viewModel);
-        
+
         viewModel.OnInitialized();
     }
 
@@ -553,8 +598,9 @@ internal class DocumentUpdater
         if (node.Metadata?.PairNodeGuid != null)
         {
             if (node.Metadata.PairNodeGuid == Guid.Empty) return;
-            
-            INodeHandler otherNode = doc.NodeGraphHandler.AllNodes.FirstOrDefault(x => x.Id == node.Metadata.PairNodeGuid);
+
+            INodeHandler otherNode =
+                doc.NodeGraphHandler.AllNodes.FirstOrDefault(x => x.Id == node.Metadata.PairNodeGuid);
             if (otherNode != null)
             {
                 bool zoneExists =
@@ -580,7 +626,9 @@ internal class DocumentUpdater
             prop.PropertyName = input.PropertyName;
             prop.IsInput = isInput;
             prop.IsFunc = input.ValueType.IsAssignableTo(typeof(Delegate));
-            prop.InternalSetValue(prop.IsFunc ? (input.InputValue as ShaderExpressionVariable)?.GetConstant() : input.InputValue);
+            prop.InternalSetValue(prop.IsFunc
+                ? (input.InputValue as ShaderExpressionVariable)?.GetConstant()
+                : input.InputValue);
             inputs.Add(prop);
         }
 
@@ -603,7 +651,7 @@ internal class DocumentUpdater
 
         doc.NodeGraphHandler.RemoveConnections(info.Id);
         doc.NodeGraphHandler.RemoveNode(info.Id);
-        
+
         doc.SnappingHandler.SnappingController.RemoveAll(info.Id.ToString());
     }
 
@@ -666,19 +714,16 @@ internal class DocumentUpdater
 
         property.Errors = info.Errors;
 
-        if(info.Errors != null)
-            return;
-
         ProcessStructureMemberProperty(info, property);
-        
+
         property.InternalSetValue(info.Value);
-        
+
         if (info.Property == CustomOutputNode.OutputNamePropertyName)
         {
             doc.NodeGraphHandler.UpdateAvailableRenderOutputs();
         }
     }
-    
+
     private void ProcessStructureMemberProperty(PropertyValueUpdated_ChangeInfo info, INodePropertyHandler property)
     {
         // TODO: This most likely can be handled inside viewmodel itself
@@ -722,7 +767,7 @@ internal class DocumentUpdater
     {
         doc.AnimationHandler.SetOnionFrames(info.OnionFrames, info.Opacity);
     }
-    
+
     private void ProcessProcessingColorSpace(ProcessingColorSpace_ChangeInfo info)
     {
         doc.SetProcessingColorSpace(info.NewColorSpace);

+ 5 - 2
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -96,8 +96,11 @@ public class PreviewPainter
             return;
         }
 
-        renderTextures[requestId]?.Dispose();
-        renderTextures.Remove(requestId);
+        if (renderTextures.TryGetValue(requestId, out var renderTexture))
+        {
+            renderTexture?.Dispose();
+            renderTextures.Remove(requestId);
+        }
     }
 
     public void Repaint()