1
0
mirror of https://github.com/Siccity/xNode.git synced 2025-12-20 09:16:01 +08:00

Merge branch 'master' into examples

This commit is contained in:
Thor Brigsted 2020-07-06 08:56:03 +02:00
commit 754a5f66af
28 changed files with 686 additions and 66 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
root = true
[*.cs]
indent_style = space
indent_size = 4
end_of_line = crlf
insert_final_newline = false
trim_trailing_whitespace = true

3
.gitignore vendored
View File

@ -23,3 +23,6 @@ sysinfo.txt
.git.meta .git.meta
.gitignore.meta .gitignore.meta
.gitattributes.meta .gitattributes.meta
# OS X only:
.DS_Store

View File

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

View File

@ -4,6 +4,7 @@
[![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues) [![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md)
[![GitHub Wiki](https://img.shields.io/badge/wiki-available-brightgreen.svg)](https://github.com/Siccity/xNode/wiki) [![GitHub Wiki](https://img.shields.io/badge/wiki-available-brightgreen.svg)](https://github.com/Siccity/xNode/wiki)
[![openupm](https://img.shields.io/npm/v/com.github.siccity.xnode?label=openupm&registry_uri=https://package.openupm.com)](https://openupm.com/packages/com.github.siccity.xnode/)
[Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki) [Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki)
@ -33,6 +34,7 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
* [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects * [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects
### Installing with Unity Package Manager ### Installing with Unity Package Manager
***Via Git URL***
*(Requires Unity version 2018.3.0b7 or above)* *(Requires Unity version 2018.3.0b7 or above)*
To install this project as a [Git dependency](https://docs.unity3d.com/Manual/upm-git.html) using the Unity Package Manager, To install this project as a [Git dependency](https://docs.unity3d.com/Manual/upm-git.html) using the Unity Package Manager,
@ -46,6 +48,14 @@ You will need to have Git installed and available in your system's PATH.
If you are using [Assembly Definitions](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html) in your project, you will need to add `XNode` and/or `XNodeEditor` as Assembly Definition References. If you are using [Assembly Definitions](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html) in your project, you will need to add `XNode` and/or `XNodeEditor` as Assembly Definition References.
***Via OpenUPM***
The package is available on the [openupm registry](https://openupm.com). It's recommended to install it via [openupm-cli](https://github.com/openupm/openupm-cli).
```
openupm add com.github.siccity.xnode
```
### Node example: ### Node example:
```csharp ```csharp
// public classes deriving from Node are registered as nodes for use within a graph // public classes deriving from Node are registered as nodes for use within a graph

View File

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

View File

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

View File

@ -0,0 +1,35 @@
using UnityEditor;
using XNode;
namespace XNodeEditor {
/// <summary>
/// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When
/// renaming a <see cref="XNode.NodeGraph"/> asset, it appears that sometimes the v2 AssetDatabase will swap which asset
/// is the main asset (present at top level) between the <see cref="XNode.NodeGraph"/> and one of its <see cref="XNode.Node"/>
/// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it
/// finds a case where a <see cref="XNode.Node"/> has been made the main asset it will swap it back to being a sub-asset
/// and rename the node to the default name for that node type.
/// </summary>
internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor {
private static void OnPostprocessAllAssets(
string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths) {
for (int i = 0; i < movedAssets.Length; i++) {
Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node;
// If the renamed asset is a node graph, but the v2 AssetDatabase has swapped a sub-asset node to be its
// main asset, reset the node graph to be the main asset and rename the node asset back to its default
// name.
if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) {
AssetDatabase.SetMainObject(nodeAsset.graph, movedAssets[i]);
AssetDatabase.ImportAsset(movedAssets[i]);
nodeAsset.name = NodeEditorUtilities.NodeDefaultName(nodeAsset.GetType());
EditorUtility.SetDirty(nodeAsset);
}
}
}
}
}

View File

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

View File

@ -21,7 +21,7 @@ namespace XNodeEditor {
public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>(); public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>();
#if ODIN_INSPECTOR #if ODIN_INSPECTOR
internal static bool inNodeEditor = false; protected internal static bool inNodeEditor = false;
#endif #endif
public virtual void OnHeaderGUI() { public virtual void OnHeaderGUI() {
@ -100,19 +100,28 @@ namespace XNodeEditor {
return NodeEditorResources.styles.nodeBody; return NodeEditorResources.styles.nodeBody;
} }
public virtual GUIStyle GetBodyHighlightStyle() {
return NodeEditorResources.styles.nodeHighlight;
}
/// <summary> Add items for the context menu when right-clicking this node. Override to add custom menu items. </summary> /// <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) { public virtual void AddContextMenuItems(GenericMenu menu) {
bool canRemove = true;
// Actions if only one node is selected // Actions if only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
XNode.Node node = Selection.activeObject as XNode.Node; XNode.Node node = Selection.activeObject as XNode.Node;
menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node)); menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node));
menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode); menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode);
canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node);
} }
// Add actions to any number of selected nodes // Add actions to any number of selected nodes
menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes); menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes);
menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes); menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes);
menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
else menu.AddItem(new GUIContent("Remove"), false, null);
// Custom sctions if only one node is selected // Custom sctions if only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
@ -125,9 +134,13 @@ namespace XNodeEditor {
public void Rename(string newName) { public void Rename(string newName) {
if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType());
target.name = newName; target.name = newName;
OnRename();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
} }
/// <summary> Called after this node's name has changed. </summary>
public virtual void OnRename() { }
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public class CustomNodeEditorAttribute : Attribute, public class CustomNodeEditorAttribute : Attribute,
XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node>.INodeEditorAttrib { XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node>.INodeEditorAttrib {

View File

@ -25,13 +25,14 @@ namespace XNodeEditor {
[NonSerialized] private XNode.NodePort autoConnectOutput = null; [NonSerialized] private XNode.NodePort autoConnectOutput = null;
[NonSerialized] private List<Vector2> draggedOutputReroutes = new List<Vector2>(); [NonSerialized] private List<Vector2> draggedOutputReroutes = new List<Vector2>();
private RerouteReference hoveredReroute = new RerouteReference(); private RerouteReference hoveredReroute = new RerouteReference();
private List<RerouteReference> selectedReroutes = new List<RerouteReference>(); public List<RerouteReference> selectedReroutes = new List<RerouteReference>();
private Vector2 dragBoxStart; private Vector2 dragBoxStart;
private UnityEngine.Object[] preBoxSelection; private UnityEngine.Object[] preBoxSelection;
private RerouteReference[] preBoxSelectionReroute; private RerouteReference[] preBoxSelectionReroute;
private Rect selectionBox; private Rect selectionBox;
private bool isDoubleClick = false; private bool isDoubleClick = false;
private Vector2 lastMousePosition; private Vector2 lastMousePosition;
private float dragThreshold = 1f;
public void Controls() { public void Controls() {
wantsMouseMove = true; wantsMouseMove = true;
@ -58,10 +59,9 @@ namespace XNodeEditor {
case EventType.MouseDrag: case EventType.MouseDrag:
if (e.button == 0) { if (e.button == 0) {
if (IsDraggingPort) { if (IsDraggingPort) {
if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) { // Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously
if (!draggedOutput.IsConnectedTo(hoveredPort)) { if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) {
draggedOutputTarget = hoveredPort; draggedOutputTarget = hoveredPort;
}
} else { } else {
draggedOutputTarget = null; draggedOutputTarget = null;
} }
@ -135,9 +135,12 @@ namespace XNodeEditor {
Repaint(); Repaint();
} }
} else if (e.button == 1 || e.button == 2) { } else if (e.button == 1 || e.button == 2) {
//check drag threshold for larger screens
if (e.delta.magnitude > dragThreshold) {
panOffset += e.delta * zoom; panOffset += e.delta * zoom;
isPanning = true; isPanning = true;
} }
}
break; break;
case EventType.MouseDown: case EventType.MouseDown:
Repaint(); Repaint();
@ -205,8 +208,8 @@ namespace XNodeEditor {
if (e.button == 0) { if (e.button == 0) {
//Port drag release //Port drag release
if (IsDraggingPort) { if (IsDraggingPort) {
//If connection is valid, save it // If connection is valid, save it
if (draggedOutputTarget != null) { if (draggedOutputTarget != null && draggedOutput.CanConnectTo(draggedOutputTarget)) {
XNode.Node node = draggedOutputTarget.node; XNode.Node node = draggedOutputTarget.node;
if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget); if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget);
@ -218,8 +221,8 @@ namespace XNodeEditor {
EditorUtility.SetDirty(graph); EditorUtility.SetDirty(graph);
} }
} }
// Open context menu for auto-connection // Open context menu for auto-connection if there is no target node
else if (NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) { else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
GenericMenu menu = new GenericMenu(); GenericMenu menu = new GenericMenu();
graphEditor.AddContextMenuItems(menu); graphEditor.AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
@ -440,6 +443,15 @@ namespace XNodeEditor {
for (int i = 0; i < nodes.Length; i++) { for (int i = 0; i < nodes.Length; i++) {
XNode.Node srcNode = nodes[i]; XNode.Node srcNode = nodes[i];
if (srcNode == null) continue; if (srcNode == null) continue;
// Check if user is allowed to add more of given node type
XNode.Node.DisallowMultipleNodesAttribute disallowAttrib;
Type nodeType = srcNode.GetType();
if (NodeEditorUtilities.GetAttrib(nodeType, out disallowAttrib)) {
int typeCount = graph.nodes.Count(x => x.GetType() == nodeType);
if (typeCount >= disallowAttrib.max) continue;
}
XNode.Node newNode = graphEditor.CopyNode(srcNode); XNode.Node newNode = graphEditor.CopyNode(srcNode);
substitutes.Add(srcNode, newNode); substitutes.Add(srcNode, newNode);
newNode.position = srcNode.position + offset; newNode.position = srcNode.position + offset;
@ -457,8 +469,8 @@ namespace XNodeEditor {
XNode.Node newNodeIn, newNodeOut; XNode.Node newNodeIn, newNodeOut;
if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) { if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) {
newNodeIn.UpdateStaticPorts(); newNodeIn.UpdatePorts();
newNodeOut.UpdateStaticPorts(); newNodeOut.UpdatePorts();
inputPort = newNodeIn.GetInputPort(inputPort.fieldName); inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
outputPort = newNodeOut.GetOutputPort(outputPort.fieldName); outputPort = newNodeOut.GetOutputPort(outputPort.fieldName);
} }
@ -527,8 +539,8 @@ namespace XNodeEditor {
XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && x.ValueType == autoConnectOutput.ValueType); XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && x.ValueType == autoConnectOutput.ValueType);
// Fallback to input port // Fallback to input port
if (inputPort == null) inputPort = node.Ports.FirstOrDefault(x => x.IsInput); if (inputPort == null) inputPort = node.Ports.FirstOrDefault(x => x.IsInput);
// Autoconnect // Autoconnect if connection is compatible
if (inputPort != null) autoConnectOutput.Connect(inputPort); if (inputPort != null && inputPort.CanConnectTo(autoConnectOutput)) autoConnectOutput.Connect(inputPort);
// Save changes // Save changes
EditorUtility.SetDirty(graph); EditorUtility.SetDirty(graph);

43
Scripts/Editor/NodeEditorGUI.cs Normal file → Executable file
View File

@ -17,7 +17,7 @@ namespace XNodeEditor {
public event Action onLateGUI; public event Action onLateGUI;
private static readonly Vector3[] polyLineTempArray = new Vector3[2]; private static readonly Vector3[] polyLineTempArray = new Vector3[2];
private void OnGUI() { protected virtual void OnGUI() {
Event e = Event.current; Event e = Event.current;
Matrix4x4 m = GUI.matrix; Matrix4x4 m = GUI.matrix;
if (graph == null) return; if (graph == null) return;
@ -142,6 +142,7 @@ namespace XNodeEditor {
for (int i = 0; i < gridPoints.Count; ++i) for (int i = 0; i < gridPoints.Count; ++i)
gridPoints[i] = GridToWindowPosition(gridPoints[i]); gridPoints[i] = GridToWindowPosition(gridPoints[i]);
Color originalHandlesColor = Handles.color;
Handles.color = gradient.Evaluate(0f); Handles.color = gradient.Evaluate(0f);
int length = gridPoints.Count; int length = gridPoints.Count;
switch (path) { switch (path) {
@ -202,6 +203,7 @@ namespace XNodeEditor {
Vector2 prev_point = point_a; Vector2 prev_point = point_a;
// Approximately one segment per 5 pixels // Approximately one segment per 5 pixels
int segments = (int) Vector2.Distance(point_a, point_b) / 5; int segments = (int) Vector2.Distance(point_a, point_b) / 5;
segments = Math.Max(segments, 1);
int draw = 0; int draw = 0;
for (int j = 0; j <= segments; j++) { for (int j = 0; j <= segments; j++) {
@ -267,7 +269,44 @@ namespace XNodeEditor {
} }
} }
break; break;
case NoodlePath.ShaderLab:
Vector2 start = gridPoints[0];
Vector2 end = gridPoints[length - 1];
//Modify first and last point in array so we can loop trough them nicely.
gridPoints[0] = gridPoints[0] + Vector2.right * (20 / zoom);
gridPoints[length - 1] = gridPoints[length - 1] + Vector2.left * (20 / zoom);
//Draw first vertical lines going out from nodes
Handles.color = gradient.Evaluate(0f);
DrawAAPolyLineNonAlloc(thickness, start, gridPoints[0]);
Handles.color = gradient.Evaluate(1f);
DrawAAPolyLineNonAlloc(thickness, end, gridPoints[length - 1]);
for (int i = 0; i < length - 1; i++) {
Vector2 point_a = gridPoints[i];
Vector2 point_b = gridPoints[i + 1];
// Draws the line with the coloring.
Vector2 prev_point = point_a;
// Approximately one segment per 5 pixels
int segments = (int) Vector2.Distance(point_a, point_b) / 5;
segments = Math.Max(segments, 1);
int draw = 0;
for (int j = 0; j <= segments; j++) {
draw++;
float t = j / (float) segments;
Vector2 lerp = Vector2.Lerp(point_a, point_b, t);
if (draw > 0) {
if (i == length - 2) Handles.color = gradient.Evaluate(t);
DrawAAPolyLineNonAlloc(thickness, prev_point, lerp);
} }
prev_point = lerp;
if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2;
}
}
gridPoints[0] = start;
gridPoints[length - 1] = end;
break;
}
Handles.color = originalHandlesColor;
} }
/// <summary> Draws all connections </summary> /// <summary> Draws all connections </summary>
@ -413,7 +452,7 @@ namespace XNodeEditor {
if (selected) { if (selected) {
GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle()); GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight); GUIStyle highlightStyle = new GUIStyle(nodeEditor.GetBodyHighlightStyle());
highlightStyle.padding = style.padding; highlightStyle.padding = style.padding;
style.padding = new RectOffset(); style.padding = new RectOffset();
GUI.color = nodeEditor.GetTint(); GUI.color = nodeEditor.GetTint();

View File

@ -312,6 +312,8 @@ namespace XNodeEditor {
}).Where(x => x.port != null); }).Where(x => x.port != null);
List<XNode.NodePort> dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); List<XNode.NodePort> dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
node.UpdatePorts();
ReorderableList list = null; ReorderableList list = null;
Dictionary<string, ReorderableList> rlc; Dictionary<string, ReorderableList> rlc;
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) { if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
@ -326,6 +328,7 @@ namespace XNodeEditor {
} }
list.list = dynamicPorts; list.list = dynamicPorts;
list.DoLayoutList(); list.DoLayoutList();
} }
private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) { private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) {
@ -337,7 +340,7 @@ namespace XNodeEditor {
list.drawElementCallback = list.drawElementCallback =
(Rect rect, int index, bool isActive, bool isFocused) => { (Rect rect, int index, bool isActive, bool isFocused) => {
XNode.NodePort port = node.GetPort(fieldName + " " + index); XNode.NodePort port = node.GetPort(fieldName + " " + index);
if (hasArrayData) { if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
if (arrayData.arraySize <= index) { if (arrayData.arraySize <= index) {
EditorGUI.LabelField(rect, "Array[" + index + "] data out of range"); EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
return; return;
@ -368,7 +371,10 @@ namespace XNodeEditor {
}; };
list.onReorderCallback = list.onReorderCallback =
(ReorderableList rl) => { (ReorderableList rl) => {
bool hasRect = false;
bool hasNewRect = false;
Rect rect = Rect.zero;
Rect newRect = Rect.zero;
// Move up // Move up
if (rl.index > reorderableListIndex) { if (rl.index > reorderableListIndex) {
for (int i = reorderableListIndex; i < rl.index; ++i) { for (int i = reorderableListIndex; i < rl.index; ++i) {
@ -377,9 +383,10 @@ namespace XNodeEditor {
port.SwapConnections(nextPort); port.SwapConnections(nextPort);
// Swap cached positions to mitigate twitching // Swap cached positions to mitigate twitching
Rect rect = NodeEditorWindow.current.portConnectionPoints[port]; hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect);
NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort]; hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect);
NodeEditorWindow.current.portConnectionPoints[nextPort] = rect; NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect?newRect:rect;
NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect?rect:newRect;
} }
} }
// Move down // Move down
@ -390,9 +397,10 @@ namespace XNodeEditor {
port.SwapConnections(nextPort); port.SwapConnections(nextPort);
// Swap cached positions to mitigate twitching // Swap cached positions to mitigate twitching
Rect rect = NodeEditorWindow.current.portConnectionPoints[port]; hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect);
NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort]; hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect);
NodeEditorWindow.current.portConnectionPoints[nextPort] = rect; NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect?newRect:rect;
NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect?rect:newRect;
} }
} }
// Apply changes // Apply changes
@ -465,7 +473,7 @@ namespace XNodeEditor {
EditorUtility.SetDirty(node); EditorUtility.SetDirty(node);
} }
if (hasArrayData) { if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
if (arrayData.arraySize <= index) { if (arrayData.arraySize <= index) {
Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped"); Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped");
Debug.Log(rl.list[0]); Debug.Log(rl.list[0]);

View File

@ -5,7 +5,7 @@ using UnityEngine;
using UnityEngine.Serialization; using UnityEngine.Serialization;
namespace XNodeEditor { namespace XNodeEditor {
public enum NoodlePath { Curvy, Straight, Angled } public enum NoodlePath { Curvy, Straight, Angled, ShaderLab }
public enum NoodleStroke { Full, Dashed } public enum NoodleStroke { Full, Dashed }
public static class NodeEditorPreferences { public static class NodeEditorPreferences {

View File

@ -74,8 +74,10 @@ namespace XNodeEditor {
Attribute attr; Attribute attr;
if (!typeTypes.TryGetValue(typeof(T), out attr)) { if (!typeTypes.TryGetValue(typeof(T), out attr)) {
if (GetAttrib<T>(classType, fieldName, out attribOut)) typeTypes.Add(typeof(T), attribOut); if (GetAttrib<T>(classType, fieldName, out attribOut)) {
else typeTypes.Add(typeof(T), null); typeTypes.Add(typeof(T), attribOut);
return true;
} else typeTypes.Add(typeof(T), null);
} }
if (attr == null) { if (attr == null) {

View File

@ -77,7 +77,16 @@ namespace XNodeEditor {
void OnFocus() { void OnFocus() {
current = this; current = this;
ValidateGraphEditor(); ValidateGraphEditor();
if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); if (graphEditor != null) {
graphEditor.OnWindowFocus();
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
dragThreshold = Math.Max(1f, Screen.width / 1000f);
}
void OnLostFocus() {
if (graphEditor != null) graphEditor.OnWindowFocusLost();
} }
[InitializeOnLoadMethod] [InitializeOnLoadMethod]
@ -97,7 +106,7 @@ namespace XNodeEditor {
/// <summary> Make sure the graph editor is assigned and to the right object </summary> /// <summary> Make sure the graph editor is assigned and to the right object </summary>
private void ValidateGraphEditor() { private void ValidateGraphEditor() {
NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this);
if (this.graphEditor != graphEditor) { if (this.graphEditor != graphEditor && graphEditor != null) {
this.graphEditor = graphEditor; this.graphEditor = graphEditor;
graphEditor.OnOpen(); graphEditor.OnOpen();
} }
@ -187,12 +196,13 @@ namespace XNodeEditor {
} }
/// <summary>Open the provided graph in the NodeEditor</summary> /// <summary>Open the provided graph in the NodeEditor</summary>
public static void Open(XNode.NodeGraph graph) { public static NodeEditorWindow Open(XNode.NodeGraph graph) {
if (!graph) return; if (!graph) return null;
NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
w.wantsMouseMove = true; w.wantsMouseMove = true;
w.graph = graph; w.graph = graph;
return w;
} }
/// <summary> Repaint all open NodeEditorWindows. </summary> /// <summary> Repaint all open NodeEditorWindows. </summary>

View File

@ -18,6 +18,12 @@ namespace XNodeEditor {
/// <summary> Called when opened by NodeEditorWindow </summary> /// <summary> Called when opened by NodeEditorWindow </summary>
public virtual void OnOpen() { } public virtual void OnOpen() { }
/// <summary> Called when NodeEditorWindow gains focus </summary>
public virtual void OnWindowFocus() { }
/// <summary> Called when NodeEditorWindow loses focus </summary>
public virtual void OnWindowFocusLost() { }
public virtual Texture2D GetGridTexture() { public virtual Texture2D GetGridTexture() {
return NodeEditorPreferences.GetSettings().gridTexture; return NodeEditorPreferences.GetSettings().gridTexture;
} }
@ -41,17 +47,38 @@ namespace XNodeEditor {
return NodeEditorUtilities.NodeDefaultPath(type); return NodeEditorUtilities.NodeDefaultPath(type);
} }
/// <summary> The order by which the menu items are displayed. </summary>
public virtual int GetNodeMenuOrder(Type type) {
//Check if type has the CreateNodeMenuAttribute
XNode.Node.CreateNodeMenuAttribute attrib;
if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path
return attrib.order;
else
return 0;
}
/// <summary> Add items for the context menu when right-clicking this node. Override to add custom menu items. </summary> /// <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) { public virtual void AddContextMenuItems(GenericMenu menu) {
Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition);
for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++) { var nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(type => GetNodeMenuOrder(type)).ToArray();
Type type = NodeEditorReflection.nodeTypes[i]; for (int i = 0; i < nodeTypes.Length; i++) {
Type type = nodeTypes[i];
//Get node context menu path //Get node context menu path
string path = GetNodeMenuName(type); string path = GetNodeMenuName(type);
if (string.IsNullOrEmpty(path)) continue; if (string.IsNullOrEmpty(path)) continue;
menu.AddItem(new GUIContent(path), false, () => { // Check if user is allowed to add more of given node type
XNode.Node.DisallowMultipleNodesAttribute disallowAttrib;
bool disallowed = false;
if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) {
int typeCount = target.nodes.Count(x => x.GetType() == type);
if (typeCount >= disallowAttrib.max) disallowed = true;
}
// Add node entry to context menu
if (disallowed) menu.AddItem(new GUIContent(path), false, null);
else menu.AddItem(new GUIContent(path), false, () => {
XNode.Node node = CreateNode(type, pos); XNode.Node node = CreateNode(type, pos);
NodeEditorWindow.current.AutoConnect(node); NodeEditorWindow.current.AutoConnect(node);
}); });
@ -133,7 +160,7 @@ namespace XNodeEditor {
/// <summary> Deal with objects dropped into the graph through DragAndDrop </summary> /// <summary> Deal with objects dropped into the graph through DragAndDrop </summary>
public virtual void OnDropObjects(UnityEngine.Object[] objects) { public virtual void OnDropObjects(UnityEngine.Object[] objects) {
Debug.Log("No OnDropObjects override defined for " + GetType()); if (GetType() != typeof(NodeGraphEditor)) Debug.Log("No OnDropObjects override defined for " + GetType());
} }
/// <summary> Create a node and save it in the graph asset </summary> /// <summary> Create a node and save it in the graph asset </summary>
@ -143,14 +170,14 @@ namespace XNodeEditor {
Undo.RegisterCreatedObjectUndo(node, "Create Node"); Undo.RegisterCreatedObjectUndo(node, "Create Node");
node.position = position; node.position = position;
if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
AssetDatabase.AddObjectToAsset(node, target); if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
NodeEditorWindow.RepaintAll(); NodeEditorWindow.RepaintAll();
return node; return node;
} }
/// <summary> Creates a copy of the original node in the graph </summary> /// <summary> Creates a copy of the original node in the graph </summary>
public XNode.Node CopyNode(XNode.Node original) { public virtual XNode.Node CopyNode(XNode.Node original) {
Undo.RecordObject(target, "Duplicate Node"); Undo.RecordObject(target, "Duplicate Node");
XNode.Node node = target.CopyNode(original); XNode.Node node = target.CopyNode(original);
Undo.RegisterCreatedObjectUndo(node, "Duplicate Node"); Undo.RegisterCreatedObjectUndo(node, "Duplicate Node");
@ -160,8 +187,25 @@ namespace XNodeEditor {
return node; return node;
} }
/// <summary> Return false for nodes that can't be removed </summary>
public virtual bool CanRemove(XNode.Node node) {
// Check graph attributes to see if this node is required
Type graphType = target.GetType();
XNode.NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll(
graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute);
if (attribs.Any(x => x.Requires(node.GetType()))) {
if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) {
return false;
}
}
return true;
}
/// <summary> Safely remove a node and all its connections. </summary> /// <summary> Safely remove a node and all its connections. </summary>
public virtual void RemoveNode(XNode.Node node) { public virtual void RemoveNode(XNode.Node node) {
if (!CanRemove(node)) return;
// Remove the node
Undo.RecordObject(node, "Delete Node"); Undo.RecordObject(node, "Delete Node");
Undo.RecordObject(target, "Delete Node"); Undo.RecordObject(target, "Delete Node");
foreach (var port in node.Ports) foreach (var port in node.Ports)

View File

@ -0,0 +1,45 @@
using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.AssetImporters;
using UnityEngine;
using XNode;
namespace XNodeEditor {
/// <summary> Deals with modified assets </summary>
class NodeGraphImporter : AssetPostprocessor {
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
foreach (string path in importedAssets) {
// Skip processing anything without the .asset extension
if (Path.GetExtension(path) != ".asset") continue;
// Get the object that is requested for deletion
NodeGraph graph = AssetDatabase.LoadAssetAtPath<NodeGraph>(path);
if (graph == null) continue;
// Get attributes
Type graphType = graph.GetType();
NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll(
graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute);
Vector2 position = Vector2.zero;
foreach (NodeGraph.RequireNodeAttribute attrib in attribs) {
if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position);
if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position);
if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position);
}
}
}
private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) {
if (!graph.nodes.Any(x => x.GetType() == type)) {
XNode.Node node = graph.AddNode(type);
node.position = position;
position.x += 200;
if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph);
}
}
}
}

View File

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

View File

@ -1,9 +1,11 @@
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
namespace XNodeEditor { namespace XNodeEditor {
/// <summary> Utility for renaming assets </summary> /// <summary> Utility for renaming assets </summary>
public class RenamePopup : EditorWindow { public class RenamePopup : EditorWindow {
private const string inputControlName = "nameInput";
public static RenamePopup current { get; private set; } public static RenamePopup current { get; private set; }
public Object target; public Object target;
public string input; public string input;
@ -19,7 +21,6 @@ namespace XNodeEditor {
window.input = target.name; window.input = target.name;
window.minSize = new Vector2(100, 44); window.minSize = new Vector2(100, 44);
window.position = new Rect(0, 0, width, 44); window.position = new Rect(0, 0, width, 44);
GUI.FocusControl("ClearAllFocus");
window.UpdatePositionToMouse(); window.UpdatePositionToMouse();
return window; return window;
} }
@ -43,12 +44,16 @@ namespace XNodeEditor {
UpdatePositionToMouse(); UpdatePositionToMouse();
firstFrame = false; firstFrame = false;
} }
GUI.SetNextControlName(inputControlName);
input = EditorGUILayout.TextField(input); input = EditorGUILayout.TextField(input);
EditorGUI.FocusTextInControl(inputControlName);
Event e = Event.current; Event e = Event.current;
// If input is empty, revert name to default instead // If input is empty, revert name to default instead
if (input == null || input.Trim() == "") { if (input == null || input.Trim() == "") {
if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) { if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = NodeEditorUtilities.NodeDefaultName(target.GetType()); target.name = NodeEditorUtilities.NodeDefaultName(target.GetType());
NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target));
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close(); Close();
target.TriggerOnValidate(); target.TriggerOnValidate();
@ -58,11 +63,21 @@ namespace XNodeEditor {
else { else {
if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = input; target.name = input;
NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target));
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close(); Close();
target.TriggerOnValidate(); target.TriggerOnValidate();
} }
} }
if (e.isKey && e.keyCode == KeyCode.Escape) {
Close();
}
}
private void OnDestroy() {
EditorGUIUtility.editingTextField = false;
} }
} }
} }

View File

@ -0,0 +1,77 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using XNode;
namespace XNodeEditor {
[CustomEditor(typeof(SceneGraph), true)]
public class SceneGraphEditor : Editor {
private SceneGraph sceneGraph;
private bool removeSafely;
private Type graphType;
public override void OnInspectorGUI() {
if (sceneGraph.graph == null) {
if (GUILayout.Button("New graph", GUILayout.Height(40))) {
if (graphType == null) {
Type[] graphTypes = NodeEditorReflection.GetDerivedTypes(typeof(NodeGraph));
GenericMenu menu = new GenericMenu();
for (int i = 0; i < graphTypes.Length; i++) {
Type graphType = graphTypes[i];
menu.AddItem(new GUIContent(graphType.Name), false, () => CreateGraph(graphType));
}
menu.ShowAsContext();
} else {
CreateGraph(graphType);
}
}
} else {
if (GUILayout.Button("Open graph", GUILayout.Height(40))) {
NodeEditorWindow.Open(sceneGraph.graph);
}
if (removeSafely) {
GUILayout.BeginHorizontal();
GUILayout.Label("Really remove graph?");
GUI.color = new Color(1, 0.8f, 0.8f);
if (GUILayout.Button("Remove")) {
removeSafely = false;
Undo.RecordObject(sceneGraph, "Removed graph");
sceneGraph.graph = null;
}
GUI.color = Color.white;
if (GUILayout.Button("Cancel")) {
removeSafely = false;
}
GUILayout.EndHorizontal();
} else {
GUI.color = new Color(1, 0.8f, 0.8f);
if (GUILayout.Button("Remove graph")) {
removeSafely = true;
}
GUI.color = Color.white;
}
}
}
private void OnEnable() {
sceneGraph = target as SceneGraph;
Type sceneGraphType = sceneGraph.GetType();
if (sceneGraphType == typeof(SceneGraph)) {
graphType = null;
} else {
Type baseType = sceneGraphType.BaseType;
if (baseType.IsGenericType) {
graphType = sceneGraphType = baseType.GetGenericArguments() [0];
}
}
}
public void CreateGraph(Type type) {
Undo.RecordObject(sceneGraph, "Create graph");
sceneGraph.graph = ScriptableObject.CreateInstance(type) as NodeGraph;
sceneGraph.graph.name = sceneGraph.name + "-graph";
}
}
}

View File

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

View File

@ -65,7 +65,7 @@ namespace XNode {
[Obsolete("Use AddDynamicInput instead")] [Obsolete("Use AddDynamicInput instead")]
public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { 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); return AddDynamicInput(type, connectionType, typeConstraint, fieldName);
} }
[Obsolete("Use AddDynamicOutput instead")] [Obsolete("Use AddDynamicOutput instead")]
@ -119,12 +119,12 @@ namespace XNode {
protected void OnEnable() { protected void OnEnable() {
if (graphHotfix != null) graph = graphHotfix; if (graphHotfix != null) graph = graphHotfix;
graphHotfix = null; graphHotfix = null;
UpdateStaticPorts(); UpdatePorts();
Init(); Init();
} }
/// <summary> Update static ports to reflect class fields. This happens automatically on enable. </summary> /// <summary> Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list. </summary>
public void UpdateStaticPorts() { public void UpdatePorts() {
NodeDataCache.UpdatePorts(this, ports); NodeDataCache.UpdatePorts(this, ports);
} }
@ -262,7 +262,7 @@ namespace XNode {
#region Attributes #region Attributes
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary> /// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Field)]
public class InputAttribute : Attribute { public class InputAttribute : Attribute {
public ShowBackingValue backingValue; public ShowBackingValue backingValue;
public ConnectionType connectionType; public ConnectionType connectionType;
@ -285,7 +285,7 @@ namespace XNode {
} }
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary> /// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Field)]
public class OutputAttribute : Attribute { public class OutputAttribute : Attribute {
public ShowBackingValue backingValue; public ShowBackingValue backingValue;
public ConnectionType connectionType; public ConnectionType connectionType;
@ -314,16 +314,41 @@ namespace XNode {
public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { } public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { }
} }
/// <summary> Manually supply node class with a context menu path </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CreateNodeMenuAttribute : Attribute { public class CreateNodeMenuAttribute : Attribute {
public string menuName; public string menuName;
public int order;
/// <summary> Manually supply node class with a context menu path </summary> /// <summary> Manually supply node class with a context menu path </summary>
/// <param name="menuName"> Path to this node in the context menu. Null or empty hides it. </param> /// <param name="menuName"> Path to this node in the context menu. Null or empty hides it. </param>
public CreateNodeMenuAttribute(string menuName) { public CreateNodeMenuAttribute(string menuName) {
this.menuName = menuName; this.menuName = menuName;
this.order = 0;
}
/// <summary> Manually supply node class with a context menu path </summary>
/// <param name="menuName"> Path to this node in the context menu. Null or empty hides it. </param>
/// <param name="order"> The order by which the menu items are displayed. </param>
public CreateNodeMenuAttribute(string menuName, int order) {
this.menuName = menuName;
this.order = order;
} }
} }
/// <summary> Prevents Node of the same type to be added more than once (configurable) to a NodeGraph </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class DisallowMultipleNodesAttribute : Attribute {
// TODO: Make inheritance work in such a way that applying [DisallowMultipleNodes(1)] to type NodeBar : Node
// while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both.
public int max;
/// <summary> Prevents Node of the same type to be added more than once (configurable) to a NodeGraph </summary>
/// <param name="max"> How many nodes to allow. Defaults to 1. </param>
public DisallowMultipleNodesAttribute(int max = 1) {
this.max = max;
}
}
/// <summary> Specify a color for this node type </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeTintAttribute : Attribute { public class NodeTintAttribute : Attribute {
public Color color; public Color color;
@ -350,6 +375,7 @@ namespace XNode {
} }
} }
/// <summary> Specify a width for this node type </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeWidthAttribute : Attribute { public class NodeWidthAttribute : Attribute {
public int width; public int width;

