1
0
mirror of https://github.com/Siccity/xNode.git synced 2026-02-14 19:11:38 +08:00

Renamed to xNode

Added XNode and XNodeEDitor namespaces
Removed unnecessary usings
This commit is contained in:
Thor Brigsted 2017-11-05 23:42:31 +01:00
parent 6fcd16abd7
commit c6a4735c71
19 changed files with 1339 additions and 1324 deletions

View File

@ -1,7 +1,7 @@
using System.Collections; using UnityEngine;
using System.Collections.Generic;
using UnityEngine;
using System; using System;
using XNode;
/// <summary> Defines an example nodegraph. </summary> /// <summary> Defines an example nodegraph. </summary>
[Serializable, CreateAssetMenu(fileName = "ExampleNodeGraph", menuName = "Node Graph/Example")] [Serializable, CreateAssetMenu(fileName = "ExampleNodeGraph", menuName = "Node Graph/Example")]
public class ExampleNodeGraph : NodeGraph { public class ExampleNodeGraph : NodeGraph {

View File

@ -1,4 +1,6 @@
namespace BasicNodes { using XNode;
namespace BasicNodes {
public class DisplayValue : Node { public class DisplayValue : Node {
[Input(ShowBackingValue.Never)] public object value; [Input(ShowBackingValue.Never)] public object value;

View File

@ -1,7 +1,5 @@
using System.Collections; using UnityEditor;
using System.Collections.Generic; using XNodeEditor;
using UnityEditor;
using UnityEngine;
namespace BasicNodes { namespace BasicNodes {
[CustomNodeEditor(typeof(DisplayValue))] [CustomNodeEditor(typeof(DisplayValue))]

View File

@ -1,4 +1,6 @@
namespace BasicNodes { using XNode;
namespace BasicNodes {
[System.Serializable] [System.Serializable]
public class MathNode : Node { public class MathNode : Node {
// Adding [Input] or [Output] is all you need to do to register a field as a valid port on your node // Adding [Input] or [Output] is all you need to do to register a field as a valid port on your node

View File

@ -1,4 +1,5 @@
using UnityEngine; using UnityEngine;
using XNode;
namespace BasicNodes { namespace BasicNodes {
public class Vector : Node { public class Vector : Node {

View File

@ -1,59 +1,61 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using XNode;
/// <summary> Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. </summary> namespace XNodeEditor {
public class NodeEditor { /// <summary> Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. </summary>
/// <summary> Fires every whenever a node was modified through the editor </summary> public class NodeEditor {
public static Action<Node> onUpdateNode; /// <summary> Fires every whenever a node was modified through the editor </summary>
public Node target; public static Action<Node> onUpdateNode;
public SerializedObject serializedObject; public Node target;
public static Dictionary<NodePort, Vector2> portPositions; public SerializedObject serializedObject;
public static Dictionary<NodePort, Vector2> portPositions;
/// <summary> Draws the node GUI.</summary> /// <summary> Draws the node GUI.</summary>
/// <param name="portPositions">Port handle positions need to be returned to the NodeEditorWindow </param> /// <param name="portPositions">Port handle positions need to be returned to the NodeEditorWindow </param>
public void OnNodeGUI() { public void OnNodeGUI() {
OnHeaderGUI(); OnHeaderGUI();
OnBodyGUI(); OnBodyGUI();
} }
public void OnHeaderGUI() { public void OnHeaderGUI() {
GUI.color = Color.white; GUI.color = Color.white;
string title = NodeEditorUtilities.PrettifyCamelCase(target.name); string title = NodeEditorUtilities.PrettifyCamelCase(target.name);
GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
} }
/// <summary> Draws standard field editors for all public fields </summary> /// <summary> Draws standard field editors for all public fields </summary>
public virtual void OnBodyGUI() { public virtual void OnBodyGUI() {
string[] excludes = { "m_Script", "graph", "position", "ports" }; string[] excludes = { "m_Script", "graph", "position", "ports" };
portPositions = new Dictionary<NodePort, Vector2>(); portPositions = new Dictionary<NodePort, Vector2>();
SerializedProperty iterator = serializedObject.GetIterator(); SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true; bool enterChildren = true;
EditorGUIUtility.labelWidth = 84; EditorGUIUtility.labelWidth = 84;
while (iterator.NextVisible(enterChildren)) { while (iterator.NextVisible(enterChildren)) {
enterChildren = false; enterChildren = false;
if (excludes.Contains(iterator.name)) continue; if (excludes.Contains(iterator.name)) continue;
NodeEditorGUILayout.PropertyField(iterator, true); NodeEditorGUILayout.PropertyField(iterator, true);
}
}
public virtual int GetWidth() {
return 200;
} }
} }
public virtual int GetWidth() { [AttributeUsage(AttributeTargets.Class)]
return 200; public class CustomNodeEditorAttribute : Attribute {
} public Type inspectedType { get { return _inspectedType; } }
} private Type _inspectedType;
/// <summary> Tells a NodeEditor which Node type it is an editor for </summary>
[AttributeUsage(AttributeTargets.Class)] /// <param name="inspectedType">Type that this editor can edit</param>
public class CustomNodeEditorAttribute : Attribute { /// <param name="contextMenuName">Path to the node</param>
public Type inspectedType { get { return _inspectedType; } } public CustomNodeEditorAttribute(Type inspectedType) {
private Type _inspectedType; _inspectedType = inspectedType;
/// <summary> Tells a NodeEditor which Node type it is an editor for </summary> }
/// <param name="inspectedType">Type that this editor can edit</param>
/// <param name="contextMenuName">Path to the node</param>
public CustomNodeEditorAttribute(Type inspectedType) {
_inspectedType = inspectedType;
} }
} }

View File

@ -1,145 +1,146 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using XNode;
public partial class NodeEditorWindow { namespace XNodeEditor {
public partial class NodeEditorWindow {
public static bool isPanning { get; private set; } public static bool isPanning { get; private set; }
public static Vector2 dragOffset; public static Vector2 dragOffset;
private bool IsDraggingNode { get { return draggedNode != null; } } private bool IsDraggingNode { get { return draggedNode != null; } }
private bool IsDraggingPort { get { return draggedOutput != null; } } private bool IsDraggingPort { get { return draggedOutput != null; } }
private bool IsHoveringPort { get { return hoveredPort != null; } } private bool IsHoveringPort { get { return hoveredPort != null; } }
private bool IsHoveringNode { get { return hoveredNode != null; } } private bool IsHoveringNode { get { return hoveredNode != null; } }
private bool HasSelectedNode { get { return selectedNode != null; } } private bool HasSelectedNode { get { return selectedNode != null; } }
private Node hoveredNode = null; private Node hoveredNode = null;
[NonSerialized] private Node selectedNode = null; [NonSerialized] private Node selectedNode = null;
[NonSerialized] private Node draggedNode = null; [NonSerialized] private Node draggedNode = null;
[NonSerialized] private NodePort hoveredPort = null; [NonSerialized] private NodePort hoveredPort = null;
[NonSerialized] private NodePort draggedOutput = null; [NonSerialized] private NodePort draggedOutput = null;
[NonSerialized] private NodePort draggedOutputTarget = null; [NonSerialized] private NodePort draggedOutputTarget = null;
private Rect nodeRects; private Rect nodeRects;
public void Controls() { public void Controls() {
wantsMouseMove = true; wantsMouseMove = true;
Event e = Event.current; Event e = Event.current;
switch (e.type) { switch (e.type) {
case EventType.MouseMove: case EventType.MouseMove:
break; break;
case EventType.ScrollWheel: case EventType.ScrollWheel:
if (e.delta.y > 0) zoom += 0.1f * zoom; if (e.delta.y > 0) zoom += 0.1f * zoom;
else zoom -= 0.1f * zoom; else zoom -= 0.1f * zoom;
break; break;
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) {
if (!draggedOutput.IsConnectedTo(hoveredPort)) { if (!draggedOutput.IsConnectedTo(hoveredPort)) {
draggedOutputTarget = hoveredPort; draggedOutputTarget = hoveredPort;
}
} else {
draggedOutputTarget = null;
} }
Repaint();
} else if (IsDraggingNode) {
draggedNode.position = WindowToGridPosition(e.mousePosition) + dragOffset;
Repaint();
}
} else if (e.button == 1) {
panOffset += e.delta * zoom;
isPanning = true;
}
break;
case EventType.KeyDown:
if (e.keyCode == KeyCode.F) Home();
break;
case EventType.MouseDown:
Repaint();
SelectNode(hoveredNode);
if (IsHoveringPort) {
if (hoveredPort.IsOutput) {
draggedOutput = hoveredPort;
} else { } else {
hoveredPort.VerifyConnections();
if (hoveredPort.IsConnected) {
Node node = hoveredPort.node;
NodePort output = hoveredPort.Connection;
hoveredPort.Disconnect(output);
draggedOutput = output;
draggedOutputTarget = hoveredPort;
NodeEditor.onUpdateNode(node);
}
}
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
draggedNode = hoveredNode;
dragOffset = hoveredNode.position - WindowToGridPosition(e.mousePosition);
}
break;
case EventType.MouseUp:
if (e.button == 0) {
//Port drag release
if (IsDraggingPort) {
//If connection is valid, save it
if (draggedOutputTarget != null) {
Node node = draggedOutputTarget.node;
if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget);
NodeEditor.onUpdateNode(node);
EditorUtility.SetDirty(graph);
}
//Release dragged connection
draggedOutput = null;
draggedOutputTarget = null; draggedOutputTarget = null;
}
Repaint();
} else if (IsDraggingNode) {
draggedNode.position = WindowToGridPosition(e.mousePosition) + dragOffset;
Repaint();
}
} else if (e.button == 1) {
panOffset += e.delta * zoom;
isPanning = true;
}
break;
case EventType.KeyDown:
if (e.keyCode == KeyCode.F) Home();
break;
case EventType.MouseDown:
Repaint();
SelectNode(hoveredNode);
if (IsHoveringPort) {
if (hoveredPort.IsOutput) {
draggedOutput = hoveredPort;
} else {
hoveredPort.VerifyConnections();
if (hoveredPort.IsConnected) {
Node node = hoveredPort.node;
NodePort output = hoveredPort.Connection;
hoveredPort.Disconnect(output);
draggedOutput = output;
draggedOutputTarget = hoveredPort;
NodeEditor.onUpdateNode(node);
}
}
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
draggedNode = hoveredNode;
dragOffset = hoveredNode.position - WindowToGridPosition(e.mousePosition);
}
break;
case EventType.MouseUp:
if (e.button == 0) {
//Port drag release
if (IsDraggingPort) {
//If connection is valid, save it
if (draggedOutputTarget != null) {
Node node = draggedOutputTarget.node;
if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget);
NodeEditor.onUpdateNode(node);
EditorUtility.SetDirty(graph); EditorUtility.SetDirty(graph);
Repaint();
} else if (IsDraggingNode) {
draggedNode = null;
} }
//Release dragged connection } else if (e.button == 1) {
draggedOutput = null; if (!isPanning) ShowContextMenu();
draggedOutputTarget = null; isPanning = false;
EditorUtility.SetDirty(graph);
Repaint();
} else if (IsDraggingNode) {
draggedNode = null;
} }
} else if (e.button == 1) { AssetDatabase.SaveAssets();
if (!isPanning) ShowContextMenu(); break;
isPanning = false; }
}
AssetDatabase.SaveAssets();
break;
} }
}
/// <summary> Puts all nodes in focus. If no nodes are present, resets view to </summary> /// <summary> Puts all nodes in focus. If no nodes are present, resets view to </summary>
public void Home() { public void Home() {
zoom = 2; zoom = 2;
panOffset = Vector2.zero; panOffset = Vector2.zero;
}
public void CreateNode(Type type, Vector2 position) {
Node node = graph.AddNode(type);
node.position = position;
Repaint();
}
/// <summary> Draw a connection as we are dragging it </summary>
public void DrawDraggedConnection() {
if (IsDraggingPort) {
if (!_portConnectionPoints.ContainsKey(draggedOutput)) return;
Vector2 from = _portConnectionPoints[draggedOutput].center;
Vector2 to = draggedOutputTarget != null ? portConnectionPoints[draggedOutputTarget].center : WindowToGridPosition(Event.current.mousePosition);
Color col = NodeEditorPreferences.GetTypeColor(draggedOutput.ValueType);
col.a = 0.6f;
DrawConnection(from, to, col);
} }
}
bool IsHoveringTitle(Node node) { public void CreateNode(Type type, Vector2 position) {
Vector2 mousePos = Event.current.mousePosition; Node node = graph.AddNode(type);
//Get node position node.position = position;
Vector2 nodePos = GridToWindowPosition(node.position); Repaint();
float width = 200; }
if (nodeWidths.ContainsKey(node)) width = nodeWidths[node];
Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom)); /// <summary> Draw a connection as we are dragging it </summary>
return windowRect.Contains(mousePos); public void DrawDraggedConnection() {
if (IsDraggingPort) {
if (!_portConnectionPoints.ContainsKey(draggedOutput)) return;
Vector2 from = _portConnectionPoints[draggedOutput].center;
Vector2 to = draggedOutputTarget != null ? portConnectionPoints[draggedOutputTarget].center : WindowToGridPosition(Event.current.mousePosition);
Color col = NodeEditorPreferences.GetTypeColor(draggedOutput.ValueType);
col.a = 0.6f;
DrawConnection(from, to, col);
}
}
bool IsHoveringTitle(Node node) {
Vector2 mousePos = Event.current.mousePosition;
//Get node position
Vector2 nodePos = GridToWindowPosition(node.position);
float width = 200;
if (nodeWidths.ContainsKey(node)) width = nodeWidths[node];
Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom));
return windowRect.Contains(mousePos);
}
} }
} }

View File

@ -1,35 +1,36 @@
using System.Collections; using UnityEditor;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine; using UnityEngine;
using XNode;
public class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor { namespace XNodeEditor {
public static AssetDeleteResult OnWillDeleteAsset(string path, RemoveAssetOptions options) { public class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor {
UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path); public static AssetDeleteResult OnWillDeleteAsset(string path, RemoveAssetOptions options) {
UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
if (!(obj is UnityEditor.MonoScript)) return AssetDeleteResult.DidNotDelete; if (!(obj is UnityEditor.MonoScript)) return AssetDeleteResult.DidNotDelete;
UnityEditor.MonoScript script = obj as UnityEditor.MonoScript; UnityEditor.MonoScript script = obj as UnityEditor.MonoScript;
System.Type scriptType = script.GetClass(); System.Type scriptType = script.GetClass();
if (scriptType != typeof(Node) && !scriptType.IsSubclassOf(typeof(Node))) return AssetDeleteResult.DidNotDelete; if (scriptType != typeof(Node) && !scriptType.IsSubclassOf(typeof(Node))) return AssetDeleteResult.DidNotDelete;
//Find ScriptableObjects using this script //Find ScriptableObjects using this script
string[] guids = AssetDatabase.FindAssets("t:" + scriptType); string[] guids = AssetDatabase.FindAssets("t:" + scriptType);
for (int i = 0; i < guids.Length; i++) { for (int i = 0; i < guids.Length; i++) {
string assetpath = AssetDatabase.GUIDToAssetPath(guids[i]); string assetpath = AssetDatabase.GUIDToAssetPath(guids[i]);
Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetpath); Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetpath);
for (int k = 0; k < objs.Length; k++) { for (int k = 0; k < objs.Length; k++) {
Node node = objs[k] as Node; Node node = objs[k] as Node;
if (node.GetType() == scriptType) { if (node.GetType() == scriptType) {
if (node != null && node.graph != null) { if (node != null && node.graph != null) {
Debug.LogWarning(node.name + " of " + node.graph + " depended on deleted script and has been removed automatically.", node.graph); Debug.LogWarning(node.name + " of " + node.graph + " depended on deleted script and has been removed automatically.", node.graph);
node.graph.RemoveNode(node); node.graph.RemoveNode(node);
}
} }
} }
}
}
return AssetDeleteResult.DidNotDelete;
} }
return AssetDeleteResult.DidNotDelete;
} }
} }

View File

@ -1,286 +1,288 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using XNode;
/// <summary> Contains GUI methods </summary> namespace XNodeEditor {
public partial class NodeEditorWindow { /// <summary> Contains GUI methods </summary>
public partial class NodeEditorWindow {
private void OnGUI() { private void OnGUI() {
Event e = Event.current; Event e = Event.current;
Matrix4x4 m = GUI.matrix; Matrix4x4 m = GUI.matrix;
Controls(); Controls();
DrawGrid(position, zoom, panOffset); DrawGrid(position, zoom, panOffset);
DrawConnections(); DrawConnections();
DrawDraggedConnection(); DrawDraggedConnection();
DrawNodes(); DrawNodes();
DrawTooltip(); DrawTooltip();
GUI.matrix = m; GUI.matrix = m;
}
public static void BeginZoomed(Rect rect, float zoom) {
GUI.EndClip();
GUIUtility.ScaleAroundPivot(Vector2.one / zoom, rect.size * 0.5f);
Vector4 padding = new Vector4(0, 22, 0, 0);
padding *= zoom;
GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (22 * zoom),
rect.width * zoom,
rect.height * zoom));
}
public static void EndZoomed(Rect rect, float zoom) {
GUIUtility.ScaleAroundPivot(Vector2.one * zoom, rect.size * 0.5f);
Vector3 offset = new Vector3(
(((rect.width * zoom) - rect.width) * 0.5f),
(((rect.height * zoom) - rect.height) * 0.5f) + (-22 * zoom) + 22,
0);
GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one);
}
public static void DrawGrid(Rect rect, float zoom, Vector2 panOffset) {
rect.position = Vector2.zero;
Vector2 center = rect.size / 2f;
Texture2D gridTex = NodeEditorResources.gridTexture;
Texture2D crossTex = NodeEditorResources.crossTexture;
// Offset from origin in tile units
float xOffset = -(center.x * zoom + panOffset.x) / gridTex.width;
float yOffset = ((center.y - rect.size.y) * zoom + panOffset.y) / gridTex.height;
Vector2 tileOffset = new Vector2(xOffset, yOffset);
// Amount of tiles
float tileAmountX = Mathf.Round(rect.size.x * zoom) / gridTex.width;
float tileAmountY = Mathf.Round(rect.size.y * zoom) / gridTex.height;
Vector2 tileAmount = new Vector2(tileAmountX, tileAmountY);
// Draw tiled background
GUI.DrawTextureWithTexCoords(rect, gridTex, new Rect(tileOffset, tileAmount));
GUI.DrawTextureWithTexCoords(rect, crossTex, new Rect(tileOffset + new Vector2(0.5f, 0.5f), tileAmount));
}
public static bool DropdownButton(string name, float width) {
return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width));
}
/// <summary> Show right-click context menu </summary>
public void ShowContextMenu() {
GenericMenu contextMenu = new GenericMenu();
Vector2 pos = WindowToGridPosition(Event.current.mousePosition);
if (hoveredNode != null) {
Node node = hoveredNode;
contextMenu.AddItem(new GUIContent("Remove"), false, () => graph.RemoveNode(node));
} else {
for (int i = 0; i < nodeTypes.Length; i++) {
Type type = nodeTypes[i];
string name = nodeTypes[i].ToString().Replace('.', '/');
Node.CreateNodeMenuAttribute attrib;
if (NodeEditorUtilities.GetAttrib(type, out attrib)) {
name = attrib.menuName;
}
contextMenu.AddItem(new GUIContent(name), false, () => {
CreateNode(type, pos);
});
}
} }
contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
}
/// <summary> Draw a bezier from startpoint to endpoint, both in grid coordinates </summary> public static void BeginZoomed(Rect rect, float zoom) {
public void DrawConnection(Vector2 startPoint, Vector2 endPoint, Color col) { GUI.EndClip();
startPoint = GridToWindowPosition(startPoint);
endPoint = GridToWindowPosition(endPoint);
Vector2 startTangent = startPoint; GUIUtility.ScaleAroundPivot(Vector2.one / zoom, rect.size * 0.5f);
if (startPoint.x < endPoint.x) startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, 0.7f); Vector4 padding = new Vector4(0, 22, 0, 0);
else startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, -0.7f); padding *= zoom;
GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (22 * zoom),
rect.width * zoom,
rect.height * zoom));
}
Vector2 endTangent = endPoint; public static void EndZoomed(Rect rect, float zoom) {
if (startPoint.x > endPoint.x) endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, -0.7f); GUIUtility.ScaleAroundPivot(Vector2.one * zoom, rect.size * 0.5f);
else endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, 0.7f); Vector3 offset = new Vector3(
(((rect.width * zoom) - rect.width) * 0.5f),
(((rect.height * zoom) - rect.height) * 0.5f) + (-22 * zoom) + 22,
0);
GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one);
}
Handles.DrawBezier(startPoint, endPoint, startTangent, endTangent, col, null, 4); public static void DrawGrid(Rect rect, float zoom, Vector2 panOffset) {
}
/// <summary> Draws all connections </summary> rect.position = Vector2.zero;
public void DrawConnections() {
foreach (Node node in graph.nodes) {
//If a null node is found, return. This can happen if the nodes associated script is deleted. It is currently not possible in Unity to delete a null asset.
if (node == null) continue;
foreach (NodePort output in node.Outputs) { Vector2 center = rect.size / 2f;
//Needs cleanup. Null checks are ugly Texture2D gridTex = NodeEditorResources.gridTexture;
if (!portConnectionPoints.ContainsKey(output)) continue; Texture2D crossTex = NodeEditorResources.crossTexture;
Vector2 from = _portConnectionPoints[output].center;
for (int k = 0; k < output.ConnectionCount; k++) {
NodePort input = output.GetConnection(k); // Offset from origin in tile units
if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return. float xOffset = -(center.x * zoom + panOffset.x) / gridTex.width;
if (!input.IsConnectedTo(output)) input.Connect(output); float yOffset = ((center.y - rect.size.y) * zoom + panOffset.y) / gridTex.height;
if (!_portConnectionPoints.ContainsKey(input)) continue;
Vector2 to = _portConnectionPoints[input].center; Vector2 tileOffset = new Vector2(xOffset, yOffset);
DrawConnection(from, to, NodeEditorPreferences.GetTypeColor(output.ValueType));
// Amount of tiles
float tileAmountX = Mathf.Round(rect.size.x * zoom) / gridTex.width;
float tileAmountY = Mathf.Round(rect.size.y * zoom) / gridTex.height;
Vector2 tileAmount = new Vector2(tileAmountX, tileAmountY);
// Draw tiled background
GUI.DrawTextureWithTexCoords(rect, gridTex, new Rect(tileOffset, tileAmount));
GUI.DrawTextureWithTexCoords(rect, crossTex, new Rect(tileOffset + new Vector2(0.5f, 0.5f), tileAmount));
}
public static bool DropdownButton(string name, float width) {
return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width));
}
/// <summary> Show right-click context menu </summary>
public void ShowContextMenu() {
GenericMenu contextMenu = new GenericMenu();
Vector2 pos = WindowToGridPosition(Event.current.mousePosition);
if (hoveredNode != null) {
Node node = hoveredNode;
contextMenu.AddItem(new GUIContent("Remove"), false, () => graph.RemoveNode(node));
} else {
for (int i = 0; i < nodeTypes.Length; i++) {
Type type = nodeTypes[i];
string name = nodeTypes[i].ToString().Replace('.', '/');
Node.CreateNodeMenuAttribute attrib;
if (NodeEditorUtilities.GetAttrib(type, out attrib)) {
name = attrib.menuName;
}
contextMenu.AddItem(new GUIContent(name), false, () => {
CreateNode(type, pos);
});
}
}
contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
}
/// <summary> Draw a bezier from startpoint to endpoint, both in grid coordinates </summary>
public void DrawConnection(Vector2 startPoint, Vector2 endPoint, Color col) {
startPoint = GridToWindowPosition(startPoint);
endPoint = GridToWindowPosition(endPoint);
Vector2 startTangent = startPoint;
if (startPoint.x < endPoint.x) startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, 0.7f);
else startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, -0.7f);
Vector2 endTangent = endPoint;
if (startPoint.x > endPoint.x) endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, -0.7f);
else endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, 0.7f);
Handles.DrawBezier(startPoint, endPoint, startTangent, endTangent, col, null, 4);
}
/// <summary> Draws all connections </summary>
public void DrawConnections() {
foreach (Node node in graph.nodes) {
//If a null node is found, return. This can happen if the nodes associated script is deleted. It is currently not possible in Unity to delete a null asset.
if (node == null) continue;
foreach (NodePort output in node.Outputs) {
//Needs cleanup. Null checks are ugly
if (!portConnectionPoints.ContainsKey(output)) continue;
Vector2 from = _portConnectionPoints[output].center;
for (int k = 0; k < output.ConnectionCount; k++) {
NodePort input = output.GetConnection(k);
if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return.
if (!input.IsConnectedTo(output)) input.Connect(output);
if (!_portConnectionPoints.ContainsKey(input)) continue;
Vector2 to = _portConnectionPoints[input].center;
DrawConnection(from, to, NodeEditorPreferences.GetTypeColor(output.ValueType));
}
} }
} }
} }
}
private void DrawNodes() {
Event e = Event.current;
if (e.type == EventType.Repaint) {
portConnectionPoints.Clear();
nodeWidths.Clear();
}
//Selected node is hashed before and after node GUI to detect changes
int nodeHash = 0;
System.Reflection.MethodInfo onValidate = null;
if (selectedNode != null) {
onValidate = selectedNode.GetType().GetMethod("OnValidate");
if (onValidate != null) nodeHash = selectedNode.GetHashCode();
}
BeginZoomed(position, zoom);
Vector2 mousePos = Event.current.mousePosition;
if (e.type != EventType.Layout) {
hoveredNode = null;
hoveredPort = null;
}
for (int n = 0; n < graph.nodes.Count; n++) {
while (graph.nodes[n] == null) graph.nodes.RemoveAt(n);
if (n >= graph.nodes.Count) return;
Node node = graph.nodes[n];
NodeEditor nodeEditor = GetNodeEditor(node.GetType());
nodeEditor.target = node;
nodeEditor.serializedObject = new SerializedObject(node);
NodeEditor.portPositions = new Dictionary<NodePort, Vector2>();
//Get node position
Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
GUILayout.BeginArea(new Rect(nodePos, new Vector2(nodeEditor.GetWidth(), 4000)));
GUIStyle style = NodeEditorResources.styles.nodeBody;
GUILayout.BeginVertical(new GUIStyle(style));
EditorGUI.BeginChangeCheck();
//Draw node contents
nodeEditor.OnNodeGUI();
//Apply
nodeEditor.serializedObject.ApplyModifiedProperties();
//If user changed a value, notify other scripts through onUpdateNode
if (EditorGUI.EndChangeCheck()) {
if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node);
}
private void DrawNodes() {
Event e = Event.current;
if (e.type == EventType.Repaint) { if (e.type == EventType.Repaint) {
nodeWidths.Add(node, nodeEditor.GetWidth()); portConnectionPoints.Clear();
nodeWidths.Clear();
foreach (var kvp in NodeEditor.portPositions) {
Vector2 portHandlePos = kvp.Value;
portHandlePos += node.position;
Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16);
portConnectionPoints.Add(kvp.Key, rect);
}
} }
GUILayout.EndVertical(); //Selected node is hashed before and after node GUI to detect changes
int nodeHash = 0;
System.Reflection.MethodInfo onValidate = null;
if (selectedNode != null) {
onValidate = selectedNode.GetType().GetMethod("OnValidate");
if (onValidate != null) nodeHash = selectedNode.GetHashCode();
}
BeginZoomed(position, zoom);
Vector2 mousePos = Event.current.mousePosition;
if (e.type != EventType.Layout) { if (e.type != EventType.Layout) {
//Check if we are hovering this node hoveredNode = null;
Vector2 nodeSize = GUILayoutUtility.GetLastRect().size; hoveredPort = null;
Rect windowRect = new Rect(nodePos, nodeSize); }
if (windowRect.Contains(mousePos)) hoveredNode = node;
//Check if we are hovering any of this nodes ports for (int n = 0; n < graph.nodes.Count; n++) {
//Check input ports while (graph.nodes[n] == null) graph.nodes.RemoveAt(n);
foreach (NodePort input in node.Inputs) { if (n >= graph.nodes.Count) return;
//Check if port rect is available Node node = graph.nodes[n];
if (!portConnectionPoints.ContainsKey(input)) continue;
Rect r = GridToWindowRect(portConnectionPoints[input]); NodeEditor nodeEditor = GetNodeEditor(node.GetType());
if (r.Contains(mousePos)) hoveredPort = input; nodeEditor.target = node;
nodeEditor.serializedObject = new SerializedObject(node);
NodeEditor.portPositions = new Dictionary<NodePort, Vector2>();
//Get node position
Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
GUILayout.BeginArea(new Rect(nodePos, new Vector2(nodeEditor.GetWidth(), 4000)));
GUIStyle style = NodeEditorResources.styles.nodeBody;
GUILayout.BeginVertical(new GUIStyle(style));
EditorGUI.BeginChangeCheck();
//Draw node contents
nodeEditor.OnNodeGUI();
//Apply
nodeEditor.serializedObject.ApplyModifiedProperties();
//If user changed a value, notify other scripts through onUpdateNode
if (EditorGUI.EndChangeCheck()) {
if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node);
} }
//Check all output ports
foreach (NodePort output in node.Outputs) { if (e.type == EventType.Repaint) {
//Check if port rect is available nodeWidths.Add(node, nodeEditor.GetWidth());
if (!portConnectionPoints.ContainsKey(output)) continue;
Rect r = GridToWindowRect(portConnectionPoints[output]); foreach (var kvp in NodeEditor.portPositions) {
if (r.Contains(mousePos)) hoveredPort = output; Vector2 portHandlePos = kvp.Value;
portHandlePos += node.position;
Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16);
portConnectionPoints.Add(kvp.Key, rect);
}
} }
GUILayout.EndVertical();
if (e.type != EventType.Layout) {
//Check if we are hovering this node
Vector2 nodeSize = GUILayoutUtility.GetLastRect().size;
Rect windowRect = new Rect(nodePos, nodeSize);
if (windowRect.Contains(mousePos)) hoveredNode = node;
//Check if we are hovering any of this nodes ports
//Check input ports
foreach (NodePort input in node.Inputs) {
//Check if port rect is available
if (!portConnectionPoints.ContainsKey(input)) continue;
Rect r = GridToWindowRect(portConnectionPoints[input]);
if (r.Contains(mousePos)) hoveredPort = input;
}
//Check all output ports
foreach (NodePort output in node.Outputs) {
//Check if port rect is available
if (!portConnectionPoints.ContainsKey(output)) continue;
Rect r = GridToWindowRect(portConnectionPoints[output]);
if (r.Contains(mousePos)) hoveredPort = output;
}
}
GUILayout.EndArea();
} }
GUILayout.EndArea(); EndZoomed(position, zoom);
//If a change in hash is detected in the selected node, call OnValidate method.
//This is done through reflection because OnValidate is only relevant in editor,
//and thus, the code should not be included in build.
if (selectedNode != null) {
if (onValidate != null && nodeHash != selectedNode.GetHashCode()) onValidate.Invoke(selectedNode, null);
}
} }
EndZoomed(position, zoom); private void DrawTooltip() {
if (hoveredPort != null) {
//If a change in hash is detected in the selected node, call OnValidate method. Type type = hoveredPort.ValueType;
//This is done through reflection because OnValidate is only relevant in editor, GUIContent content = new GUIContent();
//and thus, the code should not be included in build. content.text = TypeToString(type);
if (selectedNode != null) { Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content);
if (onValidate != null && nodeHash != selectedNode.GetHashCode()) onValidate.Invoke(selectedNode, null); Rect rect = new Rect(Event.current.mousePosition - (size), size);
EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip);
Repaint();
}
} }
}
private void DrawTooltip() { private string TypeToString(Type type) {
if (hoveredPort != null) { if (type == null) return "null";
Type type = hoveredPort.ValueType; if (type == typeof(float)) return "float";
GUIContent content = new GUIContent(); else if (type == typeof(int)) return "int";
content.text = TypeToString(type); else if (type == typeof(long)) return "long";
Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content); else if (type == typeof(double)) return "double";
Rect rect = new Rect(Event.current.mousePosition - (size), size); else if (type == typeof(string)) return "string";
EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip); else if (type == typeof(bool)) return "bool";
Repaint(); else if (type.IsGenericType) {
string s = "";
Type genericType = type.GetGenericTypeDefinition();
if (genericType == typeof(List<>)) s = "List";
else s = type.GetGenericTypeDefinition().ToString();
Type[] types = type.GetGenericArguments();
string[] stypes = new string[types.Length];
for (int i = 0; i < types.Length; i++) {
stypes[i] = TypeToString(types[i]);
}
return s + "<" + string.Join(", ", stypes) + ">";
} else if (type.IsArray) {
string rank = "";
for (int i = 1; i < type.GetArrayRank(); i++) {
rank += ",";
}
Type elementType = type.GetElementType();
if (!elementType.IsArray) return TypeToString(elementType) + "[" + rank + "]";
else {
string s = TypeToString(elementType);
int i = s.IndexOf('[');
return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i);
}
} else return hoveredPort.ValueType.ToString();
} }
} }
private string TypeToString(Type type) {
if (type == null) return "null";
if (type == typeof(float)) return "float";
else if (type == typeof(int)) return "int";
else if (type == typeof(long)) return "long";
else if (type == typeof(double)) return "double";
else if (type == typeof(string)) return "string";
else if (type == typeof(bool)) return "bool";
else if (type.IsGenericType) {
string s = "";
Type genericType = type.GetGenericTypeDefinition();
if (genericType == typeof(List<>)) s = "List";
else s = type.GetGenericTypeDefinition().ToString();
Type[] types = type.GetGenericArguments();
string[] stypes = new string[types.Length];
for (int i = 0; i < types.Length; i++) {
stypes[i] = TypeToString(types[i]);
}
return s + "<" + string.Join(", ", stypes) + ">";
} else if (type.IsArray) {
string rank = "";
for (int i = 1; i < type.GetArrayRank(); i++) {
rank += ",";
}
Type elementType = type.GetElementType();
if (!elementType.IsArray) return TypeToString(elementType) + "[" + rank + "]";
else {
string s = TypeToString(elementType);
int i = s.IndexOf('[');
return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i);
}
} else return hoveredPort.ValueType.ToString();
}
} }

View File

@ -1,39 +1,55 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using XNode;
/// <summary> UNEC-specific version of <see cref="EditorGUILayout"/> </summary> namespace XNodeEditor {
public static class NodeEditorGUILayout { /// <summary> UNEC-specific version of <see cref="EditorGUILayout"/> </summary>
public static class NodeEditorGUILayout {
public static void PropertyField(SerializedProperty property, bool includeChildren = true) { public static void PropertyField(SerializedProperty property, bool includeChildren = true) {
if (property == null) throw new NullReferenceException(); if (property == null) throw new NullReferenceException();
Node node = property.serializedObject.targetObject as Node; Node node = property.serializedObject.targetObject as Node;
NodePort port = node.GetPort(property.name); NodePort port = node.GetPort(property.name);
// If property is not a port, display a regular property field // If property is not a port, display a regular property field
if (port == null) EditorGUILayout.PropertyField(property, includeChildren, GUILayout.MinWidth(30)); if (port == null) EditorGUILayout.PropertyField(property, includeChildren, GUILayout.MinWidth(30));
else { else {
Rect rect = new Rect(); Rect rect = new Rect();
// If property is an input, display a regular property field and put a port handle on the left side // If property is an input, display a regular property field and put a port handle on the left side
if (port.direction == NodePort.IO.Input) { if (port.direction == NodePort.IO.Input) {
// Display a label if port is connected // Display a label if port is connected
if (port.IsConnected) EditorGUILayout.LabelField(property.displayName); if (port.IsConnected) EditorGUILayout.LabelField(property.displayName);
// Display an editable property field if port is not connected // Display an editable property field if port is not connected
else EditorGUILayout.PropertyField(property, includeChildren, GUILayout.MinWidth(30)); else EditorGUILayout.PropertyField(property, includeChildren, GUILayout.MinWidth(30));
rect = GUILayoutUtility.GetLastRect(); rect = GUILayoutUtility.GetLastRect();
rect.position = rect.position - new Vector2(16, 0); rect.position = rect.position - new Vector2(16, 0);
// If property is an output, display a text label and put a port handle on the right side // If property is an output, display a text label and put a port handle on the right side
} else if (port.direction == NodePort.IO.Output) { } else if (port.direction == NodePort.IO.Output) {
EditorGUILayout.LabelField(property.displayName, NodeEditorResources.styles.outputPort, GUILayout.MinWidth(30)); EditorGUILayout.LabelField(property.displayName, NodeEditorResources.styles.outputPort, GUILayout.MinWidth(30));
rect = GUILayoutUtility.GetLastRect(); rect = GUILayoutUtility.GetLastRect();
rect.position = rect.position + new Vector2(rect.width, 0); rect.position = rect.position + new Vector2(rect.width, 0);
}
rect.size = new Vector2(16, 16);
DrawPortHandle(rect, port.ValueType);
// Register the handle position
Vector2 portPos = rect.center;
if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos;
else NodeEditor.portPositions.Add(port, portPos);
} }
}
public static void PortField(NodePort port) {
if (port == null) return;
EditorGUILayout.LabelField(port.fieldName.PrettifyCamelCase(), GUILayout.MinWidth(30));
Rect rect = GUILayoutUtility.GetLastRect();
if (port.direction == NodePort.IO.Input) rect.position = rect.position - new Vector2(16, 0);
else if (port.direction == NodePort.IO.Output) rect.position = rect.position + new Vector2(rect.width, 0);
rect.size = new Vector2(16, 16); rect.size = new Vector2(16, 16);
DrawPortHandle(rect, port.ValueType); DrawPortHandle(rect, port.ValueType);
@ -43,31 +59,14 @@ public static class NodeEditorGUILayout {
if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos; if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos;
else NodeEditor.portPositions.Add(port, portPos); else NodeEditor.portPositions.Add(port, portPos);
} }
}
public static void PortField(NodePort port) { private static void DrawPortHandle(Rect rect, Type type) {
if (port == null) return; Color col = GUI.color;
EditorGUILayout.LabelField(port.fieldName.PrettifyCamelCase(), GUILayout.MinWidth(30)); GUI.color = new Color32(90, 97, 105, 255);
GUI.DrawTexture(rect, NodeEditorResources.dotOuter);
Rect rect = GUILayoutUtility.GetLastRect(); GUI.color = NodeEditorPreferences.GetTypeColor(type);
if (port.direction == NodePort.IO.Input) rect.position = rect.position - new Vector2(16, 0); GUI.DrawTexture(rect, NodeEditorResources.dot);
else if (port.direction == NodePort.IO.Output) rect.position = rect.position + new Vector2(rect.width, 0); GUI.color = col;
rect.size = new Vector2(16, 16); }
DrawPortHandle(rect, port.ValueType);
// Register the handle position
Vector2 portPos = rect.center;
if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos;
else NodeEditor.portPositions.Add(port, portPos);
}
private static void DrawPortHandle(Rect rect, Type type) {
Color col = GUI.color;
GUI.color = new Color32(90, 97, 105, 255);
GUI.DrawTexture(rect, NodeEditorResources.dotOuter);
GUI.color = NodeEditorPreferences.GetTypeColor(type);
GUI.DrawTexture(rect, NodeEditorResources.dot);
GUI.color = col;
} }
} }

View File

@ -1,98 +1,99 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
public static class NodeEditorPreferences { namespace XNodeEditor {
public static class NodeEditorPreferences {
/// <summary> Have we loaded the prefs yet </summary> /// <summary> Have we loaded the prefs yet </summary>
private static bool prefsLoaded = false; private static bool prefsLoaded = false;
private static Dictionary<string, Color> typeColors; private static Dictionary<string, Color> typeColors;
private static Dictionary<string, Color> generatedTypeColors; private static Dictionary<string, Color> generatedTypeColors;
[PreferenceItem("Node Editor")] [PreferenceItem("Node Editor")]
private static void PreferencesGUI() { private static void PreferencesGUI() {
if (!prefsLoaded) LoadPrefs(); if (!prefsLoaded) LoadPrefs();
EditorGUILayout.LabelField("Type colors", EditorStyles.boldLabel); EditorGUILayout.LabelField("Type colors", EditorStyles.boldLabel);
string[] typeKeys = new string[typeColors.Count]; string[] typeKeys = new string[typeColors.Count];
typeColors.Keys.CopyTo(typeKeys, 0); typeColors.Keys.CopyTo(typeKeys, 0);
foreach (var key in typeKeys) { foreach (var key in typeKeys) {
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
Color col = typeColors[key]; Color col = typeColors[key];
col = EditorGUILayout.ColorField(key, col); col = EditorGUILayout.ColorField(key, col);
typeColors[key] = col; typeColors[key] = col;
if (!GUILayout.Toggle(true, "")) { if (!GUILayout.Toggle(true, "")) {
typeColors.Remove(key); typeColors.Remove(key);
SavePrefs();
}
EditorGUILayout.EndHorizontal();
}
if (GUI.changed) {
SavePrefs(); SavePrefs();
} }
EditorGUILayout.EndHorizontal();
}
if (GUI.changed) {
SavePrefs();
}
string[] generatedTypeKeys = new string[generatedTypeColors.Count]; string[] generatedTypeKeys = new string[generatedTypeColors.Count];
generatedTypeColors.Keys.CopyTo(generatedTypeKeys, 0); generatedTypeColors.Keys.CopyTo(generatedTypeKeys, 0);
foreach (var key in generatedTypeKeys) { foreach (var key in generatedTypeKeys) {
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
Color col = generatedTypeColors[key]; Color col = generatedTypeColors[key];
EditorGUI.BeginDisabledGroup(true); EditorGUI.BeginDisabledGroup(true);
col = EditorGUILayout.ColorField(key, col); col = EditorGUILayout.ColorField(key, col);
EditorGUI.EndDisabledGroup(); EditorGUI.EndDisabledGroup();
if (GUILayout.Toggle(false, "")) { if (GUILayout.Toggle(false, "")) {
typeColors.Add(key, generatedTypeColors[key]); typeColors.Add(key, generatedTypeColors[key]);
generatedTypeColors.Remove(key); generatedTypeColors.Remove(key);
SavePrefs(); SavePrefs();
} }
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
}
}
private static void LoadPrefs() {
generatedTypeColors = new Dictionary<string, Color>();
typeColors = GetTypeColors();
prefsLoaded = true;
}
private static void SavePrefs() {
if (!prefsLoaded) return;
string s = "";
foreach (var item in typeColors) {
s += item.Key + "," + ColorUtility.ToHtmlStringRGB(item.Value) + ",";
}
EditorPrefs.SetString("unec_typecolors", s);
}
public static void SetDefaultTypeColors() {
EditorPrefs.SetString("unec_typecolors", "int,2568CA,string,CE743A,bool,00FF00");
}
public static Dictionary<string, Color> GetTypeColors() {
if (prefsLoaded) return typeColors;
if (!EditorPrefs.HasKey("unec_typecolors")) SetDefaultTypeColors();
string[] data = EditorPrefs.GetString("unec_typecolors").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
Dictionary<string, Color> dict = new Dictionary<string, Color>();
for (int i = 0; i < data.Length; i += 2) {
Color col;
if (ColorUtility.TryParseHtmlString("#" + data[i + 1], out col)) {
dict.Add(data[i], col);
} }
} }
return dict;
}
/// <summary> Return color based on type </summary> private static void LoadPrefs() {
public static Color GetTypeColor(System.Type type) { generatedTypeColors = new Dictionary<string, Color>();
if (!prefsLoaded) LoadPrefs(); typeColors = GetTypeColors();
if (type == null) return Color.gray; prefsLoaded = true;
if (typeColors.ContainsKey(type.Name)) return typeColors[type.Name]; }
if (generatedTypeColors.ContainsKey(type.Name)) return generatedTypeColors[type.Name];
UnityEngine.Random.InitState(type.Name.GetHashCode()); private static void SavePrefs() {
generatedTypeColors.Add(type.Name, new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value)); if (!prefsLoaded) return;
return generatedTypeColors[type.Name]; string s = "";
foreach (var item in typeColors) {
s += item.Key + "," + ColorUtility.ToHtmlStringRGB(item.Value) + ",";
}
EditorPrefs.SetString("unec_typecolors", s);
}
public static void SetDefaultTypeColors() {
EditorPrefs.SetString("unec_typecolors", "int,2568CA,string,CE743A,bool,00FF00");
}
public static Dictionary<string, Color> GetTypeColors() {
if (prefsLoaded) return typeColors;
if (!EditorPrefs.HasKey("unec_typecolors")) SetDefaultTypeColors();
string[] data = EditorPrefs.GetString("unec_typecolors").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
Dictionary<string, Color> dict = new Dictionary<string, Color>();
for (int i = 0; i < data.Length; i += 2) {
Color col;
if (ColorUtility.TryParseHtmlString("#" + data[i + 1], out col)) {
dict.Add(data[i], col);
}
}
return dict;
}
/// <summary> Return color based on type </summary>
public static Color GetTypeColor(System.Type type) {
if (!prefsLoaded) LoadPrefs();
if (type == null) return Color.gray;
if (typeColors.ContainsKey(type.Name)) return typeColors[type.Name];
if (generatedTypeColors.ContainsKey(type.Name)) return generatedTypeColors[type.Name];
UnityEngine.Random.InitState(type.Name.GetHashCode());
generatedTypeColors.Add(type.Name, new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value));
return generatedTypeColors[type.Name];
}
} }
} }

View File

@ -2,49 +2,51 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using UnityEngine; using XNode;
/// <summary> Contains reflection-related info </summary> namespace XNodeEditor {
public partial class NodeEditorWindow { /// <summary> Contains reflection-related info </summary>
[NonSerialized] private static Dictionary<Type, NodeEditor> customNodeEditor; public partial class NodeEditorWindow {
public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } [NonSerialized] private static Dictionary<Type, NodeEditor> customNodeEditor;
[NonSerialized] private static Type[] _nodeTypes = null; public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } }
[NonSerialized] private static Type[] _nodeTypes = null;
public static NodeEditor GetNodeEditor(Type node) { public static NodeEditor GetNodeEditor(Type node) {
if (customNodeEditor == null) CacheCustomNodeEditors(); if (customNodeEditor == null) CacheCustomNodeEditors();
if (customNodeEditor.ContainsKey(node)) return customNodeEditor[node]; if (customNodeEditor.ContainsKey(node)) return customNodeEditor[node];
return customNodeEditor[typeof(Node)]; return customNodeEditor[typeof(Node)];
} }
public static Type[] GetNodeTypes() { public static Type[] GetNodeTypes() {
//Get all classes deriving from Node via reflection //Get all classes deriving from Node via reflection
return GetDerivedTypes(typeof(Node)); return GetDerivedTypes(typeof(Node));
} }
public static void CacheCustomNodeEditors() { public static void CacheCustomNodeEditors() {
customNodeEditor = new Dictionary<Type, NodeEditor>(); customNodeEditor = new Dictionary<Type, NodeEditor>();
customNodeEditor.Add(typeof(Node), new NodeEditor()); customNodeEditor.Add(typeof(Node), new NodeEditor());
//Get all classes deriving from NodeEditor via reflection //Get all classes deriving from NodeEditor via reflection
Type[] nodeEditors = GetDerivedTypes(typeof(NodeEditor)); Type[] nodeEditors = GetDerivedTypes(typeof(NodeEditor));
for (int i = 0; i < nodeEditors.Length; i++) { for (int i = 0; i < nodeEditors.Length; i++) {
var attribs = nodeEditors[i].GetCustomAttributes(typeof(CustomNodeEditorAttribute), false); var attribs = nodeEditors[i].GetCustomAttributes(typeof(CustomNodeEditorAttribute), false);
if (attribs == null || attribs.Length == 0) continue; if (attribs == null || attribs.Length == 0) continue;
if (nodeEditors[i].IsAbstract) continue; if (nodeEditors[i].IsAbstract) continue;
CustomNodeEditorAttribute attrib = attribs[0] as CustomNodeEditorAttribute; CustomNodeEditorAttribute attrib = attribs[0] as CustomNodeEditorAttribute;
customNodeEditor.Add(attrib.inspectedType, Activator.CreateInstance(nodeEditors[i]) as NodeEditor); customNodeEditor.Add(attrib.inspectedType, Activator.CreateInstance(nodeEditors[i]) as NodeEditor);
}
}
public static Type[] GetDerivedTypes(Type baseType) {
//Get all classes deriving from baseType via reflection
Assembly assembly = Assembly.GetAssembly(baseType);
return assembly.GetTypes().Where(t =>
!t.IsAbstract &&
baseType.IsAssignableFrom(t)
).ToArray();
}
public static object ObjectFromType(Type type) {
return Activator.CreateInstance(type);
} }
} }
public static Type[] GetDerivedTypes(Type baseType) {
//Get all classes deriving from baseType via reflection
Assembly assembly = Assembly.GetAssembly(baseType);
return assembly.GetTypes().Where(t =>
!t.IsAbstract &&
baseType.IsAssignableFrom(t)
).ToArray();
}
public static object ObjectFromType(Type type) {
return Activator.CreateInstance(type);
}
} }

View File

@ -1,94 +1,94 @@
using System; using UnityEngine;
using UnityEditor;
using UnityEngine;
public static class NodeEditorResources { namespace XNodeEditor {
//Unec textures public static class NodeEditorResources {
public static Texture2D gridTexture { get { return _gridTexture != null ? _gridTexture : _gridTexture = GenerateGridTexture(); } } //Unec textures
private static Texture2D _gridTexture; public static Texture2D gridTexture { get { return _gridTexture != null ? _gridTexture : _gridTexture = GenerateGridTexture(); } }
public static Texture2D crossTexture { get { return _crossTexture != null ? _crossTexture : _crossTexture = GenerateCrossTexture(); } } private static Texture2D _gridTexture;
private static Texture2D _crossTexture; public static Texture2D crossTexture { get { return _crossTexture != null ? _crossTexture : _crossTexture = GenerateCrossTexture(); } }
public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load<Texture2D>("unec_dot"); } } private static Texture2D _crossTexture;
private static Texture2D _dot; public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load<Texture2D>("unec_dot"); } }
public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load<Texture2D>("unec_dot_outer"); } } private static Texture2D _dot;
private static Texture2D _dotOuter; public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load<Texture2D>("unec_dot_outer"); } }
public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load<Texture2D>("unec_node"); } } private static Texture2D _dotOuter;
private static Texture2D _nodeBody; public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load<Texture2D>("unec_node"); } }
private static Texture2D _nodeBody;
//Grid colors //Grid colors
private static Color backgroundColor = new Color(0.18f, 0.18f, 0.18f); private static Color backgroundColor = new Color(0.18f, 0.18f, 0.18f);
private static Color veinColor = new Color(0.25f, 0.25f, 0.25f); private static Color veinColor = new Color(0.25f, 0.25f, 0.25f);
private static Color arteryColor = new Color(0.34f, 0.34f, 0.34f); private static Color arteryColor = new Color(0.34f, 0.34f, 0.34f);
private static Color crossColor = new Color(0.45f, 0.45f, 0.45f); private static Color crossColor = new Color(0.45f, 0.45f, 0.45f);
//Unec styles //Unec styles
public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } }
public static Styles _styles = null; public static Styles _styles = null;
public class Styles { public class Styles {
public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip; public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip;
public Styles() { public Styles() {
GUIStyle baseStyle = new GUIStyle("Label"); GUIStyle baseStyle = new GUIStyle("Label");
baseStyle.fixedHeight = 18; baseStyle.fixedHeight = 18;
inputPort = new GUIStyle(baseStyle); inputPort = new GUIStyle(baseStyle);
inputPort.alignment = TextAnchor.UpperLeft; inputPort.alignment = TextAnchor.UpperLeft;
inputPort.padding.left = 10; inputPort.padding.left = 10;
outputPort = new GUIStyle(baseStyle); outputPort = new GUIStyle(baseStyle);
outputPort.alignment = TextAnchor.UpperRight; outputPort.alignment = TextAnchor.UpperRight;
outputPort.padding.right = 10; outputPort.padding.right = 10;
nodeHeader = new GUIStyle(); nodeHeader = new GUIStyle();
nodeHeader.alignment = TextAnchor.MiddleCenter; nodeHeader.alignment = TextAnchor.MiddleCenter;
nodeHeader.fontStyle = FontStyle.Bold; nodeHeader.fontStyle = FontStyle.Bold;
nodeHeader.normal.textColor = Color.white; nodeHeader.normal.textColor = Color.white;
nodeBody = new GUIStyle(); nodeBody = new GUIStyle();
nodeBody.normal.background = NodeEditorResources.nodeBody; nodeBody.normal.background = NodeEditorResources.nodeBody;
nodeBody.border = new RectOffset(32, 32, 32, 32); nodeBody.border = new RectOffset(32, 32, 32, 32);
nodeBody.padding = new RectOffset(16, 16, 4, 16); nodeBody.padding = new RectOffset(16, 16, 4, 16);
tooltip = new GUIStyle("helpBox"); tooltip = new GUIStyle("helpBox");
tooltip.alignment = TextAnchor.MiddleCenter; tooltip.alignment = TextAnchor.MiddleCenter;
}
}
public static Texture2D GenerateGridTexture() {
Texture2D tex = new Texture2D(64, 64);
Color[] cols = new Color[64 * 64];
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) {
Color col = backgroundColor;
if (y % 16 == 0 || x % 16 == 0) col = veinColor;
if (y == 63 || x == 63) col = arteryColor;
cols[(y * 64) + x] = col;
} }
} }
tex.SetPixels(cols);
tex.wrapMode = TextureWrapMode.Repeat;
tex.filterMode = FilterMode.Bilinear;
tex.name = "Grid";
tex.Apply();
return tex;
}
public static Texture2D GenerateCrossTexture() { public static Texture2D GenerateGridTexture() {
Texture2D tex = new Texture2D(64, 64); Texture2D tex = new Texture2D(64, 64);
Color[] cols = new Color[64 * 64]; Color[] cols = new Color[64 * 64];
for (int y = 0; y < 64; y++) { for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) { for (int x = 0; x < 64; x++) {
Color col = crossColor; Color col = backgroundColor;
if (y != 31 && x != 31) col.a = 0; if (y % 16 == 0 || x % 16 == 0) col = veinColor;
cols[(y * 64) + x] = col; if (y == 63 || x == 63) col = arteryColor;
cols[(y * 64) + x] = col;
}
} }
tex.SetPixels(cols);
tex.wrapMode = TextureWrapMode.Repeat;
tex.filterMode = FilterMode.Bilinear;
tex.name = "Grid";
tex.Apply();
return tex;
}
public static Texture2D GenerateCrossTexture() {
Texture2D tex = new Texture2D(64, 64);
Color[] cols = new Color[64 * 64];
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) {
Color col = crossColor;
if (y != 31 && x != 31) col.a = 0;
cols[(y * 64) + x] = col;
}
}
tex.SetPixels(cols);
tex.wrapMode = TextureWrapMode.Clamp;
tex.filterMode = FilterMode.Bilinear;
tex.name = "Grid";
tex.Apply();
return tex;
} }
tex.SetPixels(cols);
tex.wrapMode = TextureWrapMode.Clamp;
tex.filterMode = FilterMode.Bilinear;
tex.name = "Grid";
tex.Apply();
return tex;
} }
} }

View File

@ -1,54 +1,53 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using UnityEngine;
/// <summary> A set of editor-only utilities and extensions for UnityNodeEditorBase </summary> namespace XNodeEditor {
public static class NodeEditorUtilities { /// <summary> A set of editor-only utilities and extensions for UnityNodeEditorBase </summary>
public static class NodeEditorUtilities {
public static bool GetAttrib<T>(Type classType, out T attribOut) where T : Attribute { public static bool GetAttrib<T>(Type classType, out T attribOut) where T : Attribute {
object[] attribs = classType.GetCustomAttributes(typeof(T), false); object[] attribs = classType.GetCustomAttributes(typeof(T), false);
return GetAttrib(attribs, out attribOut); return GetAttrib(attribs, out attribOut);
}
public static bool GetAttrib<T>(object[] attribs, out T attribOut) where T : Attribute {
for (int i = 0; i < attribs.Length; i++) {
if (attribs[i].GetType() == typeof(T)) {
attribOut = attribs[i] as T;
return true;
}
} }
attribOut = null;
return false;
}
public static bool HasAttrib<T>(object[] attribs) where T : Attribute { public static bool GetAttrib<T>(object[] attribs, out T attribOut) where T : Attribute {
for (int i = 0; i < attribs.Length; i++) { for (int i = 0; i < attribs.Length; i++) {
if (attribs[i].GetType() == typeof(T)) { if (attribs[i].GetType() == typeof(T)) {
return true; attribOut = attribs[i] as T;
return true;
}
} }
attribOut = null;
return false;
} }
return false;
}
/// <summary> Turns camelCaseString into Camel Case String </summary> public static bool HasAttrib<T>(object[] attribs) where T : Attribute {
public static string PrettifyCamelCase(this string camelCase) { for (int i = 0; i < attribs.Length; i++) {
if (string.IsNullOrEmpty(camelCase)) return ""; if (attribs[i].GetType() == typeof(T)) {
string s = System.Text.RegularExpressions.Regex.Replace(camelCase, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim(); return true;
return char.ToUpper(s[0]) + s.Substring(1); }
} }
return false;
}
/// <summary> Returns true if this can be casted to <see cref="Type"/></summary> /// <summary> Turns camelCaseString into Camel Case String </summary>
public static bool IsCastableTo(this Type from, Type to) { public static string PrettifyCamelCase(this string camelCase) {
if (to.IsAssignableFrom(from)) return true; if (string.IsNullOrEmpty(camelCase)) return "";
var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static) string s = System.Text.RegularExpressions.Regex.Replace(camelCase, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim();
.Where( return char.ToUpper(s[0]) + s.Substring(1);
m => m.ReturnType == to && }
(m.Name == "op_Implicit" ||
m.Name == "op_Explicit") /// <summary> Returns true if this can be casted to <see cref="Type"/></summary>
); public static bool IsCastableTo(this Type from, Type to) {
return methods.Count() > 0; if (to.IsAssignableFrom(from)) return true;
var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(
m => m.ReturnType == to &&
(m.Name == "op_Implicit" ||
m.Name == "op_Explicit")
);
return methods.Count() > 0;
}
} }
} }

View File

@ -1,97 +1,97 @@
using System; using System.Collections.Generic;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor; using UnityEditor;
using UnityEditor.Callbacks; using UnityEditor.Callbacks;
using UnityEngine; using UnityEngine;
using XNode;
[InitializeOnLoad] namespace XNodeEditor {
public partial class NodeEditorWindow : EditorWindow { [InitializeOnLoad]
public static NodeEditorWindow current; public partial class NodeEditorWindow : EditorWindow {
public static NodeEditorWindow current;
/// <summary> Stores node positions for all nodePorts. </summary> /// <summary> Stores node positions for all nodePorts. </summary>
public Dictionary<NodePort, Rect> portConnectionPoints { get { return _portConnectionPoints; } } public Dictionary<NodePort, Rect> portConnectionPoints { get { return _portConnectionPoints; } }
private Dictionary<NodePort, Rect> _portConnectionPoints = new Dictionary<NodePort, Rect>(); private Dictionary<NodePort, Rect> _portConnectionPoints = new Dictionary<NodePort, Rect>();
public Dictionary<Node, float> nodeWidths { get { return _nodeWidths; } } public Dictionary<Node, float> nodeWidths { get { return _nodeWidths; } }
private Dictionary<Node, float> _nodeWidths = new Dictionary<Node, float>(); private Dictionary<Node, float> _nodeWidths = new Dictionary<Node, float>();
public NodeGraph graph; public NodeGraph graph;
public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } } public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } }
private Vector2 _panOffset; private Vector2 _panOffset;
public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, 1f, 5f); Repaint(); } } public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, 1f, 5f); Repaint(); } }
private float _zoom = 1; private float _zoom = 1;
void OnFocus() { void OnFocus() {
AssetDatabase.SaveAssets();
current = this;
}
partial void OnEnable();
/// <summary> Create editor window </summary>
//[MenuItem("Window/UNEC")]
public static NodeEditorWindow Init() {
NodeEditorWindow w = CreateInstance<NodeEditorWindow>();
w.titleContent = new GUIContent("UNEC");
w.wantsMouseMove = true;
w.Show();
return w;
}
public void Save() {
if (AssetDatabase.Contains(graph)) {
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
} else SaveAs();
}
public void SaveAs() {
string path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", "");
if (string.IsNullOrEmpty(path)) return;
else {
NodeGraph existingGraph = AssetDatabase.LoadAssetAtPath<NodeGraph>(path);
if (existingGraph != null) AssetDatabase.DeleteAsset(path);
AssetDatabase.CreateAsset(graph, path);
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets(); AssetDatabase.SaveAssets();
current = this;
} }
}
private void DraggableWindow(int windowID) { partial void OnEnable();
GUI.DragWindow(); /// <summary> Create editor window </summary>
} //[MenuItem("Window/UNEC")]
public static NodeEditorWindow Init() {
public Vector2 WindowToGridPosition(Vector2 windowPosition) { NodeEditorWindow w = CreateInstance<NodeEditorWindow>();
return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom; w.titleContent = new GUIContent("UNEC");
} w.wantsMouseMove = true;
w.Show();
public Vector2 GridToWindowPosition(Vector2 gridPosition) { return w;
return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom); }
}
public void Save() {
public Rect GridToWindowRect(Rect gridRect) { if (AssetDatabase.Contains(graph)) {
gridRect.position = GridToWindowPositionNoClipped(gridRect.position); EditorUtility.SetDirty(graph);
return gridRect; AssetDatabase.SaveAssets();
} } else SaveAs();
}
public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) {
Vector2 center = position.size * 0.5f; public void SaveAs() {
float xOffset = (center.x * zoom + (panOffset.x + gridPosition.x)); string path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", "");
float yOffset = (center.y * zoom + (panOffset.y + gridPosition.y)); if (string.IsNullOrEmpty(path)) return;
return new Vector2(xOffset, yOffset); else {
} NodeGraph existingGraph = AssetDatabase.LoadAssetAtPath<NodeGraph>(path);
if (existingGraph != null) AssetDatabase.DeleteAsset(path);
public void SelectNode(Node node) { AssetDatabase.CreateAsset(graph, path);
selectedNode = node; EditorUtility.SetDirty(graph);
} AssetDatabase.SaveAssets();
}
[OnOpenAsset(0)] }
public static bool OnOpen(int instanceID, int line) {
NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as NodeGraph; private void DraggableWindow(int windowID) {
if (nodeGraph != null) { GUI.DragWindow();
NodeEditorWindow w = Init(); }
w.graph = nodeGraph;
return true; public Vector2 WindowToGridPosition(Vector2 windowPosition) {
return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom;
}
public Vector2 GridToWindowPosition(Vector2 gridPosition) {
return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom);
}
public Rect GridToWindowRect(Rect gridRect) {
gridRect.position = GridToWindowPositionNoClipped(gridRect.position);
return gridRect;
}
public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) {
Vector2 center = position.size * 0.5f;
float xOffset = (center.x * zoom + (panOffset.x + gridPosition.x));
float yOffset = (center.y * zoom + (panOffset.y + gridPosition.y));
return new Vector2(xOffset, yOffset);
}
public void SelectNode(Node node) {
selectedNode = node;
}
[OnOpenAsset(0)]
public static bool OnOpen(int instanceID, int line) {
NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as NodeGraph;
if (nodeGraph != null) {
NodeEditorWindow w = Init();
w.graph = nodeGraph;
return true;
}
return false;
} }
return false;
} }
} }

View File

@ -2,198 +2,200 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
/// <summary> Base class for all nodes </summary> namespace XNode {
[Serializable] /// <summary> Base class for all nodes </summary>
public abstract class Node : ScriptableObject { [Serializable]
public enum ShowBackingValue { public abstract class Node : ScriptableObject {
/// <summary> Never show the backing value </summary> public enum ShowBackingValue {
Never, /// <summary> Never show the backing value </summary>
/// <summary> Show the backing value only when the port does not have any active connections </summary> Never,
Unconnected, /// <summary> Show the backing value only when the port does not have any active connections </summary>
/// <summary> Always show the backing value </summary> Unconnected,
Always /// <summary> Always show the backing value </summary>
} Always
/// <summary> Iterate over all ports on this node. </summary>
public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values) yield return port; } }
/// <summary> Iterate over all outputs on this node. </summary>
public IEnumerable<NodePort> Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } }
/// <summary> Iterate over all inputs on this node. </summary>
public IEnumerable<NodePort> Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } }
/// <summary> Iterate over all instane ports on this node. </summary>
public IEnumerable<NodePort> InstancePorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } }
/// <summary> Iterate over all instance outputs on this node. </summary>
public IEnumerable<NodePort> InstanceOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } }
/// <summary> Iterate over all instance inputs on this node. </summary>
public IEnumerable<NodePort> InstanceInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } }
/// <summary> Parent <see cref="NodeGraph"/> </summary>
[SerializeField] public NodeGraph graph;
/// <summary> Position on the <see cref="NodeGraph"/> </summary>
[SerializeField] public Vector2 position;
/// <summary> Input <see cref="NodePort"/>s. It is recommended not to modify these at hand. Instead, see <see cref="InputAttribute"/> </summary>
[SerializeField] private NodePortDictionary ports = new NodePortDictionary();
protected void OnEnable() {
NodeDataCache.UpdatePorts(this, ports);
Init();
}
/// <summary> Initialize node. Called on creation. </summary>
protected virtual void Init() { name = GetType().Name; }
/// <summary> Checks all connections for invalid references, and removes them. </summary>
public void VerifyConnections() {
foreach (NodePort port in Ports) port.VerifyConnections();
}
#region Instance Ports
/// <summary> Returns input port at index </summary>
public NodePort AddInstanceInput(Type type, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Input, fieldName);
}
/// <summary> Returns input port at index </summary>
public NodePort AddInstanceOutput(Type type, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Output, fieldName);
}
private NodePort AddInstancePort(Type type, NodePort.IO direction, string fieldName = null) {
if (fieldName == null) {
fieldName = "instanceInput_0";
int i = 0;
while (HasPort(fieldName)) fieldName = "instanceInput_" + (++i);
} else if (HasPort(fieldName)) {
Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this);
return ports[fieldName];
} }
NodePort port = new NodePort(fieldName, type, direction, this);
ports.Add(fieldName, port);
return port;
}
public bool RemoveInstancePort(string fieldName) { /// <summary> Iterate over all ports on this node. </summary>
NodePort port = GetPort(fieldName); public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values) yield return port; } }
if (port == null || port.IsStatic) return false; /// <summary> Iterate over all outputs on this node. </summary>
port.ClearConnections(); public IEnumerable<NodePort> Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } }
ports.Remove(fieldName); /// <summary> Iterate over all inputs on this node. </summary>
return true; public IEnumerable<NodePort> Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } }
} /// <summary> Iterate over all instane ports on this node. </summary>
#endregion public IEnumerable<NodePort> InstancePorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } }
/// <summary> Iterate over all instance outputs on this node. </summary>
public IEnumerable<NodePort> InstanceOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } }
/// <summary> Iterate over all instance inputs on this node. </summary>
public IEnumerable<NodePort> InstanceInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } }
/// <summary> Parent <see cref="NodeGraph"/> </summary>
[SerializeField] public NodeGraph graph;
/// <summary> Position on the <see cref="NodeGraph"/> </summary>
[SerializeField] public Vector2 position;
/// <summary> Input <see cref="NodePort"/>s. It is recommended not to modify these at hand. Instead, see <see cref="InputAttribute"/> </summary>
[SerializeField] private NodePortDictionary ports = new NodePortDictionary();
#region Ports protected void OnEnable() {
/// <summary> Returns output port which matches fieldName </summary> NodeDataCache.UpdatePorts(this, ports);
public NodePort GetOutputPort(string fieldName) { Init();
NodePort port = GetPort(fieldName); }
if (port == null || port.direction != NodePort.IO.Output) return null;
else return port;
}
/// <summary> Returns input port which matches fieldName </summary> /// <summary> Initialize node. Called on creation. </summary>
public NodePort GetInputPort(string fieldName) { protected virtual void Init() { name = GetType().Name; }
NodePort port = GetPort(fieldName);
if (port == null || port.direction != NodePort.IO.Input) return null;
else return port;
}
/// <summary> Returns port which matches fieldName </summary> /// <summary> Checks all connections for invalid references, and removes them. </summary>
public NodePort GetPort(string fieldName) { public void VerifyConnections() {
if (ports.ContainsKey(fieldName)) return ports[fieldName]; foreach (NodePort port in Ports) port.VerifyConnections();
else return null; }
}
public bool HasPort(string fieldName) { #region Instance Ports
return ports.ContainsKey(fieldName); /// <summary> Returns input port at index </summary>
} public NodePort AddInstanceInput(Type type, string fieldName = null) {
#endregion return AddInstancePort(type, NodePort.IO.Input, fieldName);
}
#region Inputs/Outputs /// <summary> Returns input port at index </summary>
/// <summary> Return input value for a specified port. Returns fallback value if no ports are connected </summary> public NodePort AddInstanceOutput(Type type, string fieldName = null) {
/// <param name="fieldName">Field name of requested input port</param> return AddInstancePort(type, NodePort.IO.Output, fieldName);
/// <param name="fallback">If no ports are connected, this value will be returned</param> }
public T GetInputValue<T>(string fieldName, T fallback = default(T)) {
NodePort port = GetPort(fieldName);
if (port != null && port.IsConnected) return port.GetInputValue<T>();
else return fallback;
}
/// <summary> Return all input values for a specified port. Returns fallback value if no ports are connected </summary> private NodePort AddInstancePort(Type type, NodePort.IO direction, string fieldName = null) {
/// <param name="fieldName">Field name of requested input port</param> if (fieldName == null) {
/// <param name="fallback">If no ports are connected, this value will be returned</param> fieldName = "instanceInput_0";
public T[] GetInputValues<T>(string fieldName, params T[] fallback) { int i = 0;
NodePort port = GetPort(fieldName); while (HasPort(fieldName)) fieldName = "instanceInput_" + (++i);
if (port != null && port.IsConnected) return port.GetInputValues<T>(); } else if (HasPort(fieldName)) {
else return fallback; Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this);
} return ports[fieldName];
}
NodePort port = new NodePort(fieldName, type, direction, this);
ports.Add(fieldName, port);
return port;
}
/// <summary> Returns a value based on requested port output. Should be overridden before used. </summary> public bool RemoveInstancePort(string fieldName) {
/// <param name="port">The requested port.</param> NodePort port = GetPort(fieldName);
public virtual object GetValue(NodePort port) { if (port == null || port.IsStatic) return false;
Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType()); port.ClearConnections();
return null; ports.Remove(fieldName);
} return true;
#endregion }
#endregion
/// <summary> Called whenever a connection is being made between two <see cref="NodePort"/>s</summary> #region Ports
/// <param name="from">Output</param> <param name="to">Input</param> /// <summary> Returns output port which matches fieldName </summary>
public virtual void OnCreateConnection(NodePort from, NodePort to) { } public NodePort GetOutputPort(string fieldName) {
NodePort port = GetPort(fieldName);
if (port == null || port.direction != NodePort.IO.Output) return null;
else return port;
}
/// <summary> Disconnect everything from this node </summary> /// <summary> Returns input port which matches fieldName </summary>
public void ClearConnections() { public NodePort GetInputPort(string fieldName) {
foreach (NodePort port in Ports) port.ClearConnections(); NodePort port = GetPort(fieldName);
} if (port == null || port.direction != NodePort.IO.Input) return null;
else return port;
}
public override int GetHashCode() { /// <summary> Returns port which matches fieldName </summary>
return JsonUtility.ToJson(this).GetHashCode(); public NodePort GetPort(string fieldName) {
} if (ports.ContainsKey(fieldName)) return ports[fieldName];
else return null;
}
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInput(string)"/> </summary> public bool HasPort(string fieldName) {
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] return ports.ContainsKey(fieldName);
public class InputAttribute : Attribute { }
public ShowBackingValue backingValue; #endregion
#region Inputs/Outputs
/// <summary> Return input value for a specified port. Returns fallback value if no ports are connected </summary>
/// <param name="fieldName">Field name of requested input port</param>
/// <param name="fallback">If no ports are connected, this value will be returned</param>
public T GetInputValue<T>(string fieldName, T fallback = default(T)) {
NodePort port = GetPort(fieldName);
if (port != null && port.IsConnected) return port.GetInputValue<T>();
else return fallback;
}
/// <summary> Return all input values for a specified port. Returns fallback value if no ports are connected </summary>
/// <param name="fieldName">Field name of requested input port</param>
/// <param name="fallback">If no ports are connected, this value will be returned</param>
public T[] GetInputValues<T>(string fieldName, params T[] fallback) {
NodePort port = GetPort(fieldName);
if (port != null && port.IsConnected) return port.GetInputValues<T>();
else return fallback;
}
/// <summary> Returns a value based on requested port output. Should be overridden before used. </summary>
/// <param name="port">The requested port.</param>
public virtual object GetValue(NodePort port) {
Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType());
return null;
}
#endregion
/// <summary> Called whenever a connection is being made between two <see cref="NodePort"/>s</summary>
/// <param name="from">Output</param> <param name="to">Input</param>
public virtual void OnCreateConnection(NodePort from, NodePort to) { }
/// <summary> Disconnect everything from this node </summary>
public void ClearConnections() {
foreach (NodePort port in Ports) port.ClearConnections();
}
public override int GetHashCode() {
return JsonUtility.ToJson(this).GetHashCode();
}
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInput(string)"/> </summary> /// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInput(string)"/> </summary>
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param> [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected) { this.backingValue = backingValue; } public class InputAttribute : Attribute {
} public ShowBackingValue backingValue;
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutput(string)"/> </summary> /// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInput(string)"/> </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] /// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
public class OutputAttribute : Attribute { public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected) { this.backingValue = backingValue; }
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutput(string)"/> </summary>
public OutputAttribute() { }
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CreateNodeMenuAttribute : Attribute {
public string menuName;
/// <summary> Manually supply node class with a context menu path </summary>
/// <param name="menuName"> Path to this node in the context menu </param>
public CreateNodeMenuAttribute(string menuName) {
this.menuName = menuName;
} }
}
[Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver { /// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutput(string)"/> </summary>
[SerializeField] private List<string> keys = new List<string>(); [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
[SerializeField] private List<NodePort> values = new List<NodePort>(); public class OutputAttribute : Attribute {
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutput(string)"/> </summary>
public OutputAttribute() { }
}
public void OnBeforeSerialize() { [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
keys.Clear(); public class CreateNodeMenuAttribute : Attribute {
values.Clear(); public string menuName;
foreach (KeyValuePair<string, NodePort> pair in this) { /// <summary> Manually supply node class with a context menu path </summary>
keys.Add(pair.Key); /// <param name="menuName"> Path to this node in the context menu </param>
values.Add(pair.Value); public CreateNodeMenuAttribute(string menuName) {
this.menuName = menuName;
} }
} }
public void OnAfterDeserialize() { [Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver {
this.Clear(); [SerializeField] private List<string> keys = new List<string>();
[SerializeField] private List<NodePort> values = new List<NodePort>();
if (keys.Count != values.Count) public void OnBeforeSerialize() {
throw new System.Exception(string.Format("there are {0} keys and {1} values after deserialization. Make sure that both key and value types are serializable.")); keys.Clear();
values.Clear();
foreach (KeyValuePair<string, NodePort> pair in this) {
keys.Add(pair.Key);
values.Add(pair.Value);
}
}
for (int i = 0; i < keys.Count; i++) public void OnAfterDeserialize() {
this.Add(keys[i], values[i]); this.Clear();
if (keys.Count != values.Count)
throw new System.Exception(string.Format("there are {0} keys and {1} values after deserialization. Make sure that both key and value types are serializable."));
for (int i = 0; i < keys.Count; i++)
this.Add(keys[i], values[i]);
}
} }
} }
} }

View File

@ -1,100 +1,100 @@
using System.Collections; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using UnityEditor;
using UnityEngine; using UnityEngine;
/// <summary> Precaches reflection data in editor so we won't have to do it runtime </summary> namespace XNode {
public static class NodeDataCache { /// <summary> Precaches reflection data in editor so we won't have to do it runtime </summary>
private static PortDataCache portDataCache; public static class NodeDataCache {
private static bool Initialized { get { return portDataCache != null; } } private static PortDataCache portDataCache;
private static bool Initialized { get { return portDataCache != null; } }
/// <summary> Update static ports to reflect class fields. </summary> /// <summary> Update static ports 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();
Dictionary<string, NodePort> staticPorts = new Dictionary<string, NodePort>(); Dictionary<string, NodePort> staticPorts = new Dictionary<string, NodePort>();
System.Type nodeType = node.GetType(); System.Type nodeType = node.GetType();
if (!portDataCache.ContainsKey(nodeType)) return; if (!portDataCache.ContainsKey(nodeType)) return;
for (int i = 0; i < portDataCache[nodeType].Count; i++) { for (int i = 0; i < portDataCache[nodeType].Count; i++) {
staticPorts.Add(portDataCache[nodeType][i].fieldName, portDataCache[nodeType][i]); staticPorts.Add(portDataCache[nodeType][i].fieldName, portDataCache[nodeType][i]);
}
// Cleanup port dict - Remove nonexisting static ports - update static port types
foreach (NodePort port in ports.Values.ToList()) {
if (staticPorts.ContainsKey(port.fieldName)) {
NodePort staticPort = staticPorts[port.fieldName];
if (port.IsDynamic || port.direction != staticPort.direction) ports.Remove(port.fieldName);
else port.ValueType = staticPort.ValueType;
} else if (port.IsStatic) ports.Remove(port.fieldName);
}
// Add missing ports
foreach (NodePort staticPort in staticPorts.Values) {
if (!ports.ContainsKey(staticPort.fieldName)) {
ports.Add(staticPort.fieldName, new NodePort(staticPort, node));
} }
}
}
private static void BuildCache() { // Cleanup port dict - Remove nonexisting static ports - update static port types
portDataCache = new PortDataCache(); foreach (NodePort port in ports.Values.ToList()) {
System.Type baseType = typeof(Node); if (staticPorts.ContainsKey(port.fieldName)) {
Assembly assembly = Assembly.GetAssembly(baseType); NodePort staticPort = staticPorts[port.fieldName];
System.Type[] nodeTypes = assembly.GetTypes().Where(t => if (port.IsDynamic || port.direction != staticPort.direction) ports.Remove(port.fieldName);
!t.IsAbstract && else port.ValueType = staticPort.ValueType;
baseType.IsAssignableFrom(t) } else if (port.IsStatic) ports.Remove(port.fieldName);
).ToArray();
for (int i = 0; i < nodeTypes.Length; i++) {
CachePorts(nodeTypes[i]);
}
}
private static void CachePorts(System.Type nodeType) {
System.Reflection.FieldInfo[] fieldInfo = nodeType.GetFields();
for (int i = 0; i < fieldInfo.Length; i++) {
//Get InputAttribute and OutputAttribute
object[] attribs = fieldInfo[i].GetCustomAttributes(false);
Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute;
Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute;
if (inputAttrib == null && outputAttrib == null) continue;
if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo + " cannot be both input and output.");
else {
if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new List<NodePort>());
portDataCache[nodeType].Add(new NodePort(fieldInfo[i]));
} }
} // Add missing ports
} foreach (NodePort staticPort in staticPorts.Values) {
if (!ports.ContainsKey(staticPort.fieldName)) {
[System.Serializable] ports.Add(staticPort.fieldName, new NodePort(staticPort, node));
private class PortDataCache : Dictionary<System.Type, List<NodePort>>, ISerializationCallbackReceiver { }
[SerializeField] private List<System.Type> keys = new List<System.Type>();
[SerializeField] private List<List<NodePort>> values = new List<List<NodePort>>();
// save the dictionary to lists
public void OnBeforeSerialize() {
keys.Clear();
values.Clear();
foreach (var pair in this) {
keys.Add(pair.Key);
values.Add(pair.Value);
} }
} }
// load dictionary from lists private static void BuildCache() {
public void OnAfterDeserialize() { portDataCache = new PortDataCache();
this.Clear(); System.Type baseType = typeof(Node);
Assembly assembly = Assembly.GetAssembly(baseType);
System.Type[] nodeTypes = assembly.GetTypes().Where(t =>
!t.IsAbstract &&
baseType.IsAssignableFrom(t)
).ToArray();
if (keys.Count != values.Count) for (int i = 0; i < nodeTypes.Length; i++) {
throw new System.Exception(string.Format("there are {0} keys and {1} values after deserialization. Make sure that both key and value types are serializable.")); CachePorts(nodeTypes[i]);
}
}
for (int i = 0; i < keys.Count; i++) private static void CachePorts(System.Type nodeType) {
this.Add(keys[i], values[i]); System.Reflection.FieldInfo[] fieldInfo = nodeType.GetFields();
for (int i = 0; i < fieldInfo.Length; i++) {
//Get InputAttribute and OutputAttribute
object[] attribs = fieldInfo[i].GetCustomAttributes(false);
Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute;
Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute;
if (inputAttrib == null && outputAttrib == null) continue;
if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo + " cannot be both input and output.");
else {
if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new List<NodePort>());
portDataCache[nodeType].Add(new NodePort(fieldInfo[i]));
}
}
}
[System.Serializable]
private class PortDataCache : Dictionary<System.Type, List<NodePort>>, ISerializationCallbackReceiver {
[SerializeField] private List<System.Type> keys = new List<System.Type>();
[SerializeField] private List<List<NodePort>> values = new List<List<NodePort>>();
// save the dictionary to lists
public void OnBeforeSerialize() {
keys.Clear();
values.Clear();
foreach (var pair in this) {
keys.Add(pair.Key);
values.Add(pair.Value);
}
}
// load dictionary from lists
public void OnAfterDeserialize() {
this.Clear();
if (keys.Count != values.Count)
throw new System.Exception(string.Format("there are {0} keys and {1} values after deserialization. Make sure that both key and value types are serializable."));
for (int i = 0; i < keys.Count; i++)
this.Add(keys[i], values[i]);
}
} }
} }
} }

View File

@ -2,46 +2,48 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
/// <summary> Base class for all node graphs </summary> namespace XNode {
[Serializable] /// <summary> Base class for all node graphs </summary>
public abstract class NodeGraph : ScriptableObject { [Serializable]
public abstract class NodeGraph : ScriptableObject {
/// <summary> All nodes in the graph. <para/> /// <summary> All nodes in the graph. <para/>
/// See: <see cref="AddNode{T}"/> </summary> /// See: <see cref="AddNode{T}"/> </summary>
[SerializeField] public List<Node> nodes = new List<Node>(); [SerializeField] public List<Node> nodes = new List<Node>();
public T AddNode<T>() where T : Node { public T AddNode<T>() where T : Node {
return AddNode(typeof(T)) as T; return AddNode(typeof(T)) as T;
}
public virtual Node AddNode(Type type) {
Node node = ScriptableObject.CreateInstance(type) as Node;
#if UNITY_EDITOR
if (!Application.isPlaying) {
UnityEditor.AssetDatabase.AddObjectToAsset(node, this);
UnityEditor.AssetDatabase.SaveAssets();
} }
#endif
nodes.Add(node);
node.graph = this;
return node;
}
/// <summary> Safely remove a node and all its connections </summary> public virtual Node AddNode(Type type) {
/// <param name="node"></param> Node node = ScriptableObject.CreateInstance(type) as Node;
public void RemoveNode(Node node) {
node.ClearConnections();
#if UNITY_EDITOR #if UNITY_EDITOR
if (!Application.isPlaying) { if (!Application.isPlaying) {
DestroyImmediate(node, true); UnityEditor.AssetDatabase.AddObjectToAsset(node, this);
UnityEditor.AssetDatabase.SaveAssets(); UnityEditor.AssetDatabase.SaveAssets();
} }
#endif #endif
nodes.Remove(node); nodes.Add(node);
} node.graph = this;
return node;
}
/// <summary> Remove all nodes and connections from the graph </summary> /// <summary> Safely remove a node and all its connections </summary>
public void Clear() { /// <param name="node"></param>
nodes.Clear(); public void RemoveNode(Node node) {
node.ClearConnections();
#if UNITY_EDITOR
if (!Application.isPlaying) {
DestroyImmediate(node, true);
UnityEditor.AssetDatabase.SaveAssets();
}
#endif
nodes.Remove(node);
}
/// <summary> Remove all nodes and connections from the graph </summary>
public void Clear() {
nodes.Clear();
}
} }
} }

View File

@ -1,246 +1,247 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using UnityEngine; using UnityEngine;
[Serializable] namespace XNode {
public class NodePort { [Serializable]
public enum IO { Input, Output } public class NodePort {
public enum IO { Input, Output }
public int ConnectionCount { get { return connections.Count; } } public int ConnectionCount { get { return connections.Count; } }
/// <summary> Return the first connection </summary> /// <summary> Return the first connection </summary>
public NodePort Connection { get { return connections.Count > 0 ? connections[0].Port : null; } } public NodePort Connection { get { return connections.Count > 0 ? connections[0].Port : null; } }
public IO direction { get { return _direction; } } public IO direction { get { return _direction; } }
/// <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; } }
public bool IsInput { get { return direction == IO.Input; } } public bool IsInput { get { return direction == IO.Input; } }
public bool IsOutput { get { return direction == IO.Output; } } public bool IsOutput { get { return direction == IO.Output; } }
public string fieldName { get { return _fieldName; } } public string fieldName { get { return _fieldName; } }
public Node node { get { return _node; } } public Node node { get { return _node; } }
public bool IsDynamic { get { return _dynamic; } } public bool IsDynamic { get { return _dynamic; } }
public bool IsStatic { get { return !_dynamic; } } public bool IsStatic { get { return !_dynamic; } }
public Type ValueType { public Type ValueType {
get { get {
if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) valueType = Type.GetType(_typeQualifiedName, false); if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) valueType = Type.GetType(_typeQualifiedName, false);
return valueType; return valueType;
} }
set { set {
valueType = value; valueType = value;
if (value != null) _typeQualifiedName = value.AssemblyQualifiedName; if (value != null) _typeQualifiedName = value.AssemblyQualifiedName;
}
}
private Type valueType;
[SerializeField] private string _fieldName;
[SerializeField] private Node _node;
[SerializeField] private string _typeQualifiedName;
[SerializeField] private List<PortConnection> connections = new List<PortConnection>();
[SerializeField] private IO _direction;
[SerializeField] private bool _dynamic;
/// <summary> Construct a static targetless nodeport. Used as a template. </summary>
public NodePort(FieldInfo fieldInfo) {
_fieldName = fieldInfo.Name;
ValueType = fieldInfo.FieldType;
_dynamic = false;
var attribs = fieldInfo.GetCustomAttributes(false);
for (int i = 0; i < attribs.Length; i++) {
if (attribs[i] is Node.InputAttribute) _direction = IO.Input;
else if (attribs[i] is Node.OutputAttribute) _direction = IO.Output;
}
}
/// <summary> Copy a nodePort but assign it to another node. </summary>
public NodePort(NodePort nodePort, Node node) {
_fieldName = nodePort._fieldName;
ValueType = nodePort.valueType;
_direction = nodePort.direction;
_dynamic = nodePort._dynamic;
_node = node;
}
/// <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 node) {
_fieldName = fieldName;
this.ValueType = type;
_direction = direction;
_node = node;
_dynamic = true;
}
/// <summary> Checks all connections for invalid references, and removes them. </summary>
public void VerifyConnections() {
for (int i = connections.Count - 1; i >= 0; i--) {
if (connections[i].node != null &&
!string.IsNullOrEmpty(connections[i].fieldName) &&
connections[i].node.GetPort(connections[i].fieldName) != null)
continue;
connections.RemoveAt(i);
}
}
/// <summary> Return the output value of this node through its parent nodes GetValue override method. </summary>
/// <returns> <see cref="Node.GetValue(NodePort)"/> </returns>
public object GetOutputValue() {
if (direction == IO.Input) return null;
return node.GetValue(this);
}
/// <summary> Return the output value of the first connected port. Returns null if none found or invalid.</summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public object GetInputValue() {
NodePort connectedPort = Connection;
if (connectedPort == null) return null;
return connectedPort.GetOutputValue();
}
/// <summary> Return the output values of all connected ports. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public object[] GetInputValues() {
object[] objs = new object[ConnectionCount];
for (int i = 0; i < ConnectionCount; i++) {
NodePort connectedPort = connections[i].Port;
if (connectedPort == null) { // if we happen to find a null port, remove it and look again
connections.RemoveAt(i);
i--;
continue;
} }
objs[i] = connectedPort.GetOutputValue();
} }
return objs; private Type valueType;
}
/// <summary> Return the output value of the first connected port. Returns null if none found or invalid. </summary> [SerializeField] private string _fieldName;
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns> [SerializeField] private Node _node;
public T GetInputValue<T>() { [SerializeField] private string _typeQualifiedName;
object obj = GetInputValue(); [SerializeField] private List<PortConnection> connections = new List<PortConnection>();
return obj is T ? (T) obj : default(T); [SerializeField] private IO _direction;
} [SerializeField] private bool _dynamic;
/// <summary> Return the output values of all connected ports. </summary> /// <summary> Construct a static targetless nodeport. Used as a template. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns> public NodePort(FieldInfo fieldInfo) {
public T[] GetInputValues<T>() { _fieldName = fieldInfo.Name;
object[] objs = GetInputValues(); ValueType = fieldInfo.FieldType;
T[] ts = new T[objs.Length]; _dynamic = false;
for (int i = 0; i < objs.Length; i++) { var attribs = fieldInfo.GetCustomAttributes(false);
if (objs[i] is T) ts[i] = (T) objs[i]; for (int i = 0; i < attribs.Length; i++) {
if (attribs[i] is Node.InputAttribute) _direction = IO.Input;
else if (attribs[i] is Node.OutputAttribute) _direction = IO.Output;
}
} }
return ts;
}
/// <summary> Return true if port is connected and has a valid input. </summary> /// <summary> Copy a nodePort but assign it to another node. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns> public NodePort(NodePort nodePort, Node node) {
public bool TryGetInputValue<T>(out T value) { _fieldName = nodePort._fieldName;
object obj = GetInputValue(); ValueType = nodePort.valueType;
if (obj is T) { _direction = nodePort.direction;
value = (T) obj; _dynamic = nodePort._dynamic;
return true; _node = node;
} else { }
value = default(T);
/// <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 node) {
_fieldName = fieldName;
this.ValueType = type;
_direction = direction;
_node = node;
_dynamic = true;
}
/// <summary> Checks all connections for invalid references, and removes them. </summary>
public void VerifyConnections() {
for (int i = connections.Count - 1; i >= 0; i--) {
if (connections[i].node != null &&
!string.IsNullOrEmpty(connections[i].fieldName) &&
connections[i].node.GetPort(connections[i].fieldName) != null)
continue;
connections.RemoveAt(i);
}
}
/// <summary> Return the output value of this node through its parent nodes GetValue override method. </summary>
/// <returns> <see cref="Node.GetValue(NodePort)"/> </returns>
public object GetOutputValue() {
if (direction == IO.Input) return null;
return node.GetValue(this);
}
/// <summary> Return the output value of the first connected port. Returns null if none found or invalid.</summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public object GetInputValue() {
NodePort connectedPort = Connection;
if (connectedPort == null) return null;
return connectedPort.GetOutputValue();
}
/// <summary> Return the output values of all connected ports. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public object[] GetInputValues() {
object[] objs = new object[ConnectionCount];
for (int i = 0; i < ConnectionCount; i++) {
NodePort connectedPort = connections[i].Port;
if (connectedPort == null) { // if we happen to find a null port, remove it and look again
connections.RemoveAt(i);
i--;
continue;
}
objs[i] = connectedPort.GetOutputValue();
}
return objs;
}
/// <summary> Return the output value of the first connected port. Returns null if none found or invalid. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public T GetInputValue<T>() {
object obj = GetInputValue();
return obj is T ? (T) obj : default(T);
}
/// <summary> Return the output values of all connected ports. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public T[] GetInputValues<T>() {
object[] objs = GetInputValues();
T[] ts = new T[objs.Length];
for (int i = 0; i < objs.Length; i++) {
if (objs[i] is T) ts[i] = (T) objs[i];
}
return ts;
}
/// <summary> Return true if port is connected and has a valid input. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public bool TryGetInputValue<T>(out T value) {
object obj = GetInputValue();
if (obj is T) {
value = (T) obj;
return true;
} else {
value = default(T);
return false;
}
}
/// <summary> Return the sum of all inputs. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public float GetInputSum(float fallback) {
object[] objs = GetInputValues();
if (objs.Length == 0) return fallback;
float result = 0;
for (int i = 0; i < objs.Length; i++) {
if (objs[i] is float) result += (float) objs[i];
}
return result;
}
/// <summary> Return the sum of all inputs. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public int GetInputSum(int fallback) {
object[] objs = GetInputValues();
if (objs.Length == 0) return fallback;
int result = 0;
for (int i = 0; i < objs.Length; i++) {
if (objs[i] is int) result += (int) objs[i];
}
return result;
}
/// <summary> Connect this <see cref="NodePort"/> to another </summary>
/// <param name="port">The <see cref="NodePort"/> to connect to</param>
public void Connect(NodePort port) {
if (connections == null) connections = new List<PortConnection>();
if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; }
if (port == this) { Debug.LogWarning("Attempting to connect port to self."); return; }
if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; }
if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; }
connections.Add(new PortConnection(port));
if (port.connections == null) port.connections = new List<PortConnection>();
if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this));
node.OnCreateConnection(this, port);
port.node.OnCreateConnection(this, port);
}
public NodePort GetConnection(int i) {
//If the connection is broken for some reason, remove it.
if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) {
connections.RemoveAt(i);
return null;
}
NodePort port = connections[i].node.GetPort(connections[i].fieldName);
if (port == null) {
connections.RemoveAt(i);
return null;
}
return port;
}
public bool IsConnectedTo(NodePort port) {
for (int i = 0; i < connections.Count; i++) {
if (connections[i].Port == port) return true;
}
return false; return false;
} }
}
/// <summary> Return the sum of all inputs. </summary> public void Disconnect(NodePort port) {
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns> for (int i = connections.Count - 1; i >= 0; i--) {
public float GetInputSum(float fallback) { //Remove matching ports.
object[] objs = GetInputValues(); if (connections[i].Port == port) {
if (objs.Length == 0) return fallback; connections.RemoveAt(i);
float result = 0; }
for (int i = 0; i < objs.Length; i++) { }
if (objs[i] is float) result += (float) objs[i]; for (int i = 0; i < port.connections.Count; i++) {
} if (port.connections[i].Port == this) {
return result; port.connections.RemoveAt(i);
} }
/// <summary> Return the sum of all inputs. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public int GetInputSum(int fallback) {
object[] objs = GetInputValues();
if (objs.Length == 0) return fallback;
int result = 0;
for (int i = 0; i < objs.Length; i++) {
if (objs[i] is int) result += (int) objs[i];
}
return result;
}
/// <summary> Connect this <see cref="NodePort"/> to another </summary>
/// <param name="port">The <see cref="NodePort"/> to connect to</param>
public void Connect(NodePort port) {
if (connections == null) connections = new List<PortConnection>();
if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; }
if (port == this) { Debug.LogWarning("Attempting to connect port to self."); return; }
if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; }
if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; }
connections.Add(new PortConnection(port));
if (port.connections == null) port.connections = new List<PortConnection>();
if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this));
node.OnCreateConnection(this, port);
port.node.OnCreateConnection(this, port);
}
public NodePort GetConnection(int i) {
//If the connection is broken for some reason, remove it.
if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) {
connections.RemoveAt(i);
return null;
}
NodePort port = connections[i].node.GetPort(connections[i].fieldName);
if (port == null) {
connections.RemoveAt(i);
return null;
}
return port;
}
public bool IsConnectedTo(NodePort port) {
for (int i = 0; i < connections.Count; i++) {
if (connections[i].Port == port) return true;
}
return false;
}
public void Disconnect(NodePort port) {
for (int i = connections.Count - 1; i >= 0; i--) {
//Remove matching ports.
if (connections[i].Port == port) {
connections.RemoveAt(i);
} }
} }
for (int i = 0; i < port.connections.Count; i++) {
if (port.connections[i].Port == this) { public void ClearConnections() {
port.connections.RemoveAt(i); while (connections.Count > 0) {
Disconnect(connections[0].Port);
} }
} }
}
public void ClearConnections() { [Serializable]
while (connections.Count > 0) { private class PortConnection {
Disconnect(connections[0].Port); [SerializeField] public string fieldName;
} [SerializeField] public Node node;
} public NodePort Port { get { return port != null ? port : port = GetPort(); } }
[NonSerialized] private NodePort port;
[Serializable] public PortConnection(NodePort port) {
private class PortConnection { this.port = port;
[SerializeField] public string fieldName; node = port.node;
[SerializeField] public Node node; fieldName = port.fieldName;
public NodePort Port { get { return port != null ? port : port = GetPort(); } } }
[NonSerialized] private NodePort port;
public PortConnection(NodePort port) { /// <summary> Returns the port that this <see cref="PortConnection"/> points to </summary>
this.port = port; private NodePort GetPort() {
node = port.node; if (node == null || string.IsNullOrEmpty(fieldName)) return null;
fieldName = port.fieldName; return node.GetPort(fieldName);
} }
/// <summary> Returns the port that this <see cref="PortConnection"/> points to </summary>
private NodePort GetPort() {
if (node == null || string.IsNullOrEmpty(fieldName)) return null;
return node.GetPort(fieldName);
} }
} }
} }