1
0
mirror of https://github.com/Siccity/xNode.git synced 2025-12-21 01:36:03 +08:00

Merge branch 'master' into examples

This commit is contained in:
Thor Brigsted 2019-05-13 10:54:45 +02:00
commit 9d2684eae4
26 changed files with 1302 additions and 401 deletions

6
.gitignore vendored
View File

@ -22,4 +22,8 @@ sysinfo.txt
README.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.
* Methods, Types and properties PascalCase
* 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
* 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)
[![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.
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
* 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.)
* Does not rely on any 3rd party plugins
* Custom node inspector code is very similar to regular custom inspector code
* Supported from Unity 5.3 and up
### Wiki
* [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:
```csharp
[System.Serializable]
// public classes deriving from Node are registered as nodes for use within a graph
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
[Input] public float a;
@ -71,3 +74,4 @@ Feel free to also leave suggestions/requests in the [issues](https://github.com/
Projects using xNode:
* [Graphmesh](https://github.com/Siccity/Graphmesh "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

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
@ -12,40 +12,21 @@ namespace XNodeEditor {
/// <summary> Fires every whenever a node was modified through the editor </summary>
public static Action<XNode.Node> onUpdateNode;
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 readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>();
public virtual void OnHeaderGUI() {
string title = target.name;
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));
}
GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
}
/// <summary> Draws standard field editors for all public fields </summary>
public virtual void OnBodyGUI() {
// Unity specifically requires this to save/update any serial object.
// serializedObject.Update(); must go at the start of an inspector gui, and
// serializedObject.ApplyModifiedProperties(); goes at the end.
serializedObject.Update();
string[] excludes = { "m_Script", "graph", "position", "ports" };
portPositions = new Dictionary<XNode.NodePort, Vector2>();
// Iterate through serialized properties and draw them like the Inspector (But with ports)
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
EditorGUIUtility.labelWidth = 84;
@ -54,36 +35,67 @@ namespace XNodeEditor {
if (excludes.Contains(iterator.name)) continue;
NodeEditorGUILayout.PropertyField(iterator, true);
}
// Iterate through dynamic ports and draw them in the order in which they are serialized
foreach (XNode.NodePort dynamicPort in target.DynamicPorts) {
if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue;
NodeEditorGUILayout.PortField(dynamicPort);
}
serializedObject.ApplyModifiedProperties();
}
public virtual int GetWidth() {
Type type = target.GetType();
if (NodeEditorWindow.nodeWidth.ContainsKey(type)) return NodeEditorWindow.nodeWidth[type];
int width;
if (NodeEditorWindow.nodeWidth.TryGetValue(type, out width)) return width;
else return 208;
}
public virtual Color GetTint() {
Type type = target.GetType();
if (NodeEditorWindow.nodeTint.ContainsKey(type)) return NodeEditorWindow.nodeTint[type];
Color color;
if (NodeEditorWindow.nodeTint.TryGetValue(type, out color)) return color;
else return Color.white;
}
public void InitiateRename() {
renaming = 1;
public virtual GUIStyle GetBodyStyle() {
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) {
if (newName == null || newName.Trim() == "") newName = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name);
target.name = newName;
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
}
[AttributeUsage(AttributeTargets.Class)]
public class CustomNodeEditorAttribute : Attribute,
XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node>.INodeEditorAttrib {
XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node>.INodeEditorAttrib {
private Type inspectedType;
/// <summary> Tells a NodeEditor which Node type it is an editor for </summary>
/// <param name="inspectedType">Type that this editor can edit</param>
/// <param name="contextMenuName">Path to the node</param>
public CustomNodeEditorAttribute(Type inspectedType) {
this.inspectedType = inspectedType;
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
@ -22,11 +22,11 @@ namespace XNodeEditor {
[NonSerialized] private List<Vector2> draggedOutputReroutes = new List<Vector2>();
private RerouteReference hoveredReroute = new RerouteReference();
private List<RerouteReference> selectedReroutes = new List<RerouteReference>();
private Rect nodeRects;
private Vector2 dragBoxStart;
private UnityEngine.Object[] preBoxSelection;
private RerouteReference[] preBoxSelectionReroute;
private Rect selectionBox;
private bool isDoubleClick = false;
private struct RerouteReference {
public XNode.NodePort port;
@ -52,13 +52,15 @@ namespace XNodeEditor {
case EventType.MouseMove:
break;
case EventType.ScrollWheel:
float oldZoom = zoom;
if (e.delta.y > 0) zoom += 0.1f * zoom;
else zoom -= 0.1f * zoom;
if (NodeEditorPreferences.GetSettings().zoomToMouse) panOffset += (1 - oldZoom / zoom) * (WindowToGridPosition(e.mousePosition) + panOffset);
break;
case EventType.MouseDrag:
if (e.button == 0) {
if (IsDraggingPort) {
if (IsHoveringPort && hoveredPort.IsInput) {
if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) {
if (!draggedOutput.IsConnectedTo(hoveredPort)) {
draggedOutputTarget = hoveredPort;
}
@ -81,21 +83,40 @@ namespace XNodeEditor {
for (int i = 0; i < Selection.objects.Length; i++) {
if (Selection.objects[i] is XNode.Node) {
XNode.Node node = Selection.objects[i] as XNode.Node;
Vector2 initial = node.position;
node.position = mousePos + dragOffset[i];
if (gridSnap) {
node.position.x = (Mathf.Round((node.position.x + 8) / 16) * 16) - 8;
node.position.y = (Mathf.Round((node.position.y + 8) / 16) * 16) - 8;
}
// Offset portConnectionPoints instantly if a node is dragged so they aren't delayed by a frame.
Vector2 offset = node.position - initial;
if (offset.sqrMagnitude > 0) {
foreach (XNode.NodePort output in node.Outputs) {
Rect rect;
if (portConnectionPoints.TryGetValue(output, out rect)) {
rect.position += offset;
portConnectionPoints[output] = rect;
}
}
foreach (XNode.NodePort input in node.Inputs) {
Rect rect;
if (portConnectionPoints.TryGetValue(input, out rect)) {
rect.position += offset;
portConnectionPoints[input] = rect;
}
}
}
}
}
// Move selected reroutes with offset
for (int i = 0; i < selectedReroutes.Count; i++) {
Vector2 pos = mousePos + dragOffset[Selection.objects.Length + i];
pos.x -= 8;
pos.y -= 8;
if (gridSnap) {
pos.x = (Mathf.Round((pos.x + 8) / 16) * 16);
pos.y = (Mathf.Round((pos.y + 8) / 16) * 16);
pos.x = (Mathf.Round(pos.x / 16) * 16);
pos.y = (Mathf.Round(pos.y / 16) * 16);
}
selectedReroutes[i].SetPoint(pos);
}
@ -115,12 +136,7 @@ namespace XNodeEditor {
Repaint();
}
} else if (e.button == 1 || e.button == 2) {
Vector2 tempOffset = panOffset;
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;
panOffset += e.delta * zoom;
isPanning = true;
}
break;
@ -151,6 +167,10 @@ namespace XNodeEditor {
SelectNode(hoveredNode, e.control || e.shift);
if (!e.control && !e.shift) selectedReroutes.Clear();
} 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();
currentActivity = NodeActivity.HoldNode;
} else if (IsHoveringReroute) {
@ -210,6 +230,7 @@ namespace XNodeEditor {
// If click outside node, release field focus
if (!isPanning) {
EditorGUI.FocusTextInControl(null);
EditorGUIUtility.editingTextField = false;
}
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
@ -218,6 +239,12 @@ namespace XNodeEditor {
if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) {
selectedReroutes.Clear();
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.
@ -241,26 +268,42 @@ namespace XNodeEditor {
ShowPortContextMenu(hoveredPort);
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
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) {
ShowGraphContextMenu();
GenericMenu menu = new GenericMenu();
graphEditor.AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
}
}
isPanning = false;
}
// Reset DoubleClick
isDoubleClick = false;
break;
case EventType.KeyDown:
if (EditorGUIUtility.editingTextField) break;
else if (e.keyCode == KeyCode.F) Home();
if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) {
if (IsMac()) {
if (e.keyCode == KeyCode.Return) RenameSelectedNode();
} else {
if (e.keyCode == KeyCode.F2) RenameSelectedNode();
}
break;
case EventType.ValidateCommand:
if (e.commandName == "SoftDelete") RemoveSelectedNodes();
else if (e.commandName == "Duplicate") DublicateSelectedNodes();
case EventType.ExecuteCommand:
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();
break;
case EventType.Ignore:
@ -273,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) {
dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count];
// Selected nodes
@ -295,15 +346,6 @@ namespace XNodeEditor {
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>
public void RemoveSelectedNodes() {
// We need to delete reroutes starting at the highest point index to avoid shifting indices
@ -324,7 +366,12 @@ namespace XNodeEditor {
public void RenameSelectedNode() {
if (Selection.objects.Length == 1 && Selection.activeObject is 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);
}
}
}
@ -337,8 +384,8 @@ namespace XNodeEditor {
}
}
/// <summary> Dublicate selected nodes and select the dublicates </summary>
public void DublicateSelectedNodes() {
/// <summary> Duplicate selected nodes and select the duplicates </summary>
public void DuplicateSelectedNodes() {
UnityEngine.Object[] newNodes = new UnityEngine.Object[Selection.objects.Length];
Dictionary<XNode.Node, XNode.Node> substitutes = new Dictionary<XNode.Node, XNode.Node>();
for (int i = 0; i < Selection.objects.Length; i++) {
@ -362,9 +409,8 @@ namespace XNodeEditor {
XNode.NodePort inputPort = port.direction == XNode.NodePort.IO.Input ? port : port.GetConnection(c);
XNode.NodePort outputPort = port.direction == XNode.NodePort.IO.Output ? port : port.GetConnection(c);
if (substitutes.ContainsKey(inputPort.node) && substitutes.ContainsKey(outputPort.node)) {
XNode.Node newNodeIn = substitutes[inputPort.node];
XNode.Node newNodeOut = substitutes[outputPort.node];
XNode.Node newNodeIn, newNodeOut;
if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) {
newNodeIn.UpdateStaticPorts();
newNodeOut.UpdateStaticPorts();
inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
@ -382,18 +428,19 @@ namespace XNodeEditor {
public void DrawDraggedConnection() {
if (IsDraggingPort) {
Color col = NodeEditorPreferences.GetTypeColor(draggedOutput.ValueType);
col.a = draggedOutputTarget != null ? 1.0f : 0.6f;
if (!_portConnectionPoints.ContainsKey(draggedOutput)) return;
col.a = 0.6f;
Vector2 from = _portConnectionPoints[draggedOutput].center;
Vector2 to = Vector2.zero;
Rect fromRect;
if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return;
List<Vector2> gridPoints = new List<Vector2>();
gridPoints.Add(fromRect.center);
for (int i = 0; i < draggedOutputReroutes.Count; i++) {
to = draggedOutputReroutes[i];
DrawConnection(from, to, col);
from = to;
gridPoints.Add(draggedOutputReroutes[i]);
}
to = draggedOutputTarget != null ? portConnectionPoints[draggedOutputTarget].center : WindowToGridPosition(Event.current.mousePosition);
DrawConnection(from, to, col);
if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center);
else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition));
DrawNoodle(col, gridPoints);
Color bgcol = Color.black;
Color frcol = col;
@ -416,8 +463,10 @@ namespace XNodeEditor {
Vector2 mousePos = Event.current.mousePosition;
//Get node position
Vector2 nodePos = GridToWindowPosition(node.position);
float width = 200;
if (nodeSizes.ContainsKey(node)) width = nodeSizes[node].x;
float width;
Vector2 size;
if (nodeSizes.TryGetValue(node, out size)) width = size.x;
else width = 200;
Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom));
return windowRect.Contains(mousePos);
}

View File

@ -7,24 +7,32 @@ using UnityEngine;
namespace XNodeEditor.Internal {
/// <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>
private static Dictionary<Type, Type> editorTypes;
private static Dictionary<K, T> editors = new Dictionary<K, T>();
public NodeEditorWindow window;
public K target;
public SerializedObject serializedObject;
public static T GetEditor(K target) {
public static T GetEditor(K target, NodeEditorWindow window) {
if (target == null) return null;
if (!editors.ContainsKey(target)) {
T editor;
if (!editors.TryGetValue(target, out editor)) {
Type type = target.GetType();
Type editorType = GetEditorType(type);
editors.Add(target, Activator.CreateInstance(editorType) as T);
editors[target].target = target;
editors[target].serializedObject = new SerializedObject(target);
editor = Activator.CreateInstance(editorType) as T;
editor.target = target;
editor.serializedObject = new SerializedObject(target);
editor.window = window;
editor.OnCreate();
editors.Add(target, editor);
}
T editor = editors[target];
if (editor.target == null) editor.target = target;
if (editor.window != window) editor.window = window;
if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target);
return editor;
}
@ -32,7 +40,8 @@ namespace XNodeEditor.Internal {
private static Type GetEditorType(Type type) {
if (type == null) return null;
if (editorTypes == null) CacheCustomEditors();
if (editorTypes.ContainsKey(type)) return editorTypes[type];
Type result;
if (editorTypes.TryGetValue(type, out result)) return result;
//If type isn't found, try base type
return GetEditorType(type.BaseType);
}
@ -51,6 +60,9 @@ namespace XNodeEditor.Internal {
}
}
/// <summary> Called on creation, after references have been set </summary>
public virtual void OnCreate() { }
public interface INodeEditorAttrib {
Type GetInspectedType();
}

View File

@ -10,14 +10,15 @@ namespace XNodeEditor {
public NodeGraphEditor graphEditor;
private List<UnityEngine.Object> selectionCache;
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() {
Event e = Event.current;
Matrix4x4 m = GUI.matrix;
if (graph == null) return;
graphEditor = NodeGraphEditor.GetEditor(graph);
graphEditor.position = position;
ValidateGraphEditor();
Controls();
DrawGrid(position, zoom, panOffset);
@ -28,25 +29,31 @@ namespace XNodeEditor {
DrawTooltip();
graphEditor.OnGUI();
// Run and reset onLateGUI
if (onLateGUI != null) {
onLateGUI();
onLateGUI = null;
}
GUI.matrix = m;
}
public static void BeginZoomed(Rect rect, float zoom) {
public static void BeginZoomed(Rect rect, float zoom, float topPadding) {
GUI.EndClip();
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;
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.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);
Vector3 offset = new Vector3(
(((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);
GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one);
}
@ -107,106 +114,70 @@ namespace XNodeEditor {
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>
public void DrawConnection(Vector2 startPoint, Vector2 endPoint, Color col) {
startPoint = GridToWindowPosition(startPoint);
endPoint = GridToWindowPosition(endPoint);
/// <summary> Draw a bezier from output to input in grid coordinates </summary>
public void DrawNoodle(Color col, List<Vector2> gridPoints) {
Vector2[] windowPoints = gridPoints.Select(x => GridToWindowPosition(x)).ToArray();
Handles.color = col;
int length = gridPoints.Count;
switch (NodeEditorPreferences.GetSettings().noodleType) {
case NodeEditorPreferences.NoodleType.Curve:
Vector2 startTangent = startPoint;
if (startPoint.x < endPoint.x) startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, 0.7f);
else startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, -0.7f);
Vector2 outputTangent = Vector2.right;
for (int i = 0; i < length - 1; i++) {
Vector2 inputTangent = Vector2.left;
Vector2 endTangent = endPoint;
if (startPoint.x > endPoint.x) endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, -0.7f);
else endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, 0.7f);
Handles.DrawBezier(startPoint, endPoint, startTangent, endTangent, col, null, 4);
if (i == 0) outputTangent = Vector2.right * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom;
if (i < length - 2) {
Vector2 ab = (windowPoints[i + 1] - windowPoints[i]).normalized;
Vector2 cb = (windowPoints[i + 1] - windowPoints[i + 2]).normalized;
Vector2 ac = (windowPoints[i + 2] - windowPoints[i]).normalized;
Vector2 p = (ab + cb) * 0.5f;
float tangentLength = (Vector2.Distance(windowPoints[i], windowPoints[i + 1]) + Vector2.Distance(windowPoints[i + 1], windowPoints[i + 2])) * 0.005f * zoom;
float side = ((ac.x * (windowPoints[i + 1].y - windowPoints[i].y)) - (ac.y * (windowPoints[i + 1].x - windowPoints[i].x)));
p = new Vector2(-p.y, p.x) * Mathf.Sign(side) * tangentLength;
inputTangent = p;
}
else {
inputTangent = Vector2.left * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom;
}
Handles.DrawBezier(windowPoints[i], windowPoints[i + 1], windowPoints[i] + ((outputTangent * 50) / zoom), windowPoints[i + 1] + ((inputTangent * 50) / zoom), col, null, 4);
outputTangent = -inputTangent;
}
break;
case NodeEditorPreferences.NoodleType.Line:
Handles.color = col;
Handles.DrawAAPolyLine(5, startPoint, endPoint);
for (int i = 0; i < length - 1; i++) {
Handles.DrawAAPolyLine(5, windowPoints[i], windowPoints[i + 1]);
}
break;
case NodeEditorPreferences.NoodleType.Angled:
Handles.color = col;
if (startPoint.x <= endPoint.x - (50 / zoom)) {
float midpoint = (startPoint.x + endPoint.x) * 0.5f;
Vector2 start_1 = startPoint;
Vector2 end_1 = endPoint;
start_1.x = midpoint;
end_1.x = midpoint;
Handles.DrawAAPolyLine(5, startPoint, start_1);
Handles.DrawAAPolyLine(5, start_1, end_1);
Handles.DrawAAPolyLine(5, end_1, endPoint);
} else {
float midpoint = (startPoint.y + endPoint.y) * 0.5f;
Vector2 start_1 = startPoint;
Vector2 end_1 = endPoint;
start_1.x += 25 / zoom;
end_1.x -= 25 / zoom;
Vector2 start_2 = start_1;
Vector2 end_2 = end_1;
start_2.y = midpoint;
end_2.y = midpoint;
Handles.DrawAAPolyLine(5, startPoint, start_1);
Handles.DrawAAPolyLine(5, start_1, start_2);
Handles.DrawAAPolyLine(5, start_2, end_2);
Handles.DrawAAPolyLine(5, end_2, end_1);
Handles.DrawAAPolyLine(5, end_1, endPoint);
for (int i = 0; i < length - 1; i++) {
if (i == length - 1) continue; // Skip last index
if (windowPoints[i].x <= windowPoints[i + 1].x - (50 / zoom)) {
float midpoint = (windowPoints[i].x + windowPoints[i + 1].x) * 0.5f;
Vector2 start_1 = windowPoints[i];
Vector2 end_1 = windowPoints[i + 1];
start_1.x = midpoint;
end_1.x = midpoint;
Handles.DrawAAPolyLine(5, windowPoints[i], start_1);
Handles.DrawAAPolyLine(5, start_1, end_1);
Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]);
} else {
float midpoint = (windowPoints[i].y + windowPoints[i + 1].y) * 0.5f;
Vector2 start_1 = windowPoints[i];
Vector2 end_1 = windowPoints[i + 1];
start_1.x += 25 / zoom;
end_1.x -= 25 / zoom;
Vector2 start_2 = start_1;
Vector2 end_2 = end_1;
start_2.y = midpoint;
end_2.y = midpoint;
Handles.DrawAAPolyLine(5, windowPoints[i], start_1);
Handles.DrawAAPolyLine(5, start_1, start_2);
Handles.DrawAAPolyLine(5, start_2, end_2);
Handles.DrawAAPolyLine(5, end_2, end_1);
Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]);
}
}
break;
}
@ -226,9 +197,10 @@ namespace XNodeEditor {
// Draw full connections and output > reroute
foreach (XNode.NodePort output in node.Outputs) {
//Needs cleanup. Null checks are ugly
if (!portConnectionPoints.ContainsKey(output)) continue;
Rect fromRect;
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++) {
XNode.NodePort input = output.GetConnection(k);
@ -236,19 +208,16 @@ namespace XNodeEditor {
// Error handling
if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return.
if (!input.IsConnectedTo(output)) input.Connect(output);
if (!_portConnectionPoints.ContainsKey(input)) continue;
Rect toRect;
if (!_portConnectionPoints.TryGetValue(input, out toRect)) continue;
Vector2 from = _portConnectionPoints[output].center;
Vector2 to = Vector2.zero;
List<Vector2> reroutePoints = output.GetReroutePoints(k);
// Loop through reroute points and draw the path
for (int i = 0; i < reroutePoints.Count; i++) {
to = reroutePoints[i];
DrawConnection(from, to, connectionColor);
from = to;
}
to = _portConnectionPoints[input].center;
DrawConnection(from, to, connectionColor);
List<Vector2> gridPoints = new List<Vector2>();
gridPoints.Add(fromRect.center);
gridPoints.AddRange(reroutePoints);
gridPoints.Add(toRect.center);
DrawNoodle(connectionColor, gridPoints);
// Loop through reroute points again and draw the points
for (int i = 0; i < reroutePoints.Count; i++) {
@ -283,15 +252,13 @@ namespace XNodeEditor {
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;
if (Selection.activeObject != null && Selection.activeObject is XNode.Node) {
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;
@ -319,12 +286,10 @@ namespace XNodeEditor {
if (n >= graph.nodes.Count) return;
XNode.Node node = graph.nodes[n];
NodeEditor nodeEditor = NodeEditor.GetEditor(node);
// Culling
if (e.type == EventType.Layout) {
// Cull unselected nodes outside view
if (!Selection.Contains(node) && ShouldBeCulled(nodeEditor)) {
if (!Selection.Contains(node) && ShouldBeCulled(node)) {
culledNodes.Add(node);
continue;
}
@ -334,7 +299,9 @@ namespace XNodeEditor {
_portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
NodeEditor.portPositions = new Dictionary<XNode.NodePort, Vector2>();
NodeEditor nodeEditor = NodeEditor.GetEditor(node, this);
NodeEditor.portPositions.Clear();
//Get node position
Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
@ -344,25 +311,26 @@ namespace XNodeEditor {
bool selected = selectionCache.Contains(graph.nodes[n]);
if (selected) {
GUIStyle style = new GUIStyle(NodeEditorResources.styles.nodeBody);
GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight);
highlightStyle.padding = style.padding;
style.padding = new RectOffset();
GUI.color = nodeEditor.GetTint();
GUILayout.BeginVertical(new GUIStyle(style));
GUILayout.BeginVertical(style);
GUI.color = NodeEditorPreferences.GetSettings().highlightColor;
GUILayout.BeginVertical(new GUIStyle(highlightStyle));
} else {
GUIStyle style = NodeEditorResources.styles.nodeBody;
GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
GUI.color = nodeEditor.GetTint();
GUILayout.BeginVertical(new GUIStyle(style));
GUILayout.BeginVertical(style);
}
GUI.color = guiColor;
EditorGUI.BeginChangeCheck();
//Draw node contents
nodeEditor.OnNodeGUI();
nodeEditor.OnHeaderGUI();
nodeEditor.OnBodyGUI();
//If user changed a value, notify other scripts through onUpdateNode
if (EditorGUI.EndChangeCheck()) {
@ -383,8 +351,7 @@ namespace XNodeEditor {
Vector2 portHandlePos = kvp.Value;
portHandlePos += node.position;
Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16);
if (portConnectionPoints.ContainsKey(kvp.Key)) portConnectionPoints[kvp.Key] = rect;
else portConnectionPoints.Add(kvp.Key, rect);
portConnectionPoints[kvp.Key] = rect;
}
}
@ -422,23 +389,21 @@ namespace XNodeEditor {
}
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,
//and thus, the code should not be included in build.
if (nodeHash != 0) {
if (onValidate != null && nodeHash != Selection.activeObject.GetHashCode()) onValidate.Invoke(Selection.activeObject, null);
}
if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null);
}
/// <summary> Returns true if outside window area </summary>
private bool ShouldBeCulled(XNodeEditor.NodeEditor nodeEditor) {
Vector2 nodePos = GridToWindowPositionNoClipped(nodeEditor.target.position);
private bool ShouldBeCulled(XNode.Node node) {
Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
if (nodePos.x / _zoom > position.width) return true; // Right
else if (nodePos.y / _zoom > position.height) return true; // Bottom
else if (nodeSizes.ContainsKey(nodeEditor.target)) {
Vector2 size = nodeSizes[nodeEditor.target];
else if (nodeSizes.ContainsKey(node)) {
Vector2 size = nodeSizes[node];
if (nodePos.x + size.x < 0) return true; // Left
else if (nodePos.y + size.y < 0) return true; // Top
}

View File

@ -1,8 +1,7 @@
fileFormatVersion: 2
guid: 756276bfe9a0c2f4da3930ba1964f58d
timeCreated: 1505420917
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0

View File

@ -1,14 +1,19 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace XNodeEditor {
/// <summary> xNode-specific version of <see cref="EditorGUILayout"/> </summary>
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>
public static void PropertyField(SerializedProperty property, bool includeChildren = true, params GUILayoutOption[] options) {
PropertyField(property, (GUIContent) null, includeChildren, options);
@ -36,13 +41,36 @@ namespace XNodeEditor {
else {
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 (port.direction == XNode.NodePort.IO.Input) {
// Get data from [Input] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.InputAttribute inputAttribute;
if (NodeEditorUtilities.GetAttrib(port.node.GetType(), property.name, out inputAttribute)) showBacking = inputAttribute.backingValue;
bool dynamicPortList = false;
if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) {
dynamicPortList = inputAttribute.dynamicPortList;
showBacking = inputAttribute.backingValue;
}
//Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
bool useLayoutSpace = dynamicPortList ||
showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
if (spacePadding > 0 && useLayoutSpace) {
GUILayout.Space(spacePadding);
spacePadding = 0;
}
if (dynamicPortList) {
Type type = GetType(property);
XNode.Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
return;
}
switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected
@ -61,14 +89,33 @@ namespace XNodeEditor {
}
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
} else if (port.direction == XNode.NodePort.IO.Output) {
// Get data from [Output] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.OutputAttribute outputAttribute;
if (NodeEditorUtilities.GetAttrib(port.node.GetType(), property.name, out outputAttribute)) showBacking = outputAttribute.backingValue;
bool dynamicPortList = false;
if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) {
dynamicPortList = outputAttribute.dynamicPortList;
showBacking = outputAttribute.backingValue;
}
//Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
bool useLayoutSpace = dynamicPortList ||
showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
if (spacePadding > 0 && useLayoutSpace) {
GUILayout.Space(spacePadding);
spacePadding = 0;
}
if (dynamicPortList) {
Type type = GetType(property);
XNode.Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
return;
}
switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected
@ -87,23 +134,29 @@ namespace XNodeEditor {
}
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);
Color backgroundColor = new Color32(90, 97, 105, 255);
if (NodeEditorWindow.nodeTint.ContainsKey(port.node.GetType())) backgroundColor *= NodeEditorWindow.nodeTint[port.node.GetType()];
Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType);
Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col);
// Register the handle position
Vector2 portPos = rect.center;
if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos;
else NodeEditor.portPositions.Add(port, portPos);
NodeEditor.portPositions[port] = portPos;
}
}
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>
public static void PortField(XNode.NodePort port, params GUILayoutOption[] options) {
PortField(null, port, options);
@ -113,7 +166,7 @@ namespace XNodeEditor {
public static void PortField(GUIContent label, XNode.NodePort port, params GUILayoutOption[] options) {
if (port == null) return;
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));
// If property is an input, display a regular property field and put a port handle on the left side
@ -121,13 +174,49 @@ namespace XNodeEditor {
// Display a label
EditorGUILayout.LabelField(content, options);
Rect rect = GUILayoutUtility.GetLastRect();
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
else if (port.direction == XNode.NodePort.IO.Output) {
// Display a label
EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options);
Rect rect = GUILayoutUtility.GetLastRect();
position = rect.position + new Vector2(rect.width, 0);
}
PortField(position, port);
}
/// <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 tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col);
// Register the handle position
Vector2 portPos = rect.center;
NodeEditor.portPositions[port] = portPos;
}
/// <summary> Add a port field to previous layout element. </summary>
public static void AddPortField(XNode.NodePort port) {
if (port == null) return;
Rect rect = new Rect();
// 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) {
rect = GUILayoutUtility.GetLastRect();
rect.position = rect.position - new Vector2(16, 0);
// If property is an output, display a text label and put a port handle on the right side
} else if (port.direction == XNode.NodePort.IO.Output) {
// Display a label
EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options);
rect = GUILayoutUtility.GetLastRect();
rect.position = rect.position + new Vector2(rect.width, 0);
}
@ -135,14 +224,14 @@ namespace XNodeEditor {
rect.size = new Vector2(16, 16);
Color backgroundColor = new Color32(90, 97, 105, 255);
if (NodeEditorWindow.nodeTint.ContainsKey(port.node.GetType())) backgroundColor *= NodeEditorWindow.nodeTint[port.node.GetType()];
Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType);
Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col);
// Register the handle position
Vector2 portPos = rect.center;
if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos;
else NodeEditor.portPositions.Add(port, portPos);
NodeEditor.portPositions[port] = portPos;
}
/// <summary> Draws an input and an output port on the same line </summary>
@ -162,70 +251,235 @@ namespace XNodeEditor {
GUI.color = col;
}
/// <summary> Draw an editable list of instance ports. Port names are named as "[fieldName] [index]" </summary>
#region Obsolete
[Obsolete("Use IsDynamicPortListPort instead")]
public static bool IsInstancePortListPort(XNode.NodePort port) {
return IsDynamicPortListPort(port);
}
[Obsolete("Use DynamicPortList instead")]
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) {
DynamicPortList(fieldName, type, serializedObject, io, connectionType, typeConstraint, onCreation);
}
#endregion
/// <summary> Is this port part of a DynamicPortList? </summary>
public static bool IsDynamicPortListPort(XNode.NodePort port) {
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 dynamic ports. Port names are named as "[fieldName] [index]" </summary>
/// <param name="fieldName">Supply a list for editable values</param>
/// <param name="type">Value type of added instance ports</param>
/// <param name="type">Value type of added dynamic ports</param>
/// <param name="serializedObject">The serializedObject of the node</param>
/// <param name="connectionType">Connection type of added instance ports</param>
public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple) {
/// <param name="connectionType">Connection type of added dynamic ports</param>
/// <param name="onCreation">Called on the list on creation. Use this if you want to customize the created ReorderableList</param>
public static void DynamicPortList(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;
SerializedProperty arrayData = serializedObject.FindProperty(fieldName);
var indexedPorts = node.DynamicPorts.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> dynamicPorts = 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);
list = CreateReorderableList(fieldName, dynamicPorts, 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 = dynamicPorts;
list.DoLayoutList();
}
private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) {
bool hasArrayData = arrayData != null && arrayData.isArray;
int arraySize = hasArrayData ? arrayData.arraySize : 0;
XNode.Node node = serializedObject.targetObject as XNode.Node;
ReorderableList list = new ReorderableList(dynamicPorts, null, true, true, true, true);
string label = arrayData != null ? arrayData.displayName : ObjectNames.NicifyVariableName(fieldName);
List<XNode.NodePort> instancePorts = node.InstancePorts.Where(x => x.fieldName.StartsWith(fieldName)).OrderBy(x => x.fieldName).ToList();
list.drawElementCallback =
(Rect rect, int index, bool isActive, bool isFocused) => {
XNode.NodePort port = node.GetPort(fieldName + " " + index);
if (hasArrayData) {
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.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++) {
GUILayout.BeginHorizontal();
if (GUILayout.Button("-", GUILayout.Width(30))) {
// Clear the removed ports connections
instancePorts[i].ClearConnections();
// Move following connections one step up to replace the missing connection
for (int k = i + 1; k < instancePorts.Count(); k++) {
for (int j = 0; j < instancePorts[k].ConnectionCount; j++) {
XNode.NodePort other = instancePorts[k].GetConnection(j);
instancePorts[k].Disconnect(other);
instancePorts[k - 1].Connect(other);
// Move up
if (rl.index > reorderableListIndex) {
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;
}
}
// Remove the last instance port, to avoid messing up the indexing
node.RemoveInstancePort(instancePorts[instancePorts.Count() - 1].fieldName);
// 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 dynamic 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.AddDynamicOutput(type, connectionType, XNode.Node.TypeConstraint.None, newName);
else node.AddDynamicInput(type, connectionType, typeConstraint, newName);
serializedObject.Update();
EditorUtility.SetDirty(node);
if (hasArrayData) {
arrayData.DeleteArrayElementAtIndex(i);
arraySize--;
arrayData.InsertArrayElementAtIndex(arrayData.arraySize);
}
i--;
} else {
if (hasArrayData) {
if (i < arraySize) {
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(i);
if (itemData != null) EditorGUILayout.PropertyField(itemData, new GUIContent(ObjectNames.NicifyVariableName(fieldName) + " " + i));
else EditorGUILayout.LabelField("[Missing array data]");
} else EditorGUILayout.LabelField("[Out of bounds]");
serializedObject.ApplyModifiedProperties();
};
list.onRemoveCallback =
(ReorderableList rl) => {
var indexedPorts = node.DynamicPorts.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 };
});
dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
int index = rl.index;
if (dynamicPorts.Count > index) {
// Clear the removed ports connections
dynamicPorts[index].ClearConnections();
// Move following connections one step up to replace the missing connection
for (int k = index + 1; k < dynamicPorts.Count(); k++) {
for (int j = 0; j < dynamicPorts[k].ConnectionCount; j++) {
XNode.NodePort other = dynamicPorts[k].GetConnection(j);
dynamicPorts[k].Disconnect(other);
dynamicPorts[k - 1].Connect(other);
}
}
// Remove the last dynamic port, to avoid messing up the indexing
node.RemoveDynamicPort(dynamicPorts[dynamicPorts.Count() - 1].fieldName);
serializedObject.Update();
EditorUtility.SetDirty(node);
} else {
EditorGUILayout.LabelField(instancePorts[i].fieldName);
Debug.LogWarning("DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + ". Skipping.");
}
NodeEditorGUILayout.PortField(new GUIContent(), node.GetPort(instancePorts[i].fieldName), GUILayout.Width(-4));
if (hasArrayData) {
arrayData.DeleteArrayElementAtIndex(index);
// Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues
if (dynamicPorts.Count <= arrayData.arraySize) {
while (dynamicPorts.Count <= arrayData.arraySize) {
arrayData.DeleteArrayElementAtIndex(arrayData.arraySize - 1);
}
UnityEngine.Debug.LogWarning("Array size exceeded dynamic ports size. Excess items removed.");
}
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
}
};
if (hasArrayData) {
int dynamicPortCount = dynamicPorts.Count;
while (dynamicPortCount < arrayData.arraySize) {
// Add dynamic port postfixed with an index number
string newName = arrayData.name + " 0";
int i = 0;
while (node.HasPort(newName)) newName = arrayData.name + " " + (++i);
if (io == XNode.NodePort.IO.Output) node.AddDynamicOutput(type, connectionType, typeConstraint, newName);
else node.AddDynamicInput(type, connectionType, typeConstraint, newName);
EditorUtility.SetDirty(node);
dynamicPortCount++;
}
GUILayout.EndHorizontal();
}
GUILayout.BeginHorizontal();
EditorGUILayout.Space();
if (GUILayout.Button("+", GUILayout.Width(30))) {
string newName = fieldName + " 0";
int i = 0;
while (node.HasPort(newName)) newName = fieldName + " " + (++i);
instancePorts.Add(node.AddInstanceOutput(type, connectionType, newName));
while (arrayData.arraySize < dynamicPortCount) {
arrayData.InsertArrayElementAtIndex(arrayData.arraySize);
}
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
EditorUtility.SetDirty(node);
if (hasArrayData) arrayData.InsertArrayElementAtIndex(arraySize);
}
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>
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>();
[System.Serializable]
@ -23,9 +23,16 @@ namespace XNodeEditor {
[SerializeField] private Color32 _gridBgColor = new Color(0.18f, 0.18f, 0.18f);
public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } }
[Obsolete("Use maxZoom instead")]
public float zoomOutLimit { get { return maxZoom; } set { maxZoom = value; } }
[UnityEngine.Serialization.FormerlySerializedAs("zoomOutLimit")]
public float maxZoom = 5f;
public float minZoom = 1f;
public Color32 highlightColor = new Color32(255, 255, 255, 255);
public bool gridSnap = true;
public bool autoSave = true;
public bool zoomToMouse = true;
[SerializeField] private string typeColorsData = "";
[NonSerialized] public Dictionary<string, Color> typeColors = new Dictionary<string, Color>();
public NoodleType noodleType = NoodleType.Curve;
@ -80,7 +87,20 @@ namespace XNodeEditor {
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")]
#endif
private static void PreferencesGUI() {
VerifyLoaded();
Settings settings = NodeEditorPreferences.settings[lastKey];
@ -98,7 +118,12 @@ namespace XNodeEditor {
//Label
EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel);
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);
EditorGUILayout.LabelField("Zoom");
EditorGUI.indentLevel++;
settings.maxZoom = EditorGUILayout.FloatField(new GUIContent("Max", "Upper limit to zoom"), settings.maxZoom);
settings.minZoom = EditorGUILayout.FloatField(new GUIContent("Min", "Lower limit to zoom"), settings.minZoom);
EditorGUI.indentLevel--;
settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor);
settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor);
if (GUI.changed) {
@ -133,19 +158,22 @@ namespace XNodeEditor {
//Label
EditorGUILayout.LabelField("Types", EditorStyles.boldLabel);
//Clone keys so we can enumerate the dictionary and make changes.
var typeColorKeys = new List<Type>(typeColors.Keys);
//Display type colors. Save them if they are edited by the user
List<string> typeColorKeys = new List<string>(typeColors.Keys);
foreach (string typeColorKey in typeColorKeys) {
Color col = typeColors[typeColorKey];
foreach (var type in typeColorKeys) {
string typeColorKey = NodeEditorUtilities.PrettyName(type);
Color col = typeColors[type];
EditorGUI.BeginChangeCheck();
EditorGUILayout.BeginHorizontal();
col = EditorGUILayout.ColorField(typeColorKey, col);
EditorGUILayout.EndHorizontal();
if (EditorGUI.EndChangeCheck()) {
typeColors[typeColorKey] = col;
typeColors[type] = col;
if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col;
else settings.typeColors.Add(typeColorKey, col);
SavePrefs(typeColorKey, settings);
SavePrefs(key, settings);
NodeEditorWindow.RepaintAll();
}
}
@ -165,7 +193,7 @@ namespace XNodeEditor {
public static void ResetPrefs() {
if (EditorPrefs.HasKey(lastKey)) EditorPrefs.DeleteKey(lastKey);
if (settings.ContainsKey(lastKey)) settings.Remove(lastKey);
typeColors = new Dictionary<string, Color>();
typeColors = new Dictionary<Type, Color>();
VerifyLoaded();
NodeEditorWindow.RepaintAll();
}
@ -184,19 +212,21 @@ namespace XNodeEditor {
public static Color GetTypeColor(System.Type type) {
VerifyLoaded();
if (type == null) return Color.gray;
string typeName = type.PrettyName();
if (!typeColors.ContainsKey(typeName)) {
if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(typeName, settings[lastKey].typeColors[typeName]);
Color col;
if (!typeColors.TryGetValue(type, out col)) {
string typeName = type.PrettyName();
if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]);
else {
#if UNITY_5_4_OR_NEWER
UnityEngine.Random.InitState(typeName.GetHashCode());
#else
UnityEngine.Random.seed = typeName.GetHashCode();
#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;
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() {
//Get all classes deriving from Node via reflection
return GetDerivedTypes(typeof(XNode.Node));
@ -30,9 +42,9 @@ namespace XNodeEditor {
public static Dictionary<Type, Color> GetNodeTint() {
Dictionary<Type, Color> tints = new Dictionary<Type, Color>();
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;
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);
}
return tints;
@ -41,32 +53,44 @@ namespace XNodeEditor {
public static Dictionary<Type, int> GetNodeWidth() {
Dictionary<Type, int> widths = new Dictionary<Type, int>();
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;
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);
}
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>
public static Type[] GetDerivedTypes(Type baseType) {
List<System.Type> types = new List<System.Type>();
System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies) {
types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
try {
types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
} catch(ReflectionTypeLoadException) {}
}
return types.ToArray();
}
public static object ObjectFromType(Type type) {
return Activator.CreateInstance(type);
}
public static object ObjectFromFieldName(object obj, string fieldName) {
Type type = obj.GetType();
FieldInfo fieldInfo = type.GetField(fieldName);
return fieldInfo.GetValue(obj);
public static 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));
}
}
}
public static KeyValuePair<ContextMenu, MethodInfo>[] GetContextMenuMethods(object obj) {
@ -99,6 +123,9 @@ namespace XNodeEditor {
/// <summary> Very crude. Uses a lot of reflection. </summary>
public static void OpenPreferences() {
try {
#if UNITY_2018_3_OR_NEWER
SettingsService.OpenUserPreferences("Preferences/Node Editor");
#else
//Open preferences window
Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow));
Type type = assembly.GetType("UnityEditor.PreferencesWindow");
@ -130,10 +157,11 @@ namespace XNodeEditor {
return;
}
}
#endif
} catch (Exception 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.");
}
}
}
}
}

View File

@ -15,6 +15,9 @@ namespace XNodeEditor {
/// <summary>C#'s Script Icon [The one MonoBhevaiour Scripts have].</summary>
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 {
object[] attribs = classType.GetCustomAttributes(typeof(T), false);
return GetAttrib(attribs, out attribOut);
@ -22,7 +25,7 @@ namespace XNodeEditor {
public static bool GetAttrib<T>(object[] attribs, out T attribOut) where T : Attribute {
for (int i = 0; i < attribs.Length; i++) {
if (attribs[i].GetType() == typeof(T)) {
if (attribs[i] is T){
attribOut = attribs[i] as T;
return true;
}
@ -32,7 +35,15 @@ namespace XNodeEditor {
}
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);
}
@ -45,6 +56,34 @@ namespace XNodeEditor {
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>
public static bool IsCastableTo(this Type from, Type to) {
if (to.IsAssignableFrom(from)) return true;

View File

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
@ -11,21 +11,88 @@ namespace XNodeEditor {
/// <summary> Stores node positions for all nodePorts. </summary>
public Dictionary<XNode.NodePort, Rect> portConnectionPoints { get { return _portConnectionPoints; } }
private Dictionary<XNode.NodePort, Rect> _portConnectionPoints = new Dictionary<XNode.NodePort, Rect>();
[SerializeField] private NodePortReference[] _references = new NodePortReference[0];
[SerializeField] private Rect[] _rects = new Rect[0];
[System.Serializable] private class NodePortReference {
[SerializeField] private XNode.Node _node;
[SerializeField] private string _name;
public NodePortReference(XNode.NodePort nodePort) {
_node = nodePort.node;
_name = nodePort.fieldName;
}
public XNode.NodePort GetNodePort() {
if (_node == null) {
return null;
}
return _node.GetPort(_name);
}
}
private void OnDisable() {
// Cache portConnectionPoints before serialization starts
int count = portConnectionPoints.Count;
_references = new NodePortReference[count];
_rects = new Rect[count];
int index = 0;
foreach (var portConnectionPoint in portConnectionPoints) {
_references[index] = new NodePortReference(portConnectionPoint.Key);
_rects[index] = portConnectionPoint.Value;
index++;
}
}
private void OnEnable() {
// Reload portConnectionPoints if there are any
int length = _references.Length;
if (length == _rects.Length) {
for (int i = 0; i < length; i++) {
XNode.NodePort nodePort = _references[i].GetNodePort();
if (nodePort != null)
_portConnectionPoints.Add(nodePort, _rects[i]);
}
}
}
public Dictionary<XNode.Node, Vector2> nodeSizes { get { return _nodeSizes; } }
private Dictionary<XNode.Node, Vector2> _nodeSizes = new Dictionary<XNode.Node, Vector2>();
public XNode.NodeGraph graph;
public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } }
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, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom); Repaint(); } }
private float _zoom = 1;
void OnFocus() {
current = this;
graphEditor = NodeGraphEditor.GetEditor(graph);
ValidateGraphEditor();
if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
partial void OnEnable();
[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>
public static NodeEditorWindow Init() {
NodeEditorWindow w = CreateInstance<NodeEditorWindow>();
@ -79,8 +146,9 @@ namespace XNodeEditor {
public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) {
Vector2 center = position.size * 0.5f;
float xOffset = (center.x * zoom + (panOffset.x + gridPosition.x));
float yOffset = (center.y * zoom + (panOffset.y + gridPosition.y));
// UI Sharpness complete fix - Round final offset not panOffset
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);
}
@ -102,14 +170,21 @@ namespace XNodeEditor {
public static bool OnOpen(int instanceID, int line) {
XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph;
if (nodeGraph != null) {
NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
w.wantsMouseMove = true;
w.graph = nodeGraph;
Open(nodeGraph);
return true;
}
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>
public static void RepaintAll() {
NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll<NodeEditorWindow>();
@ -118,4 +193,4 @@ namespace XNodeEditor {
}
}
}
}
}

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>
[CustomNodeGraphEditor(typeof(XNode.NodeGraph))]
public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase<NodeGraphEditor, NodeGraphEditor.CustomNodeGraphEditorAttribute, XNode.NodeGraph> {
/// <summary> The position of the window in screen space. </summary>
public Rect position;
[Obsolete("Use window.position instead")]
public Rect position { get { return window.position; } set { window.position = value; } }
/// <summary> Are we currently renaming a node? </summary>
protected bool isRenaming;
public virtual void OnGUI() { }
/// <summary> Called when opened by NodeEditorWindow </summary>
public virtual void OnOpen() { }
public virtual Texture2D GetGridTexture() {
return NodeEditorPreferences.GetSettings().gridTexture;
}
@ -38,10 +41,48 @@ namespace XNodeEditor {
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) {
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)) {
// Automatically remove redundant 'Node' postfix
string typeName = type.Name;
if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node"));
node.name = UnityEditor.ObjectNames.NicifyVariableName(typeName);
}
AssetDatabase.AddObjectToAsset(node, target);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
NodeEditorWindow.RepaintAll();
}
/// <summary> Creates a copy of the original node in the graph </summary>
public XNode.Node CopyNode(XNode.Node original) {
XNode.Node node = target.CopyNode(original);
@ -65,7 +106,7 @@ namespace XNodeEditor {
public string editorPrefsKey;
/// <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="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") {
this.inspectedType = inspectedType;
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,18 +41,69 @@ namespace XNode {
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,
}
#region Obsolete
[Obsolete("Use DynamicPorts instead")]
public IEnumerable<NodePort> InstancePorts { get { return DynamicPorts; } }
[Obsolete("Use DynamicOutputs instead")]
public IEnumerable<NodePort> InstanceOutputs { get { return DynamicOutputs; } }
[Obsolete("Use DynamicInputs instead")]
public IEnumerable<NodePort> InstanceInputs { get { return DynamicInputs; } }
[Obsolete("Use AddDynamicInput instead")]
public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddInstanceInput(type, connectionType, typeConstraint, fieldName);
}
[Obsolete("Use AddDynamicOutput instead")]
public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicOutput(type, connectionType, typeConstraint, fieldName);
}
[Obsolete("Use AddDynamicPort instead")]
private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName);
}
[Obsolete("Use RemoveDynamicPort instead")]
public void RemoveInstancePort(string fieldName) {
RemoveDynamicPort(fieldName);
}
[Obsolete("Use RemoveDynamicPort instead")]
public void RemoveInstancePort(NodePort port) {
RemoveDynamicPort(port);
}
[Obsolete("Use ClearDynamicPorts instead")]
public void ClearInstancePorts() {
ClearDynamicPorts();
}
#endregion
/// <summary> Iterate over all ports on this node. </summary>
public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values) yield return port; } }
/// <summary> Iterate over all outputs on this node. </summary>
public IEnumerable<NodePort> Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } }
/// <summary> Iterate over all inputs on this node. </summary>
public IEnumerable<NodePort> Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } }
/// <summary> Iterate over all instane ports on this node. </summary>
public IEnumerable<NodePort> InstancePorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } }
/// <summary> Iterate over all instance outputs on this node. </summary>
public IEnumerable<NodePort> InstanceOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } }
/// <summary> Iterate over all instance inputs on this node. </summary>
public IEnumerable<NodePort> InstanceInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } }
/// <summary> Iterate over all dynamic ports on this node. </summary>
public IEnumerable<NodePort> DynamicPorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } }
/// <summary> Iterate over all dynamic outputs on this node. </summary>
public IEnumerable<NodePort> DynamicOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } }
/// <summary> Iterate over all dynamic inputs on this node. </summary>
public IEnumerable<NodePort> DynamicInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } }
/// <summary> Parent <see cref="NodeGraph"/> </summary>
[SerializeField] public NodeGraph graph;
/// <summary> Position on the <see cref="NodeGraph"/> </summary>
@ -60,7 +111,12 @@ namespace XNode {
/// <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();
/// <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() {
if (graphHotfix != null) graph = graphHotfix;
graphHotfix = null;
UpdateStaticPorts();
Init();
}
@ -70,7 +126,7 @@ namespace XNode {
NodeDataCache.UpdatePorts(this, ports);
}
/// <summary> Initialize node. Called on creation. </summary>
/// <summary> Initialize node. Called on enable. </summary>
protected virtual void Init() { }
/// <summary> Checks all connections for invalid references, and removes them. </summary>
@ -78,60 +134,59 @@ namespace XNode {
foreach (NodePort port in Ports) port.VerifyConnections();
}
#region Instance Ports
/// <summary> Convenience function.
/// </summary>
#region Dynamic Ports
/// <summary> Convenience function. </summary>
/// <seealso cref="AddInstancePort"/>
/// <seealso cref="AddInstanceOutput"/>
public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Input, connectionType, fieldName);
public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicPort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName);
}
/// <summary> Convenience function.
/// </summary>
/// <summary> Convenience function. </summary>
/// <seealso cref="AddInstancePort"/>
/// <seealso cref="AddInstanceInput"/>
public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Output, connectionType, fieldName);
public NodePort AddDynamicOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName);
}
/// <summary> Add a dynamic, serialized port to this node.
/// </summary>
/// <seealso cref="AddInstanceInput"/>
/// <seealso cref="AddInstanceOutput"/>
private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) {
/// <summary> Add a dynamic, serialized port to this node. </summary>
/// <seealso cref="AddDynamicInput"/>
/// <seealso cref="AddDynamicOutput"/>
private NodePort AddDynamicPort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
if (fieldName == null) {
fieldName = "instanceInput_0";
fieldName = "dynamicInput_0";
int i = 0;
while (HasPort(fieldName)) fieldName = "instanceInput_" + (++i);
while (HasPort(fieldName)) fieldName = "dynamicInput_" + (++i);
} else if (HasPort(fieldName)) {
Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this);
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);
return port;
}
/// <summary> Remove an instance port from the node </summary>
public void RemoveInstancePort(string fieldName) {
RemoveInstancePort(GetPort(fieldName));
/// <summary> Remove an dynamic port from the node </summary>
public void RemoveDynamicPort(string fieldName) {
NodePort dynamicPort = GetPort(fieldName);
if (dynamicPort == null) throw new ArgumentException("port " + fieldName + " doesn't exist");
RemoveDynamicPort(GetPort(fieldName));
}
/// <summary> Remove an instance port from the node </summary>
public void RemoveInstancePort(NodePort port) {
/// <summary> Remove an dynamic port from the node </summary>
public void RemoveDynamicPort(NodePort port) {
if (port == null) throw new ArgumentNullException("port");
else if (port.IsStatic) throw new ArgumentException("cannot remove static port");
port.ClearConnections();
ports.Remove(port.fieldName);
}
/// <summary> Removes all instance ports from the node </summary>
[ContextMenu("Clear Instance Ports")]
public void ClearInstancePorts() {
List<NodePort> instancePorts = new List<NodePort>(InstancePorts);
foreach (NodePort port in instancePorts) {
RemoveInstancePort(port);
/// <summary> Removes all dynamic ports from the node </summary>
[ContextMenu("Clear Dynamic Ports")]
public void ClearDynamicPorts() {
List<NodePort> dynamicPorts = new List<NodePort>(DynamicPorts);
foreach (NodePort port in dynamicPorts) {
RemoveDynamicPort(port);
}
}
#endregion
@ -153,7 +208,8 @@ namespace XNode {
/// <summary> Returns port which matches fieldName </summary>
public NodePort GetPort(string fieldName) {
if (ports.ContainsKey(fieldName)) return ports[fieldName];
NodePort port;
if (ports.TryGetValue(fieldName, out port)) return port;
else return null;
}
@ -202,22 +258,27 @@ namespace XNode {
foreach (NodePort port in Ports) port.ClearConnections();
}
public override int GetHashCode() {
return JsonUtility.ToJson(this).GetHashCode();
}
#region Attributes
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public class InputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
[Obsolete("Use dynamicPortList instead")]
public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } }
public bool dynamicPortList;
public TypeConstraint typeConstraint;
/// <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="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="dynamicPortList">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 dynamicPortList = false) {
this.backingValue = backingValue;
this.connectionType = connectionType;
this.dynamicPortList = dynamicPortList;
this.typeConstraint = typeConstraint;
}
}
@ -226,13 +287,18 @@ namespace XNode {
public class OutputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
[Obsolete("Use dynamicPortList instead")]
public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } }
public bool dynamicPortList;
/// <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="connectionType">Should we allow multiple connections? </param>
public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple) {
/// <param name="dynamicPortList">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 dynamicPortList = false) {
this.backingValue = backingValue;
this.connectionType = connectionType;
this.dynamicPortList = dynamicPortList;
}
}
@ -247,19 +313,19 @@ namespace XNode {
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeTint : Attribute {
public class NodeTintAttribute : Attribute {
public Color color;
/// <summary> Specify a color for this node type </summary>
/// <param name="r"> Red [0.0f .. 1.0f] </param>
/// <param name="g"> Green [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);
}
/// <summary> Specify a color for this node type </summary>
/// <param name="hex"> HEX color value </param>
public NodeTint(string hex) {
public NodeTintAttribute(string hex) {
ColorUtility.TryParseHtmlString(hex, out color);
}
@ -267,20 +333,21 @@ namespace XNode {
/// <param name="r"> Red [0 .. 255] </param>
/// <param name="g"> Green [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);
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeWidth : Attribute {
public class NodeWidthAttribute : Attribute {
public int width;
/// <summary> Specify a width for this node type </summary>
/// <param name="width"> Width </param>
public NodeWidth(int width) {
public NodeWidthAttribute(int width) {
this.width = width;
}
}
#endregion
[Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver {
[SerializeField] private List<string> keys = new List<string>();
@ -306,4 +373,4 @@ namespace XNode {
}
}
}
}
}

View File

@ -16,9 +16,10 @@ namespace XNode {
Dictionary<string, NodePort> staticPorts = new Dictionary<string, NodePort>();
System.Type nodeType = node.GetType();
if (portDataCache.ContainsKey(nodeType)) {
for (int i = 0; i < portDataCache[nodeType].Count; i++) {
staticPorts.Add(portDataCache[nodeType][i].fieldName, portDataCache[nodeType][i]);
List<NodePort> typePortCache;
if (portDataCache.TryGetValue(nodeType, out typePortCache)) {
for (int i = 0; i < typePortCache.Count; i++) {
staticPorts.Add(typePortCache[i].fieldName, portDataCache[nodeType][i]);
}
}
@ -26,10 +27,10 @@ namespace XNode {
// Loop through current node ports
foreach (NodePort port in ports.Values.ToList()) {
// If port still exists, check it it has been changed
if (staticPorts.ContainsKey(port.fieldName)) {
NodePort staticPort = staticPorts[port.fieldName];
NodePort staticPort;
if (staticPorts.TryGetValue(port.fieldName, out staticPort)) {
// 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;
}
// If port doesn't exist anymore, remove it
@ -67,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) {
System.Reflection.FieldInfo[] fieldInfo = nodeType.GetFields();
for (int i = 0; i < fieldInfo.Length; i++) {
List<System.Reflection.FieldInfo> fieldInfo = GetNodeFields(nodeType);
for (int i = 0; i < fieldInfo.Count; i++) {
//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.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute;

View File

@ -11,38 +11,40 @@ namespace XNode {
/// See: <see cref="AddNode{T}"/> </summary>
[SerializeField] public List<Node> nodes = new List<Node>();
/// <summary> Add a node to the graph by type </summary>
/// <summary> Add a node to the graph by type (convenience method - will call the System.Type version) </summary>
public T AddNode<T>() where T : Node {
return AddNode(typeof(T)) as T;
}
/// <summary> Add a node to the graph by type </summary>
public virtual Node AddNode(Type type) {
Node.graphHotfix = this;
Node node = ScriptableObject.CreateInstance(type) as Node;
nodes.Add(node);
node.graph = this;
nodes.Add(node);
return node;
}
/// <summary> Creates a copy of the original node in the graph </summary>
public virtual Node CopyNode(Node original) {
Node.graphHotfix = this;
Node node = ScriptableObject.Instantiate(original);
node.graph = this;
node.ClearConnections();
nodes.Add(node);
node.graph = this;
return node;
}
/// <summary> Safely remove a node and all its connections </summary>
/// <param name="node"> The node to remove </param>
public void RemoveNode(Node node) {
public virtual void RemoveNode(Node node) {
node.ClearConnections();
nodes.Remove(node);
if (Application.isPlaying) Destroy(node);
}
/// <summary> Remove all nodes and connections from the graph </summary>
public void Clear() {
public virtual void Clear() {
if (Application.isPlaying) {
for (int i = 0; i < nodes.Count; i++) {
Destroy(nodes[i]);
@ -52,12 +54,13 @@ namespace XNode {
}
/// <summary> Create a new deep copy of this graph </summary>
public XNode.NodeGraph Copy() {
public virtual XNode.NodeGraph Copy() {
// Instantiate a new nodegraph instance
NodeGraph graph = Instantiate(this);
// Instantiate all nodes inside the graph
for (int i = 0; i < nodes.Count; i++) {
if (nodes[i] == null) continue;
Node.graphHotfix = graph;
Node node = Instantiate(nodes[i]) as Node;
node.graph = graph;
graph.nodes[i] = node;
@ -74,7 +77,7 @@ namespace XNode {
return graph;
}
private void OnDestroy() {
protected virtual void OnDestroy() {
// Remove all nodes prior to graph destruction
Clear();
}

View File

@ -21,6 +21,7 @@ namespace XNode {
public IO direction { get { return _direction; } }
public Node.ConnectionType connectionType { get { return _connectionType; } }
public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } }
/// <summary> Is this port connected to anytihng? </summary>
public bool IsConnected { get { return connections.Count != 0; } }
@ -49,6 +50,7 @@ namespace XNode {
[SerializeField] private List<PortConnection> connections = new List<PortConnection>();
[SerializeField] private IO _direction;
[SerializeField] private Node.ConnectionType _connectionType;
[SerializeField] private Node.TypeConstraint _typeConstraint;
[SerializeField] private bool _dynamic;
/// <summary> Construct a static targetless nodeport. Used as a template. </summary>
@ -61,6 +63,7 @@ namespace XNode {
if (attribs[i] is Node.InputAttribute) {
_direction = IO.Input;
_connectionType = (attribs[i] as Node.InputAttribute).connectionType;
_typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint;
} else if (attribs[i] is Node.OutputAttribute) {
_direction = IO.Output;
_connectionType = (attribs[i] as Node.OutputAttribute).connectionType;
@ -75,17 +78,19 @@ namespace XNode {
_direction = nodePort.direction;
_dynamic = nodePort._dynamic;
_connectionType = nodePort._connectionType;
_typeConstraint = nodePort._typeConstraint;
_node = node;
}
/// <summary> Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. </summary>
public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node node) {
public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) {
_fieldName = fieldName;
this.ValueType = type;
_direction = direction;
_node = node;
_dynamic = true;
_connectionType = connectionType;
_typeConstraint = typeConstraint;
}
/// <summary> Checks all connections for invalid references, and removes them. </summary>
@ -202,6 +207,15 @@ namespace XNode {
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) {
//If the connection is broken for some reason, remove it.
if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) {
@ -231,6 +245,23 @@ namespace XNode {
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>
public void Disconnect(NodePort port) {
// Remove this ports connection to the other
@ -252,6 +283,25 @@ namespace XNode {
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() {
while (connections.Count > 0) {
Disconnect(connections[0].Port);
@ -263,6 +313,58 @@ namespace XNode {
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>
public void Redirect(List<Node> oldNodes, List<Node> newNodes) {
foreach (PortConnection connection in connections) {