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

Post fix 223: Allow dynamic port lists (declared as both arrays and lists) to connect to fields of their underlying types.

Such port lists are already drawn with elements of their underlying types, and allowing connections is logical.
This commit is contained in:
Lumos 2019-12-21 18:09:10 +01:00
parent 5005b5e4f9
commit 3024b9e8d0
11 changed files with 199 additions and 30 deletions

View File

@ -5,14 +5,22 @@ If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)!
## Pull Requests ## Pull Requests
Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs. Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs.
* Avoid including irellevant whitespace or formatting changes. These are the main points to follow:
* Comment your code.
* Spell check your code / comments 1) Use formatting which is consistent with the rest of xNode base (see below)
* Use consistent formatting 2) Keep _one feature_ per PR (see below)
3) xNode aims to be compatible with C# 4.x, do not use new language features
4) Avoid including irellevant whitespace or formatting changes
5) Comment your code
6) Spell check your code / comments
7) Use concrete types, not *var*
8) Use english language
## New features ## New features
xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings. xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings.
Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible.
If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel. If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel.
## Coding conventions ## Coding conventions

View File

@ -0,0 +1,73 @@
using UnityEditor;
using UnityEngine;
#if ODIN_INSPECTOR
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
#endif
namespace XNodeEditor {
/// <summary> Override graph inspector to show an 'Open Graph' button at the top </summary>
[CustomEditor(typeof(XNode.NodeGraph), true)]
#if ODIN_INSPECTOR
public class GlobalGraphEditor : OdinEditor {
public override void OnInspectorGUI() {
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
}
base.OnInspectorGUI();
}
}
#else
public class GlobalGraphEditor : Editor {
public override void OnInspectorGUI() {
serializedObject.Update();
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
}
GUILayout.Space(EditorGUIUtility.singleLineHeight);
GUILayout.Label("Raw data", "BoldLabel");
DrawDefaultInspector();
serializedObject.ApplyModifiedProperties();
}
}
#endif
[CustomEditor(typeof(XNode.Node), true)]
#if ODIN_INSPECTOR
public class GlobalNodeEditor : OdinEditor {
public override void OnInspectorGUI() {
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
SerializedProperty graphProp = serializedObject.FindProperty("graph");
NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
w.Home(); // Focus selected node
}
base.OnInspectorGUI();
}
}
#else
public class GlobalNodeEditor : Editor {
public override void OnInspectorGUI() {
serializedObject.Update();
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
SerializedProperty graphProp = serializedObject.FindProperty("graph");
NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
w.Home(); // Focus selected node
}
GUILayout.Space(EditorGUIUtility.singleLineHeight);
GUILayout.Label("Raw data", "BoldLabel");
// Now draw the node itself.
DrawDefaultInspector();
serializedObject.ApplyModifiedProperties();
}
}
#endif
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bdd6e443125ccac4dad0665515759637
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -21,7 +21,7 @@ namespace XNodeEditor {
public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>(); public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>();
#if ODIN_INSPECTOR #if ODIN_INSPECTOR
internal static bool inNodeEditor = false; protected internal static bool inNodeEditor = false;
#endif #endif
public virtual void OnHeaderGUI() { public virtual void OnHeaderGUI() {
@ -129,9 +129,14 @@ namespace XNodeEditor {
public void Rename(string newName) { public void Rename(string newName) {
if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType());
target.name = newName; target.name = newName;
OnRename();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
} }
/// <summary> Called after this node's name has changed. </summary>
public virtual void OnRename() { }
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public class CustomNodeEditorAttribute : Attribute, public class CustomNodeEditorAttribute : Attribute,
XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node>.INodeEditorAttrib { XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node>.INodeEditorAttrib {

View File

@ -58,10 +58,9 @@ namespace XNodeEditor {
case EventType.MouseDrag: case EventType.MouseDrag:
if (e.button == 0) { if (e.button == 0) {
if (IsDraggingPort) { if (IsDraggingPort) {
if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) { // Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously
if (!draggedOutput.IsConnectedTo(hoveredPort)) { if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) {
draggedOutputTarget = hoveredPort; draggedOutputTarget = hoveredPort;
}
} else { } else {
draggedOutputTarget = null; draggedOutputTarget = null;
} }
@ -205,8 +204,8 @@ namespace XNodeEditor {
if (e.button == 0) { if (e.button == 0) {
//Port drag release //Port drag release
if (IsDraggingPort) { if (IsDraggingPort) {
//If connection is valid, save it // If connection is valid, save it
if (draggedOutputTarget != null) { if (draggedOutputTarget != null && draggedOutput.CanConnectTo(draggedOutputTarget)) {
XNode.Node node = draggedOutputTarget.node; XNode.Node node = draggedOutputTarget.node;
if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget); if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget);
@ -218,8 +217,8 @@ namespace XNodeEditor {
EditorUtility.SetDirty(graph); EditorUtility.SetDirty(graph);
} }
} }
// Open context menu for auto-connection // Open context menu for auto-connection if there is no target node
else if (NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) { else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
GenericMenu menu = new GenericMenu(); GenericMenu menu = new GenericMenu();
graphEditor.AddContextMenuItems(menu); graphEditor.AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
@ -457,8 +456,8 @@ namespace XNodeEditor {
XNode.Node newNodeIn, newNodeOut; XNode.Node newNodeIn, newNodeOut;
if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) { if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) {
newNodeIn.UpdateStaticPorts(); newNodeIn.UpdatePorts();
newNodeOut.UpdateStaticPorts(); newNodeOut.UpdatePorts();
inputPort = newNodeIn.GetInputPort(inputPort.fieldName); inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
outputPort = newNodeOut.GetOutputPort(outputPort.fieldName); outputPort = newNodeOut.GetOutputPort(outputPort.fieldName);
} }

View File

@ -312,6 +312,8 @@ namespace XNodeEditor {
}).Where(x => x.port != null); }).Where(x => x.port != null);
List<XNode.NodePort> dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); List<XNode.NodePort> dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
node.UpdatePorts();
ReorderableList list = null; ReorderableList list = null;
Dictionary<string, ReorderableList> rlc; Dictionary<string, ReorderableList> rlc;
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) { if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
@ -326,6 +328,7 @@ namespace XNodeEditor {
} }
list.list = dynamicPorts; list.list = dynamicPorts;
list.DoLayoutList(); list.DoLayoutList();
} }
private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) { private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) {
@ -337,7 +340,7 @@ namespace XNodeEditor {
list.drawElementCallback = list.drawElementCallback =
(Rect rect, int index, bool isActive, bool isFocused) => { (Rect rect, int index, bool isActive, bool isFocused) => {
XNode.NodePort port = node.GetPort(fieldName + " " + index); XNode.NodePort port = node.GetPort(fieldName + " " + index);
if (hasArrayData) { if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
if (arrayData.arraySize <= index) { if (arrayData.arraySize <= index) {
EditorGUI.LabelField(rect, "Array[" + index + "] data out of range"); EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
return; return;
@ -465,7 +468,7 @@ namespace XNodeEditor {
EditorUtility.SetDirty(node); EditorUtility.SetDirty(node);
} }
if (hasArrayData) { if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
if (arrayData.arraySize <= index) { if (arrayData.arraySize <= index) {
Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped"); Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped");
Debug.Log(rl.list[0]); Debug.Log(rl.list[0]);

View File

@ -187,12 +187,13 @@ namespace XNodeEditor {
} }
/// <summary>Open the provided graph in the NodeEditor</summary> /// <summary>Open the provided graph in the NodeEditor</summary>
public static void Open(XNode.NodeGraph graph) { public static NodeEditorWindow Open(XNode.NodeGraph graph) {
if (!graph) return; if (!graph) return null;
NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
w.wantsMouseMove = true; w.wantsMouseMove = true;
w.graph = graph; w.graph = graph;
return w;
} }
/// <summary> Repaint all open NodeEditorWindows. </summary> /// <summary> Repaint all open NodeEditorWindows. </summary>

View File

@ -49,6 +49,7 @@ namespace XNodeEditor {
if (input == null || input.Trim() == "") { if (input == null || input.Trim() == "") {
if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) { if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = NodeEditorUtilities.NodeDefaultName(target.GetType()); target.name = NodeEditorUtilities.NodeDefaultName(target.GetType());
NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close(); Close();
target.TriggerOnValidate(); target.TriggerOnValidate();
@ -58,6 +59,7 @@ namespace XNodeEditor {
else { else {
if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = input; target.name = input;
NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close(); Close();
target.TriggerOnValidate(); target.TriggerOnValidate();

View File

@ -119,12 +119,12 @@ namespace XNode {
protected void OnEnable() { protected void OnEnable() {
if (graphHotfix != null) graph = graphHotfix; if (graphHotfix != null) graph = graphHotfix;
graphHotfix = null; graphHotfix = null;
UpdateStaticPorts(); UpdatePorts();
Init(); Init();
} }
/// <summary> Update static ports to reflect class fields. This happens automatically on enable. </summary> /// <summary> Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list. </summary>
public void UpdateStaticPorts() { public void UpdatePorts() {
NodeDataCache.UpdatePorts(this, ports); NodeDataCache.UpdatePorts(this, ports);
} }
@ -262,7 +262,7 @@ namespace XNode {
#region Attributes #region Attributes
/// <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)]
public class InputAttribute : Attribute { public class InputAttribute : Attribute {
public ShowBackingValue backingValue; public ShowBackingValue backingValue;
public ConnectionType connectionType; public ConnectionType connectionType;
@ -285,7 +285,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>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Field)]
public class OutputAttribute : Attribute { public class OutputAttribute : Attribute {
public ShowBackingValue backingValue; public ShowBackingValue backingValue;
public ConnectionType connectionType; public ConnectionType connectionType;

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using UnityEngine; using UnityEngine;
@ -9,7 +10,7 @@ namespace XNode {
private static PortDataCache portDataCache; private static PortDataCache portDataCache;
private static bool Initialized { get { return portDataCache != null; } } private static bool Initialized { get { return portDataCache != null; } }
/// <summary> Update static ports to reflect class fields. </summary> /// <summary> Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. </summary>
public static void UpdatePorts(Node node, Dictionary<string, NodePort> ports) { public static void UpdatePorts(Node node, Dictionary<string, NodePort> ports) {
if (!Initialized) BuildCache(); if (!Initialized) BuildCache();
@ -17,6 +18,8 @@ namespace XNode {
Dictionary<string, List<NodePort>> removedPorts = new Dictionary<string, List<NodePort>>(); Dictionary<string, List<NodePort>> removedPorts = new Dictionary<string, List<NodePort>>();
System.Type nodeType = node.GetType(); System.Type nodeType = node.GetType();
List<NodePort> dynamicListPorts = new List<NodePort>();
List<NodePort> typePortCache; List<NodePort> typePortCache;
if (portDataCache.TryGetValue(nodeType, out typePortCache)) { if (portDataCache.TryGetValue(nodeType, out typePortCache)) {
for (int i = 0; i < typePortCache.Count; i++) { for (int i = 0; i < typePortCache.Count; i++) {
@ -25,6 +28,7 @@ namespace XNode {
} }
// Cleanup port dict - Remove nonexisting static ports - update static port types // Cleanup port dict - Remove nonexisting static ports - update static port types
// AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation.
// Loop through current node ports // Loop through current node ports
foreach (NodePort port in ports.Values.ToList()) { foreach (NodePort port in ports.Values.ToList()) {
// If port still exists, check it it has been changed // If port still exists, check it it has been changed
@ -43,6 +47,10 @@ namespace XNode {
port.ClearConnections(); port.ClearConnections();
ports.Remove(port.fieldName); ports.Remove(port.fieldName);
} }
// If the port is dynamic and is managed by a dynamic port list, flag it for reference updates
else if (IsDynamicListPort(port)) {
dynamicListPorts.Add(port);
}
} }
// Add missing ports // Add missing ports
foreach (NodePort staticPort in staticPorts.Values) { foreach (NodePort staticPort in staticPorts.Values) {
@ -60,6 +68,56 @@ namespace XNode {
ports.Add(staticPort.fieldName, port); ports.Add(staticPort.fieldName, port);
} }
} }
// Finally, make sure dynamic list port settings correspond to the settings of their "backing port"
foreach (NodePort listPort in dynamicListPorts) {
// At this point we know that ports here are dynamic list ports
// which have passed name/"backing port" checks, ergo we can proceed more safely.
string backingPortName = listPort.fieldName.Split(' ')[0];
NodePort backingPort = staticPorts[backingPortName];
// Update port constraints. Creating a new port instead will break the editor, mandating the need for setters.
listPort.ValueType = GetBackingValueType(backingPort.ValueType);
listPort.direction = backingPort.direction;
listPort.connectionType = backingPort.connectionType;
listPort.typeConstraint = backingPort.typeConstraint;
}
}
/// <summary>
/// Extracts the underlying types from arrays and lists, the only serialisable collections which will have
/// their elements drawn otherwise. If the given type is not applicable (i.e. if the dynamic list port was not
/// defined as an array or a list), returns the given type itself.
/// </summary>
private static Type GetBackingValueType(Type portValType) {
if (portValType.HasElementType) {
return portValType.GetElementType();
}
if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) {
return portValType.GetGenericArguments()[0];
}
return portValType;
}
/// <summary>Returns true if the given port is in a dynamic port list.</summary>
private static bool IsDynamicListPort(NodePort port) {
// Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have
// no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port.
// Thus, we need to check for attributes... (but at least we don't need to look at all fields this time)
string[] fieldNameParts = port.fieldName.Split(' ');
if (fieldNameParts.Length != 2) return false;
FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]);
if (backingPortInfo == null) return false;
object[] attribs = backingPortInfo.GetCustomAttributes(true);
return attribs.Any(x => {
Node.InputAttribute inputAttribute = x as Node.InputAttribute;
Node.OutputAttribute outputAttribute = x as Node.OutputAttribute;
return inputAttribute != null && inputAttribute.dynamicPortList ||
outputAttribute != null && outputAttribute.dynamicPortList;
});
} }
/// <summary> Cache node types </summary> /// <summary> Cache node types </summary>

View File

@ -19,9 +19,18 @@ namespace XNode {
} }
} }
public IO direction { get { return _direction; } } public IO direction {
public Node.ConnectionType connectionType { get { return _connectionType; } } get { return _direction; }
public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } } internal set { _direction = value; }
}
public Node.ConnectionType connectionType {
get { return _connectionType; }
internal set { _connectionType = value; }
}
public Node.TypeConstraint typeConstraint {
get { return _typeConstraint; }
internal set { _typeConstraint = value; }
}
/// <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; } }