diff --git a/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs new file mode 100644 index 00000000..7613d680 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using GraphProcessor; +using UnityEngine; + +[Serializable, NodeMenuItem("Convert/Float to String"), ConverterNode(typeof(float), typeof(string))] +public class FloatToStringsNode : BaseNode, IConversionNode +{ + [Input("In")] + public float input; + + public int decimalPlaces = 2; + + [Output("Out")] + public string output; + + public override string name => "To String"; + + public string GetConversionInput() + { + return nameof(input); + } + + public string GetConversionOutput() + { + return nameof(output); + } + + protected override void Process() + { + output = input.ToString("F" + decimalPlace, CultureInfo.InvariantCulture); + output = val.ToString(CultureInfo.InvariantCulture); + } +} diff --git a/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta new file mode 100644 index 00000000..08d319d5 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5947dfd18c94461281d83969aff7d203 +timeCreated: 1643494663 \ No newline at end of file diff --git a/Assets/Examples/Editor/GraphAssetCallbacks.cs b/Assets/Examples/Editor/GraphAssetCallbacks.cs index dd61c37a..c44904a9 100644 --- a/Assets/Examples/Editor/GraphAssetCallbacks.cs +++ b/Assets/Examples/Editor/GraphAssetCallbacks.cs @@ -9,7 +9,7 @@ public class GraphAssetCallbacks { [MenuItem("Assets/Create/GraphProcessor", false, 10)] - public static void CreateGraphPorcessor() + public static void CreateGraphProcessor() { var graph = ScriptableObject.CreateInstance< BaseGraph >(); ProjectWindowUtil.CreateAsset(graph, "GraphProcessor.asset"); diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs index cef006d2..180df519 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs @@ -64,7 +64,7 @@ public virtual void OnDrop(GraphView graphView, Edge edge) try { this.graphView.RegisterCompleteObjectUndo("Connected " + edgeView.input.node.name + " and " + edgeView.output.node.name); - if (!this.graphView.Connect(edge as EdgeView, autoDisconnectInputs: !wasOnTheSamePort)) + if (!this.graphView.ConnectConvertable(edge as EdgeView, !wasOnTheSamePort)) this.graphView.Disconnect(edge as EdgeView); } catch (System.Exception) { diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs index da054683..9b525d12 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs @@ -337,9 +337,18 @@ bool IsPortCompatible(PortDescription description) { if ((portView.direction == Direction.Input && description.isInput) || (portView.direction == Direction.Output && !description.isInput)) return false; + + if (portView.direction == Direction.Input) + { + if (!BaseGraph.TypesAreConnectable(description.portType, portView.portType)) + return false; + } + else + { + if (!BaseGraph.TypesAreConnectable( portView.portType, description.portType)) + return false; + } - if (!BaseGraph.TypesAreConnectable(description.portType, portView.portType)) - return false; return true; } diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs index 7940f6ee..dd1239d1 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs @@ -430,7 +430,7 @@ public override List< Port > GetCompatiblePorts(Port startPort, NodeAdapter node { var compatiblePorts = new List< Port >(); - compatiblePorts.AddRange(ports.ToList().Where(p => { + compatiblePorts.AddRange(ports.Where(p => { var portView = p as PortView; if (portView.owner == (startPort as PortView).owner) @@ -779,7 +779,7 @@ public void Initialize(BaseGraph graph) { var interfaces = nodeInfo.type.GetInterfaces(); var exceptInheritedInterfaces = interfaces.Except(interfaces.SelectMany(t => t.GetInterfaces())); - foreach (var i in interfaces) + foreach (var i in exceptInheritedInterfaces) { if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICreateNodeFrom<>)) { @@ -812,7 +812,8 @@ public void ClearGraphElements() void UpdateSerializedProperties() { - serializedGraph = new SerializedObject(graph); + if(graph != null) + serializedGraph = new SerializedObject(graph); } /// @@ -1192,8 +1193,60 @@ public bool Connect(PortView inputPortView, PortView outputPortView, bool autoDi edgeView.input = inputPortView; edgeView.output = outputPortView; + if (ConversionNodeAdapter.AreAssignable(outputPort.portData.displayType, inputPort.portData.displayType)) + { + return ConnectConvertable(edgeView, autoDisconnectInputs); + } else + { + return Connect(edgeView); + } + } - return Connect(edgeView); + /// + /// Same as connect, but also adds custom conversion nodes inbetween the edges input/output, if neccessary + /// + /// + /// + /// + public bool ConnectConvertable(EdgeView e, bool autoDisconnectInputs = true) + { + if (!CanConnectEdge(e, autoDisconnectInputs)) + return false; + + var inputPortView = e.input as PortView; + var outputPortView = e.output as PortView; + var inputNodeView = inputPortView.node as BaseNodeView; + var outputNodeView = outputPortView.node as BaseNodeView; + var inputPort = inputNodeView.nodeTarget.GetPort(inputPortView.fieldName, inputPortView.portData.identifier); + var outputPort = outputNodeView.nodeTarget.GetPort(outputPortView.fieldName, outputPortView.portData.identifier); + + Type conversionNodeType = ConversionNodeAdapter.GetConversionNode(outputPort.portData.displayType, inputPort.portData.displayType); + if (conversionNodeType != null) + { + var nodePosition = (inputPort.owner.position.center + outputPort.owner.position.center) / 2.0f; + BaseNode converterNode = BaseNode.CreateFromType(conversionNodeType, nodePosition); + IConversionNode conversion = (IConversionNode)converterNode; + var converterView = AddNode(converterNode); + + // set nodes center position to be in the middle of the input/output ports + converterNode.position.center = nodePosition - new Vector2(converterNode.position.width / 2.0f,0); + converterView.SetPosition(converterNode.position); + + + var conversionInputName = conversion.GetConversionInput(); + var converterInput = converterView.inputPortViews.Find(view => view.fieldName == conversionInputName); + var conversionOutputName = conversion.GetConversionOutput(); + var converterOutput = converterView.outputPortViews.Find(view => view.fieldName == conversionOutputName); + + Connect(inputPortView, converterOutput, autoDisconnectInputs); + + e.input = converterInput; // change from original input to use the converter node + return Connect(e, autoDisconnectInputs); + } + else + { + return Connect(e, autoDisconnectInputs); + } } public bool Connect(EdgeView e, bool autoDisconnectInputs = true) diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs index 5f3dfdad..48b5d69c 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs @@ -835,6 +835,9 @@ internal void SyncSerializedPropertyPathes() var nodeIndexString = nodeIndex.ToString(); foreach (var propertyField in this.Query().ToList()) { + if(propertyField.bindingPath == null) + continue; + propertyField.Unbind(); // The property path look like this: nodes.Array.data[x].fieldName // And we want to update the value of x with the new node index: diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs index 0338c14b..843dabde 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs @@ -376,7 +376,10 @@ public bool UpdatePortsForFieldLocal(string fieldName, bool sendPortUpdatedEvent if (fieldInfo.behavior != null) { foreach (var portData in fieldInfo.behavior(edges)) - AddPortData(portData); + { + if (portData != null) + AddPortData(portData); + } } else { diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs index a5cfde55..dba87e96 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs @@ -194,7 +194,7 @@ PushDataDelegate CreatePushDataDelegateForEdge(SerializableEdge edge) // We keep slow checks inside the editor #if UNITY_EDITOR - if (!BaseGraph.TypesAreConnectable(inputField.FieldType, outputField.FieldType)) + if (!BaseGraph.TypesAreConnectable(outputField.FieldType, inputField.FieldType)) { Debug.LogError("Can't convert from " + inputField.FieldType + " to " + outputField.FieldType + ", you must specify a custom port function (i.e CustomPortInput or CustomPortOutput) for non-implicit convertions"); return null; @@ -207,14 +207,14 @@ PushDataDelegate CreatePushDataDelegateForEdge(SerializableEdge edge) inType = edge.inputPort.portData.displayType ?? inputField.FieldType; outType = edge.outputPort.portData.displayType ?? outputField.FieldType; - // If there is a user defined convertion function, then we call it + // If there is a user defined conversion function, then we call it if (TypeAdapter.AreAssignable(outType, inType)) { // We add a cast in case there we're calling the conversion method with a base class parameter (like object) var convertedParam = Expression.Convert(outputParamField, outType); - outputParamField = Expression.Call(TypeAdapter.GetConvertionMethod(outType, inType), convertedParam); + outputParamField = Expression.Call(TypeAdapter.GetConversionMethod(outType, inType), convertedParam); // In case there is a custom port behavior in the output, then we need to re-cast to the base type because - // the convertion method return type is not always assignable directly: + // the conversion method return type is not always assignable directly: outputParamField = Expression.Convert(outputParamField, inputField.FieldType); } else // otherwise we cast diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs index f7da2f09..46f660f6 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs @@ -823,27 +823,30 @@ void DestroyBrokenGraphElements() /// /// Tell if two types can be connected in the context of a graph /// - /// - /// + /// + /// /// - public static bool TypesAreConnectable(Type t1, Type t2) + public static bool TypesAreConnectable(Type from, Type to) // NOTE: Extend this later for adding conversion nodes { - if (t1 == null || t2 == null) + if (from == null || to == null) return false; - if (TypeAdapter.AreIncompatible(t1, t2)) + if (TypeAdapter.AreIncompatible(from, to)) return false; //Check if there is custom adapters for this assignation - if (CustomPortIO.IsAssignable(t1, t2)) + if (CustomPortIO.IsAssignable(from, to)) return true; //Check for type assignability - if (t2.IsReallyAssignableFrom(t1)) + if (to.IsReallyAssignableFrom(from)) return true; - // User defined type convertions - if (TypeAdapter.AreAssignable(t1, t2)) + // User defined type conversions + if (TypeAdapter.AreAssignable(from, to)) + return true; + + if (ConversionNodeAdapter.AreAssignable(from, to)) return true; return false; diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs new file mode 100644 index 00000000..d8b76d41 --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace GraphProcessor +{ + [AttributeUsage(AttributeTargets.Class)] + public class ConverterNodeAttribute : Attribute + { + public Type from, to; + + public ConverterNodeAttribute(Type from, Type to) + { + this.from = from; + this.to = to; + } + } + + public interface IConversionNode + { + public string GetConversionInput(); + public string GetConversionOutput(); + } + + public static class ConversionNodeAdapter + { + private static bool conversionsLoaded = false; + + static readonly Dictionary<(Type from, Type to), Type> adapters = new Dictionary<(Type from, Type to), Type>(); + + static void LoadAllAdapters() + { + foreach (Type currType in AppDomain.CurrentDomain.GetAllTypes()) + { + var conversionAttrib = currType.GetCustomAttribute(); + if (conversionAttrib != null) + { + Debug.Assert(typeof(IConversionNode).IsAssignableFrom(currType), + "Class marked with ConverterNode attribute must implement the IConversionNode interface"); + Debug.Assert(typeof(BaseNode).IsAssignableFrom(currType), "Class marked with ConverterNode attribute must inherit from BaseNode"); + + adapters.Add((conversionAttrib.from, conversionAttrib.to), currType); + } + } + + conversionsLoaded = true; + } + + public static bool AreAssignable(Type from, Type to) + { + if (!conversionsLoaded) + LoadAllAdapters(); + + return adapters.ContainsKey((from, to)); + } + + public static Type GetConversionNode(Type from, Type to) + { + if (!conversionsLoaded) + LoadAllAdapters(); + + return adapters.TryGetValue((from, to), out Type nodeType) ? nodeType : null; + } + } +} \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs.meta b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs.meta new file mode 100644 index 00000000..f8bb0d29 --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 604ecd0dea834136834bf1737ef7a91f +timeCreated: 1637143540 \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/CustomPortIO.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/CustomPortIO.cs index ba5db93b..4db839f2 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/CustomPortIO.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/CustomPortIO.cs @@ -79,15 +79,15 @@ static void LoadCustomPortMethods() deleg = Expression.Lambda< CustomPortIODelegate >(ex, p1, p2, p3).Compile(); #endif - if (deleg == null) + string fieldName = (portInputAttr == null) ? portOutputAttr.fieldName : portInputAttr.fieldName; + Type customType = (portInputAttr == null) ? portOutputAttr.outputType : portInputAttr.inputType; + var field = type.GetField(fieldName, bindingFlags); + if (field == null) { - Debug.LogWarning("Can't use custom IO port function " + method + ": The method have to respect this format: " + typeof(CustomPortIODelegate)); + Debug.LogWarning("Can't use custom IO port function '" + method.Name + "' of class '" + type.Name + "': No field named " + fieldName + " found"); continue ; } - - string fieldName = (portInputAttr == null) ? portOutputAttr.fieldName : portInputAttr.fieldName; - Type customType = (portInputAttr == null) ? portOutputAttr.outputType : portInputAttr.inputType; - Type fieldType = type.GetField(fieldName, bindingFlags).FieldType; + Type fieldType = field.FieldType; AddCustomIOMethod(type, fieldName, deleg); diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/TypeAdapter.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/TypeAdapter.cs index 33592e6e..7108adce 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/TypeAdapter.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/TypeAdapter.cs @@ -22,13 +22,18 @@ public abstract class ITypeAdapter // TODO: turn this back into an interface whe public virtual IEnumerable<(Type, Type)> GetIncompatibleTypes() { yield break; } } + public class ValueTypeConversion : ITypeAdapter + { + public static float ConvertIntToFloat(int from) => from; + } + public static class TypeAdapter { static Dictionary< (Type from, Type to), Func > adapters = new Dictionary< (Type, Type), Func >(); static Dictionary< (Type from, Type to), MethodInfo > adapterMethods = new Dictionary< (Type, Type), MethodInfo >(); static List< (Type from, Type to)> incompatibleTypes = new List<( Type from, Type to) >(); - [System.NonSerialized] + [NonSerialized] static bool adaptersLoaded = false; #if !ENABLE_IL2CPP @@ -67,12 +72,12 @@ static void LoadAllAdapters() { if (method.GetParameters().Length != 1) { - Debug.LogError($"Ignoring convertion method {method} because it does not have exactly one parameter"); + Debug.LogError($"Ignoring conversion method {method} because it does not have exactly one parameter"); continue; } if (method.ReturnType == typeof(void)) { - Debug.LogError($"Ignoring convertion method {method} because it does not returns anything"); + Debug.LogError($"Ignoring conversion method {method} because it does not returns anything"); continue; } Type from = method.GetParameters()[0].ParameterType; @@ -81,7 +86,7 @@ static void LoadAllAdapters() try { #if ENABLE_IL2CPP - // IL2CPP doesn't suport calling generic functions via reflection (AOT can't generate templated code) + // IL2CPP doesn't support calling generic functions via reflection (AOT can't generate templated code) Func r = (object param) => { return (object)method.Invoke(null, new object[]{ param }); }; #else MethodInfo genericHelper = typeof(TypeAdapter).GetMethod("ConvertTypeMethodHelper", @@ -97,19 +102,21 @@ static void LoadAllAdapters() adapters.Add((method.GetParameters()[0].ParameterType, method.ReturnType), r); adapterMethods.Add((method.GetParameters()[0].ParameterType, method.ReturnType), method); } catch (Exception e) { - Debug.LogError($"Failed to load the type convertion method: {method}\n{e}"); + Debug.LogError($"Failed to load the type conversion method: {method}\n{e}"); } } } } - // Ensure that the dictionary contains all the convertions in both ways + /* + // Ensure that the dictionary contains all the conversions in both ways // ex: float to vector but no vector to float foreach (var kp in adapters) { if (!adapters.ContainsKey((kp.Key.to, kp.Key.from))) - Debug.LogError($"Missing convertion method. There is one for {kp.Key.from} to {kp.Key.to} but not for {kp.Key.to} to {kp.Key.from}"); + Debug.LogError($"Missing conversion method. There is one for {kp.Key.from} to {kp.Key.to} but not for {kp.Key.to} to {kp.Key.from}"); } + */ adaptersLoaded = true; } @@ -132,7 +139,7 @@ public static bool AreAssignable(Type from, Type to) return adapters.ContainsKey((from, to)); } - public static MethodInfo GetConvertionMethod(Type from, Type to) => adapterMethods[(from, to)]; + public static MethodInfo GetConversionMethod(Type from, Type to) => adapterMethods[(from, to)]; public static object Convert(object from, Type targetType) { diff --git a/README.md b/README.md index 9e9e3556..60c58475 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,17 @@ Join the [NodeGraphProcessor Discord server](https://discord.gg/XuMd3Z5Rym)! - Graph processor which execute node's logic with a dependency order - [Documented C# API to add new nodes / graphs](https://github.com/alelievr/NodeGraphProcessor/wiki/Node-scripting-API) - Exposed parameters that can be set per-asset to customize the graph processing from scripts or the inspector -- Parameter set mode, you can now output data from thegraph using exposed parameters. Their values will be updated when the graph is processed +- Parameter set mode, you can now output data from the graph using exposed parameters. Their values will be updated when the graph is processed - Search window to create new nodes - Colored groups - Node messages (small message with it's icon beside the node) -- Stack Nodes +- Stack nodes - Relay nodes - Display additional settings in the inspector - Node creation menu on edge drop - Simplified edge connection compared to default GraphView (ShaderGraph and VFX Graph) - Multiple graph window workflow (copy/paste) -- Vertical Ports +- Vertical ports - Sticky notes (requires Unity 2020.1) - Renamable nodes