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

Merge branch 'master' into examples

# Conflicts:
#	.gitignore
This commit is contained in:
Thor Brigsted 2019-09-23 23:27:48 +02:00
commit 49e29a9f3e
41 changed files with 835 additions and 252 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: thorbrigsted
open_collective: # Replace with a single Open Collective username
ko_fi: thorbrigsted
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

4
.gitignore vendored
View File

@ -20,10 +20,6 @@
# Unity3D Generated File On Crash Reports # Unity3D Generated File On Crash Reports
sysinfo.txt sysinfo.txt
README.md.meta
LICENSE.md.meta
CONTRIBUTING.md.meta
.git.meta .git.meta
.gitignore.meta .gitignore.meta
.gitattributes.meta .gitattributes.meta

View File

@ -4,10 +4,11 @@
If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)! 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, try splitting them into separate commits. 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. * Avoid including irellevant whitespace or formatting changes.
* Comment your code. * Comment your code.
* Spell check your code / comments * Spell check your code / comments
* Use consistent formatting
## 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.
@ -15,7 +16,7 @@ xNode aims to be simple and extendible, not trying to fix all of Unity's shortco
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
Skim through the code and you'll get the hang of it quickly. Using consistent formatting is key to having a clean git history. Skim through the code and you'll get the hang of it quickly.
* Methods, Types and properties PascalCase * Methods, Types and properties PascalCase
* Variables camelCase * Variables camelCase
* Public methods XML commented. Params described if not obvious * Public methods XML commented. Params described if not obvious

7
CONTRIBUTING.md.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bc1db8b29c76d44648c9c86c2dfade6d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

7
LICENSE.md.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 77523c356ccf04f56b53e6527c6b12fd
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -7,7 +7,7 @@
[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)
[Support Me on Ko-fi](https://ko-fi.com/Z8Z5DYWA) Support xNode on [Ko-fi](https://ko-fi.com/Z8Z5DYWA) or [Patreon](https://www.patreon.com/thorbrigsted)
### xNode ### xNode
Thinking of developing a node-based plugin? Then this is for you. You can download it as an archive and unpack to a new unity project, or connect it as git submodule. Thinking of developing a node-based plugin? Then this is for you. You can download it as an archive and unpack to a new unity project, or connect it as git submodule.
@ -32,6 +32,20 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
* [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph * [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph
* [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
*(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,
add the following line to your project's `manifest.json`:
```
"com.github.siccity.xnode": "https://github.com/siccity/xNode.git"
```
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.
### 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
@ -70,8 +84,3 @@ public class MathNode : Node {
Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support. Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support.
Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page. Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page.
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")

7
README.md.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 243efae3a6b7941ad8f8e54dcf38ce8c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -12,6 +12,12 @@ namespace XNodeEditor {
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
EditorGUI.BeginProperty(position, label, property); EditorGUI.BeginProperty(position, label, property);
EnumPopup(position, property, label);
EditorGUI.EndProperty();
}
public static void EnumPopup(Rect position, SerializedProperty property, GUIContent label) {
// Throw error on wrong type // Throw error on wrong type
if (property.propertyType != SerializedPropertyType.Enum) { if (property.propertyType != SerializedPropertyType.Enum) {
throw new ArgumentException("Parameter selected must be of type System.Enum"); throw new ArgumentException("Parameter selected must be of type System.Enum");
@ -39,10 +45,9 @@ namespace XNodeEditor {
NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property); NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
} }
#endif #endif
EditorGUI.EndProperty();
} }
private void ShowContextMenuAtMouse(SerializedProperty property) { public static void ShowContextMenuAtMouse(SerializedProperty property) {
// Initialize menu // Initialize menu
GenericMenu menu = new GenericMenu(); GenericMenu menu = new GenericMenu();
@ -57,7 +62,7 @@ namespace XNodeEditor {
menu.DropDown(r); menu.DropDown(r);
} }
private void SetEnum(SerializedProperty property, int index) { private static void SetEnum(SerializedProperty property, int index) {
property.enumValueIndex = index; property.enumValueIndex = index;
property.serializedObject.ApplyModifiedProperties(); property.serializedObject.ApplyModifiedProperties();
property.serializedObject.Update(); property.serializedObject.Update();

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 327994a52f523b641898a39ff7500a02
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,48 @@
#if UNITY_EDITOR && ODIN_INSPECTOR
using System;
using System.Collections.Generic;
using System.Reflection;
using Sirenix.OdinInspector.Editor;
using UnityEngine;
using XNode;
namespace XNodeEditor {
internal class OdinNodeInGraphAttributeProcessor<T> : OdinAttributeProcessor<T> where T : Node {
public override bool CanProcessSelfAttributes(InspectorProperty property) {
return false;
}
public override bool CanProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member) {
if (!NodeEditor.inNodeEditor)
return false;
if (member.MemberType == MemberTypes.Field) {
switch (member.Name) {
case "graph":
case "position":
case "ports":
return true;
default:
break;
}
}
return false;
}
public override void ProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member, List<Attribute> attributes) {
switch (member.Name) {
case "graph":
case "position":
case "ports":
attributes.Add(new HideInInspector());
break;
default:
break;
}
}
}
}
#endif

View File

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

View File

@ -0,0 +1,49 @@
#if UNITY_EDITOR && ODIN_INSPECTOR
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities.Editor;
using UnityEngine;
using XNode;
namespace XNodeEditor {
public class InputAttributeDrawer : OdinAttributeDrawer<XNode.Node.InputAttribute> {
protected override bool CanDrawAttributeProperty(InspectorProperty property) {
Node node = property.Tree.WeakTargets[0] as Node;
return node != null;
}
protected override void DrawPropertyLayout(GUIContent label) {
Node node = Property.Tree.WeakTargets[0] as Node;
NodePort port = node.GetInputPort(Property.Name);
if (!NodeEditor.inNodeEditor) {
if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected)
CallNextDrawer(label);
return;
}
if (Property.Tree.WeakTargets.Count > 1) {
SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected");
return;
}
if (port != null) {
var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath);
if (portPropoerty == null) {
SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath);
return;
} else {
var labelWidth = Property.GetAttribute<LabelWidthAttribute>();
if (labelWidth != null)
GUIHelper.PushLabelWidth(labelWidth.Width);
NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30));
if (labelWidth != null)
GUIHelper.PopLabelWidth();
}
}
}
}
}
#endif

View File

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

View File

@ -0,0 +1,49 @@
#if UNITY_EDITOR && ODIN_INSPECTOR
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities.Editor;
using UnityEngine;
using XNode;
namespace XNodeEditor {
public class OutputAttributeDrawer : OdinAttributeDrawer<XNode.Node.OutputAttribute> {
protected override bool CanDrawAttributeProperty(InspectorProperty property) {
Node node = property.Tree.WeakTargets[0] as Node;
return node != null;
}
protected override void DrawPropertyLayout(GUIContent label) {
Node node = Property.Tree.WeakTargets[0] as Node;
NodePort port = node.GetOutputPort(Property.Name);
if (!NodeEditor.inNodeEditor) {
if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected)
CallNextDrawer(label);
return;
}
if (Property.Tree.WeakTargets.Count > 1) {
SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected");
return;
}
if (port != null) {
var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath);
if (portPropoerty == null) {
SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath);
return;
} else {
var labelWidth = Property.GetAttribute<LabelWidthAttribute>();
if (labelWidth != null)
GUIHelper.PushLabelWidth(labelWidth.Width);
NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30));
if (labelWidth != null)
GUIHelper.PopLabelWidth();
}
}
}
}
}
#endif

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a6a1bbc054e282346a02e7bbddde3206
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,20 @@
using UnityEngine;
namespace XNodeEditor.Internal {
public struct RerouteReference {
public XNode.NodePort port;
public int connectionIndex;
public int pointIndex;
public RerouteReference(XNode.NodePort port, int connectionIndex, int pointIndex) {
this.port = port;
this.connectionIndex = connectionIndex;
this.pointIndex = pointIndex;
}
public void InsertPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); }
public void SetPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex) [pointIndex] = pos; }
public void RemovePoint() { port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); }
public Vector2 GetPoint() { return port.GetReroutePoints(connectionIndex) [pointIndex]; }
}
}

View File

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

View File

@ -3,29 +3,51 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
#if ODIN_INSPECTOR
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
#endif
namespace XNodeEditor { namespace XNodeEditor {
/// <summary> Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. </summary> /// <summary> Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. </summary>
[CustomNodeEditor(typeof(XNode.Node))] [CustomNodeEditor(typeof(XNode.Node))]
public class NodeEditor : XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node> { public class NodeEditor : XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node> {
private readonly Color DEFAULTCOLOR = new Color32(90, 97, 105, 255);
/// <summary> Fires every whenever a node was modified through the editor </summary> /// <summary> Fires every whenever a node was modified through the editor </summary>
public static Action<XNode.Node> onUpdateNode; public static Action<XNode.Node> onUpdateNode;
public 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
internal static bool inNodeEditor = false;
#endif
public virtual void OnHeaderGUI() { public virtual void OnHeaderGUI() {
GUILayout.Label(target.name, 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> /// <summary> Draws standard field editors for all public fields </summary>
public virtual void OnBodyGUI() { public virtual void OnBodyGUI() {
#if ODIN_INSPECTOR
inNodeEditor = true;
#endif
// Unity specifically requires this to save/update any serial object. // Unity specifically requires this to save/update any serial object.
// serializedObject.Update(); must go at the start of an inspector gui, and // serializedObject.Update(); must go at the start of an inspector gui, and
// serializedObject.ApplyModifiedProperties(); goes at the end. // serializedObject.ApplyModifiedProperties(); goes at the end.
serializedObject.Update(); serializedObject.Update();
string[] excludes = { "m_Script", "graph", "position", "ports" }; string[] excludes = { "m_Script", "graph", "position", "ports" };
#if ODIN_INSPECTOR
InspectorUtilities.BeginDrawPropertyTree(objectTree, true);
GUIHelper.PushLabelWidth(84);
objectTree.Draw(true);
InspectorUtilities.EndDrawPropertyTree(objectTree);
GUIHelper.PopLabelWidth();
#else
// Iterate through serialized properties and draw them like the Inspector (But with ports) // Iterate through serialized properties and draw them like the Inspector (But with ports)
SerializedProperty iterator = serializedObject.GetIterator(); SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true; bool enterChildren = true;
@ -35,6 +57,7 @@ namespace XNodeEditor {
if (excludes.Contains(iterator.name)) continue; if (excludes.Contains(iterator.name)) continue;
NodeEditorGUILayout.PropertyField(iterator, true); NodeEditorGUILayout.PropertyField(iterator, true);
} }
#endif
// Iterate through dynamic ports and draw them in the order in which they are serialized // Iterate through dynamic ports and draw them in the order in which they are serialized
foreach (XNode.NodePort dynamicPort in target.DynamicPorts) { foreach (XNode.NodePort dynamicPort in target.DynamicPorts) {
@ -43,20 +66,37 @@ namespace XNodeEditor {
} }
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
#if ODIN_INSPECTOR
// Call repaint so that the graph window elements respond properly to layout changes coming from Odin
if (GUIHelper.RepaintRequested) {
GUIHelper.ClearRepaintRequest();
window.Repaint();
}
#else
window.Repaint();
#endif
#if ODIN_INSPECTOR
inNodeEditor = false;
#endif
} }
public virtual int GetWidth() { public virtual int GetWidth() {
Type type = target.GetType(); Type type = target.GetType();
int width; int width;
if (NodeEditorWindow.nodeWidth.TryGetValue(type, out width)) return width; if (type.TryGetAttributeWidth(out width)) return width;
else return 208; else return 208;
} }
/// <summary> Returns color for target node </summary>
public virtual Color GetTint() { public virtual Color GetTint() {
// Try get color from [NodeTint] attribute
Type type = target.GetType(); Type type = target.GetType();
Color color; Color color;
if (NodeEditorWindow.nodeTint.TryGetValue(type, out color)) return color; if (type.TryGetAttributeTint(out color)) return color;
else return Color.white; // Return default color (grey)
else return DEFAULTCOLOR;
} }
public virtual GUIStyle GetBodyStyle() { public virtual GUIStyle GetBodyStyle() {
@ -73,19 +113,20 @@ namespace XNodeEditor {
} }
// 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("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes); menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes);
menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
// 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) {
XNode.Node node = Selection.activeObject as XNode.Node; XNode.Node node = Selection.activeObject as XNode.Node;
NodeEditorWindow.AddCustomContextMenuItems(menu, node); menu.AddCustomContextMenuItems(node);
} }
} }
/// <summary> Rename the node asset. This will trigger a reimport of the node. </summary> /// <summary> Rename the node asset. This will trigger a reimport of the node. </summary>
public void Rename(string newName) { public void Rename(string newName) {
if (newName == null || newName.Trim() == "") newName = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name); if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType());
target.name = newName; target.name = newName;
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
} }

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using XNodeEditor.Internal;
namespace XNodeEditor { namespace XNodeEditor {
public partial class NodeEditorWindow { public partial class NodeEditorWindow {
@ -11,6 +12,8 @@ namespace XNodeEditor {
public static bool isPanning { get; private set; } public static bool isPanning { get; private set; }
public static Vector2[] dragOffset; public static Vector2[] dragOffset;
public static XNode.Node[] copyBuffer = null;
private bool IsDraggingPort { get { return draggedOutput != null; } } private bool IsDraggingPort { get { return draggedOutput != null; } }
private bool IsHoveringPort { get { return hoveredPort != null; } } private bool IsHoveringPort { get { return hoveredPort != null; } }
private bool IsHoveringNode { get { return hoveredNode != null; } } private bool IsHoveringNode { get { return hoveredNode != null; } }
@ -19,6 +22,7 @@ namespace XNodeEditor {
[NonSerialized] private XNode.NodePort hoveredPort = null; [NonSerialized] private XNode.NodePort hoveredPort = null;
[NonSerialized] private XNode.NodePort draggedOutput = null; [NonSerialized] private XNode.NodePort draggedOutput = null;
[NonSerialized] private XNode.NodePort draggedOutputTarget = null; [NonSerialized] private XNode.NodePort draggedOutputTarget = 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>(); private List<RerouteReference> selectedReroutes = new List<RerouteReference>();
@ -27,29 +31,23 @@ namespace XNodeEditor {
private RerouteReference[] preBoxSelectionReroute; private RerouteReference[] preBoxSelectionReroute;
private Rect selectionBox; private Rect selectionBox;
private bool isDoubleClick = false; private bool isDoubleClick = false;
private Vector2 lastMousePosition;
private struct RerouteReference {
public XNode.NodePort port;
public int connectionIndex;
public int pointIndex;
public RerouteReference(XNode.NodePort port, int connectionIndex, int pointIndex) {
this.port = port;
this.connectionIndex = connectionIndex;
this.pointIndex = pointIndex;
}
public void InsertPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); }
public void SetPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex) [pointIndex] = pos; }
public void RemovePoint() { port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); }
public Vector2 GetPoint() { return port.GetReroutePoints(connectionIndex) [pointIndex]; }
}
public void Controls() { public void Controls() {
wantsMouseMove = true; wantsMouseMove = true;
Event e = Event.current; Event e = Event.current;
switch (e.type) { switch (e.type) {
case EventType.DragUpdated:
case EventType.DragPerform:
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
if (e.type == EventType.DragPerform) {
DragAndDrop.AcceptDrag();
graphEditor.OnDropObjects(DragAndDrop.objectReferences);
}
break;
case EventType.MouseMove: case EventType.MouseMove:
//Keyboard commands will not get correct mouse position from Event
lastMousePosition = e.mousePosition;
break; break;
case EventType.ScrollWheel: case EventType.ScrollWheel:
float oldZoom = zoom; float oldZoom = zoom;
@ -148,8 +146,10 @@ namespace XNodeEditor {
if (IsHoveringPort) { if (IsHoveringPort) {
if (hoveredPort.IsOutput) { if (hoveredPort.IsOutput) {
draggedOutput = hoveredPort; draggedOutput = hoveredPort;
autoConnectOutput = hoveredPort;
} else { } else {
hoveredPort.VerifyConnections(); hoveredPort.VerifyConnections();
autoConnectOutput = null;
if (hoveredPort.IsConnected) { if (hoveredPort.IsConnected) {
XNode.Node node = hoveredPort.node; XNode.Node node = hoveredPort.node;
XNode.NodePort output = hoveredPort.Connection; XNode.NodePort output = hoveredPort.Connection;
@ -217,6 +217,12 @@ namespace XNodeEditor {
EditorUtility.SetDirty(graph); EditorUtility.SetDirty(graph);
} }
} }
// Open context menu for auto-connection
else if (NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
GenericMenu menu = new GenericMenu();
graphEditor.AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
}
//Release dragged connection //Release dragged connection
draggedOutput = null; draggedOutput = null;
draggedOutputTarget = null; draggedOutputTarget = null;
@ -268,11 +274,13 @@ namespace XNodeEditor {
ShowPortContextMenu(hoveredPort); ShowPortContextMenu(hoveredPort);
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false);
autoConnectOutput = null;
GenericMenu menu = new GenericMenu(); GenericMenu menu = new GenericMenu();
NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu); NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); 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. e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places.
} else if (!IsHoveringNode) { } else if (!IsHoveringNode) {
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));
@ -286,23 +294,40 @@ namespace XNodeEditor {
case EventType.KeyDown: case EventType.KeyDown:
if (EditorGUIUtility.editingTextField) break; if (EditorGUIUtility.editingTextField) break;
else if (e.keyCode == KeyCode.F) Home(); else if (e.keyCode == KeyCode.F) Home();
if (IsMac()) { if (NodeEditorUtilities.IsMac()) {
if (e.keyCode == KeyCode.Return) RenameSelectedNode(); if (e.keyCode == KeyCode.Return) RenameSelectedNode();
} else { } else {
if (e.keyCode == KeyCode.F2) RenameSelectedNode(); if (e.keyCode == KeyCode.F2) RenameSelectedNode();
} }
if (e.keyCode == KeyCode.A) {
if (Selection.objects.Any(x => graph.nodes.Contains(x as XNode.Node))) {
foreach (XNode.Node node in graph.nodes) {
DeselectNode(node);
}
} else {
foreach (XNode.Node node in graph.nodes) {
SelectNode(node, true);
}
}
}
break; break;
case EventType.ValidateCommand: case EventType.ValidateCommand:
case EventType.ExecuteCommand: case EventType.ExecuteCommand:
if (e.commandName == "SoftDelete") { if (e.commandName == "SoftDelete") {
if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
e.Use(); e.Use();
} else if (IsMac() && e.commandName == "Delete") { } else if (NodeEditorUtilities.IsMac() && e.commandName == "Delete") {
if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
e.Use(); e.Use();
} else if (e.commandName == "Duplicate") { } else if (e.commandName == "Duplicate") {
if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes(); if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes();
e.Use(); e.Use();
} else if (e.commandName == "Copy") {
if (e.type == EventType.ExecuteCommand) CopySelectedNodes();
e.Use();
} else if (e.commandName == "Paste") {
if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition));
e.Use();
} }
Repaint(); Repaint();
break; break;
@ -316,14 +341,6 @@ namespace XNodeEditor {
} }
} }
public bool IsMac() {
#if UNITY_2017_1_OR_NEWER
return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
#else
return SystemInfo.operatingSystem.StartsWith("Mac");
#endif
}
private void RecalculateDragOffsets(Event current) { private void RecalculateDragOffsets(Event current) {
dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count]; dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count];
// Selected nodes // Selected nodes
@ -340,10 +357,17 @@ namespace XNodeEditor {
} }
} }
/// <summary> Puts all nodes in focus. If no nodes are present, resets view to </summary> /// <summary> Puts all selected nodes in focus. If no nodes are present, resets view and zoom to to origin </summary>
public void Home() { public void Home() {
zoom = 2; var nodes = Selection.objects.Where(o => o is XNode.Node).Cast<XNode.Node>().ToList();
panOffset = Vector2.zero; if (nodes.Count > 0) {
Vector2 minPos = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y)));
Vector2 maxPos = nodes.Select(x => x.position + (nodeSizes.ContainsKey(x) ? nodeSizes[x] : Vector2.zero)).Aggregate((x, y) => new Vector2(Mathf.Max(x.x, y.x), Mathf.Max(x.y, y.y)));
panOffset = -(minPos + (maxPos - minPos) / 2f);
} else {
zoom = 2;
panOffset = Vector2.zero;
}
} }
/// <summary> Remove nodes in the graph in Selection.objects</summary> /// <summary> Remove nodes in the graph in Selection.objects</summary>
@ -386,41 +410,60 @@ namespace XNodeEditor {
/// <summary> Duplicate selected nodes and select the duplicates </summary> /// <summary> Duplicate selected nodes and select the duplicates </summary>
public void DuplicateSelectedNodes() { public void DuplicateSelectedNodes() {
UnityEngine.Object[] newNodes = new UnityEngine.Object[Selection.objects.Length]; // Get selected nodes which are part of this graph
XNode.Node[] selectedNodes = Selection.objects.Select(x => x as XNode.Node).Where(x => x != null && x.graph == graph).ToArray();
// Get top left node position
Vector2 topLeftNode = selectedNodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y)));
InsertDuplicateNodes(selectedNodes, topLeftNode + new Vector2(30, 30));
}
public void CopySelectedNodes() {
copyBuffer = Selection.objects.Select(x => x as XNode.Node).Where(x => x != null && x.graph == graph).ToArray();
}
public void PasteNodes(Vector2 pos) {
InsertDuplicateNodes(copyBuffer, pos);
}
private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) {
if (nodes == null || nodes.Length == 0) return;
// Get top-left node
Vector2 topLeftNode = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y)));
Vector2 offset = topLeft - topLeftNode;
UnityEngine.Object[] newNodes = new UnityEngine.Object[nodes.Length];
Dictionary<XNode.Node, XNode.Node> substitutes = new Dictionary<XNode.Node, XNode.Node>(); Dictionary<XNode.Node, XNode.Node> substitutes = new Dictionary<XNode.Node, XNode.Node>();
for (int i = 0; i < Selection.objects.Length; i++) { for (int i = 0; i < nodes.Length; i++) {
if (Selection.objects[i] is XNode.Node) { XNode.Node srcNode = nodes[i];
XNode.Node srcNode = Selection.objects[i] as XNode.Node; if (srcNode == null) continue;
if (srcNode.graph != graph) continue; // ignore nodes selected in another graph 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 + new Vector2(30, 30); newNodes[i] = newNode;
newNodes[i] = newNode;
}
} }
// Walk through the selected nodes again, recreate connections, using the new nodes // Walk through the selected nodes again, recreate connections, using the new nodes
for (int i = 0; i < Selection.objects.Length; i++) { for (int i = 0; i < nodes.Length; i++) {
if (Selection.objects[i] is XNode.Node) { XNode.Node srcNode = nodes[i];
XNode.Node srcNode = Selection.objects[i] as XNode.Node; if (srcNode == null) continue;
if (srcNode.graph != graph) continue; // ignore nodes selected in another graph foreach (XNode.NodePort port in srcNode.Ports) {
foreach (XNode.NodePort port in srcNode.Ports) { for (int c = 0; c < port.ConnectionCount; c++) {
for (int c = 0; c < port.ConnectionCount; c++) { XNode.NodePort inputPort = port.direction == XNode.NodePort.IO.Input ? port : port.GetConnection(c);
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);
XNode.NodePort outputPort = port.direction == XNode.NodePort.IO.Output ? port : port.GetConnection(c);
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.UpdateStaticPorts();
newNodeOut.UpdateStaticPorts(); newNodeOut.UpdateStaticPorts();
inputPort = newNodeIn.GetInputPort(inputPort.fieldName); inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
outputPort = newNodeOut.GetOutputPort(outputPort.fieldName); outputPort = newNodeOut.GetOutputPort(outputPort.fieldName);
}
if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort);
} }
if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort);
} }
} }
} }
// Select the new nodes
Selection.objects = newNodes; Selection.objects = newNodes;
} }
@ -470,5 +513,22 @@ namespace XNodeEditor {
Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom)); Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom));
return windowRect.Contains(mousePos); return windowRect.Contains(mousePos);
} }
/// <summary> Attempt to connect dragged output to target node </summary>
public void AutoConnect(XNode.Node node) {
if (autoConnectOutput == null) return;
// Find input port of same type
XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && x.ValueType == autoConnectOutput.ValueType);
// Fallback to input port
if (inputPort == null) inputPort = node.Ports.FirstOrDefault(x => x.IsInput);
// Autoconnect
if (inputPort != null) autoConnectOutput.Connect(inputPort);
// Save changes
EditorUtility.SetDirty(graph);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
autoConnectOutput = null;
}
} }
} }

View File

@ -6,7 +6,8 @@ namespace XNodeEditor {
class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor { class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor {
/// <summary> Automatically delete Node sub-assets before deleting their script. /// <summary> Automatically delete Node sub-assets before deleting their script.
/// <para/> This is important to do, because you can't delete null sub assets. </summary> /// This is important to do, because you can't delete null sub assets.
/// <para/> For another workaround, see: https://gitlab.com/RotaryHeart-UnityShare/subassetmissingscriptdelete </summary>
private static AssetDeleteResult OnWillDeleteAsset (string path, RemoveAssetOptions options) { private static AssetDeleteResult OnWillDeleteAsset (string path, RemoveAssetOptions options) {
// Get the object that is requested for deletion // Get the object that is requested for deletion
UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object> (path); UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object> (path);
@ -51,6 +52,8 @@ namespace XNodeEditor {
Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath); Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath);
// Ensure that all sub node assets are present in the graph node list // Ensure that all sub node assets are present in the graph node list
for (int u = 0; u < objs.Length; u++) { for (int u = 0; u < objs.Length; u++) {
// Ignore null sub assets
if (objs[u] == null) continue;
if (!graph.nodes.Contains (objs[u] as XNode.Node)) graph.nodes.Add(objs[u] as XNode.Node); if (!graph.nodes.Contains (objs[u] as XNode.Node)) graph.nodes.Add(objs[u] as XNode.Node);
} }
} }

View File

@ -4,6 +4,9 @@ using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
#if ODIN_INSPECTOR
using Sirenix.OdinInspector.Editor;
#endif
namespace XNodeEditor.Internal { namespace XNodeEditor.Internal {
/// <summary> Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) </summary> /// <summary> Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) </summary>
@ -17,6 +20,24 @@ namespace XNodeEditor.Internal {
public NodeEditorWindow window; public NodeEditorWindow window;
public K target; public K target;
public SerializedObject serializedObject; public SerializedObject serializedObject;
#if ODIN_INSPECTOR
private PropertyTree _objectTree;
public PropertyTree objectTree {
get {
if (this._objectTree == null) {
try {
bool wasInEditor = NodeEditor.inNodeEditor;
NodeEditor.inNodeEditor = true;
this._objectTree = PropertyTree.Create(this.serializedObject);
NodeEditor.inNodeEditor = wasInEditor;
} catch (ArgumentException ex) {
Debug.Log(ex);
}
}
return this._objectTree;
}
}
#endif
public static T GetEditor(K target, NodeEditorWindow window) { public static T GetEditor(K target, NodeEditorWindow window) {
if (target == null) return null; if (target == null) return null;
@ -50,7 +71,7 @@ namespace XNodeEditor.Internal {
editorTypes = new Dictionary<Type, Type>(); editorTypes = new Dictionary<Type, Type>();
//Get all classes deriving from NodeEditor via reflection //Get all classes deriving from NodeEditor via reflection
Type[] nodeEditors = XNodeEditor.NodeEditorWindow.GetDerivedTypes(typeof(T)); Type[] nodeEditors = typeof(T).GetDerivedTypes();
for (int i = 0; i < nodeEditors.Length; i++) { for (int i = 0; i < nodeEditors.Length; i++) {
if (nodeEditors[i].IsAbstract) continue; if (nodeEditors[i].IsAbstract) continue;
var attribs = nodeEditors[i].GetCustomAttributes(typeof(A), false); var attribs = nodeEditors[i].GetCustomAttributes(typeof(A), false);

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using XNodeEditor.Internal;
namespace XNodeEditor { namespace XNodeEditor {
/// <summary> Contains GUI methods </summary> /// <summary> Contains GUI methods </summary>
@ -10,6 +11,7 @@ namespace XNodeEditor {
public NodeGraphEditor graphEditor; public NodeGraphEditor graphEditor;
private List<UnityEngine.Object> selectionCache; private List<UnityEngine.Object> selectionCache;
private List<XNode.Node> culledNodes; private List<XNode.Node> culledNodes;
/// <summary> 19 if docked, 22 if not </summary>
private int topPadding { get { return isDocked() ? 19 : 22; } } 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> /// <summary> Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run.</summary>
public event Action onLateGUI; public event Action onLateGUI;
@ -200,11 +202,11 @@ namespace XNodeEditor {
Rect fromRect; Rect fromRect;
if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue; if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue;
Color connectionColor = graphEditor.GetPortColor(output);
for (int k = 0; k < output.ConnectionCount; k++) { for (int k = 0; k < output.ConnectionCount; k++) {
XNode.NodePort input = output.GetConnection(k); XNode.NodePort input = output.GetConnection(k);
Color noodleColor = graphEditor.GetNoodleColor(output, input);
// Error handling // 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 == 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 (!input.IsConnectedTo(output)) input.Connect(output);
@ -217,7 +219,7 @@ namespace XNodeEditor {
gridPoints.Add(fromRect.center); gridPoints.Add(fromRect.center);
gridPoints.AddRange(reroutePoints); gridPoints.AddRange(reroutePoints);
gridPoints.Add(toRect.center); gridPoints.Add(toRect.center);
DrawNoodle(connectionColor, gridPoints); DrawNoodle(noodleColor, gridPoints);
// Loop through reroute points again and draw the points // Loop through reroute points again and draw the points
for (int i = 0; i < reroutePoints.Count; i++) { for (int i = 0; i < reroutePoints.Count; i++) {
@ -233,7 +235,7 @@ namespace XNodeEditor {
GUI.DrawTexture(rect, NodeEditorResources.dotOuter); GUI.DrawTexture(rect, NodeEditorResources.dotOuter);
} }
GUI.color = connectionColor; GUI.color = noodleColor;
GUI.DrawTexture(rect, NodeEditorResources.dot); GUI.DrawTexture(rect, NodeEditorResources.dot);
if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef); if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef);
if (rect.Contains(mousePos)) hoveredReroute = rerouteRef; if (rect.Contains(mousePos)) hoveredReroute = rerouteRef;
@ -411,15 +413,12 @@ namespace XNodeEditor {
} }
private void DrawTooltip() { private void DrawTooltip() {
if (hoveredPort != null) { if (hoveredPort != null && NodeEditorPreferences.GetSettings().portTooltips && graphEditor != null) {
Type type = hoveredPort.ValueType; string tooltip = graphEditor.GetPortTooltip(hoveredPort);
GUIContent content = new GUIContent(); if (string.IsNullOrEmpty(tooltip)) return;
content.text = type.PrettyName(); GUIContent content = new GUIContent(tooltip);
if (hoveredPort.IsOutput) {
object obj = hoveredPort.node.GetValue(hoveredPort);
content.text += " = " + (obj != null ? obj.ToString() : "null");
}
Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content); Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content);
size.x += 8;
Rect rect = new Rect(Event.current.mousePosition - (size), size); Rect rect = new Rect(Event.current.mousePosition - (size), size);
EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip); EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip);
Repaint(); Repaint();

View File

@ -139,9 +139,8 @@ namespace XNodeEditor {
rect.size = new Vector2(16, 16); rect.size = new Vector2(16, 16);
Color backgroundColor = new Color32(90, 97, 105, 255); NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current);
Color tint; Color backgroundColor = editor.GetTint();
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col); DrawPortHandle(rect, backgroundColor, col);
@ -153,7 +152,7 @@ namespace XNodeEditor {
private static System.Type GetType(SerializedProperty property) { private static System.Type GetType(SerializedProperty property) {
System.Type parentType = property.serializedObject.targetObject.GetType(); System.Type parentType = property.serializedObject.targetObject.GetType();
System.Reflection.FieldInfo fi = NodeEditorWindow.GetFieldInfo(parentType, property.name); System.Reflection.FieldInfo fi = parentType.GetFieldInfo(property.name);
return fi.FieldType; return fi.FieldType;
} }
@ -176,7 +175,6 @@ namespace XNodeEditor {
Rect rect = GUILayoutUtility.GetLastRect(); Rect rect = GUILayoutUtility.GetLastRect();
position = rect.position - new Vector2(16, 0); position = rect.position - new Vector2(16, 0);
} }
// If property is an output, display a text label and put a port handle on the right side // If property is an output, display a text label and put a port handle on the right side
else if (port.direction == XNode.NodePort.IO.Output) { else if (port.direction == XNode.NodePort.IO.Output) {
@ -195,9 +193,8 @@ namespace XNodeEditor {
Rect rect = new Rect(position, new Vector2(16, 16)); Rect rect = new Rect(position, new Vector2(16, 16));
Color backgroundColor = new Color32(90, 97, 105, 255); NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current);
Color tint; Color backgroundColor = editor.GetTint();
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col); DrawPortHandle(rect, backgroundColor, col);
@ -223,9 +220,8 @@ namespace XNodeEditor {
rect.size = new Vector2(16, 16); rect.size = new Vector2(16, 16);
Color backgroundColor = new Color32(90, 97, 105, 255); NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current);
Color tint; Color backgroundColor = editor.GetTint();
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col); DrawPortHandle(rect, backgroundColor, col);
@ -293,7 +289,7 @@ namespace XNodeEditor {
} }
} }
return new { index = -1, port = (XNode.NodePort) null }; return new { index = -1, port = (XNode.NodePort) 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();
ReorderableList list = null; ReorderableList list = null;
@ -323,12 +319,12 @@ namespace XNodeEditor {
XNode.NodePort port = node.GetPort(fieldName + " " + index); XNode.NodePort port = node.GetPort(fieldName + " " + index);
if (hasArrayData) { if (hasArrayData) {
if (arrayData.arraySize <= index) { if (arrayData.arraySize <= index) {
EditorGUI.LabelField(rect, "Invalid element " + index); EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
return; return;
} }
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index); SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index);
EditorGUI.PropertyField(rect, itemData, true); EditorGUI.PropertyField(rect, itemData, true);
} else EditorGUI.LabelField(rect, port.fieldName); } else EditorGUI.LabelField(rect, port != null ? port.fieldName : "");
if (port != null) { if (port != null) {
Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0)); Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0));
NodeEditorGUILayout.PortField(pos, port); NodeEditorGUILayout.PortField(pos, port);
@ -422,12 +418,17 @@ namespace XNodeEditor {
} }
} }
return new { index = -1, port = (XNode.NodePort) null }; return new { index = -1, port = (XNode.NodePort) null };
}); }).Where(x => x.port != null);
dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
int index = rl.index; int index = rl.index;
if (dynamicPorts.Count > index) { if (dynamicPorts[index] == null) {
Debug.LogWarning("No port found at index " + index + " - Skipped");
} else if (dynamicPorts.Count <= index) {
Debug.LogWarning("DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + " - Skipped");
} else {
// Clear the removed ports connections // Clear the removed ports connections
dynamicPorts[index].ClearConnections(); dynamicPorts[index].ClearConnections();
// Move following connections one step up to replace the missing connection // Move following connections one step up to replace the missing connection
@ -442,11 +443,14 @@ namespace XNodeEditor {
node.RemoveDynamicPort(dynamicPorts[dynamicPorts.Count() - 1].fieldName); node.RemoveDynamicPort(dynamicPorts[dynamicPorts.Count() - 1].fieldName);
serializedObject.Update(); serializedObject.Update();
EditorUtility.SetDirty(node); EditorUtility.SetDirty(node);
} else {
Debug.LogWarning("DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + ". Skipping.");
} }
if (hasArrayData) { if (hasArrayData) {
if (arrayData.arraySize <= index) {
Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped");
Debug.Log(rl.list[0]);
return;
}
arrayData.DeleteArrayElementAtIndex(index); arrayData.DeleteArrayElementAtIndex(index);
// Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues // Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues
if (dynamicPorts.Count <= arrayData.arraySize) { if (dynamicPorts.Count <= arrayData.arraySize) {

View File

@ -32,7 +32,9 @@ namespace XNodeEditor {
public Color32 highlightColor = new Color32(255, 255, 255, 255); public Color32 highlightColor = new Color32(255, 255, 255, 255);
public bool gridSnap = true; public bool gridSnap = true;
public bool autoSave = true; public bool autoSave = true;
public bool dragToCreate = true;
public bool zoomToMouse = true; public bool zoomToMouse = true;
public bool portTooltips = true;
[SerializeField] private string typeColorsData = ""; [SerializeField] private string typeColorsData = "";
[NonSerialized] public Dictionary<string, Color> typeColors = new Dictionary<string, Color>(); [NonSerialized] public Dictionary<string, Color> typeColors = new Dictionary<string, Color>();
public NoodleType noodleType = NoodleType.Curve; public NoodleType noodleType = NoodleType.Curve;
@ -105,6 +107,9 @@ namespace XNodeEditor {
VerifyLoaded(); VerifyLoaded();
Settings settings = NodeEditorPreferences.settings[lastKey]; Settings settings = NodeEditorPreferences.settings[lastKey];
if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), GUILayout.Width(100))) Application.OpenURL("https://github.com/Siccity/xNode/wiki");
EditorGUILayout.Space();
NodeSettingsGUI(lastKey, settings); NodeSettingsGUI(lastKey, settings);
GridSettingsGUI(lastKey, settings); GridSettingsGUI(lastKey, settings);
SystemSettingsGUI(lastKey, settings); SystemSettingsGUI(lastKey, settings);
@ -147,6 +152,8 @@ namespace XNodeEditor {
EditorGUILayout.LabelField("Node", EditorStyles.boldLabel); EditorGUILayout.LabelField("Node", EditorStyles.boldLabel);
settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor); settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor);
settings.noodleType = (NoodleType) EditorGUILayout.EnumPopup("Noodle type", (Enum) settings.noodleType); settings.noodleType = (NoodleType) EditorGUILayout.EnumPopup("Noodle type", (Enum) settings.noodleType);
settings.portTooltips = EditorGUILayout.Toggle("Port Tooltips", settings.portTooltips);
settings.dragToCreate = EditorGUILayout.Toggle(new GUIContent("Drag to Create", "Drag a port connection anywhere on the grid to create and connect a node"), settings.dragToCreate);
if (GUI.changed) { if (GUI.changed) {
SavePrefs(key, settings); SavePrefs(key, settings);
NodeEditorWindow.RepaintAll(); NodeEditorWindow.RepaintAll();
@ -218,12 +225,19 @@ namespace XNodeEditor {
if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]); if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]);
else { else {
#if UNITY_5_4_OR_NEWER #if UNITY_5_4_OR_NEWER
UnityEngine.Random.State oldState = UnityEngine.Random.state;
UnityEngine.Random.InitState(typeName.GetHashCode()); UnityEngine.Random.InitState(typeName.GetHashCode());
#else #else
int oldSeed = UnityEngine.Random.seed;
UnityEngine.Random.seed = typeName.GetHashCode(); UnityEngine.Random.seed = typeName.GetHashCode();
#endif #endif
col = 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); typeColors.Add(type, col);
#if UNITY_5_4_OR_NEWER
UnityEngine.Random.state = oldState;
#else
UnityEngine.Random.seed = oldSeed;
#endif
} }
} }
return col; return col;

View File

@ -7,62 +7,55 @@ using UnityEditor;
using UnityEngine; using UnityEngine;
namespace XNodeEditor { namespace XNodeEditor {
/// <summary> Contains reflection-related info </summary> /// <summary> Contains reflection-related extensions built for xNode </summary>
public partial class NodeEditorWindow { public static class NodeEditorReflection {
/// <summary> Custom node tint colors defined with [NodeColor(r, g, b)] </summary> [NonSerialized] private static Dictionary<Type, Color> nodeTint;
public static Dictionary<Type, Color> nodeTint { get { return _nodeTint != null ? _nodeTint : _nodeTint = GetNodeTint(); } } [NonSerialized] private static Dictionary<Type, int> nodeWidth;
[NonSerialized] private static Dictionary<Type, Color> _nodeTint;
/// <summary> Custom node widths defined with [NodeWidth(width)] </summary>
public static Dictionary<Type, int> nodeWidth { get { return _nodeWidth != null ? _nodeWidth : _nodeWidth = GetNodeWidth(); } }
[NonSerialized] private static Dictionary<Type, int> _nodeWidth;
/// <summary> All available node types </summary> /// <summary> All available node types </summary>
public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } }
[NonSerialized] private static Type[] _nodeTypes = null; [NonSerialized] private static Type[] _nodeTypes = null;
private Func<bool> isDocked { /// <summary> Return a delegate used to determine whether window is docked or not. It is faster to cache this delegate than run the reflection required each time. </summary>
get { public static Func<bool> GetIsDockedDelegate(this EditorWindow window) {
if (_isDocked == null) { BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true);
MethodInfo isDockedMethod = typeof(NodeEditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); return (Func<bool>) Delegate.CreateDelegate(typeof(Func<bool>), window, isDockedMethod);
_isDocked = (Func<bool>) Delegate.CreateDelegate(typeof(Func<bool>), this, isDockedMethod);
}
return _isDocked;
}
} }
private Func<bool> _isDocked;
public static Type[] GetNodeTypes() { public static Type[] GetNodeTypes() {
//Get all classes deriving from Node via reflection //Get all classes deriving from Node via reflection
return GetDerivedTypes(typeof(XNode.Node)); return GetDerivedTypes(typeof(XNode.Node));
} }
public static Dictionary<Type, Color> GetNodeTint() { /// <summary> Custom node tint colors defined with [NodeColor(r, g, b)] </summary>
Dictionary<Type, Color> tints = new Dictionary<Type, Color>(); public static bool TryGetAttributeTint(this Type nodeType, out Color tint) {
for (int i = 0; i < nodeTypes.Length; i++) { if (nodeTint == null) {
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTintAttribute), true); CacheAttributes<Color, XNode.Node.NodeTintAttribute>(ref nodeTint, x => x.color);
if (attribs == null || attribs.Length == 0) continue;
XNode.Node.NodeTintAttribute attrib = attribs[0] as XNode.Node.NodeTintAttribute;
tints.Add(nodeTypes[i], attrib.color);
} }
return tints; return nodeTint.TryGetValue(nodeType, out tint);
} }
public static Dictionary<Type, int> GetNodeWidth() { /// <summary> Get custom node widths defined with [NodeWidth(width)] </summary>
Dictionary<Type, int> widths = new Dictionary<Type, int>(); public static bool TryGetAttributeWidth(this Type nodeType, out int width) {
for (int i = 0; i < nodeTypes.Length; i++) { if (nodeWidth == null) {
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidthAttribute), true); CacheAttributes<int, XNode.Node.NodeWidthAttribute>(ref nodeWidth, x => x.width);
if (attribs == null || attribs.Length == 0) continue; }
XNode.Node.NodeWidthAttribute attrib = attribs[0] as XNode.Node.NodeWidthAttribute; return nodeWidth.TryGetValue(nodeType, out width);
widths.Add(nodeTypes[i], attrib.width); }
private static void CacheAttributes<V, A>(ref Dictionary<Type, V> dict, Func<A, V> getter) where A : Attribute {
dict = new Dictionary<Type, V>();
for (int i = 0; i < nodeTypes.Length; i++) {
object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true);
if (attribs == null || attribs.Length == 0) continue;
A attrib = attribs[0] as A;
dict.Add(nodeTypes[i], getter(attrib));
} }
return widths;
} }
/// <summary> Get FieldInfo of a field, including those that are private and/or inherited </summary> /// <summary> Get FieldInfo of a field, including those that are private and/or inherited </summary>
public static FieldInfo GetFieldInfo(Type type, string fieldName) { public static FieldInfo GetFieldInfo(this Type type, string fieldName) {
// If we can't find field in the first run, it's probably a private field in a base class. // 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); FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Search base classes for private fields only. Public fields are found above // Search base classes for private fields only. Public fields are found above
@ -71,25 +64,45 @@ namespace XNodeEditor {
} }
/// <summary> Get all classes deriving from baseType via reflection </summary> /// <summary> Get all classes deriving from baseType via reflection </summary>
public static Type[] GetDerivedTypes(Type baseType) { public static Type[] GetDerivedTypes(this Type baseType) {
List<System.Type> types = new List<System.Type>(); List<System.Type> types = new List<System.Type>();
System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies) { foreach (Assembly assembly in assemblies) {
try { try {
types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
} catch(ReflectionTypeLoadException) {} } catch (ReflectionTypeLoadException) { }
} }
return types.ToArray(); return types.ToArray();
} }
public static void AddCustomContextMenuItems(GenericMenu contextMenu, object obj) { /// <summary> Find methods marked with the [ContextMenu] attribute and add them to the context menu </summary>
KeyValuePair<ContextMenu, System.Reflection.MethodInfo>[] items = GetContextMenuMethods(obj); public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) {
KeyValuePair<ContextMenu, MethodInfo>[] items = GetContextMenuMethods(obj);
if (items.Length != 0) { if (items.Length != 0) {
contextMenu.AddSeparator(""); contextMenu.AddSeparator("");
for (int i = 0; i < items.Length; i++) { List<string> invalidatedEntries = new List<string>();
KeyValuePair<ContextMenu, System.Reflection.MethodInfo> kvp = items[i]; foreach (KeyValuePair<ContextMenu, MethodInfo> checkValidate in items) {
contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) {
invalidatedEntries.Add(checkValidate.Key.menuItem);
}
} }
for (int i = 0; i < items.Length; i++) {
KeyValuePair<ContextMenu, MethodInfo> kvp = items[i];
if (invalidatedEntries.Contains(kvp.Key.menuItem)) {
contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem));
} else {
contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null));
}
}
}
}
/// <summary> Call OnValidate on target </summary>
public static void TriggerOnValidate(this UnityEngine.Object target) {
System.Reflection.MethodInfo onValidate = null;
if (target != null) {
onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (onValidate != null) onValidate.Invoke(target, null);
} }
} }

View File

@ -9,7 +9,7 @@ using UnityEngine;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace XNodeEditor { namespace XNodeEditor {
/// <summary> A set of editor-only utilities and extensions for UnityNodeEditorBase </summary> /// <summary> A set of editor-only utilities and extensions for xNode </summary>
public static class NodeEditorUtilities { public static class NodeEditorUtilities {
/// <summary>C#'s Script Icon [The one MonoBhevaiour Scripts have].</summary> /// <summary>C#'s Script Icon [The one MonoBhevaiour Scripts have].</summary>
@ -25,7 +25,7 @@ namespace XNodeEditor {
public static bool GetAttrib<T>(object[] attribs, out T attribOut) where T : Attribute { public static bool GetAttrib<T>(object[] attribs, out T attribOut) where T : Attribute {
for (int i = 0; i < attribs.Length; i++) { for (int i = 0; i < attribs.Length; i++) {
if (attribs[i] is T){ if (attribs[i] is T) {
attribOut = attribs[i] as T; attribOut = attribs[i] as T;
return true; return true;
} }
@ -36,7 +36,7 @@ namespace XNodeEditor {
public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute { public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute {
// If we can't find field in the first run, it's probably a private field in a base class. // 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); FieldInfo field = classType.GetFieldInfo(fieldName);
// This shouldn't happen. Ever. // This shouldn't happen. Ever.
if (field == null) { if (field == null) {
Debug.LogWarning("Field " + fieldName + " couldnt be found"); Debug.LogWarning("Field " + fieldName + " couldnt be found");
@ -84,6 +84,14 @@ namespace XNodeEditor {
return true; return true;
} }
public static bool IsMac() {
#if UNITY_2017_1_OR_NEWER
return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
#else
return SystemInfo.operatingSystem.StartsWith("Mac");
#endif
}
/// <summary> Returns true if this can be casted to <see cref="Type"/></summary> /// <summary> Returns true if this can be casted to <see cref="Type"/></summary>
public static bool IsCastableTo(this Type from, Type to) { public static bool IsCastableTo(this Type from, Type to) {
if (to.IsAssignableFrom(from)) return true; if (to.IsAssignableFrom(from)) return true;
@ -133,6 +141,24 @@ namespace XNodeEditor {
} else return type.ToString(); } else return type.ToString();
} }
/// <summary> Returns the default name for the node type. </summary>
public static string NodeDefaultName(Type type) {
string typeName = type.Name;
// Automatically remove redundant 'Node' postfix
if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node"));
typeName = UnityEditor.ObjectNames.NicifyVariableName(typeName);
return typeName;
}
/// <summary> Returns the default creation path for the node type. </summary>
public static string NodeDefaultPath(Type type) {
string typePath = type.ToString().Replace('.', '/');
// Automatically remove redundant 'Node' postfix
if (typePath.EndsWith("Node")) typePath = typePath.Substring(0, typePath.LastIndexOf("Node"));
typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath);
return typePath;
}
/// <summary>Creates a new C# Class.</summary> /// <summary>Creates a new C# Class.</summary>
[MenuItem("Assets/Create/xNode/Node C# Script", false, 89)] [MenuItem("Assets/Create/xNode/Node C# Script", false, 89)]
private static void CreateNode() { private static void CreateNode() {

View File

@ -2,6 +2,8 @@ using System.Collections.Generic;
using UnityEditor; using UnityEditor;
using UnityEditor.Callbacks; using UnityEditor.Callbacks;
using UnityEngine; using UnityEngine;
using System;
using Object = UnityEngine.Object;
namespace XNodeEditor { namespace XNodeEditor {
[InitializeOnLoad] [InitializeOnLoad]
@ -14,6 +16,14 @@ namespace XNodeEditor {
[SerializeField] private NodePortReference[] _references = new NodePortReference[0]; [SerializeField] private NodePortReference[] _references = new NodePortReference[0];
[SerializeField] private Rect[] _rects = new Rect[0]; [SerializeField] private Rect[] _rects = new Rect[0];
private Func<bool> isDocked {
get {
if (_isDocked == null) _isDocked = this.GetIsDockedDelegate();
return _isDocked;
}
}
private Func<bool> _isDocked;
[System.Serializable] private class NodePortReference { [System.Serializable] private class NodePortReference {
[SerializeField] private XNode.Node _node; [SerializeField] private XNode.Node _node;
[SerializeField] private string _name; [SerializeField] private string _name;

View File

@ -38,49 +38,72 @@ namespace XNodeEditor {
if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path
return attrib.menuName; return attrib.menuName;
else // Return generated path else // Return generated path
return ObjectNames.NicifyVariableName(type.ToString().Replace('.', '/')); return NodeEditorUtilities.NodeDefaultPath(type);
} }
/// <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 < NodeEditorWindow.nodeTypes.Length; i++) { for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++) {
Type type = NodeEditorWindow.nodeTypes[i]; Type type = NodeEditorReflection.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, () => { menu.AddItem(new GUIContent(path), false, () => {
CreateNode(type, pos); XNode.Node node = CreateNode(type, pos);
NodeEditorWindow.current.AutoConnect(node);
}); });
} }
menu.AddSeparator(""); menu.AddSeparator("");
menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorWindow.OpenPreferences()); if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos));
NodeEditorWindow.AddCustomContextMenuItems(menu, target); else menu.AddDisabledItem(new GUIContent("Paste"));
menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorReflection.OpenPreferences());
menu.AddCustomContextMenuItems(target);
} }
/// <summary> Returned color is used to color noodles </summary>
public virtual Color GetNoodleColor(XNode.NodePort output, XNode.NodePort input) {
return GetTypeColor(output.ValueType);
}
/// <summary> Returned color is used to color ports </summary>
public virtual Color GetPortColor(XNode.NodePort port) { public virtual Color GetPortColor(XNode.NodePort port) {
return GetTypeColor(port.ValueType); return GetTypeColor(port.ValueType);
} }
/// <summary> Returns generated color for a type. This color is editable in preferences </summary>
public virtual Color GetTypeColor(Type type) { public virtual Color GetTypeColor(Type type) {
return NodeEditorPreferences.GetTypeColor(type); return NodeEditorPreferences.GetTypeColor(type);
} }
/// <summary> Override to display custom tooltips </summary>
public virtual string GetPortTooltip(XNode.NodePort port) {
Type portType = port.ValueType;
string tooltip = "";
tooltip = portType.PrettyName();
if (port.IsOutput) {
object obj = port.node.GetValue(port);
tooltip += " = " + (obj != null ? obj.ToString() : "null");
}
return tooltip;
}
/// <summary> Deal with objects dropped into the graph through DragAndDrop </summary>
public virtual void OnDropObjects(UnityEngine.Object[] objects) {
Debug.Log("No OnDropItems 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>
public virtual void CreateNode(Type type, Vector2 position) { public virtual XNode.Node CreateNode(Type type, Vector2 position) {
XNode.Node node = target.AddNode(type); XNode.Node node = target.AddNode(type);
node.position = position; node.position = position;
if (string.IsNullOrEmpty(node.name)) { if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
// 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); AssetDatabase.AddObjectToAsset(node, target);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
NodeEditorWindow.RepaintAll(); NodeEditorWindow.RepaintAll();
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>
@ -93,15 +116,15 @@ namespace XNodeEditor {
} }
/// <summary> Safely remove a node and all its connections. </summary> /// <summary> Safely remove a node and all its connections. </summary>
public void RemoveNode(XNode.Node node) { public virtual void RemoveNode(XNode.Node node) {
UnityEngine.Object.DestroyImmediate(node, true);
target.RemoveNode(node); target.RemoveNode(node);
UnityEngine.Object.DestroyImmediate(node, true);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
} }
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public class CustomNodeGraphEditorAttribute : Attribute, public class CustomNodeGraphEditorAttribute : Attribute,
XNodeEditor.Internal.NodeEditorBase<NodeGraphEditor, NodeGraphEditor.CustomNodeGraphEditorAttribute, XNode.NodeGraph>.INodeEditorAttrib { XNodeEditor.Internal.NodeEditorBase<NodeGraphEditor, NodeGraphEditor.CustomNodeGraphEditorAttribute, XNode.NodeGraph>.INodeEditorAttrib {
private Type inspectedType; private Type inspectedType;
public string editorPrefsKey; public string editorPrefsKey;
/// <summary> Tells a NodeGraphEditor which Graph type it is an editor for </summary> /// <summary> Tells a NodeGraphEditor which Graph type it is an editor for </summary>

View File

@ -2,65 +2,67 @@
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 {
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;
private bool firstFrame = true; private bool firstFrame = true;
/// <summary> Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply. /// <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) { public static RenamePopup Show(Object target, float width = 200) {
RenamePopup window = EditorWindow.GetWindow<RenamePopup>(true, "Rename " + target.name, true); RenamePopup window = EditorWindow.GetWindow<RenamePopup>(true, "Rename " + target.name, true);
if (current != null) current.Close(); if (current != null) current.Close();
current = window; current = window;
window.target = target; window.target = target;
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"); GUI.FocusControl("ClearAllFocus");
window.UpdatePositionToMouse(); window.UpdatePositionToMouse();
return window; return window;
} }
private void UpdatePositionToMouse() { private void UpdatePositionToMouse() {
if (Event.current == null) return; if (Event.current == null) return;
Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
Rect pos = position; Rect pos = position;
pos.x = mousePoint.x - position.width * 0.5f; pos.x = mousePoint.x - position.width * 0.5f;
pos.y = mousePoint.y - 10; pos.y = mousePoint.y - 10;
position = pos; position = pos;
} }
private void OnLostFocus() { private void OnLostFocus() {
// Make the popup close on lose focus // Make the popup close on lose focus
Close(); Close();
} }
private void OnGUI() { private void OnGUI() {
if (firstFrame) { if (firstFrame) {
UpdatePositionToMouse(); UpdatePositionToMouse();
firstFrame = false; firstFrame = false;
} }
input = EditorGUILayout.TextField(input); input = EditorGUILayout.TextField(input);
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 = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name); target.name = NodeEditorUtilities.NodeDefaultName(target.GetType());
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close(); Close();
} target.TriggerOnValidate();
} }
// Rename asset to input text }
else { // Rename asset to input text
if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { else {
target.name = input; if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) {
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); target.name = input;
Close(); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
} Close();
} target.TriggerOnValidate();
} }
} }
}
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,17 @@
{
"name": "XNodeEditor",
"references": [
"XNode"
],
"optionalUnityReferences": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": []
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 002c1bbed08fa44d282ef34fd5edb138
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -290,16 +290,26 @@ namespace XNode {
[Obsolete("Use dynamicPortList instead")] [Obsolete("Use dynamicPortList instead")]
public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } }
public bool dynamicPortList; public bool dynamicPortList;
public TypeConstraint typeConstraint;
/// <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>
/// <param name="typeConstraint">Constrains which input connections can be made from this port </param>
/// <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, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) {
this.backingValue = backingValue;
this.connectionType = connectionType;
this.dynamicPortList = dynamicPortList;
this.typeConstraint = typeConstraint;
}
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary> /// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param> /// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
/// <param name="connectionType">Should we allow multiple connections? </param> /// <param name="connectionType">Should we allow multiple connections? </param>
/// <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> /// <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) { [Obsolete("Use constructor with TypeConstraint")]
this.backingValue = backingValue; public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { }
this.connectionType = connectionType;
this.dynamicPortList = dynamicPortList;
}
} }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]

View File

@ -14,6 +14,7 @@ namespace XNode {
if (!Initialized) BuildCache(); if (!Initialized) BuildCache();
Dictionary<string, NodePort> staticPorts = new Dictionary<string, NodePort>(); Dictionary<string, NodePort> staticPorts = new Dictionary<string, NodePort>();
Dictionary<string, List<NodePort>> removedPorts = new Dictionary<string, List<NodePort>>();
System.Type nodeType = node.GetType(); System.Type nodeType = node.GetType();
List<NodePort> typePortCache; List<NodePort> typePortCache;
@ -30,39 +31,63 @@ namespace XNode {
NodePort staticPort; NodePort staticPort;
if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { if (staticPorts.TryGetValue(port.fieldName, out staticPort)) {
// If port exists but with wrong settings, remove it. Re-add it later. // If port exists but with wrong settings, remove it. Re-add it later.
if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction || port.typeConstraint != staticPort.typeConstraint) ports.Remove(port.fieldName); if (port.IsDynamic || port.direction != staticPort.direction || port.connectionType != staticPort.connectionType || port.typeConstraint != staticPort.typeConstraint) {
else port.ValueType = staticPort.ValueType; // If port is not dynamic and direction hasn't changed, add it to the list so we can try reconnecting the ports connections.
if (!port.IsDynamic && port.direction == staticPort.direction) removedPorts.Add(port.fieldName, port.GetConnections());
port.ClearConnections();
ports.Remove(port.fieldName);
} else port.ValueType = staticPort.ValueType;
} }
// If port doesn't exist anymore, remove it // If port doesn't exist anymore, remove it
else if (port.IsStatic) ports.Remove(port.fieldName); else if (port.IsStatic) {
port.ClearConnections();
ports.Remove(port.fieldName);
}
} }
// Add missing ports // Add missing ports
foreach (NodePort staticPort in staticPorts.Values) { foreach (NodePort staticPort in staticPorts.Values) {
if (!ports.ContainsKey(staticPort.fieldName)) { if (!ports.ContainsKey(staticPort.fieldName)) {
ports.Add(staticPort.fieldName, new NodePort(staticPort, node)); NodePort port = new NodePort(staticPort, node);
//If we just removed the port, try re-adding the connections
List<NodePort> reconnectConnections;
if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) {
for (int i = 0; i < reconnectConnections.Count; i++) {
NodePort connection = reconnectConnections[i];
if (connection == null) continue;
if (port.CanConnectTo(connection)) port.Connect(connection);
}
}
ports.Add(staticPort.fieldName, port);
} }
} }
} }
/// <summary> Cache node types </summary>
private static void BuildCache() { private static void BuildCache() {
portDataCache = new PortDataCache(); portDataCache = new PortDataCache();
System.Type baseType = typeof(Node); System.Type baseType = typeof(Node);
List<System.Type> nodeTypes = new List<System.Type>(); List<System.Type> nodeTypes = new List<System.Type>();
System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
Assembly selfAssembly = Assembly.GetAssembly(baseType);
if (selfAssembly.FullName.StartsWith("Assembly-CSharp") && !selfAssembly.FullName.Contains("-firstpass")) { // Loop through assemblies and add node types to list
// If xNode is not used as a DLL, check only CSharp (fast) foreach (Assembly assembly in assemblies) {
nodeTypes.AddRange(selfAssembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t))); // Skip certain dlls to improve performance
} else { string assemblyName = assembly.GetName().Name;
// Else, check all relevant DDLs (slower) int index = assemblyName.IndexOf('.');
// ignore all unity related assemblies if (index != -1) assemblyName = assemblyName.Substring(0, index);
foreach (Assembly assembly in assemblies) { switch (assemblyName) {
if (assembly.FullName.StartsWith("Unity")) continue; // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped
// unity created assemblies always have version 0.0.0 case "UnityEditor":
if (!assembly.FullName.Contains("Version=0.0.0")) continue; case "UnityEngine":
nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); case "System":
case "mscorlib":
continue;
default:
nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
break;
} }
} }
for (int i = 0; i < nodeTypes.Count; i++) { for (int i = 0; i < nodeTypes.Count; i++) {
CachePorts(nodeTypes[i]); CachePorts(nodeTypes[i]);
} }

View File

@ -67,6 +67,7 @@ namespace XNode {
} else if (attribs[i] is Node.OutputAttribute) { } else if (attribs[i] is Node.OutputAttribute) {
_direction = IO.Output; _direction = IO.Output;
_connectionType = (attribs[i] as Node.OutputAttribute).connectionType; _connectionType = (attribs[i] as Node.OutputAttribute).connectionType;
_typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint;
} }
} }
} }
@ -255,9 +256,12 @@ namespace XNode {
else output = port; else output = port;
// If there isn't one of each, they can't connect // If there isn't one of each, they can't connect
if (input == null || output == null) return false; if (input == null || output == null) return false;
// Check type constraints // Check input type constraints
if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; 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; if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false;
// Check output type constraints
if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !output.ValueType.IsAssignableFrom(input.ValueType)) return false;
if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && output.ValueType != input.ValueType) return false;
// Success // Success
return true; return true;
} }

13
Scripts/XNode.asmdef Normal file
View File

@ -0,0 +1,13 @@
{
"name": "XNode",
"references": [],
"optionalUnityReferences": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": []
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b8e24fd1eb19b4226afebb2810e3c19b
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

7
package.json Normal file
View File

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

7
package.json.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e9869d68f06b74538a01e9b8e406159e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: