1
0
mirror of https://github.com/Siccity/xNode.git synced 2026-02-04 14:24:54 +08:00

Merge branch 'master' into delegate-binding

This commit is contained in:
Thor Brigsted 2019-04-14 03:40:40 +02:00
commit a7c15d4814
25 changed files with 959 additions and 281 deletions

4
.gitignore vendored
View File

@ -24,3 +24,7 @@ sysinfo.txt
README.md.meta README.md.meta
LICENSE.md.meta LICENSE.md.meta
CONTRIBUTING.md.meta CONTRIBUTING.md.meta
.git.meta
.gitignore.meta
.gitattributes.meta

View File

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

View File

@ -1,4 +1,4 @@
![alt text](https://user-images.githubusercontent.com/37786733/41541140-71602302-731a-11e8-9434-79b3a57292b6.png) <img align="right" width="100" height="100" src="https://user-images.githubusercontent.com/37786733/41541140-71602302-731a-11e8-9434-79b3a57292b6.png">
[![Discord](https://img.shields.io/discord/361769369404964864.svg)](https://discord.gg/qgPrHv4) [![Discord](https://img.shields.io/discord/361769369404964864.svg)](https://discord.gg/qgPrHv4)
[![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues) [![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues)
@ -15,7 +15,9 @@ Thinking of developing a node-based plugin? Then this is for you. You can downlo
xNode is super userfriendly, intuitive and will help you reap the benefits of node graphs in no time. xNode is super userfriendly, intuitive and will help you reap the benefits of node graphs in no time.
With a minimal footprint, it is ideal as a base for custom state machines, dialogue systems, decision makers etc. With a minimal footprint, it is ideal as a base for custom state machines, dialogue systems, decision makers etc.
![editor](https://user-images.githubusercontent.com/6402525/33150712-01d60602-cfd5-11e7-83b4-eb008fd9d711.png) <p align="center">
<img src="https://user-images.githubusercontent.com/6402525/53689100-3821e680-3d4e-11e9-8440-e68bd802bfd9.png">
</p>
### Key features ### Key features
* Lightweight in runtime * Lightweight in runtime
@ -24,6 +26,7 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
* No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.) * No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.)
* Does not rely on any 3rd party plugins * Does not rely on any 3rd party plugins
* Custom node inspector code is very similar to regular custom inspector code * Custom node inspector code is very similar to regular custom inspector code
* Supported from Unity 5.3 and up
### Wiki ### Wiki
* [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph * [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph
@ -31,7 +34,7 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
### Node example: ### Node example:
```csharp ```csharp
[System.Serializable] // public classes deriving from Node are registered as nodes for use within a graph
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
[Input] public float a; [Input] public float a;
@ -71,3 +74,4 @@ Feel free to also leave suggestions/requests in the [issues](https://github.com/
Projects using xNode: Projects using xNode:
* [Graphmesh](https://github.com/Siccity/Graphmesh "Go to github page") * [Graphmesh](https://github.com/Siccity/Graphmesh "Go to github page")
* [Dialogue](https://github.com/Siccity/Dialogue "Go to github page") * [Dialogue](https://github.com/Siccity/Dialogue "Go to github page")
* [qAI](https://github.com/jlreymendez/qAI "Go to github page")

10
Scripts/Attributes.meta Normal file
View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 5644dfc7eed151045af664a9d4fd1906
folderAsset: yes
timeCreated: 1541633926
licenseType: Free
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,5 @@
using UnityEngine;
/// <summary> Draw enums correctly within nodes. Without it, enums show up at the wrong positions. </summary>
/// <remarks> Enums with this attribute are not detected by EditorGui.ChangeCheck due to waiting before executing </remarks>
public class NodeEnumAttribute : PropertyAttribute { }

View File

@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 10a8338f6c985854697b35459181af0a
timeCreated: 1541633942
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 7adf21edfb51f514fa991d7556ecd0ef
folderAsset: yes
timeCreated: 1541971984
licenseType: Free
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,66 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using XNode;
using XNodeEditor;
namespace XNodeEditor {
[CustomPropertyDrawer(typeof(NodeEnumAttribute))]
public class NodeEnumDrawer : PropertyDrawer {
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
EditorGUI.BeginProperty(position, label, property);
// Throw error on wrong type
if (property.propertyType != SerializedPropertyType.Enum) {
throw new ArgumentException("Parameter selected must be of type System.Enum");
}
// Add label
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
// Get current enum name
string enumName = "";
if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length) enumName = property.enumDisplayNames[property.enumValueIndex];
#if UNITY_2017_1_OR_NEWER
// Display dropdown
if (EditorGUI.DropdownButton(position, new GUIContent(enumName), FocusType.Passive)) {
// Position is all wrong if we show the dropdown during the node draw phase.
// Instead, add it to onLateGUI to display it later.
NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
}
#else
// Display dropdown
if (GUI.Button(position, new GUIContent(enumName), "MiniPopup")) {
// Position is all wrong if we show the dropdown during the node draw phase.
// Instead, add it to onLateGUI to display it later.
NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
}
#endif
EditorGUI.EndProperty();
}
private void ShowContextMenuAtMouse(SerializedProperty property) {
// Initialize menu
GenericMenu menu = new GenericMenu();
// Add all enum display names to menu
for (int i = 0; i < property.enumDisplayNames.Length; i++) {
int index = i;
menu.AddItem(new GUIContent(property.enumDisplayNames[i]), false, () => SetEnum(property, index));
}
// Display at cursor position
Rect r = new Rect(Event.current.mousePosition, new Vector2(0, 0));
menu.DropDown(r);
}
private void SetEnum(SerializedProperty property, int index) {
property.enumValueIndex = index;
property.serializedObject.ApplyModifiedProperties();
property.serializedObject.Update();
}
}
}

View File

@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 83db81f92abadca439507e25d517cabe
timeCreated: 1541633798
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -13,32 +13,9 @@ namespace XNodeEditor {
/// <summary> Fires every whenever a node was modified through the editor </summary> /// <summary> Fires every whenever a node was modified through the editor </summary>
public static Action<XNode.Node> onUpdateNode; public static Action<XNode.Node> onUpdateNode;
public static Dictionary<XNode.NodePort, Vector2> portPositions; public static Dictionary<XNode.NodePort, Vector2> portPositions;
public static int renaming;
/// <summary> Draws the node GUI.</summary>
/// <param name="portPositions">Port handle positions need to be returned to the NodeEditorWindow </param>
public void OnNodeGUI() {
OnHeaderGUI();
OnBodyGUI();
}
public virtual void OnHeaderGUI() { public virtual void OnHeaderGUI() {
string title = target.name; GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
if (renaming != 0 && Selection.Contains(target)) {
int controlID = EditorGUIUtility.GetControlID(FocusType.Keyboard) + 1;
if (renaming == 1) {
EditorGUIUtility.keyboardControl = controlID;
EditorGUIUtility.editingTextField = true;
renaming = 2;
}
target.name = EditorGUILayout.TextField(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
if (!EditorGUIUtility.editingTextField) {
Rename(target.name);
renaming = 0;
}
} else {
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>
@ -50,6 +27,7 @@ namespace XNodeEditor {
string[] excludes = { "m_Script", "graph", "position", "ports" }; string[] excludes = { "m_Script", "graph", "position", "ports" };
portPositions = new Dictionary<XNode.NodePort, Vector2>(); portPositions = new Dictionary<XNode.NodePort, Vector2>();
// Iterate through serialized properties and draw them like the Inspector (But with ports)
SerializedProperty iterator = serializedObject.GetIterator(); SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true; bool enterChildren = true;
EditorGUIUtility.labelWidth = 84; EditorGUIUtility.labelWidth = 84;
@ -58,6 +36,13 @@ namespace XNodeEditor {
if (excludes.Contains(iterator.name)) continue; if (excludes.Contains(iterator.name)) continue;
NodeEditorGUILayout.PropertyField(iterator, true); NodeEditorGUILayout.PropertyField(iterator, true);
} }
// Iterate through instance ports and draw them in the order in which they are serialized
foreach (XNode.NodePort instancePort in target.InstancePorts) {
if (NodeEditorGUILayout.IsInstancePortListPort(instancePort)) continue;
NodeEditorGUILayout.PortField(instancePort);
}
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
} }
@ -75,11 +60,33 @@ namespace XNodeEditor {
else return Color.white; else return Color.white;
} }
public void InitiateRename() { public virtual GUIStyle GetBodyStyle() {
renaming = 1; return NodeEditorResources.styles.nodeBody;
} }
/// <summary> Add items for the context menu when right-clicking this node. Override to add custom menu items. </summary>
public virtual void AddContextMenuItems(GenericMenu menu) {
// Actions if only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
XNode.Node node = Selection.activeObject as XNode.Node;
menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node));
menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode);
}
// Add actions to any number of selected nodes
menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes);
menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
// Custom sctions if only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
XNode.Node node = Selection.activeObject as XNode.Node;
NodeEditorWindow.AddCustomContextMenuItems(menu, node);
}
}
/// <summary> Rename the node asset. This will trigger a reimport of the node. </summary>
public void Rename(string newName) { public void Rename(string newName) {
if (newName == null || newName.Trim() == "") newName = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name);
target.name = newName; target.name = newName;
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
} }
@ -90,7 +97,6 @@ namespace XNodeEditor {
private Type inspectedType; private Type inspectedType;
/// <summary> Tells a NodeEditor which Node type it is an editor for </summary> /// <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="inspectedType">Type that this editor can edit</param>
/// <param name="contextMenuName">Path to the node</param>
public CustomNodeEditorAttribute(Type inspectedType) { public CustomNodeEditorAttribute(Type inspectedType) {
this.inspectedType = inspectedType; this.inspectedType = inspectedType;
} }

View File

@ -22,11 +22,11 @@ namespace XNodeEditor {
[NonSerialized] private List<Vector2> draggedOutputReroutes = new List<Vector2>(); [NonSerialized] private List<Vector2> draggedOutputReroutes = new List<Vector2>();
private RerouteReference hoveredReroute = new RerouteReference(); private RerouteReference hoveredReroute = new RerouteReference();
private List<RerouteReference> selectedReroutes = new List<RerouteReference>(); private List<RerouteReference> selectedReroutes = new List<RerouteReference>();
private Rect nodeRects;
private Vector2 dragBoxStart; private Vector2 dragBoxStart;
private UnityEngine.Object[] preBoxSelection; private UnityEngine.Object[] preBoxSelection;
private RerouteReference[] preBoxSelectionReroute; private RerouteReference[] preBoxSelectionReroute;
private Rect selectionBox; private Rect selectionBox;
private bool isDoubleClick = false;
private struct RerouteReference { private struct RerouteReference {
public XNode.NodePort port; public XNode.NodePort port;
@ -52,13 +52,15 @@ namespace XNodeEditor {
case EventType.MouseMove: case EventType.MouseMove:
break; break;
case EventType.ScrollWheel: case EventType.ScrollWheel:
float oldZoom = zoom;
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;
if (NodeEditorPreferences.GetSettings().zoomToMouse) panOffset += (1 - oldZoom / zoom) * (WindowToGridPosition(e.mousePosition) + panOffset);
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 && draggedOutput.CanConnectTo(hoveredPort)) {
if (!draggedOutput.IsConnectedTo(hoveredPort)) { if (!draggedOutput.IsConnectedTo(hoveredPort)) {
draggedOutputTarget = hoveredPort; draggedOutputTarget = hoveredPort;
} }
@ -134,12 +136,7 @@ namespace XNodeEditor {
Repaint(); Repaint();
} }
} else if (e.button == 1 || e.button == 2) { } else if (e.button == 1 || e.button == 2) {
Vector2 tempOffset = panOffset; panOffset += e.delta * zoom;
tempOffset += e.delta * zoom;
// Round value to increase crispyness of UI text
tempOffset.x = Mathf.Round(tempOffset.x);
tempOffset.y = Mathf.Round(tempOffset.y);
panOffset = tempOffset;
isPanning = true; isPanning = true;
} }
break; break;
@ -170,6 +167,10 @@ namespace XNodeEditor {
SelectNode(hoveredNode, e.control || e.shift); SelectNode(hoveredNode, e.control || e.shift);
if (!e.control && !e.shift) selectedReroutes.Clear(); if (!e.control && !e.shift) selectedReroutes.Clear();
} else if (e.control || e.shift) DeselectNode(hoveredNode); } else if (e.control || e.shift) DeselectNode(hoveredNode);
// Cache double click state, but only act on it in MouseUp - Except ClickCount only works in mouseDown.
isDoubleClick = (e.clickCount == 2);
e.Use(); e.Use();
currentActivity = NodeActivity.HoldNode; currentActivity = NodeActivity.HoldNode;
} else if (IsHoveringReroute) { } else if (IsHoveringReroute) {
@ -229,6 +230,7 @@ namespace XNodeEditor {
// If click outside node, release field focus // If click outside node, release field focus
if (!isPanning) { if (!isPanning) {
EditorGUI.FocusTextInControl(null); EditorGUI.FocusTextInControl(null);
EditorGUIUtility.editingTextField = false;
} }
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
} }
@ -237,6 +239,12 @@ namespace XNodeEditor {
if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) { if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) {
selectedReroutes.Clear(); selectedReroutes.Clear();
SelectNode(hoveredNode, false); SelectNode(hoveredNode, false);
// Double click to center node
if (isDoubleClick) {
Vector2 nodeDimension = nodeSizes.ContainsKey(hoveredNode) ? nodeSizes[hoveredNode] / 2 : Vector2.zero;
panOffset = -hoveredNode.position - nodeDimension;
}
} }
// If click reroute, select it. // If click reroute, select it.
@ -260,26 +268,42 @@ namespace XNodeEditor {
ShowPortContextMenu(hoveredPort); ShowPortContextMenu(hoveredPort);
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false);
ShowNodeContextMenu(); GenericMenu menu = new GenericMenu();
NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places.
} else if (!IsHoveringNode) { } else if (!IsHoveringNode) {
ShowGraphContextMenu(); GenericMenu menu = new GenericMenu();
graphEditor.AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
} }
} }
isPanning = false; isPanning = false;
} }
// Reset DoubleClick
isDoubleClick = false;
break; break;
case EventType.KeyDown: case EventType.KeyDown:
if (EditorGUIUtility.editingTextField) break; if (EditorGUIUtility.editingTextField) break;
else if (e.keyCode == KeyCode.F) Home(); else if (e.keyCode == KeyCode.F) Home();
if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { if (IsMac()) {
if (e.keyCode == KeyCode.Return) RenameSelectedNode(); if (e.keyCode == KeyCode.Return) RenameSelectedNode();
} else { } else {
if (e.keyCode == KeyCode.F2) RenameSelectedNode(); if (e.keyCode == KeyCode.F2) RenameSelectedNode();
} }
break; break;
case EventType.ValidateCommand: case EventType.ValidateCommand:
if (e.commandName == "SoftDelete") RemoveSelectedNodes(); case EventType.ExecuteCommand:
else if (e.commandName == "Duplicate") DublicateSelectedNodes(); if (e.commandName == "SoftDelete") {
if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
e.Use();
} else if (IsMac() && e.commandName == "Delete") {
if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
e.Use();
} else if (e.commandName == "Duplicate") {
if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes();
e.Use();
}
Repaint(); Repaint();
break; break;
case EventType.Ignore: case EventType.Ignore:
@ -292,6 +316,14 @@ namespace XNodeEditor {
} }
} }
public bool IsMac() {
#if UNITY_2017_1_OR_NEWER
return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
#else
return SystemInfo.operatingSystem.StartsWith("Mac");
#endif
}
private void RecalculateDragOffsets(Event current) { private void RecalculateDragOffsets(Event current) {
dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count]; dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count];
// Selected nodes // Selected nodes
@ -314,15 +346,6 @@ namespace XNodeEditor {
panOffset = Vector2.zero; panOffset = Vector2.zero;
} }
public void CreateNode(Type type, Vector2 position) {
XNode.Node node = graph.AddNode(type);
node.position = position;
node.name = UnityEditor.ObjectNames.NicifyVariableName(type.Name);
AssetDatabase.AddObjectToAsset(node, graph);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
Repaint();
}
/// <summary> Remove nodes in the graph in Selection.objects</summary> /// <summary> Remove nodes in the graph in Selection.objects</summary>
public void RemoveSelectedNodes() { public void RemoveSelectedNodes() {
// We need to delete reroutes starting at the highest point index to avoid shifting indices // We need to delete reroutes starting at the highest point index to avoid shifting indices
@ -343,7 +366,12 @@ namespace XNodeEditor {
public void RenameSelectedNode() { public void RenameSelectedNode() {
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
XNode.Node node = Selection.activeObject as XNode.Node; XNode.Node node = Selection.activeObject as XNode.Node;
NodeEditor.GetEditor(node).InitiateRename(); Vector2 size;
if (nodeSizes.TryGetValue(node, out size)) {
RenamePopup.Show(Selection.activeObject, size.x);
} else {
RenamePopup.Show(Selection.activeObject);
}
} }
} }
@ -356,8 +384,8 @@ namespace XNodeEditor {
} }
} }
/// <summary> Dublicate selected nodes and select the dublicates </summary> /// <summary> Duplicate selected nodes and select the duplicates </summary>
public void DublicateSelectedNodes() { public void DuplicateSelectedNodes() {
UnityEngine.Object[] newNodes = new UnityEngine.Object[Selection.objects.Length]; UnityEngine.Object[] newNodes = new UnityEngine.Object[Selection.objects.Length];
Dictionary<XNode.Node, XNode.Node> substitutes = new Dictionary<XNode.Node, XNode.Node>(); Dictionary<XNode.Node, XNode.Node> substitutes = new Dictionary<XNode.Node, XNode.Node>();
for (int i = 0; i < Selection.objects.Length; i++) { for (int i = 0; i < Selection.objects.Length; i++) {
@ -404,7 +432,7 @@ namespace XNodeEditor {
Rect fromRect; Rect fromRect;
if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return; if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return;
Vector2 from = fromRect.center; Vector2 from = fromRect.center;
col.a = 0.6f; col.a = draggedOutputTarget != null ? 1.0f : 0.6f;
Vector2 to = Vector2.zero; Vector2 to = Vector2.zero;
for (int i = 0; i < draggedOutputReroutes.Count; i++) { for (int i = 0; i < draggedOutputReroutes.Count; i++) {
to = draggedOutputReroutes[i]; to = draggedOutputReroutes[i];

View File

@ -7,14 +7,18 @@ using UnityEngine;
namespace XNodeEditor.Internal { namespace XNodeEditor.Internal {
/// <summary> Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) </summary> /// <summary> Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) </summary>
public class NodeEditorBase<T, A, K> where A : Attribute, NodeEditorBase<T, A, K>.INodeEditorAttrib where T : NodeEditorBase<T, A, K> where K : ScriptableObject { /// <typeparam name="T">Editor Type. Should be the type of the deriving script itself (eg. NodeEditor) </typeparam>
/// <typeparam name="A">Attribute Type. The attribute used to connect with the runtime type (eg. CustomNodeEditorAttribute) </typeparam>
/// <typeparam name="K">Runtime Type. The ScriptableObject this can be an editor for (eg. Node) </typeparam>
public abstract class NodeEditorBase<T, A, K> where A : Attribute, NodeEditorBase<T, A, K>.INodeEditorAttrib where T : NodeEditorBase<T, A, K> where K : ScriptableObject {
/// <summary> Custom editors defined with [CustomNodeEditor] </summary> /// <summary> Custom editors defined with [CustomNodeEditor] </summary>
private static Dictionary<Type, Type> editorTypes; private static Dictionary<Type, Type> editorTypes;
private static Dictionary<K, T> editors = new Dictionary<K, T>(); private static Dictionary<K, T> editors = new Dictionary<K, T>();
public NodeEditorWindow window;
public K target; public K target;
public SerializedObject serializedObject; public SerializedObject serializedObject;
public static T GetEditor(K target) { public static T GetEditor(K target, NodeEditorWindow window) {
if (target == null) return null; if (target == null) return null;
T editor; T editor;
if (!editors.TryGetValue(target, out editor)) { if (!editors.TryGetValue(target, out editor)) {
@ -23,9 +27,12 @@ namespace XNodeEditor.Internal {
editor = Activator.CreateInstance(editorType) as T; editor = Activator.CreateInstance(editorType) as T;
editor.target = target; editor.target = target;
editor.serializedObject = new SerializedObject(target); editor.serializedObject = new SerializedObject(target);
editor.window = window;
editor.OnCreate();
editors.Add(target, editor); editors.Add(target, editor);
} }
if (editor.target == null) editor.target = target; if (editor.target == null) editor.target = target;
if (editor.window != window) editor.window = window;
if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target); if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target);
return editor; return editor;
} }
@ -53,6 +60,9 @@ namespace XNodeEditor.Internal {
} }
} }
/// <summary> Called on creation, after references have been set </summary>
public virtual void OnCreate() { }
public interface INodeEditorAttrib { public interface INodeEditorAttrib {
Type GetInspectedType(); Type GetInspectedType();
} }

View File

@ -10,14 +10,15 @@ namespace XNodeEditor {
public NodeGraphEditor graphEditor; public NodeGraphEditor graphEditor;
private List<UnityEngine.Object> selectionCache; private List<UnityEngine.Object> selectionCache;
private List<XNode.Node> culledNodes; private List<XNode.Node> culledNodes;
private int topPadding { get { return isDocked() ? 19 : 22; } }
/// <summary> Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run.</summary>
public event Action onLateGUI;
private void OnGUI() { private void OnGUI() {
Event e = Event.current; Event e = Event.current;
Matrix4x4 m = GUI.matrix; Matrix4x4 m = GUI.matrix;
if (graph == null) return; if (graph == null) return;
graphEditor = NodeGraphEditor.GetEditor(graph); ValidateGraphEditor();
graphEditor.position = position;
Controls(); Controls();
DrawGrid(position, zoom, panOffset); DrawGrid(position, zoom, panOffset);
@ -28,25 +29,31 @@ namespace XNodeEditor {
DrawTooltip(); DrawTooltip();
graphEditor.OnGUI(); graphEditor.OnGUI();
// Run and reset onLateGUI
if (onLateGUI != null) {
onLateGUI();
onLateGUI = null;
}
GUI.matrix = m; GUI.matrix = m;
} }
public static void BeginZoomed(Rect rect, float zoom) { public static void BeginZoomed(Rect rect, float zoom, float topPadding) {
GUI.EndClip(); GUI.EndClip();
GUIUtility.ScaleAroundPivot(Vector2.one / zoom, rect.size * 0.5f); GUIUtility.ScaleAroundPivot(Vector2.one / zoom, rect.size * 0.5f);
Vector4 padding = new Vector4(0, 22, 0, 0); Vector4 padding = new Vector4(0, topPadding, 0, 0);
padding *= zoom; padding *= zoom;
GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (22 * zoom), GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (topPadding * zoom),
rect.width * zoom, rect.width * zoom,
rect.height * zoom)); rect.height * zoom));
} }
public static void EndZoomed(Rect rect, float zoom) { public static void EndZoomed(Rect rect, float zoom, float topPadding) {
GUIUtility.ScaleAroundPivot(Vector2.one * zoom, rect.size * 0.5f); GUIUtility.ScaleAroundPivot(Vector2.one * zoom, rect.size * 0.5f);
Vector3 offset = new Vector3( Vector3 offset = new Vector3(
(((rect.width * zoom) - rect.width) * 0.5f), (((rect.width * zoom) - rect.width) * 0.5f),
(((rect.height * zoom) - rect.height) * 0.5f) + (-22 * zoom) + 22, (((rect.height * zoom) - rect.height) * 0.5f) + (-topPadding * zoom) + topPadding,
0); 0);
GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one); GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one);
} }
@ -107,60 +114,6 @@ namespace XNodeEditor {
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
} }
/// <summary> Show right-click context menu for selected nodes </summary>
public void ShowNodeContextMenu() {
GenericMenu contextMenu = new GenericMenu();
// If only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
XNode.Node node = Selection.activeObject as XNode.Node;
contextMenu.AddItem(new GUIContent("Move To Top"), false, () => MoveNodeToTop(node));
contextMenu.AddItem(new GUIContent("Rename"), false, RenameSelectedNode);
}
contextMenu.AddItem(new GUIContent("Duplicate"), false, DublicateSelectedNodes);
contextMenu.AddItem(new GUIContent("Remove"), false, RemoveSelectedNodes);
// If only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
XNode.Node node = Selection.activeObject as XNode.Node;
AddCustomContextMenuItems(contextMenu, node);
}
contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
}
/// <summary> Show right-click context menu for current graph </summary>
void ShowGraphContextMenu() {
GenericMenu contextMenu = new GenericMenu();
Vector2 pos = WindowToGridPosition(Event.current.mousePosition);
for (int i = 0; i < nodeTypes.Length; i++) {
Type type = nodeTypes[i];
//Get node context menu path
string path = graphEditor.GetNodeMenuName(type);
if (string.IsNullOrEmpty(path)) continue;
contextMenu.AddItem(new GUIContent(path), false, () => {
CreateNode(type, pos);
});
}
contextMenu.AddSeparator("");
contextMenu.AddItem(new GUIContent("Preferences"), false, () => OpenPreferences());
AddCustomContextMenuItems(contextMenu, graph);
contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
}
void AddCustomContextMenuItems(GenericMenu contextMenu, object obj) {
KeyValuePair<ContextMenu, System.Reflection.MethodInfo>[] items = GetContextMenuMethods(obj);
if (items.Length != 0) {
contextMenu.AddSeparator("");
for (int i = 0; i < items.Length; i++) {
KeyValuePair<ContextMenu, System.Reflection.MethodInfo> kvp = items[i];
contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null));
}
}
}
/// <summary> Draw a bezier from startpoint to endpoint, both in grid coordinates </summary> /// <summary> Draw a bezier from startpoint to endpoint, both in grid coordinates </summary>
public void DrawConnection(Vector2 startPoint, Vector2 endPoint, Color col) { public void DrawConnection(Vector2 startPoint, Vector2 endPoint, Color col) {
startPoint = GridToWindowPosition(startPoint); startPoint = GridToWindowPosition(startPoint);
@ -229,7 +182,7 @@ namespace XNodeEditor {
Rect fromRect; Rect fromRect;
if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue; if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue;
Color connectionColor = graphEditor.GetTypeColor(output.ValueType); Color connectionColor = graphEditor.GetPortColor(output);
for (int k = 0; k < output.ConnectionCount; k++) { for (int k = 0; k < output.ConnectionCount; k++) {
XNode.NodePort input = output.GetConnection(k); XNode.NodePort input = output.GetConnection(k);
@ -286,15 +239,13 @@ namespace XNodeEditor {
selectionCache = new List<UnityEngine.Object>(Selection.objects); selectionCache = new List<UnityEngine.Object>(Selection.objects);
} }
//Active node is hashed before and after node GUI to detect changes
int nodeHash = 0;
System.Reflection.MethodInfo onValidate = null; System.Reflection.MethodInfo onValidate = null;
if (Selection.activeObject != null && Selection.activeObject is XNode.Node) { if (Selection.activeObject != null && Selection.activeObject is XNode.Node) {
onValidate = Selection.activeObject.GetType().GetMethod("OnValidate"); onValidate = Selection.activeObject.GetType().GetMethod("OnValidate");
if (onValidate != null) nodeHash = Selection.activeObject.GetHashCode(); if (onValidate != null) EditorGUI.BeginChangeCheck();
} }
BeginZoomed(position, zoom); BeginZoomed(position, zoom, topPadding);
Vector2 mousePos = Event.current.mousePosition; Vector2 mousePos = Event.current.mousePosition;
@ -335,7 +286,7 @@ namespace XNodeEditor {
_portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); _portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
} }
NodeEditor nodeEditor = NodeEditor.GetEditor(node); NodeEditor nodeEditor = NodeEditor.GetEditor(node, this);
NodeEditor.portPositions = new Dictionary<XNode.NodePort, Vector2>(); NodeEditor.portPositions = new Dictionary<XNode.NodePort, Vector2>();
@ -347,25 +298,26 @@ namespace XNodeEditor {
bool selected = selectionCache.Contains(graph.nodes[n]); bool selected = selectionCache.Contains(graph.nodes[n]);
if (selected) { if (selected) {
GUIStyle style = new GUIStyle(NodeEditorResources.styles.nodeBody); GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight); GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight);
highlightStyle.padding = style.padding; highlightStyle.padding = style.padding;
style.padding = new RectOffset(); style.padding = new RectOffset();
GUI.color = nodeEditor.GetTint(); GUI.color = nodeEditor.GetTint();
GUILayout.BeginVertical(new GUIStyle(style)); GUILayout.BeginVertical(style);
GUI.color = NodeEditorPreferences.GetSettings().highlightColor; GUI.color = NodeEditorPreferences.GetSettings().highlightColor;
GUILayout.BeginVertical(new GUIStyle(highlightStyle)); GUILayout.BeginVertical(new GUIStyle(highlightStyle));
} else { } else {
GUIStyle style = NodeEditorResources.styles.nodeBody; GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
GUI.color = nodeEditor.GetTint(); GUI.color = nodeEditor.GetTint();
GUILayout.BeginVertical(new GUIStyle(style)); GUILayout.BeginVertical(style);
} }
GUI.color = guiColor; GUI.color = guiColor;
EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck();
//Draw node contents //Draw node contents
nodeEditor.OnNodeGUI(); nodeEditor.OnHeaderGUI();
nodeEditor.OnBodyGUI();
//If user changed a value, notify other scripts through onUpdateNode //If user changed a value, notify other scripts through onUpdateNode
if (EditorGUI.EndChangeCheck()) { if (EditorGUI.EndChangeCheck()) {
@ -425,14 +377,12 @@ namespace XNodeEditor {
} }
if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray(); if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray();
EndZoomed(position, zoom); EndZoomed(position, zoom, topPadding);
//If a change in hash is detected in the selected node, call OnValidate method. //If a change in is detected in the selected node, call OnValidate method.
//This is done through reflection because OnValidate is only relevant in editor, //This is done through reflection because OnValidate is only relevant in editor,
//and thus, the code should not be included in build. //and thus, the code should not be included in build.
if (nodeHash != 0) { if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null);
if (onValidate != null && nodeHash != Selection.activeObject.GetHashCode()) onValidate.Invoke(Selection.activeObject, null);
}
} }
private bool ShouldBeCulled(XNode.Node node) { private bool ShouldBeCulled(XNode.Node node) {

View File

@ -1,14 +1,19 @@
using System; using System;
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 UnityEditor;
using UnityEditorInternal;
using UnityEngine; using UnityEngine;
namespace XNodeEditor { namespace XNodeEditor {
/// <summary> xNode-specific version of <see cref="EditorGUILayout"/> </summary> /// <summary> xNode-specific version of <see cref="EditorGUILayout"/> </summary>
public static class NodeEditorGUILayout { public static class NodeEditorGUILayout {
private static readonly Dictionary<UnityEngine.Object, Dictionary<string, ReorderableList>> reorderableListCache = new Dictionary<UnityEngine.Object, Dictionary<string, ReorderableList>>();
private static int reorderableListIndex = -1;
/// <summary> Make a field for a serialized property. Automatically displays relevant node port. </summary> /// <summary> Make a field for a serialized property. Automatically displays relevant node port. </summary>
public static void PropertyField(SerializedProperty property, bool includeChildren = true, params GUILayoutOption[] options) { public static void PropertyField(SerializedProperty property, bool includeChildren = true, params GUILayoutOption[] options) {
PropertyField(property, (GUIContent) null, includeChildren, options); PropertyField(property, (GUIContent) null, includeChildren, options);
@ -36,13 +41,36 @@ namespace XNodeEditor {
else { else {
Rect rect = new Rect(); Rect rect = new Rect();
float spacePadding = 0;
SpaceAttribute spaceAttribute;
if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out spaceAttribute)) spacePadding = spaceAttribute.height;
// 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 == XNode.NodePort.IO.Input) { if (port.direction == XNode.NodePort.IO.Input) {
// Get data from [Input] attribute // Get data from [Input] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected; XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.InputAttribute inputAttribute; XNode.Node.InputAttribute inputAttribute;
if (NodeEditorUtilities.GetAttrib(port.node.GetType(), property.name, out inputAttribute)) showBacking = inputAttribute.backingValue; bool instancePortList = false;
if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) {
instancePortList = inputAttribute.instancePortList;
showBacking = inputAttribute.backingValue;
}
//Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
bool useLayoutSpace = instancePortList ||
showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
if (spacePadding > 0 && useLayoutSpace) {
GUILayout.Space(spacePadding);
spacePadding = 0;
}
if (instancePortList) {
Type type = GetType(property);
XNode.Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
InstancePortList(property.name, type, property.serializedObject, port.direction, connectionType);
return;
}
switch (showBacking) { switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected: case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected // Display a label if port is connected
@ -61,14 +89,33 @@ namespace XNodeEditor {
} }
rect = GUILayoutUtility.GetLastRect(); rect = GUILayoutUtility.GetLastRect();
rect.position = rect.position - new Vector2(16, 0); rect.position = rect.position - new Vector2(16, -spacePadding);
// 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 == XNode.NodePort.IO.Output) { } else if (port.direction == XNode.NodePort.IO.Output) {
// Get data from [Output] attribute // Get data from [Output] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected; XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.OutputAttribute outputAttribute; XNode.Node.OutputAttribute outputAttribute;
if (NodeEditorUtilities.GetAttrib(port.node.GetType(), property.name, out outputAttribute)) showBacking = outputAttribute.backingValue; bool instancePortList = false;
if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) {
instancePortList = outputAttribute.instancePortList;
showBacking = outputAttribute.backingValue;
}
//Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
bool useLayoutSpace = instancePortList ||
showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
if (spacePadding > 0 && useLayoutSpace) {
GUILayout.Space(spacePadding);
spacePadding = 0;
}
if (instancePortList) {
Type type = GetType(property);
XNode.Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
InstancePortList(property.name, type, property.serializedObject, port.direction, connectionType);
return;
}
switch (showBacking) { switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected: case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected // Display a label if port is connected
@ -87,7 +134,7 @@ namespace XNodeEditor {
} }
rect = GUILayoutUtility.GetLastRect(); rect = GUILayoutUtility.GetLastRect();
rect.position = rect.position + new Vector2(rect.width, 0); rect.position = rect.position + new Vector2(rect.width, spacePadding);
} }
rect.size = new Vector2(16, 16); rect.size = new Vector2(16, 16);
@ -95,7 +142,7 @@ namespace XNodeEditor {
Color backgroundColor = new Color32(90, 97, 105, 255); Color backgroundColor = new Color32(90, 97, 105, 255);
Color tint; Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col); DrawPortHandle(rect, backgroundColor, col);
// Register the handle position // Register the handle position
@ -105,6 +152,12 @@ namespace XNodeEditor {
} }
} }
private static System.Type GetType(SerializedProperty property) {
System.Type parentType = property.serializedObject.targetObject.GetType();
System.Reflection.FieldInfo fi = NodeEditorWindow.GetFieldInfo(parentType, property.name);
return fi.FieldType;
}
/// <summary> Make a simple port field. </summary> /// <summary> Make a simple port field. </summary>
public static void PortField(XNode.NodePort port, params GUILayoutOption[] options) { public static void PortField(XNode.NodePort port, params GUILayoutOption[] options) {
PortField(null, port, options); PortField(null, port, options);
@ -114,7 +167,7 @@ namespace XNodeEditor {
public static void PortField(GUIContent label, XNode.NodePort port, params GUILayoutOption[] options) { public static void PortField(GUIContent label, XNode.NodePort port, params GUILayoutOption[] options) {
if (port == null) return; if (port == null) return;
if (options == null) options = new GUILayoutOption[] { GUILayout.MinWidth(30) }; if (options == null) options = new GUILayoutOption[] { GUILayout.MinWidth(30) };
Rect rect = new Rect(); Vector2 position = Vector3.zero;
GUIContent content = label != null ? label : new GUIContent(ObjectNames.NicifyVariableName(port.fieldName)); GUIContent content = label != null ? label : new GUIContent(ObjectNames.NicifyVariableName(port.fieldName));
// 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
@ -122,23 +175,31 @@ namespace XNodeEditor {
// Display a label // Display a label
EditorGUILayout.LabelField(content, options); EditorGUILayout.LabelField(content, options);
rect = GUILayoutUtility.GetLastRect(); Rect rect = GUILayoutUtility.GetLastRect();
rect.position = rect.position - new Vector2(16, 0); 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 == XNode.NodePort.IO.Output) { else if (port.direction == XNode.NodePort.IO.Output) {
// Display a label // Display a label
EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options); EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options);
rect = GUILayoutUtility.GetLastRect(); Rect rect = GUILayoutUtility.GetLastRect();
rect.position = rect.position + new Vector2(rect.width, 0); position = rect.position + new Vector2(rect.width, 0);
}
PortField(position, port);
} }
rect.size = new Vector2(16, 16); /// <summary> Make a simple port field. </summary>
public static void PortField(Vector2 position, XNode.NodePort port) {
if (port == null) return;
Rect rect = new Rect(position, new Vector2(16, 16));
Color backgroundColor = new Color32(90, 97, 105, 255); Color backgroundColor = new Color32(90, 97, 105, 255);
Color tint; Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col); DrawPortHandle(rect, backgroundColor, col);
// Register the handle position // Register the handle position
@ -167,7 +228,7 @@ namespace XNodeEditor {
Color backgroundColor = new Color32(90, 97, 105, 255); Color backgroundColor = new Color32(90, 97, 105, 255);
Color tint; Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col); DrawPortHandle(rect, backgroundColor, col);
// Register the handle position // Register the handle position
@ -193,9 +254,16 @@ namespace XNodeEditor {
GUI.color = col; GUI.color = col;
} }
[Obsolete("Use InstancePortList(string, Type, SerializedObject, NodePort.IO, Node.ConnectionType) instead")] /// <summary> Is this port part of an InstancePortList? </summary>
public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple) { public static bool IsInstancePortListPort(XNode.NodePort port) {
InstancePortList(fieldName, type, serializedObject, XNode.NodePort.IO.Output, connectionType); string[] parts = port.fieldName.Split(' ');
if (parts.Length != 2) return false;
Dictionary<string, ReorderableList> cache;
if (reorderableListCache.TryGetValue(port.node, out cache)) {
ReorderableList list;
if (cache.TryGetValue(parts[0], out list)) return true;
}
return false;
} }
/// <summary> Draw an editable list of instance ports. Port names are named as "[fieldName] [index]" </summary> /// <summary> Draw an editable list of instance ports. Port names are named as "[fieldName] [index]" </summary>
@ -203,28 +271,156 @@ namespace XNodeEditor {
/// <param name="type">Value type of added instance ports</param> /// <param name="type">Value type of added instance ports</param>
/// <param name="serializedObject">The serializedObject of the node</param> /// <param name="serializedObject">The serializedObject of the node</param>
/// <param name="connectionType">Connection type of added instance ports</param> /// <param name="connectionType">Connection type of added instance ports</param>
public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple) { /// <param name="onCreation">Called on the list on creation. Use this if you want to customize the created ReorderableList</param>
public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action<ReorderableList> onCreation = null) {
XNode.Node node = serializedObject.targetObject as XNode.Node; XNode.Node node = serializedObject.targetObject as XNode.Node;
var indexedPorts = node.InstancePorts.Select(x => {
string[] split = x.fieldName.Split(' ');
if (split != null && split.Length == 2 && split[0] == fieldName) {
int i = -1;
if (int.TryParse(split[1], out i)) {
return new { index = i, port = x };
}
}
return new { index = -1, port = (XNode.NodePort) null };
});
List<XNode.NodePort> instancePorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
ReorderableList list = null;
Dictionary<string, ReorderableList> rlc;
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
if (!rlc.TryGetValue(fieldName, out list)) list = null;
}
// If a ReorderableList isn't cached for this array, do so.
if (list == null) {
SerializedProperty arrayData = serializedObject.FindProperty(fieldName); SerializedProperty arrayData = serializedObject.FindProperty(fieldName);
list = CreateReorderableList(fieldName, instancePorts, arrayData, type, serializedObject, io, connectionType, typeConstraint, onCreation);
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) rlc.Add(fieldName, list);
else reorderableListCache.Add(serializedObject.targetObject, new Dictionary<string, ReorderableList>() { { fieldName, list } });
}
list.list = instancePorts;
list.DoLayoutList();
}
private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> instancePorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) {
bool hasArrayData = arrayData != null && arrayData.isArray; bool hasArrayData = arrayData != null && arrayData.isArray;
int arraySize = hasArrayData ? arrayData.arraySize : 0; XNode.Node node = serializedObject.targetObject as XNode.Node;
ReorderableList list = new ReorderableList(instancePorts, null, true, true, true, true);
string label = arrayData != null ? arrayData.displayName : ObjectNames.NicifyVariableName(fieldName);
Predicate<string> isMatchingInstancePort = list.drawElementCallback =
x => { (Rect rect, int index, bool isActive, bool isFocused) => {
string[] split = x.Split(' '); XNode.NodePort port = node.GetPort(fieldName + " " + index);
if (split != null && split.Length == 2) return split[0] == fieldName; if (hasArrayData) {
else return false; if (arrayData.arraySize <= index) {
EditorGUI.LabelField(rect, "Invalid element " + index);
return;
}
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index);
EditorGUI.PropertyField(rect, itemData, true);
} else EditorGUI.LabelField(rect, port.fieldName);
if (port != null) {
Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0));
NodeEditorGUILayout.PortField(pos, port);
}
}; };
List<XNode.NodePort> instancePorts = node.InstancePorts.Where(x => isMatchingInstancePort(x.fieldName)).OrderBy(x => x.fieldName).ToList(); list.elementHeightCallback =
(int index) => {
if (hasArrayData) {
if (arrayData.arraySize <= index) return EditorGUIUtility.singleLineHeight;
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index);
return EditorGUI.GetPropertyHeight(itemData);
} else return EditorGUIUtility.singleLineHeight;
};
list.drawHeaderCallback =
(Rect rect) => {
EditorGUI.LabelField(rect, label);
};
list.onSelectCallback =
(ReorderableList rl) => {
reorderableListIndex = rl.index;
};
list.onReorderCallback =
(ReorderableList rl) => {
for (int i = 0; i < instancePorts.Count(); i++) { // Move up
GUILayout.BeginHorizontal(); if (rl.index > reorderableListIndex) {
// 'Remove' button for (int i = reorderableListIndex; i < rl.index; ++i) {
if (GUILayout.Button("-", GUILayout.Width(20))) { XNode.NodePort port = node.GetPort(fieldName + " " + i);
XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i + 1));
port.SwapConnections(nextPort);
// Swap cached positions to mitigate twitching
Rect rect = NodeEditorWindow.current.portConnectionPoints[port];
NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort];
NodeEditorWindow.current.portConnectionPoints[nextPort] = rect;
}
}
// Move down
else {
for (int i = reorderableListIndex; i > rl.index; --i) {
XNode.NodePort port = node.GetPort(fieldName + " " + i);
XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i - 1));
port.SwapConnections(nextPort);
// Swap cached positions to mitigate twitching
Rect rect = NodeEditorWindow.current.portConnectionPoints[port];
NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort];
NodeEditorWindow.current.portConnectionPoints[nextPort] = rect;
}
}
// Apply changes
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
// Move array data if there is any
if (hasArrayData) {
arrayData.MoveArrayElement(reorderableListIndex, rl.index);
}
// Apply changes
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
NodeEditorWindow.current.Repaint();
EditorApplication.delayCall += NodeEditorWindow.current.Repaint;
};
list.onAddCallback =
(ReorderableList rl) => {
// Add instance port postfixed with an index number
string newName = fieldName + " 0";
int i = 0;
while (node.HasPort(newName)) newName = fieldName + " " + (++i);
if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, XNode.Node.TypeConstraint.None, newName);
else node.AddInstanceInput(type, connectionType, typeConstraint, newName);
serializedObject.Update();
EditorUtility.SetDirty(node);
if (hasArrayData) {
arrayData.InsertArrayElementAtIndex(arrayData.arraySize);
}
serializedObject.ApplyModifiedProperties();
};
list.onRemoveCallback =
(ReorderableList rl) => {
var indexedPorts = node.InstancePorts.Select(x => {
string[] split = x.fieldName.Split(' ');
if (split != null && split.Length == 2 && split[0] == fieldName) {
int i = -1;
if (int.TryParse(split[1], out i)) {
return new { index = i, port = x };
}
}
return new { index = -1, port = (XNode.NodePort) null };
});
instancePorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
int index = rl.index;
// Clear the removed ports connections // Clear the removed ports connections
instancePorts[i].ClearConnections(); instancePorts[index].ClearConnections();
// Move following connections one step up to replace the missing connection // Move following connections one step up to replace the missing connection
for (int k = i + 1; k < instancePorts.Count(); k++) { for (int k = index + 1; k < instancePorts.Count(); k++) {
for (int j = 0; j < instancePorts[k].ConnectionCount; j++) { for (int j = 0; j < instancePorts[k].ConnectionCount; j++) {
XNode.NodePort other = instancePorts[k].GetConnection(j); XNode.NodePort other = instancePorts[k].GetConnection(j);
instancePorts[k].Disconnect(other); instancePorts[k].Disconnect(other);
@ -236,54 +432,39 @@ namespace XNodeEditor {
serializedObject.Update(); serializedObject.Update();
EditorUtility.SetDirty(node); EditorUtility.SetDirty(node);
if (hasArrayData) { if (hasArrayData) {
arrayData.DeleteArrayElementAtIndex(i); arrayData.DeleteArrayElementAtIndex(index);
arraySize--;
// Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues // Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues
if (instancePorts.Count <= arraySize) { if (instancePorts.Count <= arrayData.arraySize) {
while (instancePorts.Count <= arraySize) { while (instancePorts.Count <= arrayData.arraySize) {
arrayData.DeleteArrayElementAtIndex(--arraySize); arrayData.DeleteArrayElementAtIndex(arrayData.arraySize - 1);
} }
Debug.LogWarning("Array size exceeded instance ports size. Excess items removed."); UnityEngine.Debug.LogWarning("Array size exceeded instance ports size. Excess items removed.");
} }
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
serializedObject.Update(); serializedObject.Update();
} }
i--; };
GUILayout.EndHorizontal();
} else {
if (hasArrayData) { if (hasArrayData) {
if (i < arraySize) { int instancePortCount = instancePorts.Count;
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(i); while (instancePortCount < arrayData.arraySize) {
if (itemData != null) EditorGUILayout.PropertyField(itemData, new GUIContent(ObjectNames.NicifyVariableName(fieldName) + " " + i), true); // Add instance port postfixed with an index number
else EditorGUILayout.LabelField("[Missing array data]"); string newName = arrayData.name + " 0";
} else EditorGUILayout.LabelField("[Out of bounds]");
} else {
EditorGUILayout.LabelField(instancePorts[i].fieldName);
}
GUILayout.EndHorizontal();
NodeEditorGUILayout.AddPortField(node.GetPort(instancePorts[i].fieldName));
}
// GUILayout.EndHorizontal();
}
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
// 'Add' button
if (GUILayout.Button("+", GUILayout.Width(20))) {
string newName = fieldName + " 0";
int i = 0; int i = 0;
while (node.HasPort(newName)) newName = fieldName + " " + (++i); while (node.HasPort(newName)) newName = arrayData.name + " " + (++i);
if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, typeConstraint, newName);
if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, newName); else node.AddInstanceInput(type, connectionType, typeConstraint, newName);
else node.AddInstanceInput(type, connectionType, newName);
serializedObject.Update();
EditorUtility.SetDirty(node); EditorUtility.SetDirty(node);
if (hasArrayData) arrayData.InsertArrayElementAtIndex(arraySize); instancePortCount++;
}
while (arrayData.arraySize < instancePortCount) {
arrayData.InsertArrayElementAtIndex(arrayData.arraySize);
}
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
serializedObject.Update();
} }
GUILayout.EndHorizontal(); if (onCreation != null) onCreation(list);
return list;
} }
} }
} }

View File

@ -12,7 +12,7 @@ namespace XNodeEditor {
/// <summary> The last key we checked. This should be the one we modify </summary> /// <summary> The last key we checked. This should be the one we modify </summary>
private static string lastKey = "xNode.Settings"; private static string lastKey = "xNode.Settings";
private static Dictionary<string, Color> typeColors = new Dictionary<string, Color>(); private static Dictionary<Type, Color> typeColors = new Dictionary<Type, Color>();
private static Dictionary<string, Settings> settings = new Dictionary<string, Settings>(); private static Dictionary<string, Settings> settings = new Dictionary<string, Settings>();
[System.Serializable] [System.Serializable]
@ -23,9 +23,11 @@ namespace XNodeEditor {
[SerializeField] private Color32 _gridBgColor = new Color(0.18f, 0.18f, 0.18f); [SerializeField] private Color32 _gridBgColor = new Color(0.18f, 0.18f, 0.18f);
public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } } public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } }
public float zoomOutLimit = 5f;
public Color32 highlightColor = new Color32(255, 255, 255, 255); public Color32 highlightColor = new Color32(255, 255, 255, 255);
public bool gridSnap = true; public bool gridSnap = true;
public bool autoSave = true; public bool autoSave = true;
public bool zoomToMouse = true;
[SerializeField] private string typeColorsData = ""; [SerializeField] private string typeColorsData = "";
[NonSerialized] public Dictionary<string, Color> typeColors = new Dictionary<string, Color>(); [NonSerialized] public Dictionary<string, Color> typeColors = new Dictionary<string, Color>();
public NoodleType noodleType = NoodleType.Curve; public NoodleType noodleType = NoodleType.Curve;
@ -80,7 +82,20 @@ namespace XNodeEditor {
return settings[lastKey]; return settings[lastKey];
} }
#if UNITY_2019_1_OR_NEWER
[SettingsProvider]
public static SettingsProvider CreateXNodeSettingsProvider() {
SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) {
guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); },
keywords = new HashSet<string>(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" })
};
return provider;
}
#endif
#if !UNITY_2019_1_OR_NEWER
[PreferenceItem("Node Editor")] [PreferenceItem("Node Editor")]
#endif
private static void PreferencesGUI() { private static void PreferencesGUI() {
VerifyLoaded(); VerifyLoaded();
Settings settings = NodeEditorPreferences.settings[lastKey]; Settings settings = NodeEditorPreferences.settings[lastKey];
@ -98,7 +113,8 @@ namespace XNodeEditor {
//Label //Label
EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel); EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel);
settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap); settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap);
settings.zoomToMouse = EditorGUILayout.Toggle(new GUIContent("Zoom to Mouse", "Zooms towards mouse position"), settings.zoomToMouse);
settings.zoomOutLimit = EditorGUILayout.FloatField(new GUIContent("Zoom out Limit", "Upper limit to zoom"), settings.zoomOutLimit);
settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor); settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor);
settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor); settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor);
if (GUI.changed) { if (GUI.changed) {
@ -134,15 +150,16 @@ namespace XNodeEditor {
EditorGUILayout.LabelField("Types", EditorStyles.boldLabel); EditorGUILayout.LabelField("Types", EditorStyles.boldLabel);
//Display type colors. Save them if they are edited by the user //Display type colors. Save them if they are edited by the user
List<string> typeColorKeys = new List<string>(typeColors.Keys); foreach (var typeColor in typeColors) {
foreach (string typeColorKey in typeColorKeys) { Type type = typeColor.Key;
Color col = typeColors[typeColorKey]; string typeColorKey = NodeEditorUtilities.PrettyName(type);
Color col = typeColor.Value;
EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck();
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
col = EditorGUILayout.ColorField(typeColorKey, col); col = EditorGUILayout.ColorField(typeColorKey, col);
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
if (EditorGUI.EndChangeCheck()) { if (EditorGUI.EndChangeCheck()) {
typeColors[typeColorKey] = col; typeColors[type] = col;
if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col; if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col;
else settings.typeColors.Add(typeColorKey, col); else settings.typeColors.Add(typeColorKey, col);
SavePrefs(typeColorKey, settings); SavePrefs(typeColorKey, settings);
@ -165,7 +182,7 @@ namespace XNodeEditor {
public static void ResetPrefs() { public static void ResetPrefs() {
if (EditorPrefs.HasKey(lastKey)) EditorPrefs.DeleteKey(lastKey); if (EditorPrefs.HasKey(lastKey)) EditorPrefs.DeleteKey(lastKey);
if (settings.ContainsKey(lastKey)) settings.Remove(lastKey); if (settings.ContainsKey(lastKey)) settings.Remove(lastKey);
typeColors = new Dictionary<string, Color>(); typeColors = new Dictionary<Type, Color>();
VerifyLoaded(); VerifyLoaded();
NodeEditorWindow.RepaintAll(); NodeEditorWindow.RepaintAll();
} }
@ -184,19 +201,21 @@ namespace XNodeEditor {
public static Color GetTypeColor(System.Type type) { public static Color GetTypeColor(System.Type type) {
VerifyLoaded(); VerifyLoaded();
if (type == null) return Color.gray; if (type == null) return Color.gray;
Color col;
if (!typeColors.TryGetValue(type, out col)) {
string typeName = type.PrettyName(); string typeName = type.PrettyName();
if (!typeColors.ContainsKey(typeName)) { if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]);
if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(typeName, settings[lastKey].typeColors[typeName]);
else { else {
#if UNITY_5_4_OR_NEWER #if UNITY_5_4_OR_NEWER
UnityEngine.Random.InitState(typeName.GetHashCode()); UnityEngine.Random.InitState(typeName.GetHashCode());
#else #else
UnityEngine.Random.seed = typeName.GetHashCode(); UnityEngine.Random.seed = typeName.GetHashCode();
#endif #endif
typeColors.Add(typeName, new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value)); col = new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value);
typeColors.Add(type, col);
} }
} }
return typeColors[typeName]; return col;
} }
} }
} }

View File

@ -22,6 +22,18 @@ namespace XNodeEditor {
[NonSerialized] private static Type[] _nodeTypes = null; [NonSerialized] private static Type[] _nodeTypes = null;
private Func<bool> isDocked {
get {
if (_isDocked == null) {
BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
MethodInfo isDockedMethod = typeof(NodeEditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true);
_isDocked = (Func<bool>) Delegate.CreateDelegate(typeof(Func<bool>), this, isDockedMethod);
}
return _isDocked;
}
}
private Func<bool> _isDocked;
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(XNode.Node)); return GetDerivedTypes(typeof(XNode.Node));
@ -30,9 +42,9 @@ namespace XNodeEditor {
public static Dictionary<Type, Color> GetNodeTint() { public static Dictionary<Type, Color> GetNodeTint() {
Dictionary<Type, Color> tints = new Dictionary<Type, Color>(); Dictionary<Type, Color> tints = new Dictionary<Type, Color>();
for (int i = 0; i < nodeTypes.Length; i++) { for (int i = 0; i < nodeTypes.Length; i++) {
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTint), true); var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTintAttribute), true);
if (attribs == null || attribs.Length == 0) continue; if (attribs == null || attribs.Length == 0) continue;
XNode.Node.NodeTint attrib = attribs[0] as XNode.Node.NodeTint; XNode.Node.NodeTintAttribute attrib = attribs[0] as XNode.Node.NodeTintAttribute;
tints.Add(nodeTypes[i], attrib.color); tints.Add(nodeTypes[i], attrib.color);
} }
return tints; return tints;
@ -41,32 +53,44 @@ namespace XNodeEditor {
public static Dictionary<Type, int> GetNodeWidth() { public static Dictionary<Type, int> GetNodeWidth() {
Dictionary<Type, int> widths = new Dictionary<Type, int>(); Dictionary<Type, int> widths = new Dictionary<Type, int>();
for (int i = 0; i < nodeTypes.Length; i++) { for (int i = 0; i < nodeTypes.Length; i++) {
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidth), true); var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidthAttribute), true);
if (attribs == null || attribs.Length == 0) continue; if (attribs == null || attribs.Length == 0) continue;
XNode.Node.NodeWidth attrib = attribs[0] as XNode.Node.NodeWidth; XNode.Node.NodeWidthAttribute attrib = attribs[0] as XNode.Node.NodeWidthAttribute;
widths.Add(nodeTypes[i], attrib.width); widths.Add(nodeTypes[i], attrib.width);
} }
return widths; return widths;
} }
/// <summary> Get FieldInfo of a field, including those that are private and/or inherited </summary>
public static FieldInfo GetFieldInfo(Type type, string fieldName) {
// If we can't find field in the first run, it's probably a private field in a base class.
FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Search base classes for private fields only. Public fields are found above
while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
return field;
}
/// <summary> Get all classes deriving from baseType via reflection </summary> /// <summary> Get all classes deriving from baseType via reflection </summary>
public static Type[] GetDerivedTypes(Type baseType) { public static Type[] GetDerivedTypes(Type baseType) {
List<System.Type> types = new List<System.Type>(); List<System.Type> types = new List<System.Type>();
System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies) { foreach (Assembly assembly in assemblies) {
try {
types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
} catch(ReflectionTypeLoadException) {}
} }
return types.ToArray(); return types.ToArray();
} }
public static object ObjectFromType(Type type) { public static void AddCustomContextMenuItems(GenericMenu contextMenu, object obj) {
return Activator.CreateInstance(type); KeyValuePair<ContextMenu, System.Reflection.MethodInfo>[] items = GetContextMenuMethods(obj);
if (items.Length != 0) {
contextMenu.AddSeparator("");
for (int i = 0; i < items.Length; i++) {
KeyValuePair<ContextMenu, System.Reflection.MethodInfo> kvp = items[i];
contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null));
}
} }
public static object ObjectFromFieldName(object obj, string fieldName) {
Type type = obj.GetType();
FieldInfo fieldInfo = type.GetField(fieldName);
return fieldInfo.GetValue(obj);
} }
public static KeyValuePair<ContextMenu, MethodInfo>[] GetContextMenuMethods(object obj) { public static KeyValuePair<ContextMenu, MethodInfo>[] GetContextMenuMethods(object obj) {
@ -99,6 +123,9 @@ namespace XNodeEditor {
/// <summary> Very crude. Uses a lot of reflection. </summary> /// <summary> Very crude. Uses a lot of reflection. </summary>
public static void OpenPreferences() { public static void OpenPreferences() {
try { try {
#if UNITY_2018_3_OR_NEWER
SettingsService.OpenUserPreferences("Preferences/Node Editor");
#else
//Open preferences window //Open preferences window
Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow)); Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow));
Type type = assembly.GetType("UnityEditor.PreferencesWindow"); Type type = assembly.GetType("UnityEditor.PreferencesWindow");
@ -130,6 +157,7 @@ namespace XNodeEditor {
return; return;
} }
} }
#endif
} catch (Exception e) { } catch (Exception e) {
Debug.LogError(e); Debug.LogError(e);
Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number."); Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number.");

View File

@ -15,6 +15,9 @@ namespace XNodeEditor {
/// <summary>C#'s Script Icon [The one MonoBhevaiour Scripts have].</summary> /// <summary>C#'s Script Icon [The one MonoBhevaiour Scripts have].</summary>
private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D); private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D);
/// Saves Attribute from Type+Field for faster lookup. Resets on recompiles.
private static Dictionary<Type, Dictionary<string, Dictionary<Type, Attribute>>> typeAttributes = new Dictionary<Type, Dictionary<string, Dictionary<Type, Attribute>>>();
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);
@ -22,7 +25,7 @@ namespace XNodeEditor {
public static bool GetAttrib<T>(object[] attribs, out T attribOut) 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] is T){
attribOut = attribs[i] as T; attribOut = attribs[i] as T;
return true; return true;
} }
@ -32,7 +35,15 @@ namespace XNodeEditor {
} }
public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute { public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute {
object[] attribs = classType.GetField(fieldName).GetCustomAttributes(typeof(T), false); // If we can't find field in the first run, it's probably a private field in a base class.
FieldInfo field = NodeEditorWindow.GetFieldInfo(classType, fieldName);
// This shouldn't happen. Ever.
if (field == null) {
Debug.LogWarning("Field " + fieldName + " couldnt be found");
attribOut = null;
return false;
}
object[] attribs = field.GetCustomAttributes(typeof(T), true);
return GetAttrib(attribs, out attribOut); return GetAttrib(attribs, out attribOut);
} }
@ -45,6 +56,34 @@ namespace XNodeEditor {
return false; return false;
} }
public static bool GetCachedAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute {
Dictionary<string, Dictionary<Type, Attribute>> typeFields;
if (!typeAttributes.TryGetValue(classType, out typeFields)) {
typeFields = new Dictionary<string, Dictionary<Type, Attribute>>();
typeAttributes.Add(classType, typeFields);
}
Dictionary<Type, Attribute> typeTypes;
if (!typeFields.TryGetValue(fieldName, out typeTypes)) {
typeTypes = new Dictionary<Type, Attribute>();
typeFields.Add(fieldName, typeTypes);
}
Attribute attr;
if (!typeTypes.TryGetValue(typeof(T), out attr)) {
if (GetAttrib<T>(classType, fieldName, out attribOut)) typeTypes.Add(typeof(T), attribOut);
else typeTypes.Add(typeof(T), null);
}
if (attr == null) {
attribOut = null;
return false;
}
attribOut = attr as T;
return true;
}
/// <summary> Returns true if this can be casted to <see cref="Type"/></summary> /// <summary> Returns true if this can be casted to <see cref="Type"/></summary>
public static bool IsCastableTo(this Type from, Type to) { public static bool IsCastableTo(this Type from, Type to) {
if (to.IsAssignableFrom(from)) return true; if (to.IsAssignableFrom(from)) return true;

View File

@ -61,15 +61,38 @@ namespace XNodeEditor {
public XNode.NodeGraph graph; public XNode.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, NodeEditorPreferences.GetSettings().zoomOutLimit); Repaint(); } }
private float _zoom = 1; private float _zoom = 1;
void OnFocus() { void OnFocus() {
current = this; current = this;
graphEditor = NodeGraphEditor.GetEditor(graph); ValidateGraphEditor();
if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
} }
[InitializeOnLoadMethod]
private static void OnLoad() {
Selection.selectionChanged -= OnSelectionChanged;
Selection.selectionChanged += OnSelectionChanged;
}
/// <summary> Handle Selection Change events</summary>
private static void OnSelectionChanged() {
XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph;
if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) {
Open(nodeGraph);
}
}
/// <summary> Make sure the graph editor is assigned and to the right object </summary>
private void ValidateGraphEditor() {
NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this);
if (this.graphEditor != graphEditor) {
this.graphEditor = graphEditor;
graphEditor.OnOpen();
}
}
/// <summary> Create editor window </summary> /// <summary> Create editor window </summary>
public static NodeEditorWindow Init() { public static NodeEditorWindow Init() {
NodeEditorWindow w = CreateInstance<NodeEditorWindow>(); NodeEditorWindow w = CreateInstance<NodeEditorWindow>();
@ -123,8 +146,9 @@ namespace XNodeEditor {
public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) { public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) {
Vector2 center = position.size * 0.5f; Vector2 center = position.size * 0.5f;
float xOffset = (center.x * zoom + (panOffset.x + gridPosition.x)); // UI Sharpness complete fix - Round final offset not panOffset
float yOffset = (center.y * zoom + (panOffset.y + gridPosition.y)); float xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x));
float yOffset = Mathf.Round(center.y * zoom + (panOffset.y + gridPosition.y));
return new Vector2(xOffset, yOffset); return new Vector2(xOffset, yOffset);
} }
@ -146,14 +170,21 @@ namespace XNodeEditor {
public static bool OnOpen(int instanceID, int line) { public static bool OnOpen(int instanceID, int line) {
XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph; XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph;
if (nodeGraph != null) { if (nodeGraph != null) {
NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; Open(nodeGraph);
w.wantsMouseMove = true;
w.graph = nodeGraph;
return true; return true;
} }
return false; return false;
} }
/// <summary>Open the provided graph in the NodeEditor</summary>
public static void Open(XNode.NodeGraph graph) {
if (!graph) return;
NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
w.wantsMouseMove = true;
w.graph = graph;
}
/// <summary> Repaint all open NodeEditorWindows. </summary> /// <summary> Repaint all open NodeEditorWindows. </summary>
public static void RepaintAll() { public static void RepaintAll() {
NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll<NodeEditorWindow>(); NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll<NodeEditorWindow>();

View File

@ -8,13 +8,16 @@ namespace XNodeEditor {
/// <summary> Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. </summary> /// <summary> Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. </summary>
[CustomNodeGraphEditor(typeof(XNode.NodeGraph))] [CustomNodeGraphEditor(typeof(XNode.NodeGraph))]
public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase<NodeGraphEditor, NodeGraphEditor.CustomNodeGraphEditorAttribute, XNode.NodeGraph> { public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase<NodeGraphEditor, NodeGraphEditor.CustomNodeGraphEditorAttribute, XNode.NodeGraph> {
/// <summary> The position of the window in screen space. </summary> [Obsolete("Use window.position instead")]
public Rect position; public Rect position { get { return window.position; } set { window.position = value; } }
/// <summary> Are we currently renaming a node? </summary> /// <summary> Are we currently renaming a node? </summary>
protected bool isRenaming; protected bool isRenaming;
public virtual void OnGUI() { } public virtual void OnGUI() { }
/// <summary> Called when opened by NodeEditorWindow </summary>
public virtual void OnOpen() { }
public virtual Texture2D GetGridTexture() { public virtual Texture2D GetGridTexture() {
return NodeEditorPreferences.GetSettings().gridTexture; return NodeEditorPreferences.GetSettings().gridTexture;
} }
@ -38,10 +41,43 @@ namespace XNodeEditor {
return ObjectNames.NicifyVariableName(type.ToString().Replace('.', '/')); return ObjectNames.NicifyVariableName(type.ToString().Replace('.', '/'));
} }
/// <summary> Add items for the context menu when right-clicking this node. Override to add custom menu items. </summary>
public virtual void AddContextMenuItems(GenericMenu menu) {
Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition);
for (int i = 0; i < NodeEditorWindow.nodeTypes.Length; i++) {
Type type = NodeEditorWindow.nodeTypes[i];
//Get node context menu path
string path = GetNodeMenuName(type);
if (string.IsNullOrEmpty(path)) continue;
menu.AddItem(new GUIContent(path), false, () => {
CreateNode(type, pos);
});
}
menu.AddSeparator("");
menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorWindow.OpenPreferences());
NodeEditorWindow.AddCustomContextMenuItems(menu, target);
}
public virtual Color GetPortColor(XNode.NodePort port) {
return GetTypeColor(port.ValueType);
}
public virtual Color GetTypeColor(Type type) { public virtual Color GetTypeColor(Type type) {
return NodeEditorPreferences.GetTypeColor(type); return NodeEditorPreferences.GetTypeColor(type);
} }
/// <summary> Create a node and save it in the graph asset </summary>
public virtual void CreateNode(Type type, Vector2 position) {
XNode.Node node = target.AddNode(type);
node.position = position;
if (string.IsNullOrEmpty(node.name)) node.name = UnityEditor.ObjectNames.NicifyVariableName(type.Name);
AssetDatabase.AddObjectToAsset(node, target);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
NodeEditorWindow.RepaintAll();
}
/// <summary> Creates a copy of the original node in the graph </summary> /// <summary> Creates a copy of the original node in the graph </summary>
public XNode.Node CopyNode(XNode.Node original) { public XNode.Node CopyNode(XNode.Node original) {
XNode.Node node = target.CopyNode(original); XNode.Node node = target.CopyNode(original);
@ -65,7 +101,7 @@ namespace XNodeEditor {
public string editorPrefsKey; public string editorPrefsKey;
/// <summary> Tells a NodeGraphEditor which Graph type it is an editor for </summary> /// <summary> Tells a NodeGraphEditor which Graph type it is an editor for </summary>
/// <param name="inspectedType">Type that this editor can edit</param> /// <param name="inspectedType">Type that this editor can edit</param>
/// <param name="uniquePreferencesID">Define unique key for unique layout settings instance</param> /// <param name="editorPrefsKey">Define unique key for unique layout settings instance</param>
public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") { public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") {
this.inspectedType = inspectedType; this.inspectedType = inspectedType;
this.editorPrefsKey = editorPrefsKey; this.editorPrefsKey = editorPrefsKey;

View File

@ -0,0 +1,66 @@
using UnityEditor;
using UnityEngine;
namespace XNodeEditor {
/// <summary> Utility for renaming assets </summary>
public class RenamePopup : EditorWindow {
public static RenamePopup current { get; private set; }
public Object target;
public string input;
private bool firstFrame = true;
/// <summary> Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply.
public static RenamePopup Show(Object target, float width = 200) {
RenamePopup window = EditorWindow.GetWindow<RenamePopup>(true, "Rename " + target.name, true);
if (current != null) current.Close();
current = window;
window.target = target;
window.input = target.name;
window.minSize = new Vector2(100, 44);
window.position = new Rect(0, 0, width, 44);
GUI.FocusControl("ClearAllFocus");
window.UpdatePositionToMouse();
return window;
}
private void UpdatePositionToMouse() {
if (Event.current == null) return;
Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
Rect pos = position;
pos.x = mousePoint.x - position.width * 0.5f;
pos.y = mousePoint.y - 10;
position = pos;
}
private void OnLostFocus() {
// Make the popup close on lose focus
Close();
}
private void OnGUI() {
if (firstFrame) {
UpdatePositionToMouse();
firstFrame = false;
}
input = EditorGUILayout.TextField(input);
Event e = Event.current;
// If input is empty, revert name to default instead
if (input == null || input.Trim() == "") {
if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name);
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close();
}
}
// Rename asset to input text
else {
if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = input;
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close();
}
}
}
}
}

View File

@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 4ef3ddc25518318469bce838980c64be
timeCreated: 1552067957
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -41,6 +41,16 @@ namespace XNode {
Override, Override,
} }
/// <summary> Tells which types of input to allow </summary>
public enum TypeConstraint {
/// <summary> Allow all types of input</summary>
None,
/// <summary> Allow similar and inherited types </summary>
Inherited,
/// <summary> Allow only similar types </summary>
Strict,
}
/// <summary> Iterate over all ports on this node. </summary> /// <summary> Iterate over all ports on this node. </summary>
public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values) yield return port; } } public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values) yield return port; } }
/// <summary> Iterate over all outputs on this node. </summary> /// <summary> Iterate over all outputs on this node. </summary>
@ -60,7 +70,12 @@ namespace XNode {
/// <summary> It is recommended not to modify these at hand. Instead, see <see cref="InputAttribute"/> and <see cref="OutputAttribute"/> </summary> /// <summary> It is recommended not to modify these at hand. Instead, see <see cref="InputAttribute"/> and <see cref="OutputAttribute"/> </summary>
[SerializeField] private NodePortDictionary ports = new NodePortDictionary(); [SerializeField] private NodePortDictionary ports = new NodePortDictionary();
/// <summary> Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable </summary>
public static NodeGraph graphHotfix;
protected void OnEnable() { protected void OnEnable() {
if (graphHotfix != null) graph = graphHotfix;
graphHotfix = null;
UpdateStaticPorts(); UpdateStaticPorts();
Init(); Init();
} }
@ -70,7 +85,7 @@ namespace XNode {
NodeDataCache.UpdatePorts(this, ports); NodeDataCache.UpdatePorts(this, ports);
} }
/// <summary> Initialize node. Called on creation. </summary> /// <summary> Initialize node. Called on enable. </summary>
protected virtual void Init() { } protected virtual void Init() { }
/// <summary> Checks all connections for invalid references, and removes them. </summary> /// <summary> Checks all connections for invalid references, and removes them. </summary>
@ -79,27 +94,24 @@ namespace XNode {
} }
#region Instance Ports #region Instance Ports
/// <summary> Convenience function. /// <summary> Convenience function. </summary>
/// </summary>
/// <seealso cref="AddInstancePort"/> /// <seealso cref="AddInstancePort"/>
/// <seealso cref="AddInstanceOutput"/> /// <seealso cref="AddInstanceOutput"/>
public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) { public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Input, connectionType, fieldName); return AddInstancePort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName);
} }
/// <summary> Convenience function. /// <summary> Convenience function. </summary>
/// </summary>
/// <seealso cref="AddInstancePort"/> /// <seealso cref="AddInstancePort"/>
/// <seealso cref="AddInstanceInput"/> /// <seealso cref="AddInstanceInput"/>
public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) { public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Output, connectionType, fieldName); return AddInstancePort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName);
} }
/// <summary> Add a dynamic, serialized port to this node. /// <summary> Add a dynamic, serialized port to this node. </summary>
/// </summary>
/// <seealso cref="AddInstanceInput"/> /// <seealso cref="AddInstanceInput"/>
/// <seealso cref="AddInstanceOutput"/> /// <seealso cref="AddInstanceOutput"/>
private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) { private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
if (fieldName == null) { if (fieldName == null) {
fieldName = "instanceInput_0"; fieldName = "instanceInput_0";
int i = 0; int i = 0;
@ -108,13 +120,15 @@ namespace XNode {
Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this);
return ports[fieldName]; return ports[fieldName];
} }
NodePort port = new NodePort(fieldName, type, direction, connectionType, this); NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this);
ports.Add(fieldName, port); ports.Add(fieldName, port);
return port; return port;
} }
/// <summary> Remove an instance port from the node </summary> /// <summary> Remove an instance port from the node </summary>
public void RemoveInstancePort(string fieldName) { public void RemoveInstancePort(string fieldName) {
NodePort instancePort = GetPort(fieldName);
if (instancePort == null) throw new ArgumentException("port " + fieldName + " doesn't exist");
RemoveInstancePort(GetPort(fieldName)); RemoveInstancePort(GetPort(fieldName));
} }
@ -198,22 +212,25 @@ namespace XNode {
foreach (NodePort port in Ports) port.ClearConnections(); foreach (NodePort port in Ports) port.ClearConnections();
} }
public override int GetHashCode() { #region Attributes
return JsonUtility.ToJson(this).GetHashCode();
}
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary> /// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public class InputAttribute : Attribute { public class InputAttribute : Attribute {
public ShowBackingValue backingValue; public ShowBackingValue backingValue;
public ConnectionType connectionType; public ConnectionType connectionType;
public bool instancePortList;
public TypeConstraint typeConstraint;
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary> /// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param> /// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
/// <param name="connectionType">Should we allow multiple connections? </param> /// <param name="connectionType">Should we allow multiple connections? </param>
public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple) { /// <param name="typeConstraint">Constrains which input connections can be made to this port </param>
/// <param name="instancePortList">If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays </param>
public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool instancePortList = false) {
this.backingValue = backingValue; this.backingValue = backingValue;
this.connectionType = connectionType; this.connectionType = connectionType;
this.instancePortList = instancePortList;
this.typeConstraint = typeConstraint;
} }
} }
@ -222,13 +239,16 @@ namespace XNode {
public class OutputAttribute : Attribute { public class OutputAttribute : Attribute {
public ShowBackingValue backingValue; public ShowBackingValue backingValue;
public ConnectionType connectionType; public ConnectionType connectionType;
public bool instancePortList;
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary> /// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param> /// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
/// <param name="connectionType">Should we allow multiple connections? </param> /// <param name="connectionType">Should we allow multiple connections? </param>
public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple) { /// <param name="instancePortList">If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays </param>
public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool instancePortList = false) {
this.backingValue = backingValue; this.backingValue = backingValue;
this.connectionType = connectionType; this.connectionType = connectionType;
this.instancePortList = instancePortList;
} }
} }
@ -243,19 +263,19 @@ namespace XNode {
} }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeTint : Attribute { public class NodeTintAttribute : Attribute {
public Color color; public Color color;
/// <summary> Specify a color for this node type </summary> /// <summary> Specify a color for this node type </summary>
/// <param name="r"> Red [0.0f .. 1.0f] </param> /// <param name="r"> Red [0.0f .. 1.0f] </param>
/// <param name="g"> Green [0.0f .. 1.0f] </param> /// <param name="g"> Green [0.0f .. 1.0f] </param>
/// <param name="b"> Blue [0.0f .. 1.0f] </param> /// <param name="b"> Blue [0.0f .. 1.0f] </param>
public NodeTint(float r, float g, float b) { public NodeTintAttribute(float r, float g, float b) {
color = new Color(r, g, b); color = new Color(r, g, b);
} }
/// <summary> Specify a color for this node type </summary> /// <summary> Specify a color for this node type </summary>
/// <param name="hex"> HEX color value </param> /// <param name="hex"> HEX color value </param>
public NodeTint(string hex) { public NodeTintAttribute(string hex) {
ColorUtility.TryParseHtmlString(hex, out color); ColorUtility.TryParseHtmlString(hex, out color);
} }
@ -263,20 +283,21 @@ namespace XNode {
/// <param name="r"> Red [0 .. 255] </param> /// <param name="r"> Red [0 .. 255] </param>
/// <param name="g"> Green [0 .. 255] </param> /// <param name="g"> Green [0 .. 255] </param>
/// <param name="b"> Blue [0 .. 255] </param> /// <param name="b"> Blue [0 .. 255] </param>
public NodeTint(byte r, byte g, byte b) { public NodeTintAttribute(byte r, byte g, byte b) {
color = new Color32(r, g, b, byte.MaxValue); color = new Color32(r, g, b, byte.MaxValue);
} }
} }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeWidth : Attribute { public class NodeWidthAttribute : Attribute {
public int width; public int width;
/// <summary> Specify a width for this node type </summary> /// <summary> Specify a width for this node type </summary>
/// <param name="width"> Width </param> /// <param name="width"> Width </param>
public NodeWidth(int width) { public NodeWidthAttribute(int width) {
this.width = width; this.width = width;
} }
} }
#endregion
[Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver { [Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver {
[SerializeField] private List<string> keys = new List<string>(); [SerializeField] private List<string> keys = new List<string>();

View File

@ -30,7 +30,7 @@ namespace XNode {
NodePort staticPort; NodePort staticPort;
if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { if (staticPorts.TryGetValue(port.fieldName, out staticPort)) {
// If port exists but with wrong settings, remove it. Re-add it later. // If port exists but with wrong settings, remove it. Re-add it later.
if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction) ports.Remove(port.fieldName); if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction || port.typeConstraint != staticPort.typeConstraint) ports.Remove(port.fieldName);
else port.ValueType = staticPort.ValueType; else port.ValueType = staticPort.ValueType;
} }
// If port doesn't exist anymore, remove it // If port doesn't exist anymore, remove it
@ -68,12 +68,24 @@ namespace XNode {
} }
} }
public static List<FieldInfo> GetNodeFields(System.Type nodeType) {
List<System.Reflection.FieldInfo> fieldInfo = new List<System.Reflection.FieldInfo>(nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance));
// GetFields doesnt return inherited private fields, so walk through base types and pick those up
System.Type tempType = nodeType;
while ((tempType = tempType.BaseType) != typeof(XNode.Node)) {
fieldInfo.AddRange(tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance));
}
return fieldInfo;
}
private static void CachePorts(System.Type nodeType) { private static void CachePorts(System.Type nodeType) {
System.Reflection.FieldInfo[] fieldInfo = nodeType.GetFields(); List<System.Reflection.FieldInfo> fieldInfo = GetNodeFields(nodeType);
for (int i = 0; i < fieldInfo.Length; i++) {
for (int i = 0; i < fieldInfo.Count; i++) {
//Get InputAttribute and OutputAttribute //Get InputAttribute and OutputAttribute
object[] attribs = fieldInfo[i].GetCustomAttributes(false); object[] attribs = fieldInfo[i].GetCustomAttributes(true);
Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; 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; Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute;

View File

@ -18,18 +18,20 @@ namespace XNode {
/// <summary> Add a node to the graph by type </summary> /// <summary> Add a node to the graph by type </summary>
public virtual Node AddNode(Type type) { public virtual Node AddNode(Type type) {
Node.graphHotfix = this;
Node node = ScriptableObject.CreateInstance(type) as Node; Node node = ScriptableObject.CreateInstance(type) as Node;
nodes.Add(node);
node.graph = this; node.graph = this;
nodes.Add(node);
return node; return node;
} }
/// <summary> Creates a copy of the original node in the graph </summary> /// <summary> Creates a copy of the original node in the graph </summary>
public virtual Node CopyNode(Node original) { public virtual Node CopyNode(Node original) {
Node.graphHotfix = this;
Node node = ScriptableObject.Instantiate(original); Node node = ScriptableObject.Instantiate(original);
node.graph = this;
node.ClearConnections(); node.ClearConnections();
nodes.Add(node); nodes.Add(node);
node.graph = this;
return node; return node;
} }
@ -58,6 +60,7 @@ namespace XNode {
// Instantiate all nodes inside the graph // Instantiate all nodes inside the graph
for (int i = 0; i < nodes.Count; i++) { for (int i = 0; i < nodes.Count; i++) {
if (nodes[i] == null) continue; if (nodes[i] == null) continue;
Node.graphHotfix = graph;
Node node = Instantiate(nodes[i]) as Node; Node node = Instantiate(nodes[i]) as Node;
node.graph = graph; node.graph = graph;
graph.nodes[i] = node; graph.nodes[i] = node;

View File

@ -21,6 +21,7 @@ namespace XNode {
public IO direction { get { return _direction; } } public IO direction { get { return _direction; } }
public Node.ConnectionType connectionType { get { return _connectionType; } } public Node.ConnectionType connectionType { get { return _connectionType; } }
public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } }
/// <summary> Is this port connected to anytihng? </summary> /// <summary> Is this port connected to anytihng? </summary>
public bool IsConnected { get { return connections.Count != 0; } } public bool IsConnected { get { return connections.Count != 0; } }
@ -50,6 +51,7 @@ namespace XNode {
[SerializeField] private List<PortConnection> connections = new List<PortConnection>(); [SerializeField] private List<PortConnection> connections = new List<PortConnection>();
[SerializeField] private IO _direction; [SerializeField] private IO _direction;
[SerializeField] private Node.ConnectionType _connectionType; [SerializeField] private Node.ConnectionType _connectionType;
[SerializeField] private Node.TypeConstraint _typeConstraint;
[SerializeField] private bool _dynamic; [SerializeField] private bool _dynamic;
/// <summary> Construct a static targetless nodeport. Used as a template. </summary> /// <summary> Construct a static targetless nodeport. Used as a template. </summary>
@ -62,6 +64,7 @@ namespace XNode {
if (attribs[i] is Node.InputAttribute) { if (attribs[i] is Node.InputAttribute) {
_direction = IO.Input; _direction = IO.Input;
_connectionType = (attribs[i] as Node.InputAttribute).connectionType; _connectionType = (attribs[i] as Node.InputAttribute).connectionType;
_typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint;
} else if (attribs[i] is Node.OutputAttribute) { } else if (attribs[i] is Node.OutputAttribute) {
_direction = IO.Output; _direction = IO.Output;
_connectionType = (attribs[i] as Node.OutputAttribute).connectionType; _connectionType = (attribs[i] as Node.OutputAttribute).connectionType;
@ -76,17 +79,19 @@ namespace XNode {
_direction = nodePort.direction; _direction = nodePort.direction;
_dynamic = nodePort._dynamic; _dynamic = nodePort._dynamic;
_connectionType = nodePort._connectionType; _connectionType = nodePort._connectionType;
_typeConstraint = nodePort._typeConstraint;
_node = node; _node = node;
} }
/// <summary> Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. </summary> /// <summary> Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. </summary>
public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node node) { public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) {
_fieldName = fieldName; _fieldName = fieldName;
this.ValueType = type; this.ValueType = type;
_direction = direction; _direction = direction;
_node = node; _node = node;
_dynamic = true; _dynamic = true;
_connectionType = connectionType; _connectionType = connectionType;
_typeConstraint = typeConstraint;
} }
/// <summary> Initialize the delegate for this NodePort. </summary> /// <summary> Initialize the delegate for this NodePort. </summary>
@ -160,6 +165,15 @@ namespace XNode {
port.node.OnCreateConnection(this, port); port.node.OnCreateConnection(this, port);
} }
public List<NodePort> GetConnections() {
List<NodePort> result = new List<NodePort>();
for (int i = 0; i < connections.Count; i++) {
NodePort port = GetConnection(i);
if (port != null) result.Add(port);
}
return result;
}
public NodePort GetConnection(int i) { public NodePort GetConnection(int i) {
//If the connection is broken for some reason, remove it. //If the connection is broken for some reason, remove it.
if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) { if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) {
@ -189,6 +203,23 @@ namespace XNode {
return false; return false;
} }
/// <summary> Returns true if this port can connect to specified port </summary>
public bool CanConnectTo(NodePort port) {
// Figure out which is input and which is output
NodePort input = null, output = null;
if (IsInput) input = this;
else output = this;
if (port.IsInput) input = port;
else output = port;
// If there isn't one of each, they can't connect
if (input == null || output == null) return false;
// Check type constraints
if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false;
if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false;
// Success
return true;
}
/// <summary> Disconnect this port from another port </summary> /// <summary> Disconnect this port from another port </summary>
public void Disconnect(NodePort port) { public void Disconnect(NodePort port) {
// Remove this ports connection to the other // Remove this ports connection to the other
@ -210,6 +241,25 @@ namespace XNode {
if (port != null) port.node.OnRemoveConnection(port); if (port != null) port.node.OnRemoveConnection(port);
} }
/// <summary> Disconnect this port from another port </summary>
public void Disconnect(int i) {
// Remove the other ports connection to this port
NodePort otherPort = connections[i].Port;
if (otherPort != null) {
for (int k = 0; k < otherPort.connections.Count; k++) {
if (otherPort.connections[k].Port == this) {
otherPort.connections.RemoveAt(i);
}
}
}
// Remove this ports connection to the other
connections.RemoveAt(i);
// Trigger OnRemoveConnection
node.OnRemoveConnection(this);
if (otherPort != null) otherPort.node.OnRemoveConnection(otherPort);
}
public void ClearConnections() { public void ClearConnections() {
while (connections.Count > 0) { while (connections.Count > 0) {
Disconnect(connections[0].Port); Disconnect(connections[0].Port);
@ -221,6 +271,58 @@ namespace XNode {
return connections[index].reroutePoints; return connections[index].reroutePoints;
} }
/// <summary> Swap connections with another node </summary>
public void SwapConnections(NodePort targetPort) {
int aConnectionCount = connections.Count;
int bConnectionCount = targetPort.connections.Count;
List<NodePort> portConnections = new List<NodePort>();
List<NodePort> targetPortConnections = new List<NodePort>();
// Cache port connections
for (int i = 0; i < aConnectionCount; i++)
portConnections.Add(connections[i].Port);
// Cache target port connections
for (int i = 0; i < bConnectionCount; i++)
targetPortConnections.Add(targetPort.connections[i].Port);
ClearConnections();
targetPort.ClearConnections();
// Add port connections to targetPort
for (int i = 0; i < portConnections.Count; i++)
targetPort.Connect(portConnections[i]);
// Add target port connections to this one
for (int i = 0; i < targetPortConnections.Count; i++)
Connect(targetPortConnections[i]);
}
/// <summary> Copy all connections pointing to a node and add them to this one </summary>
public void AddConnections(NodePort targetPort) {
int connectionCount = targetPort.ConnectionCount;
for (int i = 0; i < connectionCount; i++) {
PortConnection connection = targetPort.connections[i];
NodePort otherPort = connection.Port;
Connect(otherPort);
}
}
/// <summary> Move all connections pointing to this node, to another node </summary>
public void MoveConnections(NodePort targetPort) {
int connectionCount = connections.Count;
// Add connections to target port
for (int i = 0; i < connectionCount; i++) {
PortConnection connection = targetPort.connections[i];
NodePort otherPort = connection.Port;
Connect(otherPort);
}
ClearConnections();
}
/// <summary> Swap connected nodes from the old list with nodes from the new list </summary> /// <summary> Swap connected nodes from the old list with nodes from the new list </summary>
public void Redirect(List<Node> oldNodes, List<Node> newNodes) { public void Redirect(List<Node> oldNodes, List<Node> newNodes) {
foreach (PortConnection connection in connections) { foreach (PortConnection connection in connections) {