View File

@ -9,7 +9,7 @@ namespace XNode {
private static PortDataCache portDataCache; private static PortDataCache portDataCache;
private static bool Initialized { get { return portDataCache != null; } } private static bool Initialized { get { return portDataCache != null; } }
/// <summary> Update static ports to reflect class fields. </summary> /// <summary> Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. </summary>
public static void UpdatePorts(Node node, Dictionary<string, NodePort> ports) { public static void UpdatePorts(Node node, Dictionary<string, NodePort> ports) {
if (!Initialized) BuildCache(); if (!Initialized) BuildCache();
@ -17,6 +17,8 @@ namespace XNode {
Dictionary<string, List<NodePort>> removedPorts = new Dictionary<string, List<NodePort>>(); Dictionary<string, List<NodePort>> removedPorts = new Dictionary<string, List<NodePort>>();
System.Type nodeType = node.GetType(); System.Type nodeType = node.GetType();
List<NodePort> dynamicListPorts = new List<NodePort>();
List<NodePort> typePortCache; List<NodePort> typePortCache;
if (portDataCache.TryGetValue(nodeType, out typePortCache)) { if (portDataCache.TryGetValue(nodeType, out typePortCache)) {
for (int i = 0; i < typePortCache.Count; i++) { for (int i = 0; i < typePortCache.Count; i++) {
@ -25,6 +27,7 @@ namespace XNode {
} }
// Cleanup port dict - Remove nonexisting static ports - update static port types // Cleanup port dict - Remove nonexisting static ports - update static port types
// AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation.
// Loop through current node ports // Loop through current node ports
foreach (NodePort port in ports.Values.ToList()) { foreach (NodePort port in ports.Values.ToList()) {
// If port still exists, check it it has been changed // If port still exists, check it it has been changed
@ -43,6 +46,10 @@ namespace XNode {
port.ClearConnections(); port.ClearConnections();
ports.Remove(port.fieldName); ports.Remove(port.fieldName);
} }
// If the port is dynamic and is managed by a dynamic port list, flag it for reference updates
else if (IsDynamicListPort(port)) {
dynamicListPorts.Add(port);
}
} }
// Add missing ports // Add missing ports
foreach (NodePort staticPort in staticPorts.Values) { foreach (NodePort staticPort in staticPorts.Values) {
@ -60,6 +67,55 @@ namespace XNode {
ports.Add(staticPort.fieldName, port); ports.Add(staticPort.fieldName, port);
} }
} }
// Finally, make sure dynamic list port settings correspond to the settings of their "backing port"
foreach (NodePort listPort in dynamicListPorts) {
// At this point we know that ports here are dynamic list ports
// which have passed name/"backing port" checks, ergo we can proceed more safely.
string backingPortName = listPort.fieldName.Split(' ')[0];
NodePort backingPort = staticPorts[backingPortName];
// Update port constraints. Creating a new port instead will break the editor, mandating the need for setters.
listPort.ValueType = GetBackingValueType(backingPort.ValueType);
listPort.direction = backingPort.direction;
listPort.connectionType = backingPort.connectionType;
listPort.typeConstraint = backingPort.typeConstraint;
}
}
/// <summary>
/// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists
/// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not
/// defined as an array or a list), returns the given type itself.
/// </summary>
private static System.Type GetBackingValueType(System.Type portValType) {
if (portValType.HasElementType) {
return portValType.GetElementType();
}
if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) {
return portValType.GetGenericArguments()[0];
}
return portValType;
}
/// <summary>Returns true if the given port is in a dynamic port list.</summary>
private static bool IsDynamicListPort(NodePort port) {
// Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have
// no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port.
// Thus, we need to check for attributes... (but at least we don't need to look at all fields this time)
string[] fieldNameParts = port.fieldName.Split(' ');
if (fieldNameParts.Length != 2) return false;
FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]);
if (backingPortInfo == null) return false;
object[] attribs = backingPortInfo.GetCustomAttributes(true);
return attribs.Any(x => {
Node.InputAttribute inputAttribute = x as Node.InputAttribute;
Node.OutputAttribute outputAttribute = x as Node.OutputAttribute;
return inputAttribute != null && inputAttribute.dynamicPortList ||
outputAttribute != null && outputAttribute.dynamicPortList;
});
} }
/// <summary> Cache node types </summary> /// <summary> Cache node types </summary>
@ -81,6 +137,7 @@ namespace XNode {
case "UnityEngine": case "UnityEngine":
case "System": case "System":
case "mscorlib": case "mscorlib":
case "Microsoft":
continue; continue;
default: default:
nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
@ -99,7 +156,14 @@ namespace XNode {
// GetFields doesnt return inherited private fields, so walk through base types and pick those up // GetFields doesnt return inherited private fields, so walk through base types and pick those up
System.Type tempType = nodeType; System.Type tempType = nodeType;
while ((tempType = tempType.BaseType) != typeof(XNode.Node)) { while ((tempType = tempType.BaseType) != typeof(XNode.Node)) {
fieldInfo.AddRange(tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)); FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
for (int i = 0; i < parentFields.Length; i++) {
// Ensure that we do not already have a member with this type and name
FieldInfo parentField = parentFields[i];
if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) {
fieldInfo.Add(parentField);
}
}
} }
return fieldInfo; return fieldInfo;
} }

View File

@ -81,5 +81,44 @@ namespace XNode {
// Remove all nodes prior to graph destruction // Remove all nodes prior to graph destruction
Clear(); Clear();
} }
#region Attributes
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted. </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class RequireNodeAttribute : Attribute {
public Type type0;
public Type type1;
public Type type2;
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary>
public RequireNodeAttribute(Type type) {
this.type0 = type;
this.type1 = null;
this.type2 = null;
}
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary>
public RequireNodeAttribute(Type type, Type type2) {
this.type0 = type;
this.type1 = type2;
this.type2 = null;
}
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary>
public RequireNodeAttribute(Type type, Type type2, Type type3) {
this.type0 = type;
this.type1 = type2;
this.type2 = type3;
}
public bool Requires(Type type) {
if (type == null) return false;
if (type == type0) return true;
else if (type == type1) return true;
else if (type == type2) return true;
return false;
}
}
#endregion
} }
} }

View File

@ -19,9 +19,18 @@ namespace XNode {
} }
} }
public IO direction { get { return _direction; } } public IO direction {
public Node.ConnectionType connectionType { get { return _connectionType; } } get { return _direction; }
public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } } internal set { _direction = value; }
}
public Node.ConnectionType connectionType {
get { return _connectionType; }
internal set { _connectionType = value; }
}
public Node.TypeConstraint typeConstraint {
get { return _typeConstraint; }
internal set { _typeConstraint = value; }
}
/// <summary> Is this port connected to anytihng? </summary> /// <summary> Is this port connected to anytihng? </summary>
public bool IsConnected { get { return connections.Count != 0; } } public bool IsConnected { get { return connections.Count != 0; } }

23
Scripts/SceneGraph.cs Normal file
View File

@ -0,0 +1,23 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XNode;
namespace XNode {
/// <summary> Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects. </summary>
public class SceneGraph : MonoBehaviour {
public NodeGraph graph;
}
/// <summary> Derive from this class to create a SceneGraph with a specific graph type. </summary>
/// <example>
/// <code>
/// public class MySceneGraph : SceneGraph<MyGraph> {
///
/// }
/// </code>
/// </example>
public class SceneGraph<T> : SceneGraph where T : NodeGraph {
public new T graph { get { return base.graph as T; } set { base.graph = value; } }
}
}

View File

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

View File

@ -1,7 +1,7 @@
{ {
"name": "com.github.siccity.xnode", "name": "com.github.siccity.xnode",
"description": "xNode provides a set of APIs and an editor interface for creating and editing custom node graphs.", "description": "xNode provides a set of APIs and an editor interface for creating and editing custom node graphs.",
"version": "1.7.0", "version": "1.8.0",
"unity": "2018.1", "unity": "2018.1",
"displayName": "xNode" "displayName": "xNode"
} }