Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement custom node-based Conversions #198

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Assets/Examples/Editor/GraphAssetCallbacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<>))
{
Expand Down Expand Up @@ -812,7 +812,8 @@ public void ClearGraphElements()

void UpdateSerializedProperties()
{
serializedGraph = new SerializedObject(graph);
if(graph != null)
serializedGraph = new SerializedObject(graph);
}

/// <summary>
Expand Down Expand Up @@ -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);
/// <summary>
/// Same as connect, but also adds custom conversion nodes inbetween the edges input/output, if neccessary
/// </summary>
/// <param name="e"></param>
/// <param name="autoDisconnectInputs"></param>
/// <returns></returns>
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,9 @@ internal void SyncSerializedPropertyPathes()
var nodeIndexString = nodeIndex.ToString();
foreach (var propertyField in this.Query<PropertyField>().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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
21 changes: 12 additions & 9 deletions Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -823,27 +823,30 @@ void DestroyBrokenGraphElements()
/// <summary>
/// Tell if two types can be connected in the context of a graph
/// </summary>
/// <param name="t1"></param>
/// <param name="t2"></param>
/// <param name="from"></param>
/// <param name="to"></param>
/// <returns></returns>
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ConverterNodeAttribute>();
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;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading