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

Merge branch 'master' into feature/nodegroups

# Conflicts:
#	Scripts/Editor/NodeEditorAction.cs
#	Scripts/Editor/NodeEditorGUI.cs
#	Scripts/Editor/NodeGraphEditor.cs
This commit is contained in:
Thor Brigsted 2019-07-02 08:53:32 +02:00
commit 884878c638
32 changed files with 920 additions and 391 deletions

7
.gitignore vendored
View File

@ -21,6 +21,7 @@
sysinfo.txt
/Examples/
README.md.meta
LICENSE.md.meta
CONTRIBUTING.md.meta
.git.meta
.gitignore.meta
.gitattributes.meta

View File

@ -4,10 +4,11 @@
If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)!
## 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.
* Comment your code.
* Spell check your code / comments
* Use consistent formatting
## New features
xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings.
@ -15,9 +16,17 @@ 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.
## 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
* Variables camelCase
* Public methods XML commented
* Public methods XML commented. Params described if not obvious
* Explicit usage of brackets when doing multiple math operations on the same line
## Formatting
I use VSCode with the C# FixFormat extension and the following setting overrides:
```json
"csharpfixformat.style.spaces.beforeParenthesis": false,
"csharpfixformat.style.indent.regionIgnored": true
```
* Open braces on same line as condition
* 4 spaces for indentation.

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

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

@ -24,12 +24,21 @@ namespace XNodeEditor {
string enumName = "";
if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length) enumName = property.enumDisplayNames[property.enumValueIndex];
#if UNITY_2017_1_OR_NEWER
// Display dropdown
if (EditorGUI.DropdownButton(position, new GUIContent(enumName), FocusType.Passive)) {
// Position is all wrong if we show the dropdown during the node draw phase.
// Instead, add it to onLateGUI to display it later.
NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
}
#else
// Display dropdown
if (GUI.Button(position, new GUIContent(enumName), "MiniPopup")) {
// Position is all wrong if we show the dropdown during the node draw phase.
// Instead, add it to onLateGUI to display it later.
NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
}
#endif
EditorGUI.EndProperty();
}

View File

@ -6,41 +6,17 @@ using UnityEngine;
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>
[CustomNodeEditor(typeof(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>
public static Action<XNode.Node> onUpdateNode;
public static Dictionary<XNode.NodePort, Vector2> portPositions;
public int renaming;
public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>();
public virtual void OnHeaderGUI() {
string title = target.name;
if (renaming != 0) {
if (Selection.Contains(target)) {
int controlID = EditorGUIUtility.GetControlID(FocusType.Keyboard) + 1;
if (renaming == 1) {
EditorGUIUtility.keyboardControl = controlID;
EditorGUIUtility.editingTextField = true;
renaming = 2;
}
target.name = EditorGUILayout.TextField(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
if (!EditorGUIUtility.editingTextField) {
Debug.Log("Finish renaming");
Rename(target.name);
renaming = 0;
}
}
else {
// Selection changed, so stop renaming.
GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
Rename(target.name);
renaming = 0;
}
} else {
GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
}
GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
}
/// <summary> Draws standard field editors for all public fields </summary>
@ -50,8 +26,8 @@ namespace XNodeEditor {
// serializedObject.ApplyModifiedProperties(); goes at the end.
serializedObject.Update();
string[] excludes = { "m_Script", "graph", "position", "ports" };
portPositions = new Dictionary<XNode.NodePort, Vector2>();
// Iterate through serialized properties and draw them like the Inspector (But with ports)
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
EditorGUIUtility.labelWidth = 84;
@ -60,6 +36,13 @@ namespace XNodeEditor {
if (excludes.Contains(iterator.name)) continue;
NodeEditorGUILayout.PropertyField(iterator, true);
}
// Iterate through dynamic ports and draw them in the order in which they are serialized
foreach (XNode.NodePort dynamicPort in target.DynamicPorts) {
if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue;
NodeEditorGUILayout.PortField(dynamicPort);
}
serializedObject.ApplyModifiedProperties();
}
@ -70,11 +53,14 @@ namespace XNodeEditor {
else return 208;
}
/// <summary> Returns color for target node </summary>
public virtual Color GetTint() {
// Try get color from [NodeTint] attribute
Type type = target.GetType();
Color color;
if (NodeEditorWindow.nodeTint.TryGetValue(type, out color)) return color;
else return Color.white;
// Return default color (grey)
else return DEFAULTCOLOR;
}
public virtual GUIStyle GetBodyStyle() {
@ -91,6 +77,7 @@ namespace XNodeEditor {
}
// 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("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
@ -101,11 +88,9 @@ namespace XNodeEditor {
}
}
public void InitiateRename() {
renaming = 1;
}
/// <summary> Rename the node asset. This will trigger a reimport of the node. </summary>
public void Rename(string newName) {
if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType());
target.name = newName;
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
}

View File

@ -11,6 +11,8 @@ namespace XNodeEditor {
public static bool isPanning { get; private set; }
public static Vector2[] dragOffset;
public static XNode.Node[] copyBuffer = null;
private bool IsDraggingPort { get { return draggedOutput != null; } }
private bool IsHoveringPort { get { return hoveredPort != null; } }
private bool IsHoveringNode { get { return hoveredNode != null; } }
@ -29,11 +31,12 @@ namespace XNodeEditor {
public static NodeGroupSide resizingGroupSide;
private RerouteReference hoveredReroute = new RerouteReference();
private List<RerouteReference> selectedReroutes = new List<RerouteReference>();
private Rect nodeRects;
private Vector2 dragBoxStart;
private UnityEngine.Object[] preBoxSelection;
private RerouteReference[] preBoxSelectionReroute;
private Rect selectionBox;
private bool isDoubleClick = false;
private Vector2 lastMousePosition;
private struct RerouteReference {
public XNode.NodePort port;
@ -56,7 +59,17 @@ namespace XNodeEditor {
wantsMouseMove = true;
Event e = Event.current;
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:
//Keyboard commands will not get correct mouse position from Event
lastMousePosition = e.mousePosition;
break;
case EventType.ScrollWheel:
float oldZoom = zoom;
@ -67,7 +80,7 @@ namespace XNodeEditor {
case EventType.MouseDrag:
if (e.button == 0) {
if (IsDraggingPort) {
if (IsHoveringPort && hoveredPort.IsInput) {
if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) {
if (!draggedOutput.IsConnectedTo(hoveredPort)) {
draggedOutputTarget = hoveredPort;
}
@ -191,12 +204,7 @@ namespace XNodeEditor {
Repaint();
}
} else if (e.button == 1 || e.button == 2) {
Vector2 tempOffset = panOffset;
tempOffset += e.delta * zoom;
// Round value to increase crispyness of UI text
tempOffset.x = Mathf.Round(tempOffset.x);
tempOffset.y = Mathf.Round(tempOffset.y);
panOffset = tempOffset;
panOffset += e.delta * zoom;
isPanning = true;
}
break;
@ -227,6 +235,10 @@ namespace XNodeEditor {
SelectNode(hoveredNode, e.control || e.shift);
if (!e.control && !e.shift) selectedReroutes.Clear();
} else if (e.control || e.shift) DeselectNode(hoveredNode);
// Cache double click state, but only act on it in MouseUp - Except ClickCount only works in mouseDown.
isDoubleClick = (e.clickCount == 2);
e.Use();
currentActivity = NodeActivity.HoldNode;
} else if (IsHoveringReroute) {
@ -304,6 +316,7 @@ namespace XNodeEditor {
// If click outside node, release field focus
if (!isPanning) {
EditorGUI.FocusTextInControl(null);
EditorGUIUtility.editingTextField = false;
}
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
@ -312,6 +325,12 @@ namespace XNodeEditor {
if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) {
selectedReroutes.Clear();
SelectNode(hoveredNode, false);
// Double click to center node
if (isDoubleClick) {
Vector2 nodeDimension = nodeSizes.ContainsKey(hoveredNode) ? nodeSizes[hoveredNode] / 2 : Vector2.zero;
panOffset = -hoveredNode.position - nodeDimension;
}
}
if (currentActivity == NodeActivity.HoldGroup && deselectingGroup) {
@ -345,8 +364,9 @@ namespace XNodeEditor {
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false);
GenericMenu menu = new GenericMenu();
NodeEditor.GetEditor(hoveredNode).AddContextMenuItems(menu);
NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places.
} else if (IsHoveringGroup && !IsHoveringNode) {
if (!Selection.Contains(hoveredGroup)) SelectGroup(hoveredGroup, false);
GenericMenu menu = new GenericMenu();
@ -360,11 +380,13 @@ namespace XNodeEditor {
}
isPanning = false;
}
// Reset DoubleClick
isDoubleClick = false;
break;
case EventType.KeyDown:
if (EditorGUIUtility.editingTextField) break;
else if (e.keyCode == KeyCode.F) Home();
if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) {
if (IsMac()) {
if (e.keyCode == KeyCode.Return) RenameSelectedNode();
} else {
if (e.keyCode == KeyCode.F2) RenameSelectedNode();
@ -375,12 +397,18 @@ namespace XNodeEditor {
if (e.commandName == "SoftDelete") {
if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
e.Use();
} else if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX && e.commandName == "Delete") {
} else if (IsMac() && e.commandName == "Delete") {
if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
e.Use();
} else if (e.commandName == "Duplicate") {
if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes();
e.Use();
} 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();
break;
@ -394,6 +422,14 @@ namespace XNodeEditor {
}
}
public bool IsMac() {
#if UNITY_2017_1_OR_NEWER
return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
#else
return SystemInfo.operatingSystem.StartsWith("Mac");
#endif
}
private void RecalculateDragOffsets(Event current) {
dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count];
// Selected nodes
@ -444,7 +480,12 @@ namespace XNodeEditor {
public void RenameSelectedNode() {
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
XNode.Node node = Selection.activeObject as XNode.Node;
NodeEditor.GetEditor(node).InitiateRename();
Vector2 size;
if (nodeSizes.TryGetValue(node, out size)) {
RenamePopup.Show(Selection.activeObject, size.x);
} else {
RenamePopup.Show(Selection.activeObject);
}
}
}
@ -475,47 +516,60 @@ namespace XNodeEditor {
/// <summary> Duplicate selected nodes and select the duplicates </summary>
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>();
for (int i = 0; i < Selection.objects.Length; i++) {
if (Selection.objects[i] is XNode.Node) {
XNode.Node srcNode = Selection.objects[i] as XNode.Node;
if (srcNode.graph != graph) continue; // ignore nodes selected in another graph
XNode.Node newNode = graphEditor.CopyNode(srcNode);
substitutes.Add(srcNode, newNode);
newNode.position = srcNode.position + new Vector2(30, 30);
newNodes[i] = newNode;
} else if (Selection.objects[i] is XNode.NodeGroup) {
XNode.NodeGroup srcGroup = Selection.objects[i] as XNode.NodeGroup;
if (srcGroup.graph != graph) continue; // ignore groups selected in another graph
XNode.NodeGroup newGroup = graphEditor.CopyGroup(srcGroup);
newGroup.position = srcGroup.position + new Vector2(30, 30);
newNodes[i] = newGroup;
}
for (int i = 0; i < nodes.Length; i++) {
XNode.Node srcNode = nodes[i];
if (srcNode == null) continue;
XNode.Node newNode = graphEditor.CopyNode(srcNode);
substitutes.Add(srcNode, newNode);
newNode.position = srcNode.position + offset;
newNodes[i] = newNode;
}
// Walk through the selected nodes again, recreate connections, using the new nodes
for (int i = 0; i < Selection.objects.Length; i++) {
if (Selection.objects[i] is XNode.Node) {
XNode.Node srcNode = Selection.objects[i] as XNode.Node;
if (srcNode.graph != graph) continue; // ignore nodes selected in another graph
foreach (XNode.NodePort port in srcNode.Ports) {
for (int c = 0; c < port.ConnectionCount; 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);
for (int i = 0; i < nodes.Length; i++) {
XNode.Node srcNode = nodes[i];
if (srcNode == null) continue;
foreach (XNode.NodePort port in srcNode.Ports) {
for (int c = 0; c < port.ConnectionCount; 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.Node newNodeIn, newNodeOut;
if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) {
newNodeIn.UpdateStaticPorts();
newNodeOut.UpdateStaticPorts();
inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
outputPort = newNodeOut.GetOutputPort(outputPort.fieldName);
}
if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort);
XNode.Node newNodeIn, newNodeOut;
if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) {
newNodeIn.UpdateStaticPorts();
newNodeOut.UpdateStaticPorts();
inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
outputPort = newNodeOut.GetOutputPort(outputPort.fieldName);
}
if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort);
}
}
}
// Select the new nodes
Selection.objects = newNodes;
}
@ -523,19 +577,19 @@ namespace XNodeEditor {
public void DrawDraggedConnection() {
if (IsDraggingPort) {
Color col = NodeEditorPreferences.GetTypeColor(draggedOutput.ValueType);
col.a = draggedOutputTarget != null ? 1.0f : 0.6f;
Rect fromRect;
if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return;
Vector2 from = fromRect.center;
col.a = 0.6f;
Vector2 to = Vector2.zero;
List<Vector2> gridPoints = new List<Vector2>();
gridPoints.Add(fromRect.center);
for (int i = 0; i < draggedOutputReroutes.Count; i++) {
to = draggedOutputReroutes[i];
DrawConnection(from, to, col);
from = to;
gridPoints.Add(draggedOutputReroutes[i]);
}
to = draggedOutputTarget != null ? portConnectionPoints[draggedOutputTarget].center : WindowToGridPosition(Event.current.mousePosition);
DrawConnection(from, to, col);
if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center);
else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition));
DrawNoodle(col, gridPoints);
Color bgcol = Color.black;
Color frcol = col;

View File

@ -6,7 +6,8 @@ namespace XNodeEditor {
class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor {
/// <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) {
// Get the object that is requested for deletion
UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object> (path);
@ -51,6 +52,8 @@ namespace XNodeEditor {
Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath);
// Ensure that all sub node assets are present in the graph node list
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);
}
}

View File

@ -14,10 +14,11 @@ namespace XNodeEditor.Internal {
/// <summary> Custom editors defined with [CustomNodeEditor] </summary>
private static Dictionary<Type, Type> editorTypes;
private static Dictionary<K, T> editors = new Dictionary<K, T>();
public NodeEditorWindow window;
public K target;
public SerializedObject serializedObject;
public static T GetEditor(K target) {
public static T GetEditor(K target, NodeEditorWindow window) {
if (target == null) return null;
T editor;
if (!editors.TryGetValue(target, out editor)) {
@ -26,9 +27,12 @@ namespace XNodeEditor.Internal {
editor = Activator.CreateInstance(editorType) as T;
editor.target = target;
editor.serializedObject = new SerializedObject(target);
editor.window = window;
editor.OnCreate();
editors.Add(target, editor);
}
if (editor.target == null) editor.target = target;
if (editor.window != window) editor.window = window;
if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target);
return editor;
}
@ -56,6 +60,9 @@ namespace XNodeEditor.Internal {
}
}
/// <summary> Called on creation, after references have been set </summary>
public virtual void OnCreate() { }
public interface INodeEditorAttrib {
Type GetInspectedType();
}

View File

@ -10,20 +10,23 @@ namespace XNodeEditor {
public NodeGraphEditor graphEditor;
private List<UnityEngine.Object> selectionCache;
private List<XNode.Node> culledNodes;
/// <summary> 19 if docked, 22 if not </summary>
private int topPadding { get { return isDocked() ? 19 : 22; } }
/// <summary> 0 if docked, 3 if not </summary>
private int leftPadding { get { return isDocked() ? 2 : 0; } }
/// <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 XNode.NodeGroup renamingGroup;
public bool renamingStarted = false;
private Matrix4x4 _prevGuiMatrix;
private Matrix4x4 prevGuiMatrix;
private void OnGUI() {
Event e = Event.current;
Matrix4x4 m = GUI.matrix;
if (graph == null) return;
graphEditor = NodeGraphEditor.GetEditor(graph);
graphEditor.position = position;
ValidateGraphEditor();
Controls();
if (e.type == EventType.Layout) {
@ -37,7 +40,7 @@ namespace XNodeEditor {
DrawNodes();
DrawSelectionBox();
DrawTooltip();
graphEditor.OnGUI();
DrawGraphOnGUI();
// Run and reset onLateGUI
if (onLateGUI != null) {
@ -48,6 +51,39 @@ namespace XNodeEditor {
GUI.matrix = m;
}
public void BeginZoomed() {
GUI.EndGroup();
Rect position = new Rect(this.position);
position.x = 0;
position.y = topPadding;
Vector2 topLeft = new Vector2(position.xMin, position.yMin - topPadding);
Rect clippedArea = ScaleSizeBy(position, zoom, topLeft);
GUI.BeginGroup(clippedArea);
prevGuiMatrix = GUI.matrix;
Matrix4x4 translation = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one);
Matrix4x4 scale = Matrix4x4.Scale(new Vector3(1.0f / zoom, 1.0f / zoom, 1.0f));
GUI.matrix = translation * scale * translation.inverse * GUI.matrix;
}
public void EndZoomed() {
GUI.matrix = prevGuiMatrix;
GUI.EndGroup();
GUI.BeginGroup(new Rect(0.0f, topPadding - (topPadding * zoom), Screen.width, Screen.height));
}
/// <summary> Ends the GUI Group temporarily to draw any additional elements in the NodeGraphEditor. </summary>
private void DrawGraphOnGUI() {
GUI.EndGroup();
Rect rect = new Rect(new Vector2(leftPadding, topPadding), new Vector2(Screen.width, Screen.height));
GUI.BeginGroup(rect);
graphEditor.OnGUI();
GUI.EndGroup();
GUI.BeginGroup(new Rect(0.0f, topPadding - (topPadding * zoom), Screen.width, Screen.height));
}
public static Rect ScaleSizeBy(Rect rect, float scale, Vector2 pivotPoint) {
Rect result = rect;
result.x -= pivotPoint.x;
@ -61,33 +97,6 @@ namespace XNodeEditor {
return result;
}
public static Vector2 TopLeft(Rect rect) {
return new Vector2(rect.xMin, rect.yMin);
}
public void BeginZoomed() {
GUI.EndGroup();
Rect position = new Rect(this.position);
position.x = 0;
position.y = topPadding;
Rect clippedArea = ScaleSizeBy(position, zoom, TopLeft(position));
GUI.BeginGroup(clippedArea);
_prevGuiMatrix = GUI.matrix;
Matrix4x4 translation = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one);
Matrix4x4 scale = Matrix4x4.Scale(new Vector3(1.0f / zoom, 1.0f / zoom, 1.0f));
GUI.matrix = translation * scale * translation.inverse * GUI.matrix;
}
public void EndZoomed() {
GUI.matrix = _prevGuiMatrix;
GUI.EndGroup();
GUI.BeginGroup(new Rect(0.0f, topPadding, Screen.width, Screen.height));
}
public void DrawGrid(Rect rect, float zoom, Vector2 panOffset) {
rect.position = Vector2.zero;
@ -144,52 +153,69 @@ namespace XNodeEditor {
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
/// <summary> Draw a bezier from startpoint to endpoint, both in grid coordinates </summary>
public void DrawConnection(Vector2 startPoint, Vector2 endPoint, Color col) {
startPoint = GridToWindowPosition(startPoint);
endPoint = GridToWindowPosition(endPoint);
/// <summary> Draw a bezier from output to input in grid coordinates </summary>
public void DrawNoodle(Color col, List<Vector2> gridPoints) {
Vector2[] windowPoints = gridPoints.Select(x => GridToWindowPosition(x)).ToArray();
Handles.color = col;
int length = gridPoints.Count;
switch (NodeEditorPreferences.GetSettings().noodleType) {
case NodeEditorPreferences.NoodleType.Curve:
Vector2 startTangent = startPoint;
if (startPoint.x < endPoint.x) startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, 0.7f);
else startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, -0.7f);
Vector2 outputTangent = Vector2.right;
for (int i = 0; i < length - 1; i++) {
Vector2 inputTangent = Vector2.left;
Vector2 endTangent = endPoint;
if (startPoint.x > endPoint.x) endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, -0.7f);
else endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, 0.7f);
Handles.DrawBezier(startPoint, endPoint, startTangent, endTangent, col, null, 4);
if (i == 0) outputTangent = Vector2.right * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom;
if (i < length - 2) {
Vector2 ab = (windowPoints[i + 1] - windowPoints[i]).normalized;
Vector2 cb = (windowPoints[i + 1] - windowPoints[i + 2]).normalized;
Vector2 ac = (windowPoints[i + 2] - windowPoints[i]).normalized;
Vector2 p = (ab + cb) * 0.5f;
float tangentLength = (Vector2.Distance(windowPoints[i], windowPoints[i + 1]) + Vector2.Distance(windowPoints[i + 1], windowPoints[i + 2])) * 0.005f * zoom;
float side = ((ac.x * (windowPoints[i + 1].y - windowPoints[i].y)) - (ac.y * (windowPoints[i + 1].x - windowPoints[i].x)));
p = new Vector2(-p.y, p.x) * Mathf.Sign(side) * tangentLength;
inputTangent = p;
} else {
inputTangent = Vector2.left * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom;
}
Handles.DrawBezier(windowPoints[i], windowPoints[i + 1], windowPoints[i] + ((outputTangent * 50) / zoom), windowPoints[i + 1] + ((inputTangent * 50) / zoom), col, null, 4);
outputTangent = -inputTangent;
}
break;
case NodeEditorPreferences.NoodleType.Line:
Handles.color = col;
Handles.DrawAAPolyLine(5, startPoint, endPoint);
for (int i = 0; i < length - 1; i++) {
Handles.DrawAAPolyLine(5, windowPoints[i], windowPoints[i + 1]);
}
break;
case NodeEditorPreferences.NoodleType.Angled:
Handles.color = col;
if (startPoint.x <= endPoint.x - (50 / zoom)) {
float midpoint = (startPoint.x + endPoint.x) * 0.5f;
Vector2 start_1 = startPoint;
Vector2 end_1 = endPoint;
start_1.x = midpoint;
end_1.x = midpoint;
Handles.DrawAAPolyLine(5, startPoint, start_1);
Handles.DrawAAPolyLine(5, start_1, end_1);
Handles.DrawAAPolyLine(5, end_1, endPoint);
} else {
float midpoint = (startPoint.y + endPoint.y) * 0.5f;
Vector2 start_1 = startPoint;
Vector2 end_1 = endPoint;
start_1.x += 25 / zoom;
end_1.x -= 25 / zoom;
Vector2 start_2 = start_1;
Vector2 end_2 = end_1;
start_2.y = midpoint;
end_2.y = midpoint;
Handles.DrawAAPolyLine(5, startPoint, start_1);
Handles.DrawAAPolyLine(5, start_1, start_2);
Handles.DrawAAPolyLine(5, start_2, end_2);
Handles.DrawAAPolyLine(5, end_2, end_1);
Handles.DrawAAPolyLine(5, end_1, endPoint);
for (int i = 0; i < length - 1; i++) {
if (i == length - 1) continue; // Skip last index
if (windowPoints[i].x <= windowPoints[i + 1].x - (50 / zoom)) {
float midpoint = (windowPoints[i].x + windowPoints[i + 1].x) * 0.5f;
Vector2 start_1 = windowPoints[i];
Vector2 end_1 = windowPoints[i + 1];
start_1.x = midpoint;
end_1.x = midpoint;
Handles.DrawAAPolyLine(5, windowPoints[i], start_1);
Handles.DrawAAPolyLine(5, start_1, end_1);
Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]);
} else {
float midpoint = (windowPoints[i].y + windowPoints[i + 1].y) * 0.5f;
Vector2 start_1 = windowPoints[i];
Vector2 end_1 = windowPoints[i + 1];
start_1.x += 25 / zoom;
end_1.x -= 25 / zoom;
Vector2 start_2 = start_1;
Vector2 end_2 = end_1;
start_2.y = midpoint;
end_2.y = midpoint;
Handles.DrawAAPolyLine(5, windowPoints[i], start_1);
Handles.DrawAAPolyLine(5, start_1, start_2);
Handles.DrawAAPolyLine(5, start_2, end_2);
Handles.DrawAAPolyLine(5, end_2, end_1);
Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]);
}
}
break;
}
@ -212,7 +238,7 @@ namespace XNodeEditor {
Rect fromRect;
if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue;
Color connectionColor = graphEditor.GetTypeColor(output.ValueType);
Color connectionColor = graphEditor.GetPortColor(output);
for (int k = 0; k < output.ConnectionCount; k++) {
XNode.NodePort input = output.GetConnection(k);
@ -223,18 +249,13 @@ namespace XNodeEditor {
Rect toRect;
if (!_portConnectionPoints.TryGetValue(input, out toRect)) continue;
Vector2 from = fromRect.center;
Vector2 to = Vector2.zero;
List<Vector2> reroutePoints = output.GetReroutePoints(k);
// Loop through reroute points and draw the path
for (int i = 0; i < reroutePoints.Count; i++) {
to = reroutePoints[i];
DrawConnection(from, to, connectionColor);
from = to;
}
to = toRect.center;
DrawConnection(from, to, connectionColor);
List<Vector2> gridPoints = new List<Vector2>();
gridPoints.Add(fromRect.center);
gridPoints.AddRange(reroutePoints);
gridPoints.Add(toRect.center);
DrawNoodle(connectionColor, gridPoints);
// Loop through reroute points again and draw the points
for (int i = 0; i < reroutePoints.Count; i++) {
@ -266,14 +287,6 @@ namespace XNodeEditor {
private void DrawNodes() {
Event e = Event.current;
//Active node is hashed before and after node GUI to detect changes
int nodeHash = 0;
System.Reflection.MethodInfo onValidate = null;
if (Selection.activeObject != null && Selection.activeObject is XNode.Node) {
onValidate = Selection.activeObject.GetType().GetMethod("OnValidate");
if (onValidate != null) nodeHash = Selection.activeObject.GetHashCode();
}
BeginZoomed();
Vector2 mousePos = Event.current.mousePosition;
@ -315,9 +328,9 @@ namespace XNodeEditor {
_portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
NodeEditor nodeEditor = NodeEditor.GetEditor(node);
NodeEditor nodeEditor = NodeEditor.GetEditor(node, this);
NodeEditor.portPositions = new Dictionary<XNode.NodePort, Vector2>();
NodeEditor.portPositions.Clear();
//Get node position
Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
@ -367,8 +380,7 @@ namespace XNodeEditor {
Vector2 portHandlePos = kvp.Value;
portHandlePos += node.position;
Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16);
if (portConnectionPoints.ContainsKey(kvp.Key)) portConnectionPoints[kvp.Key] = rect;
else portConnectionPoints.Add(kvp.Key, rect);
portConnectionPoints[kvp.Key] = rect;
}
}
@ -407,13 +419,6 @@ namespace XNodeEditor {
if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray();
EndZoomed();
//If a change in hash is detected in the selected node, call OnValidate method.
//This is done through reflection because OnValidate is only relevant in editor,
//and thus, the code should not be included in build.
if (nodeHash != 0) {
if (onValidate != null && nodeHash != Selection.activeObject.GetHashCode()) onValidate.Invoke(Selection.activeObject, null);
}
}
private bool ShouldBeCulled(XNode.Node node) {
@ -430,14 +435,10 @@ namespace XNodeEditor {
}
private void DrawTooltip() {
if (hoveredPort != null) {
Type type = hoveredPort.ValueType;
GUIContent content = new GUIContent();
content.text = type.PrettyName();
if (hoveredPort.IsOutput) {
object obj = hoveredPort.node.GetValue(hoveredPort);
content.text += " = " + (obj != null ? obj.ToString() : "null");
}
if (hoveredPort != null && NodeEditorPreferences.GetSettings().portTooltips && graphEditor != null) {
string tooltip = graphEditor.GetPortTooltip(hoveredPort);
if (string.IsNullOrEmpty(tooltip)) return;
GUIContent content = new GUIContent(tooltip);
Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content);
Rect rect = new Rect(Event.current.mousePosition - (size), size);
EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip);

View File

@ -50,14 +50,14 @@ namespace XNodeEditor {
// Get data from [Input] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.InputAttribute inputAttribute;
bool instancePortList = false;
bool dynamicPortList = false;
if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) {
instancePortList = inputAttribute.instancePortList;
dynamicPortList = inputAttribute.dynamicPortList;
showBacking = inputAttribute.backingValue;
}
//Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
bool useLayoutSpace = instancePortList ||
bool useLayoutSpace = dynamicPortList ||
showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
if (spacePadding > 0 && useLayoutSpace) {
@ -65,10 +65,10 @@ namespace XNodeEditor {
spacePadding = 0;
}
if (instancePortList) {
if (dynamicPortList) {
Type type = GetType(property);
XNode.Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
InstancePortList(property.name, type, property.serializedObject, port.direction, connectionType);
DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
return;
}
switch (showBacking) {
@ -95,14 +95,14 @@ namespace XNodeEditor {
// Get data from [Output] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.OutputAttribute outputAttribute;
bool instancePortList = false;
bool dynamicPortList = false;
if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) {
instancePortList = outputAttribute.instancePortList;
dynamicPortList = outputAttribute.dynamicPortList;
showBacking = outputAttribute.backingValue;
}
//Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
bool useLayoutSpace = instancePortList ||
bool useLayoutSpace = dynamicPortList ||
showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
if (spacePadding > 0 && useLayoutSpace) {
@ -110,10 +110,10 @@ namespace XNodeEditor {
spacePadding = 0;
}
if (instancePortList) {
if (dynamicPortList) {
Type type = GetType(property);
XNode.Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
InstancePortList(property.name, type, property.serializedObject, port.direction, connectionType);
DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
return;
}
switch (showBacking) {
@ -142,19 +142,18 @@ namespace XNodeEditor {
Color backgroundColor = new Color32(90, 97, 105, 255);
Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType);
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col);
// Register the handle position
Vector2 portPos = rect.center;
if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos;
else NodeEditor.portPositions.Add(port, portPos);
NodeEditor.portPositions[port] = portPos;
}
}
private static System.Type GetType(SerializedProperty property) {
System.Type parentType = property.serializedObject.targetObject.GetType();
System.Reflection.FieldInfo fi = parentType.GetField(property.propertyPath);
System.Reflection.FieldInfo fi = NodeEditorWindow.GetFieldInfo(parentType, property.name);
return fi.FieldType;
}
@ -199,13 +198,12 @@ namespace XNodeEditor {
Color backgroundColor = new Color32(90, 97, 105, 255);
Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType);
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col);
// Register the handle position
Vector2 portPos = rect.center;
if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos;
else NodeEditor.portPositions.Add(port, portPos);
NodeEditor.portPositions[port] = portPos;
}
/// <summary> Add a port field to previous layout element. </summary>
@ -228,13 +226,12 @@ namespace XNodeEditor {
Color backgroundColor = new Color32(90, 97, 105, 255);
Color tint;
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType);
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
DrawPortHandle(rect, backgroundColor, col);
// Register the handle position
Vector2 portPos = rect.center;
if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos;
else NodeEditor.portPositions.Add(port, portPos);
NodeEditor.portPositions[port] = portPos;
}
/// <summary> Draws an input and an output port on the same line </summary>
@ -254,23 +251,50 @@ namespace XNodeEditor {
GUI.color = col;
}
/// <summary> Draw an editable list of instance ports. Port names are named as "[fieldName] [index]" </summary>
/// <param name="fieldName">Supply a list for editable values</param>
/// <param name="type">Value type of added instance ports</param>
/// <param name="serializedObject">The serializedObject of the node</param>
/// <param name="connectionType">Connection type of added instance ports</param>
/// <param name="onCreation">Called on the list on creation. Use this if you want to customize the created ReorderableList</param>
public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, Action<ReorderableList> onCreation = null) {
XNode.Node node = serializedObject.targetObject as XNode.Node;
SerializedProperty arrayData = serializedObject.FindProperty(fieldName);
#region Obsolete
[Obsolete("Use IsDynamicPortListPort instead")]
public static bool IsInstancePortListPort(XNode.NodePort port) {
return IsDynamicPortListPort(port);
}
Predicate<string> isMatchingInstancePort =
x => {
string[] split = x.Split(' ');
if (split != null && split.Length == 2) return split[0] == fieldName;
else return false;
};
List<XNode.NodePort> instancePorts = node.InstancePorts.Where(x => isMatchingInstancePort(x.fieldName)).OrderBy(x => x.fieldName).ToList();
[Obsolete("Use DynamicPortList instead")]
public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action<ReorderableList> onCreation = null) {
DynamicPortList(fieldName, type, serializedObject, io, connectionType, typeConstraint, onCreation);
}
#endregion
/// <summary> Is this port part of a DynamicPortList? </summary>
public static bool IsDynamicPortListPort(XNode.NodePort port) {
string[] parts = port.fieldName.Split(' ');
if (parts.Length != 2) return false;
Dictionary<string, ReorderableList> cache;
if (reorderableListCache.TryGetValue(port.node, out cache)) {
ReorderableList list;
if (cache.TryGetValue(parts[0], out list)) return true;
}
return false;
}
/// <summary> Draw an editable list of dynamic ports. Port names are named as "[fieldName] [index]" </summary>
/// <param name="fieldName">Supply a list for editable values</param>
/// <param name="type">Value type of added dynamic ports</param>
/// <param name="serializedObject">The serializedObject of the node</param>
/// <param name="connectionType">Connection type of added dynamic ports</param>
/// <param name="onCreation">Called on the list on creation. Use this if you want to customize the created ReorderableList</param>
public static void DynamicPortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action<ReorderableList> onCreation = null) {
XNode.Node node = serializedObject.targetObject as XNode.Node;
var indexedPorts = node.DynamicPorts.Select(x => {
string[] split = x.fieldName.Split(' ');
if (split != null && split.Length == 2 && split[0] == fieldName) {
int i = -1;
if (int.TryParse(split[1], out i)) {
return new { index = i, port = x };
}
}
return new { index = -1, port = (XNode.NodePort) null };
});
List<XNode.NodePort> dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
ReorderableList list = null;
Dictionary<string, ReorderableList> rlc;
@ -279,34 +303,37 @@ namespace XNodeEditor {
}
// If a ReorderableList isn't cached for this array, do so.
if (list == null) {
string label = serializedObject.FindProperty(fieldName).displayName;
list = CreateReorderableList(instancePorts, arrayData, type, serializedObject, io, label, connectionType, onCreation);
SerializedProperty arrayData = serializedObject.FindProperty(fieldName);
list = CreateReorderableList(fieldName, dynamicPorts, arrayData, type, serializedObject, io, connectionType, typeConstraint, onCreation);
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) rlc.Add(fieldName, list);
else reorderableListCache.Add(serializedObject.targetObject, new Dictionary<string, ReorderableList>() { { fieldName, list } });
}
list.list = instancePorts;
list.list = dynamicPorts;
list.DoLayoutList();
}
private static ReorderableList CreateReorderableList(List<XNode.NodePort> instancePorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, string label, XNode.Node.ConnectionType connectionType, 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) {
bool hasArrayData = arrayData != null && arrayData.isArray;
int arraySize = hasArrayData ? arrayData.arraySize : 0;
XNode.Node node = serializedObject.targetObject as XNode.Node;
ReorderableList list = new ReorderableList(instancePorts, null, true, true, true, true);
ReorderableList list = new ReorderableList(dynamicPorts, null, true, true, true, true);
string label = arrayData != null ? arrayData.displayName : ObjectNames.NicifyVariableName(fieldName);
list.drawElementCallback =
(Rect rect, int index, bool isActive, bool isFocused) => {
XNode.NodePort port = node.GetPort(arrayData.name + " " + index);
XNode.NodePort port = node.GetPort(fieldName + " " + index);
if (hasArrayData) {
if (arrayData.arraySize <= index) {
EditorGUI.LabelField(rect, "Invalid element " + index);
string portInfo = port != null ? port.fieldName : "";
EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
return;
}
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index);
EditorGUI.PropertyField(rect, itemData);
} else EditorGUI.LabelField(rect, port.fieldName);
Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0));
NodeEditorGUILayout.PortField(pos, port);
EditorGUI.PropertyField(rect, itemData, true);
} else EditorGUI.LabelField(rect, port != null ? port.fieldName : "");
if (port != null) {
Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0));
NodeEditorGUILayout.PortField(pos, port);
}
};
list.elementHeightCallback =
(int index) => {
@ -318,7 +345,7 @@ namespace XNodeEditor {
};
list.drawHeaderCallback =
(Rect rect) => {
EditorGUI.LabelField(rect, label + "(" + serializedObject.targetObject.name + ")");
EditorGUI.LabelField(rect, label);
};
list.onSelectCallback =
(ReorderableList rl) => {
@ -330,8 +357,8 @@ namespace XNodeEditor {
// Move up
if (rl.index > reorderableListIndex) {
for (int i = reorderableListIndex; i < rl.index; ++i) {
XNode.NodePort port = node.GetPort(arrayData.name + " " + i);
XNode.NodePort nextPort = node.GetPort(arrayData.name + " " + (i + 1));
XNode.NodePort port = node.GetPort(fieldName + " " + i);
XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i + 1));
port.SwapConnections(nextPort);
// Swap cached positions to mitigate twitching
@ -343,8 +370,8 @@ namespace XNodeEditor {
// Move down
else {
for (int i = reorderableListIndex; i > rl.index; --i) {
XNode.NodePort port = node.GetPort(arrayData.name + " " + i);
XNode.NodePort nextPort = node.GetPort(arrayData.name + " " + (i - 1));
XNode.NodePort port = node.GetPort(fieldName + " " + i);
XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i - 1));
port.SwapConnections(nextPort);
// Swap cached positions to mitigate twitching
@ -370,66 +397,92 @@ namespace XNodeEditor {
};
list.onAddCallback =
(ReorderableList rl) => {
// Add instance port postfixed with an index number
string newName = arrayData.name + " 0";
// Add dynamic port postfixed with an index number
string newName = fieldName + " 0";
int i = 0;
while (node.HasPort(newName)) newName = arrayData.name + " " + (++i);
while (node.HasPort(newName)) newName = fieldName + " " + (++i);
if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, newName);
else node.AddInstanceInput(type, connectionType, newName);
if (io == XNode.NodePort.IO.Output) node.AddDynamicOutput(type, connectionType, XNode.Node.TypeConstraint.None, newName);
else node.AddDynamicInput(type, connectionType, typeConstraint, newName);
serializedObject.Update();
EditorUtility.SetDirty(node);
if (hasArrayData) arrayData.InsertArrayElementAtIndex(arraySize);
if (hasArrayData) {
arrayData.InsertArrayElementAtIndex(arrayData.arraySize);
}
serializedObject.ApplyModifiedProperties();
};
list.onRemoveCallback =
(ReorderableList rl) => {
int index = rl.index;
// Clear the removed ports connections
instancePorts[index].ClearConnections();
// Move following connections one step up to replace the missing connection
for (int k = index + 1; k < instancePorts.Count(); k++) {
for (int j = 0; j < instancePorts[k].ConnectionCount; j++) {
XNode.NodePort other = instancePorts[k].GetConnection(j);
instancePorts[k].Disconnect(other);
instancePorts[k - 1].Connect(other);
}
}
// Remove the last instance port, to avoid messing up the indexing
node.RemoveInstancePort(instancePorts[instancePorts.Count() - 1].fieldName);
serializedObject.Update();
EditorUtility.SetDirty(node);
if (hasArrayData) {
arrayData.DeleteArrayElementAtIndex(index);
arraySize--;
// Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues
if (instancePorts.Count <= arraySize) {
while (instancePorts.Count <= arraySize) {
arrayData.DeleteArrayElementAtIndex(--arraySize);
var indexedPorts = node.DynamicPorts.Select(x => {
string[] split = x.fieldName.Split(' ');
if (split != null && split.Length == 2 && split[0] == fieldName) {
int i = -1;
if (int.TryParse(split[1], out i)) {
return new { index = i, port = x };
}
UnityEngine.Debug.LogWarning("Array size exceeded instance ports size. Excess items removed.");
}
return new { index = -1, port = (XNode.NodePort) null };
});
dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
int index = rl.index;
if (dynamicPorts[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
dynamicPorts[index].ClearConnections();
// Move following connections one step up to replace the missing connection
for (int k = index + 1; k < dynamicPorts.Count(); k++) {
for (int j = 0; j < dynamicPorts[k].ConnectionCount; j++) {
XNode.NodePort other = dynamicPorts[k].GetConnection(j);
dynamicPorts[k].Disconnect(other);
dynamicPorts[k - 1].Connect(other);
}
}
// Remove the last dynamic port, to avoid messing up the indexing
node.RemoveDynamicPort(dynamicPorts[dynamicPorts.Count() - 1].fieldName);
serializedObject.Update();
EditorUtility.SetDirty(node);
}
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);
// Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues
if (dynamicPorts.Count <= arrayData.arraySize) {
while (dynamicPorts.Count <= arrayData.arraySize) {
arrayData.DeleteArrayElementAtIndex(arrayData.arraySize - 1);
}
UnityEngine.Debug.LogWarning("Array size exceeded dynamic ports size. Excess items removed.");
}
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
}
};
if (hasArrayData) {
int instancePortCount = instancePorts.Count;
while (instancePortCount < arraySize) {
// Add instance port postfixed with an index number
int dynamicPortCount = dynamicPorts.Count;
while (dynamicPortCount < arrayData.arraySize) {
// Add dynamic port postfixed with an index number
string newName = arrayData.name + " 0";
int i = 0;
while (node.HasPort(newName)) newName = arrayData.name + " " + (++i);
if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, newName);
else node.AddInstanceInput(type, connectionType, newName);
if (io == XNode.NodePort.IO.Output) node.AddDynamicOutput(type, connectionType, typeConstraint, newName);
else node.AddDynamicInput(type, connectionType, typeConstraint, newName);
EditorUtility.SetDirty(node);
instancePortCount++;
dynamicPortCount++;
}
while (arraySize < instancePortCount) {
arrayData.InsertArrayElementAtIndex(arraySize);
arraySize++;
while (arrayData.arraySize < dynamicPortCount) {
arrayData.InsertArrayElementAtIndex(arrayData.arraySize);
}
serializedObject.ApplyModifiedProperties();
serializedObject.Update();

View File

@ -23,10 +23,17 @@ namespace XNodeEditor {
[SerializeField] private Color32 _gridBgColor = new Color(0.18f, 0.18f, 0.18f);
public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } }
[Obsolete("Use maxZoom instead")]
public float zoomOutLimit { get { return maxZoom; } set { maxZoom = value; } }
[UnityEngine.Serialization.FormerlySerializedAs("zoomOutLimit")]
public float maxZoom = 5f;
public float minZoom = 1f;
public Color32 highlightColor = new Color32(255, 255, 255, 255);
public bool gridSnap = true;
public bool autoSave = true;
public bool zoomToMouse = true;
public bool portTooltips = true;
[SerializeField] private string typeColorsData = "";
[NonSerialized] public Dictionary<string, Color> typeColors = new Dictionary<string, Color>();
public NoodleType noodleType = NoodleType.Curve;
@ -81,7 +88,20 @@ namespace XNodeEditor {
return settings[lastKey];
}
#if UNITY_2019_1_OR_NEWER
[SettingsProvider]
public static SettingsProvider CreateXNodeSettingsProvider() {
SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) {
guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); },
keywords = new HashSet<string>(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" })
};
return provider;
}
#endif
#if !UNITY_2019_1_OR_NEWER
[PreferenceItem("Node Editor")]
#endif
private static void PreferencesGUI() {
VerifyLoaded();
Settings settings = NodeEditorPreferences.settings[lastKey];
@ -100,7 +120,11 @@ namespace XNodeEditor {
EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel);
settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap);
settings.zoomToMouse = EditorGUILayout.Toggle(new GUIContent("Zoom to Mouse", "Zooms towards mouse position"), settings.zoomToMouse);
EditorGUILayout.LabelField("Zoom");
EditorGUI.indentLevel++;
settings.maxZoom = EditorGUILayout.FloatField(new GUIContent("Max", "Upper limit to zoom"), settings.maxZoom);
settings.minZoom = EditorGUILayout.FloatField(new GUIContent("Min", "Lower limit to zoom"), settings.minZoom);
EditorGUI.indentLevel--;
settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor);
settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor);
if (GUI.changed) {
@ -124,6 +148,7 @@ namespace XNodeEditor {
EditorGUILayout.LabelField("Node", EditorStyles.boldLabel);
settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor);
settings.noodleType = (NoodleType) EditorGUILayout.EnumPopup("Noodle type", (Enum) settings.noodleType);
settings.portTooltips = EditorGUILayout.Toggle("Port Tooltips", settings.portTooltips);
if (GUI.changed) {
SavePrefs(key, settings);
NodeEditorWindow.RepaintAll();
@ -135,11 +160,13 @@ namespace XNodeEditor {
//Label
EditorGUILayout.LabelField("Types", EditorStyles.boldLabel);
//Clone keys so we can enumerate the dictionary and make changes.
var typeColorKeys = new List<Type>(typeColors.Keys);
//Display type colors. Save them if they are edited by the user
foreach (var typeColor in typeColors) {
Type type = typeColor.Key;
foreach (var type in typeColorKeys) {
string typeColorKey = NodeEditorUtilities.PrettyName(type);
Color col = typeColor.Value;
Color col = typeColors[type];
EditorGUI.BeginChangeCheck();
EditorGUILayout.BeginHorizontal();
col = EditorGUILayout.ColorField(typeColorKey, col);
@ -148,7 +175,7 @@ namespace XNodeEditor {
typeColors[type] = col;
if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col;
else settings.typeColors.Add(typeColorKey, col);
SavePrefs(typeColorKey, settings);
SavePrefs(key, settings);
NodeEditorWindow.RepaintAll();
}
}
@ -193,12 +220,19 @@ namespace XNodeEditor {
if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]);
else {
#if UNITY_5_4_OR_NEWER
UnityEngine.Random.State oldState = UnityEngine.Random.state;
UnityEngine.Random.InitState(typeName.GetHashCode());
#else
int oldSeed = UnityEngine.Random.seed;
UnityEngine.Random.seed = typeName.GetHashCode();
#endif
col = new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value);
typeColors.Add(type, col);
#if UNITY_5_4_OR_NEWER
UnityEngine.Random.state = oldState;
#else
UnityEngine.Random.seed = oldSeed;
#endif
}
}
return col;

View File

@ -42,9 +42,9 @@ namespace XNodeEditor {
public static Dictionary<Type, Color> GetNodeTint() {
Dictionary<Type, Color> tints = new Dictionary<Type, Color>();
for (int i = 0; i < nodeTypes.Length; i++) {
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTint), true);
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTintAttribute), true);
if (attribs == null || attribs.Length == 0) continue;
XNode.Node.NodeTint attrib = attribs[0] as XNode.Node.NodeTint;
XNode.Node.NodeTintAttribute attrib = attribs[0] as XNode.Node.NodeTintAttribute;
tints.Add(nodeTypes[i], attrib.color);
}
return tints;
@ -53,32 +53,62 @@ namespace XNodeEditor {
public static Dictionary<Type, int> GetNodeWidth() {
Dictionary<Type, int> widths = new Dictionary<Type, int>();
for (int i = 0; i < nodeTypes.Length; i++) {
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidth), true);
var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidthAttribute), true);
if (attribs == null || attribs.Length == 0) continue;
XNode.Node.NodeWidth attrib = attribs[0] as XNode.Node.NodeWidth;
XNode.Node.NodeWidthAttribute attrib = attribs[0] as XNode.Node.NodeWidthAttribute;
widths.Add(nodeTypes[i], attrib.width);
}
return widths;
}
/// <summary> Get FieldInfo of a field, including those that are private and/or inherited </summary>
public static FieldInfo GetFieldInfo(Type type, string fieldName) {
// If we can't find field in the first run, it's probably a private field in a base class.
FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Search base classes for private fields only. Public fields are found above
while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
return field;
}
/// <summary> Get all classes deriving from baseType via reflection </summary>
public static Type[] GetDerivedTypes(Type baseType) {
List<System.Type> types = new List<System.Type>();
System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies) {
types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
try {
types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
} catch (ReflectionTypeLoadException) { }
}
return types.ToArray();
}
public static void AddCustomContextMenuItems(GenericMenu contextMenu, object obj) {
KeyValuePair<ContextMenu, System.Reflection.MethodInfo>[] items = GetContextMenuMethods(obj);
KeyValuePair<ContextMenu, MethodInfo>[] items = GetContextMenuMethods(obj);
if (items.Length != 0) {
contextMenu.AddSeparator("");
for (int i = 0; i < items.Length; i++) {
KeyValuePair<ContextMenu, System.Reflection.MethodInfo> kvp = items[i];
contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null));
List<string> invalidatedEntries = new List<string>();
foreach (KeyValuePair<ContextMenu, MethodInfo> checkValidate in items) {
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(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);
}
}
@ -153,4 +183,4 @@ namespace XNodeEditor {
}
}
}
}
}

View File

@ -25,7 +25,7 @@ namespace XNodeEditor {
public static bool GetAttrib<T>(object[] attribs, out T attribOut) where T : Attribute {
for (int i = 0; i < attribs.Length; i++) {
if (attribs[i].GetType() == typeof(T)) {
if (attribs[i] is T){
attribOut = attribs[i] as T;
return true;
}
@ -35,7 +35,15 @@ namespace XNodeEditor {
}
public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute {
object[] attribs = classType.GetField(fieldName).GetCustomAttributes(typeof(T), false);
// If we can't find field in the first run, it's probably a private field in a base class.
FieldInfo field = NodeEditorWindow.GetFieldInfo(classType, fieldName);
// This shouldn't happen. Ever.
if (field == null) {
Debug.LogWarning("Field " + fieldName + " couldnt be found");
attribOut = null;
return false;
}
object[] attribs = field.GetCustomAttributes(typeof(T), true);
return GetAttrib(attribs, out attribOut);
}
@ -125,6 +133,15 @@ namespace XNodeEditor {
} 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>Creates a new C# Class.</summary>
[MenuItem("Assets/Create/xNode/Node C# Script", false, 89)]
private static void CreateNode() {

View File

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

View File

@ -8,13 +8,16 @@ namespace XNodeEditor {
/// <summary> Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. </summary>
[CustomNodeGraphEditor(typeof(XNode.NodeGraph))]
public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase<NodeGraphEditor, NodeGraphEditor.CustomNodeGraphEditorAttribute, XNode.NodeGraph> {
/// <summary> The position of the window in screen space. </summary>
public Rect position;
[Obsolete("Use window.position instead")]
public Rect position { get { return window.position; } set { window.position = value; } }
/// <summary> Are we currently renaming a node? </summary>
protected bool isRenaming;
public virtual void OnGUI() { }
/// <summary> Called when opened by NodeEditorWindow </summary>
public virtual void OnOpen() { }
public virtual Texture2D GetGridTexture() {
return NodeEditorPreferences.GetSettings().gridTexture;
}
@ -55,6 +58,8 @@ namespace XNodeEditor {
menu.AddSeparator("");
menu.AddItem(new GUIContent("Add group"), false, () => CreateGroup(pos));
menu.AddSeparator("");
if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos));
else menu.AddDisabledItem(new GUIContent("Paste"));
menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorWindow.OpenPreferences());
NodeEditorWindow.AddCustomContextMenuItems(menu, target);
}
@ -89,15 +94,35 @@ namespace XNodeEditor {
menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
}
public virtual Color GetPortColor(XNode.NodePort port) {
return GetTypeColor(port.ValueType);
}
public virtual Color GetTypeColor(Type type) {
return NodeEditorPreferences.GetTypeColor(type);
}
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>
public virtual void CreateNode(Type type, Vector2 position) {
XNode.Node node = target.AddNode(type);
node.position = position;
node.name = UnityEditor.ObjectNames.NicifyVariableName(type.Name);
if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
AssetDatabase.AddObjectToAsset(node, target);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
NodeEditorWindow.RepaintAll();
@ -113,9 +138,9 @@ namespace XNodeEditor {
}
/// <summary> Safely remove a node and all its connections. </summary>
public void RemoveNode(XNode.Node node) {
UnityEngine.Object.DestroyImmediate(node, true);
public virtual void RemoveNode(XNode.Node node) {
target.RemoveNode(node);
UnityEngine.Object.DestroyImmediate(node, true);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
@ -147,7 +172,7 @@ namespace XNodeEditor {
[AttributeUsage(AttributeTargets.Class)]
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;
public string editorPrefsKey;
/// <summary> Tells a NodeGraphEditor which Graph type it is an editor for </summary>

View File

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

View File

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

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

@ -41,18 +41,69 @@ namespace XNode {
Override,
}
/// <summary> Tells which types of input to allow </summary>
public enum TypeConstraint {
/// <summary> Allow all types of input</summary>
None,
/// <summary> Allow similar and inherited types </summary>
Inherited,
/// <summary> Allow only similar types </summary>
Strict,
}
#region Obsolete
[Obsolete("Use DynamicPorts instead")]
public IEnumerable<NodePort> InstancePorts { get { return DynamicPorts; } }
[Obsolete("Use DynamicOutputs instead")]
public IEnumerable<NodePort> InstanceOutputs { get { return DynamicOutputs; } }
[Obsolete("Use DynamicInputs instead")]
public IEnumerable<NodePort> InstanceInputs { get { return DynamicInputs; } }
[Obsolete("Use AddDynamicInput instead")]
public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddInstanceInput(type, connectionType, typeConstraint, fieldName);
}
[Obsolete("Use AddDynamicOutput instead")]
public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicOutput(type, connectionType, typeConstraint, fieldName);
}
[Obsolete("Use AddDynamicPort instead")]
private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName);
}
[Obsolete("Use RemoveDynamicPort instead")]
public void RemoveInstancePort(string fieldName) {
RemoveDynamicPort(fieldName);
}
[Obsolete("Use RemoveDynamicPort instead")]
public void RemoveInstancePort(NodePort port) {
RemoveDynamicPort(port);
}
[Obsolete("Use ClearDynamicPorts instead")]
public void ClearInstancePorts() {
ClearDynamicPorts();
}
#endregion
/// <summary> Iterate over all ports on this node. </summary>
public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values) yield return port; } }
/// <summary> Iterate over all outputs on this node. </summary>
public IEnumerable<NodePort> Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } }
/// <summary> Iterate over all inputs on this node. </summary>
public IEnumerable<NodePort> Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } }
/// <summary> Iterate over all instane ports on this node. </summary>
public IEnumerable<NodePort> InstancePorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } }
/// <summary> Iterate over all instance outputs on this node. </summary>
public IEnumerable<NodePort> InstanceOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } }
/// <summary> Iterate over all instance inputs on this node. </summary>
public IEnumerable<NodePort> InstanceInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } }
/// <summary> Iterate over all dynamic ports on this node. </summary>
public IEnumerable<NodePort> DynamicPorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } }
/// <summary> Iterate over all dynamic outputs on this node. </summary>
public IEnumerable<NodePort> DynamicOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } }
/// <summary> Iterate over all dynamic inputs on this node. </summary>
public IEnumerable<NodePort> DynamicInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } }
/// <summary> Parent <see cref="NodeGraph"/> </summary>
[SerializeField] public NodeGraph graph;
/// <summary> Position on the <see cref="NodeGraph"/> </summary>
@ -63,7 +114,6 @@ namespace XNode {
/// <summary> Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable </summary>
public static NodeGraph graphHotfix;
protected void OnEnable() {
if (graphHotfix != null) graph = graphHotfix;
graphHotfix = null;
@ -76,7 +126,7 @@ namespace XNode {
NodeDataCache.UpdatePorts(this, ports);
}
/// <summary> Initialize node. Called on creation. </summary>
/// <summary> Initialize node. Called on enable. </summary>
protected virtual void Init() { }
/// <summary> Checks all connections for invalid references, and removes them. </summary>
@ -84,57 +134,59 @@ namespace XNode {
foreach (NodePort port in Ports) port.VerifyConnections();
}
#region Instance Ports
#region Dynamic Ports
/// <summary> Convenience function. </summary>
/// <seealso cref="AddInstancePort"/>
/// <seealso cref="AddInstanceOutput"/>
public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Input, connectionType, fieldName);
public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicPort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName);
}
/// <summary> Convenience function. </summary>
/// <seealso cref="AddInstancePort"/>
/// <seealso cref="AddInstanceInput"/>
public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) {
return AddInstancePort(type, NodePort.IO.Output, connectionType, fieldName);
public NodePort AddDynamicOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName);
}
/// <summary> Add a dynamic, serialized port to this node. </summary>
/// <seealso cref="AddInstanceInput"/>
/// <seealso cref="AddInstanceOutput"/>
private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) {
/// <seealso cref="AddDynamicInput"/>
/// <seealso cref="AddDynamicOutput"/>
private NodePort AddDynamicPort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
if (fieldName == null) {
fieldName = "instanceInput_0";
fieldName = "dynamicInput_0";
int i = 0;
while (HasPort(fieldName)) fieldName = "instanceInput_" + (++i);
while (HasPort(fieldName)) fieldName = "dynamicInput_" + (++i);
} else if (HasPort(fieldName)) {
Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this);
return ports[fieldName];
}
NodePort port = new NodePort(fieldName, type, direction, connectionType, this);
NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this);
ports.Add(fieldName, port);
return port;
}
/// <summary> Remove an instance port from the node </summary>
public void RemoveInstancePort(string fieldName) {
RemoveInstancePort(GetPort(fieldName));
/// <summary> Remove an dynamic port from the node </summary>
public void RemoveDynamicPort(string fieldName) {
NodePort dynamicPort = GetPort(fieldName);
if (dynamicPort == null) throw new ArgumentException("port " + fieldName + " doesn't exist");
RemoveDynamicPort(GetPort(fieldName));
}
/// <summary> Remove an instance port from the node </summary>
public void RemoveInstancePort(NodePort port) {
/// <summary> Remove an dynamic port from the node </summary>
public void RemoveDynamicPort(NodePort port) {
if (port == null) throw new ArgumentNullException("port");
else if (port.IsStatic) throw new ArgumentException("cannot remove static port");
port.ClearConnections();
ports.Remove(port.fieldName);
}
/// <summary> Removes all instance ports from the node </summary>
[ContextMenu("Clear Instance Ports")]
public void ClearInstancePorts() {
List<NodePort> instancePorts = new List<NodePort>(InstancePorts);
foreach (NodePort port in instancePorts) {
RemoveInstancePort(port);
/// <summary> Removes all dynamic ports from the node </summary>
[ContextMenu("Clear Dynamic Ports")]
public void ClearDynamicPorts() {
List<NodePort> dynamicPorts = new List<NodePort>(DynamicPorts);
foreach (NodePort port in dynamicPorts) {
RemoveDynamicPort(port);
}
}
#endregion
@ -206,24 +258,27 @@ namespace XNode {
foreach (NodePort port in Ports) port.ClearConnections();
}
public override int GetHashCode() {
return JsonUtility.ToJson(this).GetHashCode();
}
#region Attributes
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public class InputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
public bool instancePortList;
[Obsolete("Use dynamicPortList instead")]
public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } }
public bool dynamicPortList;
public TypeConstraint typeConstraint;
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
/// <param name="connectionType">Should we allow multiple connections? </param>
public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, bool instancePortList = false) {
/// <param name="typeConstraint">Constrains which input connections can be made to this port </param>
/// <param name="dynamicPortList">If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays </param>
public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) {
this.backingValue = backingValue;
this.connectionType = connectionType;
this.instancePortList = instancePortList;
this.dynamicPortList = dynamicPortList;
this.typeConstraint = typeConstraint;
}
}
@ -232,15 +287,18 @@ namespace XNode {
public class OutputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
public bool instancePortList;
[Obsolete("Use dynamicPortList instead")]
public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } }
public bool dynamicPortList;
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
/// <param name="connectionType">Should we allow multiple connections? </param>
public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool instancePortList = false) {
/// <param name="dynamicPortList">If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays </param>
public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool dynamicPortList = false) {
this.backingValue = backingValue;
this.connectionType = connectionType;
this.instancePortList = instancePortList;
this.dynamicPortList = dynamicPortList;
}
}
@ -255,19 +313,19 @@ namespace XNode {
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeTint : Attribute {
public class NodeTintAttribute : Attribute {
public Color color;
/// <summary> Specify a color for this node type </summary>
/// <param name="r"> Red [0.0f .. 1.0f] </param>
/// <param name="g"> Green [0.0f .. 1.0f] </param>
/// <param name="b"> Blue [0.0f .. 1.0f] </param>
public NodeTint(float r, float g, float b) {
public NodeTintAttribute(float r, float g, float b) {
color = new Color(r, g, b);
}
/// <summary> Specify a color for this node type </summary>
/// <param name="hex"> HEX color value </param>
public NodeTint(string hex) {
public NodeTintAttribute(string hex) {
ColorUtility.TryParseHtmlString(hex, out color);
}
@ -275,20 +333,21 @@ namespace XNode {
/// <param name="r"> Red [0 .. 255] </param>
/// <param name="g"> Green [0 .. 255] </param>
/// <param name="b"> Blue [0 .. 255] </param>
public NodeTint(byte r, byte g, byte b) {
public NodeTintAttribute(byte r, byte g, byte b) {
color = new Color32(r, g, b, byte.MaxValue);
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeWidth : Attribute {
public class NodeWidthAttribute : Attribute {
public int width;
/// <summary> Specify a width for this node type </summary>
/// <param name="width"> Width </param>
public NodeWidth(int width) {
public NodeWidthAttribute(int width) {
this.width = width;
}
}
#endregion
[Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver {
[SerializeField] private List<string> keys = new List<string>();

View File

@ -30,7 +30,7 @@ namespace XNode {
NodePort staticPort;
if (staticPorts.TryGetValue(port.fieldName, out staticPort)) {
// If port exists but with wrong settings, remove it. Re-add it later.
if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction) ports.Remove(port.fieldName);
if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction || port.typeConstraint != staticPort.typeConstraint) ports.Remove(port.fieldName);
else port.ValueType = staticPort.ValueType;
}
// If port doesn't exist anymore, remove it
@ -56,10 +56,14 @@ namespace XNode {
} else {
// Else, check all relevant DDLs (slower)
// ignore all unity related assemblies
// never ignore current executing assembly
Assembly executingAssembly = Assembly.GetExecutingAssembly();
foreach (Assembly assembly in assemblies) {
if (assembly.FullName.StartsWith("Unity")) continue;
// unity created assemblies always have version 0.0.0
if (!assembly.FullName.Contains("Version=0.0.0")) continue;
if(assembly != executingAssembly) {
if (assembly.FullName.StartsWith("Unity")) continue;
// unity created assemblies always have version 0.0.0
if (!assembly.FullName.Contains("Version=0.0.0")) continue;
}
nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
}
}
@ -68,12 +72,24 @@ namespace XNode {
}
}
public static List<FieldInfo> GetNodeFields(System.Type nodeType) {
List<System.Reflection.FieldInfo> fieldInfo = new List<System.Reflection.FieldInfo>(nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance));
// GetFields doesnt return inherited private fields, so walk through base types and pick those up
System.Type tempType = nodeType;
while ((tempType = tempType.BaseType) != typeof(XNode.Node)) {
fieldInfo.AddRange(tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance));
}
return fieldInfo;
}
private static void CachePorts(System.Type nodeType) {
System.Reflection.FieldInfo[] fieldInfo = nodeType.GetFields();
for (int i = 0; i < fieldInfo.Length; i++) {
List<System.Reflection.FieldInfo> fieldInfo = GetNodeFields(nodeType);
for (int i = 0; i < fieldInfo.Count; i++) {
//Get InputAttribute and OutputAttribute
object[] attribs = fieldInfo[i].GetCustomAttributes(false);
object[] attribs = fieldInfo[i].GetCustomAttributes(true);
Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute;
Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute;
@ -114,4 +130,4 @@ namespace XNode {
}
}
}
}
}

View File

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

View File

@ -21,6 +21,7 @@ namespace XNode {
public IO direction { get { return _direction; } }
public Node.ConnectionType connectionType { get { return _connectionType; } }
public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } }
/// <summary> Is this port connected to anytihng? </summary>
public bool IsConnected { get { return connections.Count != 0; } }
@ -49,6 +50,7 @@ namespace XNode {
[SerializeField] private List<PortConnection> connections = new List<PortConnection>();
[SerializeField] private IO _direction;
[SerializeField] private Node.ConnectionType _connectionType;
[SerializeField] private Node.TypeConstraint _typeConstraint;
[SerializeField] private bool _dynamic;
/// <summary> Construct a static targetless nodeport. Used as a template. </summary>
@ -61,6 +63,7 @@ namespace XNode {
if (attribs[i] is Node.InputAttribute) {
_direction = IO.Input;
_connectionType = (attribs[i] as Node.InputAttribute).connectionType;
_typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint;
} else if (attribs[i] is Node.OutputAttribute) {
_direction = IO.Output;
_connectionType = (attribs[i] as Node.OutputAttribute).connectionType;
@ -75,17 +78,19 @@ namespace XNode {
_direction = nodePort.direction;
_dynamic = nodePort._dynamic;
_connectionType = nodePort._connectionType;
_typeConstraint = nodePort._typeConstraint;
_node = node;
}
/// <summary> Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. </summary>
public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node node) {
public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) {
_fieldName = fieldName;
this.ValueType = type;
_direction = direction;
_node = node;
_dynamic = true;
_connectionType = connectionType;
_typeConstraint = typeConstraint;
}
/// <summary> Checks all connections for invalid references, and removes them. </summary>
@ -240,6 +245,23 @@ namespace XNode {
return false;
}
/// <summary> Returns true if this port can connect to specified port </summary>
public bool CanConnectTo(NodePort port) {
// Figure out which is input and which is output
NodePort input = null, output = null;
if (IsInput) input = this;
else output = this;
if (port.IsInput) input = port;
else output = port;
// If there isn't one of each, they can't connect
if (input == null || output == null) return false;
// Check type constraints
if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false;
if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false;
// Success
return true;
}
/// <summary> Disconnect this port from another port </summary>
public void Disconnect(NodePort port) {
// Remove this ports connection to the other

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: