1
0
mirror of https://github.com/Siccity/xNode.git synced 2026-03-26 22:49:02 +08:00

Merge remote-tracking branch 'upstream/master'

This commit is contained in:
NoiseCrime 2019-02-18 11:56:15 +00:00
commit 13031b570d
13 changed files with 129 additions and 53 deletions

View File

@ -18,6 +18,14 @@ If your feature aims to cover something not related to editing nodes, it general
Skim through the code and you'll get the hang of it quickly. Skim through the code and you'll get the hang of it quickly.
* Methods, Types and properties PascalCase * Methods, Types and properties PascalCase
* Variables camelCase * Variables camelCase
* Public methods XML commented * Public methods XML commented. Params described if not obvious
* Explicit usage of brackets when doing multiple math operations on the same line
## Formatting
I use VSCode with the C# FixFormat extension and the following setting overrides:
```json
"csharpfixformat.style.spaces.beforeParenthesis": false,
"csharpfixformat.style.indent.regionIgnored": true
```
* Open braces on same line as condition * Open braces on same line as condition
* 4 spaces for indentation. * 4 spaces for indentation.

View File

@ -24,6 +24,7 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
* No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.) * No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.)
* Does not rely on any 3rd party plugins * Does not rely on any 3rd party plugins
* Custom node inspector code is very similar to regular custom inspector code * Custom node inspector code is very similar to regular custom inspector code
* Supported from Unity 5.3 and up
### Wiki ### Wiki
* [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph * [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph

View File

@ -24,12 +24,21 @@ namespace XNodeEditor {
string enumName = ""; string enumName = "";
if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length) enumName = property.enumDisplayNames[property.enumValueIndex]; if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length) enumName = property.enumDisplayNames[property.enumValueIndex];
#if UNITY_2017_1_OR_NEWER
// Display dropdown // Display dropdown
if (EditorGUI.DropdownButton(position, new GUIContent(enumName), FocusType.Passive)) { if (EditorGUI.DropdownButton(position, new GUIContent(enumName), FocusType.Passive)) {
// Position is all wrong if we show the dropdown during the node draw phase. // Position is all wrong if we show the dropdown during the node draw phase.
// Instead, add it to onLateGUI to display it later. // Instead, add it to onLateGUI to display it later.
NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property); NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
} }
#else
// Display dropdown
if (GUI.Button(position, new GUIContent(enumName), "MiniPopup")) {
// Position is all wrong if we show the dropdown during the node draw phase.
// Instead, add it to onLateGUI to display it later.
NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
}
#endif
EditorGUI.EndProperty(); EditorGUI.EndProperty();
} }

View File

@ -106,6 +106,7 @@ namespace XNodeEditor {
} }
public void Rename(string newName) { public void Rename(string newName) {
if (newName == null || newName.Trim() == "") newName = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name);
target.name = newName; target.name = newName;
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
} }

View File

@ -22,12 +22,11 @@ namespace XNodeEditor {
[NonSerialized] private List<Vector2> draggedOutputReroutes = new List<Vector2>(); [NonSerialized] private List<Vector2> draggedOutputReroutes = new List<Vector2>();
private RerouteReference hoveredReroute = new RerouteReference(); private RerouteReference hoveredReroute = new RerouteReference();
private List<RerouteReference> selectedReroutes = new List<RerouteReference>(); private List<RerouteReference> selectedReroutes = new List<RerouteReference>();
private Rect nodeRects; // Is this used?
private Vector2 dragBoxStart; private Vector2 dragBoxStart;
private UnityEngine.Object[] preBoxSelection; private UnityEngine.Object[] preBoxSelection;
private RerouteReference[] preBoxSelectionReroute; private RerouteReference[] preBoxSelectionReroute;
private Rect selectionBox; private Rect selectionBox;
private bool isDoubleClick = false; private bool isDoubleClick = false;
private struct RerouteReference { private struct RerouteReference {
public XNode.NodePort port; public XNode.NodePort port;
@ -61,7 +60,7 @@ namespace XNodeEditor {
case EventType.MouseDrag: case EventType.MouseDrag:
if (e.button == 0) { if (e.button == 0) {
if (IsDraggingPort) { if (IsDraggingPort) {
if (IsHoveringPort && hoveredPort.IsInput) { if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) {
if (!draggedOutput.IsConnectedTo(hoveredPort)) { if (!draggedOutput.IsConnectedTo(hoveredPort)) {
draggedOutputTarget = hoveredPort; draggedOutputTarget = hoveredPort;
} }
@ -170,7 +169,7 @@ namespace XNodeEditor {
} else if (e.control || e.shift) DeselectNode(hoveredNode); } else if (e.control || e.shift) DeselectNode(hoveredNode);
// Cache double click state, but only act on it in MouseUp - Except ClickCount only works in mouseDown. // Cache double click state, but only act on it in MouseUp - Except ClickCount only works in mouseDown.
isDoubleClick = ( e.clickCount == 2 ); isDoubleClick = (e.clickCount == 2);
e.Use(); e.Use();
currentActivity = NodeActivity.HoldNode; currentActivity = NodeActivity.HoldNode;
@ -231,6 +230,7 @@ namespace XNodeEditor {
// If click outside node, release field focus // If click outside node, release field focus
if (!isPanning) { if (!isPanning) {
EditorGUI.FocusTextInControl(null); EditorGUI.FocusTextInControl(null);
EditorGUIUtility.editingTextField = false;
} }
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
} }
@ -241,9 +241,8 @@ namespace XNodeEditor {
SelectNode(hoveredNode, false); SelectNode(hoveredNode, false);
// Double click to center node // Double click to center node
if ( isDoubleClick ) if (isDoubleClick) {
{ Vector2 nodeDimension = nodeSizes.ContainsKey(hoveredNode) ? nodeSizes[hoveredNode] / 2 : Vector2.zero;
Vector2 nodeDimension = nodeSizes.ContainsKey( hoveredNode ) ? nodeSizes[ hoveredNode ] / 2 : new Vector2(0f, 0f);
panOffset = -hoveredNode.position - nodeDimension; panOffset = -hoveredNode.position - nodeDimension;
} }
} }
@ -287,7 +286,7 @@ namespace XNodeEditor {
case EventType.KeyDown: case EventType.KeyDown:
if (EditorGUIUtility.editingTextField) break; if (EditorGUIUtility.editingTextField) break;
else if (e.keyCode == KeyCode.F) Home(); else if (e.keyCode == KeyCode.F) Home();
if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { if (IsMac()) {
if (e.keyCode == KeyCode.Return) RenameSelectedNode(); if (e.keyCode == KeyCode.Return) RenameSelectedNode();
} else { } else {
if (e.keyCode == KeyCode.F2) RenameSelectedNode(); if (e.keyCode == KeyCode.F2) RenameSelectedNode();
@ -298,7 +297,7 @@ namespace XNodeEditor {
if (e.commandName == "SoftDelete") { if (e.commandName == "SoftDelete") {
if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
e.Use(); e.Use();
} else if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX && e.commandName == "Delete") { } else if (IsMac() && e.commandName == "Delete") {
if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
e.Use(); e.Use();
} else if (e.commandName == "Duplicate") { } else if (e.commandName == "Duplicate") {
@ -317,6 +316,14 @@ namespace XNodeEditor {
} }
} }
public bool IsMac() {
#if UNITY_2017_1_OR_NEWER
return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
#else
return SystemInfo.operatingSystem.StartsWith("Mac");
#endif
}
private void RecalculateDragOffsets(Event current) { private void RecalculateDragOffsets(Event current) {
dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count]; dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count];
// Selected nodes // Selected nodes
@ -420,7 +427,7 @@ namespace XNodeEditor {
Rect fromRect; Rect fromRect;
if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return; if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return;
Vector2 from = fromRect.center; Vector2 from = fromRect.center;
col.a = 0.6f; col.a = draggedOutputTarget != null ? 1.0f : 0.6f;
Vector2 to = Vector2.zero; Vector2 to = Vector2.zero;
for (int i = 0; i < draggedOutputReroutes.Count; i++) { for (int i = 0; i < draggedOutputReroutes.Count; i++) {
to = draggedOutputReroutes[i]; to = draggedOutputReroutes[i];

View File

@ -241,12 +241,10 @@ namespace XNodeEditor {
selectionCache = new List<UnityEngine.Object>(Selection.objects); selectionCache = new List<UnityEngine.Object>(Selection.objects);
} }
//Active node is hashed before and after node GUI to detect changes
int nodeHash = 0;
System.Reflection.MethodInfo onValidate = null; System.Reflection.MethodInfo onValidate = null;
if (Selection.activeObject != null && Selection.activeObject is XNode.Node) { if (Selection.activeObject != null && Selection.activeObject is XNode.Node) {
onValidate = Selection.activeObject.GetType().GetMethod("OnValidate"); onValidate = Selection.activeObject.GetType().GetMethod("OnValidate");
if (onValidate != null) nodeHash = Selection.activeObject.GetHashCode(); if (onValidate != null) EditorGUI.BeginChangeCheck();
} }
BeginZoomed(position, zoom, topPadding); BeginZoomed(position, zoom, topPadding);
@ -383,12 +381,10 @@ namespace XNodeEditor {
if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray(); if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray();
EndZoomed(position, zoom, topPadding); EndZoomed(position, zoom, topPadding);
//If a change in hash is detected in the selected node, call OnValidate method. //If a change in is detected in the selected node, call OnValidate method.
//This is done through reflection because OnValidate is only relevant in editor, //This is done through reflection because OnValidate is only relevant in editor,
//and thus, the code should not be included in build. //and thus, the code should not be included in build.
if (nodeHash != 0) { if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null);
if (onValidate != null && nodeHash != Selection.activeObject.GetHashCode()) onValidate.Invoke(Selection.activeObject, null);
}
} }
private bool ShouldBeCulled(XNode.Node node) { private bool ShouldBeCulled(XNode.Node node) {

View File

@ -154,7 +154,7 @@ namespace XNodeEditor {
private static System.Type GetType(SerializedProperty property) { private static System.Type GetType(SerializedProperty property) {
System.Type parentType = property.serializedObject.targetObject.GetType(); System.Type parentType = property.serializedObject.targetObject.GetType();
System.Reflection.FieldInfo fi = parentType.GetField(property.propertyPath); System.Reflection.FieldInfo fi = NodeEditorWindow.GetFieldInfo(property.serializedObject.targetObject.GetType(), property.name);
return fi.FieldType; return fi.FieldType;
} }
@ -260,7 +260,7 @@ namespace XNodeEditor {
/// <param name="serializedObject">The serializedObject of the node</param> /// <param name="serializedObject">The serializedObject of the node</param>
/// <param name="connectionType">Connection type of added instance ports</param> /// <param name="connectionType">Connection type of added instance ports</param>
/// <param name="onCreation">Called on the list on creation. Use this if you want to customize the created ReorderableList</param> /// <param name="onCreation">Called on the list on creation. Use this if you want to customize the created ReorderableList</param>
public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, Action<ReorderableList> onCreation = null) { public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action<ReorderableList> onCreation = null) {
XNode.Node node = serializedObject.targetObject as XNode.Node; XNode.Node node = serializedObject.targetObject as XNode.Node;
Predicate<string> isMatchingInstancePort = Predicate<string> isMatchingInstancePort =
@ -279,7 +279,7 @@ namespace XNodeEditor {
// If a ReorderableList isn't cached for this array, do so. // If a ReorderableList isn't cached for this array, do so.
if (list == null) { if (list == null) {
SerializedProperty arrayData = serializedObject.FindProperty(fieldName); SerializedProperty arrayData = serializedObject.FindProperty(fieldName);
list = CreateReorderableList(fieldName, instancePorts, arrayData, type, serializedObject, io, connectionType, onCreation); list = CreateReorderableList(fieldName, instancePorts, arrayData, type, serializedObject, io, connectionType, typeConstraint, onCreation);
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) rlc.Add(fieldName, list); if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) rlc.Add(fieldName, list);
else reorderableListCache.Add(serializedObject.targetObject, new Dictionary<string, ReorderableList>() { { fieldName, list } }); else reorderableListCache.Add(serializedObject.targetObject, new Dictionary<string, ReorderableList>() { { fieldName, list } });
} }
@ -287,7 +287,7 @@ namespace XNodeEditor {
list.DoLayoutList(); list.DoLayoutList();
} }
private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> instancePorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, Action<ReorderableList> onCreation) { private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> instancePorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) {
bool hasArrayData = arrayData != null && arrayData.isArray; bool hasArrayData = arrayData != null && arrayData.isArray;
int arraySize = hasArrayData ? arrayData.arraySize : 0; int arraySize = hasArrayData ? arrayData.arraySize : 0;
XNode.Node node = serializedObject.targetObject as XNode.Node; XNode.Node node = serializedObject.targetObject as XNode.Node;
@ -375,8 +375,8 @@ namespace XNodeEditor {
int i = 0; int i = 0;
while (node.HasPort(newName)) newName = fieldName + " " + (++i); while (node.HasPort(newName)) newName = fieldName + " " + (++i);
if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, newName); if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, XNode.Node.TypeConstraint.None, newName);
else node.AddInstanceInput(type, connectionType, newName); else node.AddInstanceInput(type, connectionType, typeConstraint, newName);
serializedObject.Update(); serializedObject.Update();
EditorUtility.SetDirty(node); EditorUtility.SetDirty(node);
if (hasArrayData) arrayData.InsertArrayElementAtIndex(arraySize); if (hasArrayData) arrayData.InsertArrayElementAtIndex(arraySize);
@ -422,8 +422,8 @@ namespace XNodeEditor {
string newName = arrayData.name + " 0"; string newName = arrayData.name + " 0";
int i = 0; int i = 0;
while (node.HasPort(newName)) newName = arrayData.name + " " + (++i); while (node.HasPort(newName)) newName = arrayData.name + " " + (++i);
if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, newName); if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, typeConstraint, newName);
else node.AddInstanceInput(type, connectionType, newName); else node.AddInstanceInput(type, connectionType, typeConstraint, newName);
EditorUtility.SetDirty(node); EditorUtility.SetDirty(node);
instancePortCount++; instancePortCount++;
} }

View File

@ -81,7 +81,20 @@ namespace XNodeEditor {
return settings[lastKey]; return settings[lastKey];
} }
#if UNITY_2019_1_OR_NEWER
[SettingsProvider]
public static SettingsProvider CreateXNodeSettingsProvider() {
SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) {
guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); },
keywords = new HashSet<string>(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" })
};
return provider;
}
#endif
#if !UNITY_2019_1_OR_NEWER
[PreferenceItem("Node Editor")] [PreferenceItem("Node Editor")]
#endif
private static void PreferencesGUI() { private static void PreferencesGUI() {
VerifyLoaded(); VerifyLoaded();
Settings settings = NodeEditorPreferences.settings[lastKey]; Settings settings = NodeEditorPreferences.settings[lastKey];

View File

@ -42,9 +42,9 @@ namespace XNodeEditor {
public static Dictionary<Type, Color> GetNodeTint() { public static Dictionary<Type, Color> GetNodeTint() {
Dictionary<Type, Color> tints = new Dictionary<Type, Color>(); Dictionary<Type, Color> tints = new Dictionary<Type, Color>();
for (int i = 0; i < nodeTypes.Length; i++) { for (int i = 0; i < nodeTypes.Length; i++) {
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTint), true); var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTintAttribute), true);
if (attribs == null || attribs.Length == 0) continue; if (attribs == null || attribs.Length == 0) continue;
XNode.Node.NodeTint attrib = attribs[0] as XNode.Node.NodeTint; XNode.Node.NodeTintAttribute attrib = attribs[0] as XNode.Node.NodeTintAttribute;
tints.Add(nodeTypes[i], attrib.color); tints.Add(nodeTypes[i], attrib.color);
} }
return tints; return tints;
@ -53,14 +53,23 @@ namespace XNodeEditor {
public static Dictionary<Type, int> GetNodeWidth() { public static Dictionary<Type, int> GetNodeWidth() {
Dictionary<Type, int> widths = new Dictionary<Type, int>(); Dictionary<Type, int> widths = new Dictionary<Type, int>();
for (int i = 0; i < nodeTypes.Length; i++) { for (int i = 0; i < nodeTypes.Length; i++) {
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidth), true); var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidthAttribute), true);
if (attribs == null || attribs.Length == 0) continue; if (attribs == null || attribs.Length == 0) continue;
XNode.Node.NodeWidth attrib = attribs[0] as XNode.Node.NodeWidth; XNode.Node.NodeWidthAttribute attrib = attribs[0] as XNode.Node.NodeWidthAttribute;
widths.Add(nodeTypes[i], attrib.width); widths.Add(nodeTypes[i], attrib.width);
} }
return widths; return widths;
} }
/// <summary> Get FieldInfo of a field, including those that are private and/or inherited </summary>
public static FieldInfo GetFieldInfo(Type type, string fieldName) {
// If we can't find field in the first run, it's probably a private field in a base class.
FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Search base classes for private fields only. Public fields are found above
while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
return field;
}
/// <summary> Get all classes deriving from baseType via reflection </summary> /// <summary> Get all classes deriving from baseType via reflection </summary>
public static Type[] GetDerivedTypes(Type baseType) { public static Type[] GetDerivedTypes(Type baseType) {
List<System.Type> types = new List<System.Type>(); List<System.Type> types = new List<System.Type>();

View File

@ -36,9 +36,7 @@ namespace XNodeEditor {
public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute { public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute {
// If we can't find field in the first run, it's probably a private field in a base class. // If we can't find field in the first run, it's probably a private field in a base class.
FieldInfo field = classType.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); FieldInfo field = NodeEditorWindow.GetFieldInfo(classType, fieldName);
// Search base classes for private fields only. Public fields are found above
while (field == null && (classType = classType.BaseType) != typeof(XNode.Node)) field = classType.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
// This shouldn't happen. Ever. // This shouldn't happen. Ever.
if (field == null) { if (field == null) {
Debug.LogWarning("Field " + fieldName + " couldnt be found"); Debug.LogWarning("Field " + fieldName + " couldnt be found");

View File

@ -41,6 +41,16 @@ namespace XNode {
Override, Override,
} }
/// <summary> Tells which types of input to allow </summary>
public enum TypeConstraint {
/// <summary> Allow all types of input</summary>
None,
/// <summary> Allow similar and inherited types </summary>
Inherited,
/// <summary> Allow only similar types </summary>
Strict,
}
/// <summary> Iterate over all ports on this node. </summary> /// <summary> Iterate over all ports on this node. </summary>
public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values) yield return port; } } public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values) yield return port; } }
/// <summary> Iterate over all outputs on this node. </summary> /// <summary> Iterate over all outputs on this node. </summary>
@ -63,7 +73,6 @@ namespace XNode {
/// <summary> Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable </summary> /// <summary> Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable </summary>
public static NodeGraph graphHotfix; public static NodeGraph graphHotfix;
protected void OnEnable() { protected void OnEnable() {
if (graphHotfix != null) graph = graphHotfix; if (graphHotfix != null) graph = graphHotfix;
graphHotfix = null; graphHotfix = null;
@ -88,21 +97,21 @@ namespace XNode {
/// <summary> Convenience function. </summary> /// <summary> Convenience function. </summary>
/// <seealso cref="AddInstancePort"/> /// <seealso cref="AddInstancePort"/>
/// <seealso cref="AddInstanceOutput"/> /// <seealso cref="AddInstanceOutput"/>
public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) { public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Input, connectionType, fieldName); return AddInstancePort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName);
} }
/// <summary> Convenience function. </summary> /// <summary> Convenience function. </summary>
/// <seealso cref="AddInstancePort"/> /// <seealso cref="AddInstancePort"/>
/// <seealso cref="AddInstanceInput"/> /// <seealso cref="AddInstanceInput"/>
public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) { public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Output, connectionType, fieldName); return AddInstancePort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName);
} }
/// <summary> Add a dynamic, serialized port to this node. </summary> /// <summary> Add a dynamic, serialized port to this node. </summary>
/// <seealso cref="AddInstanceInput"/> /// <seealso cref="AddInstanceInput"/>
/// <seealso cref="AddInstanceOutput"/> /// <seealso cref="AddInstanceOutput"/>
private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) { private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
if (fieldName == null) { if (fieldName == null) {
fieldName = "instanceInput_0"; fieldName = "instanceInput_0";
int i = 0; int i = 0;
@ -111,7 +120,7 @@ namespace XNode {
Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this);
return ports[fieldName]; return ports[fieldName];
} }
NodePort port = new NodePort(fieldName, type, direction, connectionType, this); NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this);
ports.Add(fieldName, port); ports.Add(fieldName, port);
return port; return port;
} }
@ -206,24 +215,25 @@ namespace XNode {
foreach (NodePort port in Ports) port.ClearConnections(); foreach (NodePort port in Ports) port.ClearConnections();
} }
public override int GetHashCode() { #region Attributes
return JsonUtility.ToJson(this).GetHashCode();
}
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary> /// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public class InputAttribute : Attribute { public class InputAttribute : Attribute {
public ShowBackingValue backingValue; public ShowBackingValue backingValue;
public ConnectionType connectionType; public ConnectionType connectionType;
public bool instancePortList; public bool instancePortList;
public TypeConstraint typeConstraint;
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary> /// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param> /// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
/// <param name="connectionType">Should we allow multiple connections? </param> /// <param name="connectionType">Should we allow multiple connections? </param>
public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, bool instancePortList = false) { /// <param name="typeConstraint">Constrains which input connections can be made to this port </param>
/// <param name="instancePortList">If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays </param>
public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool instancePortList = false) {
this.backingValue = backingValue; this.backingValue = backingValue;
this.connectionType = connectionType; this.connectionType = connectionType;
this.instancePortList = instancePortList; this.instancePortList = instancePortList;
this.typeConstraint = typeConstraint;
} }
} }
@ -237,6 +247,7 @@ namespace XNode {
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary> /// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param> /// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
/// <param name="connectionType">Should we allow multiple connections? </param> /// <param name="connectionType">Should we allow multiple connections? </param>
/// <param name="instancePortList">If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays </param>
public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool instancePortList = false) { public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool instancePortList = false) {
this.backingValue = backingValue; this.backingValue = backingValue;
this.connectionType = connectionType; this.connectionType = connectionType;
@ -255,19 +266,19 @@ namespace XNode {
} }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeTint : Attribute { public class NodeTintAttribute : Attribute {
public Color color; public Color color;
/// <summary> Specify a color for this node type </summary> /// <summary> Specify a color for this node type </summary>
/// <param name="r"> Red [0.0f .. 1.0f] </param> /// <param name="r"> Red [0.0f .. 1.0f] </param>
/// <param name="g"> Green [0.0f .. 1.0f] </param> /// <param name="g"> Green [0.0f .. 1.0f] </param>
/// <param name="b"> Blue [0.0f .. 1.0f] </param> /// <param name="b"> Blue [0.0f .. 1.0f] </param>
public NodeTint(float r, float g, float b) { public NodeTintAttribute(float r, float g, float b) {
color = new Color(r, g, b); color = new Color(r, g, b);
} }
/// <summary> Specify a color for this node type </summary> /// <summary> Specify a color for this node type </summary>
/// <param name="hex"> HEX color value </param> /// <param name="hex"> HEX color value </param>
public NodeTint(string hex) { public NodeTintAttribute(string hex) {
ColorUtility.TryParseHtmlString(hex, out color); ColorUtility.TryParseHtmlString(hex, out color);
} }
@ -275,20 +286,21 @@ namespace XNode {
/// <param name="r"> Red [0 .. 255] </param> /// <param name="r"> Red [0 .. 255] </param>
/// <param name="g"> Green [0 .. 255] </param> /// <param name="g"> Green [0 .. 255] </param>
/// <param name="b"> Blue [0 .. 255] </param> /// <param name="b"> Blue [0 .. 255] </param>
public NodeTint(byte r, byte g, byte b) { public NodeTintAttribute(byte r, byte g, byte b) {
color = new Color32(r, g, b, byte.MaxValue); color = new Color32(r, g, b, byte.MaxValue);
} }
} }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeWidth : Attribute { public class NodeWidthAttribute : Attribute {
public int width; public int width;
/// <summary> Specify a width for this node type </summary> /// <summary> Specify a width for this node type </summary>
/// <param name="width"> Width </param> /// <param name="width"> Width </param>
public NodeWidth(int width) { public NodeWidthAttribute(int width) {
this.width = width; this.width = width;
} }
} }
#endregion
[Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver { [Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver {
[SerializeField] private List<string> keys = new List<string>(); [SerializeField] private List<string> keys = new List<string>();

View File

@ -30,7 +30,7 @@ namespace XNode {
NodePort staticPort; NodePort staticPort;
if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { if (staticPorts.TryGetValue(port.fieldName, out staticPort)) {
// If port exists but with wrong settings, remove it. Re-add it later. // If port exists but with wrong settings, remove it. Re-add it later.
if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction) ports.Remove(port.fieldName); if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction || port.typeConstraint != staticPort.typeConstraint) ports.Remove(port.fieldName);
else port.ValueType = staticPort.ValueType; else port.ValueType = staticPort.ValueType;
} }
// If port doesn't exist anymore, remove it // If port doesn't exist anymore, remove it

View File

@ -21,6 +21,7 @@ namespace XNode {
public IO direction { get { return _direction; } } public IO direction { get { return _direction; } }
public Node.ConnectionType connectionType { get { return _connectionType; } } public Node.ConnectionType connectionType { get { return _connectionType; } }
public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } }
/// <summary> Is this port connected to anytihng? </summary> /// <summary> Is this port connected to anytihng? </summary>
public bool IsConnected { get { return connections.Count != 0; } } public bool IsConnected { get { return connections.Count != 0; } }
@ -49,6 +50,7 @@ namespace XNode {
[SerializeField] private List<PortConnection> connections = new List<PortConnection>(); [SerializeField] private List<PortConnection> connections = new List<PortConnection>();
[SerializeField] private IO _direction; [SerializeField] private IO _direction;
[SerializeField] private Node.ConnectionType _connectionType; [SerializeField] private Node.ConnectionType _connectionType;
[SerializeField] private Node.TypeConstraint _typeConstraint;
[SerializeField] private bool _dynamic; [SerializeField] private bool _dynamic;
/// <summary> Construct a static targetless nodeport. Used as a template. </summary> /// <summary> Construct a static targetless nodeport. Used as a template. </summary>
@ -61,6 +63,7 @@ namespace XNode {
if (attribs[i] is Node.InputAttribute) { if (attribs[i] is Node.InputAttribute) {
_direction = IO.Input; _direction = IO.Input;
_connectionType = (attribs[i] as Node.InputAttribute).connectionType; _connectionType = (attribs[i] as Node.InputAttribute).connectionType;
_typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint;
} else if (attribs[i] is Node.OutputAttribute) { } else if (attribs[i] is Node.OutputAttribute) {
_direction = IO.Output; _direction = IO.Output;
_connectionType = (attribs[i] as Node.OutputAttribute).connectionType; _connectionType = (attribs[i] as Node.OutputAttribute).connectionType;
@ -75,17 +78,19 @@ namespace XNode {
_direction = nodePort.direction; _direction = nodePort.direction;
_dynamic = nodePort._dynamic; _dynamic = nodePort._dynamic;
_connectionType = nodePort._connectionType; _connectionType = nodePort._connectionType;
_typeConstraint = nodePort._typeConstraint;
_node = node; _node = node;
} }
/// <summary> Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. </summary> /// <summary> Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. </summary>
public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node node) { public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) {
_fieldName = fieldName; _fieldName = fieldName;
this.ValueType = type; this.ValueType = type;
_direction = direction; _direction = direction;
_node = node; _node = node;
_dynamic = true; _dynamic = true;
_connectionType = connectionType; _connectionType = connectionType;
_typeConstraint = typeConstraint;
} }
/// <summary> Checks all connections for invalid references, and removes them. </summary> /// <summary> Checks all connections for invalid references, and removes them. </summary>
@ -240,6 +245,23 @@ namespace XNode {
return false; return false;
} }
/// <summary> Returns true if this port can connect to specified port </summary>
public bool CanConnectTo(NodePort port) {
// Figure out which is input and which is output
NodePort input = null, output = null;
if (IsInput) input = this;
else output = this;
if (port.IsInput) input = port;
else output = port;
// If there isn't one of each, they can't connect
if (input == null || output == null) return false;
// Check type constraints
if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false;
if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false;
// Success
return true;
}
/// <summary> Disconnect this port from another port </summary> /// <summary> Disconnect this port from another port </summary>
public void Disconnect(NodePort port) { public void Disconnect(NodePort port) {
// Remove this ports connection to the other // Remove this ports connection to the other