mirror of
https://github.com/Siccity/xNode.git
synced 2025-12-20 01:06:01 +08:00
Merge branch 'master' into examples
This commit is contained in:
commit
754a5f66af
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@ -0,0 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*.cs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = true
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,3 +23,6 @@ sysinfo.txt
|
||||
.git.meta
|
||||
.gitignore.meta
|
||||
.gitattributes.meta
|
||||
|
||||
# OS X only:
|
||||
.DS_Store
|
||||
@ -5,14 +5,22 @@ 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, split them into separate PRs.
|
||||
* Avoid including irellevant whitespace or formatting changes.
|
||||
* Comment your code.
|
||||
* Spell check your code / comments
|
||||
* Use consistent formatting
|
||||
These are the main points to follow:
|
||||
|
||||
1) Use formatting which is consistent with the rest of xNode base (see below)
|
||||
2) Keep _one feature_ per PR (see below)
|
||||
3) xNode aims to be compatible with C# 4.x, do not use new language features
|
||||
4) Avoid including irellevant whitespace or formatting changes
|
||||
5) Comment your code
|
||||
6) Spell check your code / comments
|
||||
7) Use concrete types, not *var*
|
||||
8) Use english language
|
||||
|
||||
## New features
|
||||
xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings.
|
||||
|
||||
Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible.
|
||||
|
||||
If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel.
|
||||
|
||||
## Coding conventions
|
||||
|
||||
10
README.md
10
README.md
@ -4,6 +4,7 @@
|
||||
[](https://github.com/Siccity/xNode/issues)
|
||||
[](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md)
|
||||
[](https://github.com/Siccity/xNode/wiki)
|
||||
[](https://openupm.com/packages/com.github.siccity.xnode/)
|
||||
|
||||
[Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki)
|
||||
|
||||
@ -33,6 +34,7 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
|
||||
* [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects
|
||||
|
||||
### Installing with Unity Package Manager
|
||||
***Via Git URL***
|
||||
*(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,
|
||||
@ -46,6 +48,14 @@ You will need to have Git installed and available in your system's PATH.
|
||||
|
||||
If you are using [Assembly Definitions](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html) in your project, you will need to add `XNode` and/or `XNodeEditor` as Assembly Definition References.
|
||||
|
||||
***Via OpenUPM***
|
||||
|
||||
The package is available on the [openupm registry](https://openupm.com). It's recommended to install it via [openupm-cli](https://github.com/openupm/openupm-cli).
|
||||
|
||||
```
|
||||
openupm add com.github.siccity.xnode
|
||||
```
|
||||
|
||||
### Node example:
|
||||
```csharp
|
||||
// public classes deriving from Node are registered as nodes for use within a graph
|
||||
|
||||
75
Scripts/Editor/GraphAndNodeEditor.cs
Normal file
75
Scripts/Editor/GraphAndNodeEditor.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
#if ODIN_INSPECTOR
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
using Sirenix.Utilities;
|
||||
using Sirenix.Utilities.Editor;
|
||||
#endif
|
||||
|
||||
namespace XNodeEditor {
|
||||
/// <summary> Override graph inspector to show an 'Open Graph' button at the top </summary>
|
||||
[CustomEditor(typeof(XNode.NodeGraph), true)]
|
||||
#if ODIN_INSPECTOR
|
||||
public class GlobalGraphEditor : OdinEditor {
|
||||
public override void OnInspectorGUI() {
|
||||
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
|
||||
NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
|
||||
}
|
||||
base.OnInspectorGUI();
|
||||
}
|
||||
}
|
||||
#else
|
||||
[CanEditMultipleObjects]
|
||||
public class GlobalGraphEditor : Editor {
|
||||
public override void OnInspectorGUI() {
|
||||
serializedObject.Update();
|
||||
|
||||
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
|
||||
NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
|
||||
}
|
||||
|
||||
GUILayout.Space(EditorGUIUtility.singleLineHeight);
|
||||
GUILayout.Label("Raw data", "BoldLabel");
|
||||
|
||||
DrawDefaultInspector();
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
[CustomEditor(typeof(XNode.Node), true)]
|
||||
#if ODIN_INSPECTOR
|
||||
public class GlobalNodeEditor : OdinEditor {
|
||||
public override void OnInspectorGUI() {
|
||||
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
|
||||
SerializedProperty graphProp = serializedObject.FindProperty("graph");
|
||||
NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
|
||||
w.Home(); // Focus selected node
|
||||
}
|
||||
base.OnInspectorGUI();
|
||||
}
|
||||
}
|
||||
#else
|
||||
[CanEditMultipleObjects]
|
||||
public class GlobalNodeEditor : Editor {
|
||||
public override void OnInspectorGUI() {
|
||||
serializedObject.Update();
|
||||
|
||||
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
|
||||
SerializedProperty graphProp = serializedObject.FindProperty("graph");
|
||||
NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
|
||||
w.Home(); // Focus selected node
|
||||
}
|
||||
|
||||
GUILayout.Space(EditorGUIUtility.singleLineHeight);
|
||||
GUILayout.Label("Raw data", "BoldLabel");
|
||||
|
||||
// Now draw the node itself.
|
||||
DrawDefaultInspector();
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
11
Scripts/Editor/GraphAndNodeEditor.cs.meta
Normal file
11
Scripts/Editor/GraphAndNodeEditor.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdd6e443125ccac4dad0665515759637
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
35
Scripts/Editor/GraphRenameFixAssetProcessor.cs
Normal file
35
Scripts/Editor/GraphRenameFixAssetProcessor.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using UnityEditor;
|
||||
using XNode;
|
||||
|
||||
namespace XNodeEditor {
|
||||
/// <summary>
|
||||
/// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When
|
||||
/// renaming a <see cref="XNode.NodeGraph"/> asset, it appears that sometimes the v2 AssetDatabase will swap which asset
|
||||
/// is the main asset (present at top level) between the <see cref="XNode.NodeGraph"/> and one of its <see cref="XNode.Node"/>
|
||||
/// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it
|
||||
/// finds a case where a <see cref="XNode.Node"/> has been made the main asset it will swap it back to being a sub-asset
|
||||
/// and rename the node to the default name for that node type.
|
||||
/// </summary>
|
||||
internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor {
|
||||
private static void OnPostprocessAllAssets(
|
||||
string[] importedAssets,
|
||||
string[] deletedAssets,
|
||||
string[] movedAssets,
|
||||
string[] movedFromAssetPaths) {
|
||||
for (int i = 0; i < movedAssets.Length; i++) {
|
||||
Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node;
|
||||
|
||||
// If the renamed asset is a node graph, but the v2 AssetDatabase has swapped a sub-asset node to be its
|
||||
// main asset, reset the node graph to be the main asset and rename the node asset back to its default
|
||||
// name.
|
||||
if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) {
|
||||
AssetDatabase.SetMainObject(nodeAsset.graph, movedAssets[i]);
|
||||
AssetDatabase.ImportAsset(movedAssets[i]);
|
||||
|
||||
nodeAsset.name = NodeEditorUtilities.NodeDefaultName(nodeAsset.GetType());
|
||||
EditorUtility.SetDirty(nodeAsset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta
Normal file
11
Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65da1ff1c50a9984a9c95fd18799e8dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -21,7 +21,7 @@ namespace XNodeEditor {
|
||||
public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>();
|
||||
|
||||
#if ODIN_INSPECTOR
|
||||
internal static bool inNodeEditor = false;
|
||||
protected internal static bool inNodeEditor = false;
|
||||
#endif
|
||||
|
||||
public virtual void OnHeaderGUI() {
|
||||
@ -67,7 +67,7 @@ namespace XNodeEditor {
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
#if ODIN_INSPECTOR
|
||||
// Call repaint so that the graph window elements respond properly to layout changes coming from Odin
|
||||
// Call repaint so that the graph window elements respond properly to layout changes coming from Odin
|
||||
if (GUIHelper.RepaintRequested) {
|
||||
GUIHelper.ClearRepaintRequest();
|
||||
window.Repaint();
|
||||
@ -100,19 +100,28 @@ namespace XNodeEditor {
|
||||
return NodeEditorResources.styles.nodeBody;
|
||||
}
|
||||
|
||||
public virtual GUIStyle GetBodyHighlightStyle() {
|
||||
return NodeEditorResources.styles.nodeHighlight;
|
||||
}
|
||||
|
||||
/// <summary> Add items for the context menu when right-clicking this node. Override to add custom menu items. </summary>
|
||||
public virtual void AddContextMenuItems(GenericMenu menu) {
|
||||
bool canRemove = true;
|
||||
// Actions if only one node is selected
|
||||
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
|
||||
XNode.Node node = Selection.activeObject as XNode.Node;
|
||||
menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node));
|
||||
menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode);
|
||||
|
||||
canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
|
||||
else menu.AddItem(new GUIContent("Remove"), false, null);
|
||||
|
||||
// Custom sctions if only one node is selected
|
||||
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
|
||||
@ -125,9 +134,13 @@ namespace XNodeEditor {
|
||||
public void Rename(string newName) {
|
||||
if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType());
|
||||
target.name = newName;
|
||||
OnRename();
|
||||
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
|
||||
}
|
||||
|
||||
/// <summary> Called after this node's name has changed. </summary>
|
||||
public virtual void OnRename() { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class CustomNodeEditorAttribute : Attribute,
|
||||
XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node>.INodeEditorAttrib {
|
||||
|
||||
@ -25,13 +25,14 @@ namespace XNodeEditor {
|
||||
[NonSerialized] private XNode.NodePort autoConnectOutput = null;
|
||||
[NonSerialized] private List<Vector2> draggedOutputReroutes = new List<Vector2>();
|
||||
private RerouteReference hoveredReroute = new RerouteReference();
|
||||
private List<RerouteReference> selectedReroutes = new List<RerouteReference>();
|
||||
public List<RerouteReference> selectedReroutes = new List<RerouteReference>();
|
||||
private Vector2 dragBoxStart;
|
||||
private UnityEngine.Object[] preBoxSelection;
|
||||
private RerouteReference[] preBoxSelectionReroute;
|
||||
private Rect selectionBox;
|
||||
private bool isDoubleClick = false;
|
||||
private Vector2 lastMousePosition;
|
||||
private float dragThreshold = 1f;
|
||||
|
||||
public void Controls() {
|
||||
wantsMouseMove = true;
|
||||
@ -58,10 +59,9 @@ namespace XNodeEditor {
|
||||
case EventType.MouseDrag:
|
||||
if (e.button == 0) {
|
||||
if (IsDraggingPort) {
|
||||
if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) {
|
||||
if (!draggedOutput.IsConnectedTo(hoveredPort)) {
|
||||
draggedOutputTarget = hoveredPort;
|
||||
}
|
||||
// Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously
|
||||
if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) {
|
||||
draggedOutputTarget = hoveredPort;
|
||||
} else {
|
||||
draggedOutputTarget = null;
|
||||
}
|
||||
@ -135,8 +135,11 @@ namespace XNodeEditor {
|
||||
Repaint();
|
||||
}
|
||||
} else if (e.button == 1 || e.button == 2) {
|
||||
panOffset += e.delta * zoom;
|
||||
isPanning = true;
|
||||
//check drag threshold for larger screens
|
||||
if (e.delta.magnitude > dragThreshold) {
|
||||
panOffset += e.delta * zoom;
|
||||
isPanning = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType.MouseDown:
|
||||
@ -205,8 +208,8 @@ namespace XNodeEditor {
|
||||
if (e.button == 0) {
|
||||
//Port drag release
|
||||
if (IsDraggingPort) {
|
||||
//If connection is valid, save it
|
||||
if (draggedOutputTarget != null) {
|
||||
// If connection is valid, save it
|
||||
if (draggedOutputTarget != null && draggedOutput.CanConnectTo(draggedOutputTarget)) {
|
||||
XNode.Node node = draggedOutputTarget.node;
|
||||
if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget);
|
||||
|
||||
@ -218,8 +221,8 @@ namespace XNodeEditor {
|
||||
EditorUtility.SetDirty(graph);
|
||||
}
|
||||
}
|
||||
// Open context menu for auto-connection
|
||||
else if (NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
|
||||
// Open context menu for auto-connection if there is no target node
|
||||
else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
|
||||
GenericMenu menu = new GenericMenu();
|
||||
graphEditor.AddContextMenuItems(menu);
|
||||
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
|
||||
@ -440,6 +443,15 @@ namespace XNodeEditor {
|
||||
for (int i = 0; i < nodes.Length; i++) {
|
||||
XNode.Node srcNode = nodes[i];
|
||||
if (srcNode == null) continue;
|
||||
|
||||
// Check if user is allowed to add more of given node type
|
||||
XNode.Node.DisallowMultipleNodesAttribute disallowAttrib;
|
||||
Type nodeType = srcNode.GetType();
|
||||
if (NodeEditorUtilities.GetAttrib(nodeType, out disallowAttrib)) {
|
||||
int typeCount = graph.nodes.Count(x => x.GetType() == nodeType);
|
||||
if (typeCount >= disallowAttrib.max) continue;
|
||||
}
|
||||
|
||||
XNode.Node newNode = graphEditor.CopyNode(srcNode);
|
||||
substitutes.Add(srcNode, newNode);
|
||||
newNode.position = srcNode.position + offset;
|
||||
@ -457,8 +469,8 @@ namespace XNodeEditor {
|
||||
|
||||
XNode.Node newNodeIn, newNodeOut;
|
||||
if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) {
|
||||
newNodeIn.UpdateStaticPorts();
|
||||
newNodeOut.UpdateStaticPorts();
|
||||
newNodeIn.UpdatePorts();
|
||||
newNodeOut.UpdatePorts();
|
||||
inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
|
||||
outputPort = newNodeOut.GetOutputPort(outputPort.fieldName);
|
||||
}
|
||||
@ -527,8 +539,8 @@ namespace XNodeEditor {
|
||||
XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && x.ValueType == autoConnectOutput.ValueType);
|
||||
// Fallback to input port
|
||||
if (inputPort == null) inputPort = node.Ports.FirstOrDefault(x => x.IsInput);
|
||||
// Autoconnect
|
||||
if (inputPort != null) autoConnectOutput.Connect(inputPort);
|
||||
// Autoconnect if connection is compatible
|
||||
if (inputPort != null && inputPort.CanConnectTo(autoConnectOutput)) autoConnectOutput.Connect(inputPort);
|
||||
|
||||
// Save changes
|
||||
EditorUtility.SetDirty(graph);
|
||||
@ -536,4 +548,4 @@ namespace XNodeEditor {
|
||||
autoConnectOutput = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
Scripts/Editor/NodeEditorGUI.cs
Normal file → Executable file
45
Scripts/Editor/NodeEditorGUI.cs
Normal file → Executable file
@ -17,7 +17,7 @@ namespace XNodeEditor {
|
||||
public event Action onLateGUI;
|
||||
private static readonly Vector3[] polyLineTempArray = new Vector3[2];
|
||||
|
||||
private void OnGUI() {
|
||||
protected virtual void OnGUI() {
|
||||
Event e = Event.current;
|
||||
Matrix4x4 m = GUI.matrix;
|
||||
if (graph == null) return;
|
||||
@ -142,6 +142,7 @@ namespace XNodeEditor {
|
||||
for (int i = 0; i < gridPoints.Count; ++i)
|
||||
gridPoints[i] = GridToWindowPosition(gridPoints[i]);
|
||||
|
||||
Color originalHandlesColor = Handles.color;
|
||||
Handles.color = gradient.Evaluate(0f);
|
||||
int length = gridPoints.Count;
|
||||
switch (path) {
|
||||
@ -202,6 +203,7 @@ namespace XNodeEditor {
|
||||
Vector2 prev_point = point_a;
|
||||
// Approximately one segment per 5 pixels
|
||||
int segments = (int) Vector2.Distance(point_a, point_b) / 5;
|
||||
segments = Math.Max(segments, 1);
|
||||
|
||||
int draw = 0;
|
||||
for (int j = 0; j <= segments; j++) {
|
||||
@ -267,7 +269,44 @@ namespace XNodeEditor {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NoodlePath.ShaderLab:
|
||||
Vector2 start = gridPoints[0];
|
||||
Vector2 end = gridPoints[length - 1];
|
||||
//Modify first and last point in array so we can loop trough them nicely.
|
||||
gridPoints[0] = gridPoints[0] + Vector2.right * (20 / zoom);
|
||||
gridPoints[length - 1] = gridPoints[length - 1] + Vector2.left * (20 / zoom);
|
||||
//Draw first vertical lines going out from nodes
|
||||
Handles.color = gradient.Evaluate(0f);
|
||||
DrawAAPolyLineNonAlloc(thickness, start, gridPoints[0]);
|
||||
Handles.color = gradient.Evaluate(1f);
|
||||
DrawAAPolyLineNonAlloc(thickness, end, gridPoints[length - 1]);
|
||||
for (int i = 0; i < length - 1; i++) {
|
||||
Vector2 point_a = gridPoints[i];
|
||||
Vector2 point_b = gridPoints[i + 1];
|
||||
// Draws the line with the coloring.
|
||||
Vector2 prev_point = point_a;
|
||||
// Approximately one segment per 5 pixels
|
||||
int segments = (int) Vector2.Distance(point_a, point_b) / 5;
|
||||
segments = Math.Max(segments, 1);
|
||||
|
||||
int draw = 0;
|
||||
for (int j = 0; j <= segments; j++) {
|
||||
draw++;
|
||||
float t = j / (float) segments;
|
||||
Vector2 lerp = Vector2.Lerp(point_a, point_b, t);
|
||||
if (draw > 0) {
|
||||
if (i == length - 2) Handles.color = gradient.Evaluate(t);
|
||||
DrawAAPolyLineNonAlloc(thickness, prev_point, lerp);
|
||||
}
|
||||
prev_point = lerp;
|
||||
if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2;
|
||||
}
|
||||
}
|
||||
gridPoints[0] = start;
|
||||
gridPoints[length - 1] = end;
|
||||
break;
|
||||
}
|
||||
Handles.color = originalHandlesColor;
|
||||
}
|
||||
|
||||
/// <summary> Draws all connections </summary>
|
||||
@ -413,7 +452,7 @@ namespace XNodeEditor {
|
||||
|
||||
if (selected) {
|
||||
GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
|
||||
GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight);
|
||||
GUIStyle highlightStyle = new GUIStyle(nodeEditor.GetBodyHighlightStyle());
|
||||
highlightStyle.padding = style.padding;
|
||||
style.padding = new RectOffset();
|
||||
GUI.color = nodeEditor.GetTint();
|
||||
@ -524,4 +563,4 @@ namespace XNodeEditor {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -312,6 +312,8 @@ namespace XNodeEditor {
|
||||
}).Where(x => x.port != null);
|
||||
List<XNode.NodePort> dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
|
||||
|
||||
node.UpdatePorts();
|
||||
|
||||
ReorderableList list = null;
|
||||
Dictionary<string, ReorderableList> rlc;
|
||||
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
|
||||
@ -326,6 +328,7 @@ namespace XNodeEditor {
|
||||
}
|
||||
list.list = dynamicPorts;
|
||||
list.DoLayoutList();
|
||||
|
||||
}
|
||||
|
||||
private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) {
|
||||
@ -337,7 +340,7 @@ namespace XNodeEditor {
|
||||
list.drawElementCallback =
|
||||
(Rect rect, int index, bool isActive, bool isFocused) => {
|
||||
XNode.NodePort port = node.GetPort(fieldName + " " + index);
|
||||
if (hasArrayData) {
|
||||
if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
|
||||
if (arrayData.arraySize <= index) {
|
||||
EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
|
||||
return;
|
||||
@ -368,7 +371,10 @@ namespace XNodeEditor {
|
||||
};
|
||||
list.onReorderCallback =
|
||||
(ReorderableList rl) => {
|
||||
|
||||
bool hasRect = false;
|
||||
bool hasNewRect = false;
|
||||
Rect rect = Rect.zero;
|
||||
Rect newRect = Rect.zero;
|
||||
// Move up
|
||||
if (rl.index > reorderableListIndex) {
|
||||
for (int i = reorderableListIndex; i < rl.index; ++i) {
|
||||
@ -377,9 +383,10 @@ namespace XNodeEditor {
|
||||
port.SwapConnections(nextPort);
|
||||
|
||||
// Swap cached positions to mitigate twitching
|
||||
Rect rect = NodeEditorWindow.current.portConnectionPoints[port];
|
||||
NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort];
|
||||
NodeEditorWindow.current.portConnectionPoints[nextPort] = rect;
|
||||
hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect);
|
||||
hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect);
|
||||
NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect?newRect:rect;
|
||||
NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect?rect:newRect;
|
||||
}
|
||||
}
|
||||
// Move down
|
||||
@ -390,9 +397,10 @@ namespace XNodeEditor {
|
||||
port.SwapConnections(nextPort);
|
||||
|
||||
// Swap cached positions to mitigate twitching
|
||||
Rect rect = NodeEditorWindow.current.portConnectionPoints[port];
|
||||
NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort];
|
||||
NodeEditorWindow.current.portConnectionPoints[nextPort] = rect;
|
||||
hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect);
|
||||
hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect);
|
||||
NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect?newRect:rect;
|
||||
NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect?rect:newRect;
|
||||
}
|
||||
}
|
||||
// Apply changes
|
||||
@ -465,7 +473,7 @@ namespace XNodeEditor {
|
||||
EditorUtility.SetDirty(node);
|
||||
}
|
||||
|
||||
if (hasArrayData) {
|
||||
if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
|
||||
if (arrayData.arraySize <= index) {
|
||||
Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped");
|
||||
Debug.Log(rl.list[0]);
|
||||
|
||||
@ -5,7 +5,7 @@ using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace XNodeEditor {
|
||||
public enum NoodlePath { Curvy, Straight, Angled }
|
||||
public enum NoodlePath { Curvy, Straight, Angled, ShaderLab }
|
||||
public enum NoodleStroke { Full, Dashed }
|
||||
|
||||
public static class NodeEditorPreferences {
|
||||
|
||||
@ -74,8 +74,10 @@ namespace XNodeEditor {
|
||||
|
||||
Attribute attr;
|
||||
if (!typeTypes.TryGetValue(typeof(T), out attr)) {
|
||||
if (GetAttrib<T>(classType, fieldName, out attribOut)) typeTypes.Add(typeof(T), attribOut);
|
||||
else typeTypes.Add(typeof(T), null);
|
||||
if (GetAttrib<T>(classType, fieldName, out attribOut)) {
|
||||
typeTypes.Add(typeof(T), attribOut);
|
||||
return true;
|
||||
} else typeTypes.Add(typeof(T), null);
|
||||
}
|
||||
|
||||
if (attr == null) {
|
||||
@ -261,4 +263,4 @@ namespace XNodeEditor {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +77,16 @@ namespace XNodeEditor {
|
||||
void OnFocus() {
|
||||
current = this;
|
||||
ValidateGraphEditor();
|
||||
if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
|
||||
if (graphEditor != null) {
|
||||
graphEditor.OnWindowFocus();
|
||||
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
dragThreshold = Math.Max(1f, Screen.width / 1000f);
|
||||
}
|
||||
|
||||
void OnLostFocus() {
|
||||
if (graphEditor != null) graphEditor.OnWindowFocusLost();
|
||||
}
|
||||
|
||||
[InitializeOnLoadMethod]
|
||||
@ -97,7 +106,7 @@ namespace XNodeEditor {
|
||||
/// <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) {
|
||||
if (this.graphEditor != graphEditor && graphEditor != null) {
|
||||
this.graphEditor = graphEditor;
|
||||
graphEditor.OnOpen();
|
||||
}
|
||||
@ -187,12 +196,13 @@ namespace XNodeEditor {
|
||||
}
|
||||
|
||||
/// <summary>Open the provided graph in the NodeEditor</summary>
|
||||
public static void Open(XNode.NodeGraph graph) {
|
||||
if (!graph) return;
|
||||
public static NodeEditorWindow Open(XNode.NodeGraph graph) {
|
||||
if (!graph) return null;
|
||||
|
||||
NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
|
||||
w.wantsMouseMove = true;
|
||||
w.graph = graph;
|
||||
return w;
|
||||
}
|
||||
|
||||
/// <summary> Repaint all open NodeEditorWindows. </summary>
|
||||
|
||||
@ -17,6 +17,12 @@ namespace XNodeEditor {
|
||||
|
||||
/// <summary> Called when opened by NodeEditorWindow </summary>
|
||||
public virtual void OnOpen() { }
|
||||
|
||||
/// <summary> Called when NodeEditorWindow gains focus </summary>
|
||||
public virtual void OnWindowFocus() { }
|
||||
|
||||
/// <summary> Called when NodeEditorWindow loses focus </summary>
|
||||
public virtual void OnWindowFocusLost() { }
|
||||
|
||||
public virtual Texture2D GetGridTexture() {
|
||||
return NodeEditorPreferences.GetSettings().gridTexture;
|
||||
@ -41,17 +47,38 @@ namespace XNodeEditor {
|
||||
return NodeEditorUtilities.NodeDefaultPath(type);
|
||||
}
|
||||
|
||||
/// <summary> The order by which the menu items are displayed. </summary>
|
||||
public virtual int GetNodeMenuOrder(Type type) {
|
||||
//Check if type has the CreateNodeMenuAttribute
|
||||
XNode.Node.CreateNodeMenuAttribute attrib;
|
||||
if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path
|
||||
return attrib.order;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary> Add items for the context menu when right-clicking this node. Override to add custom menu items. </summary>
|
||||
public virtual void AddContextMenuItems(GenericMenu menu) {
|
||||
Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition);
|
||||
for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++) {
|
||||
Type type = NodeEditorReflection.nodeTypes[i];
|
||||
var nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(type => GetNodeMenuOrder(type)).ToArray();
|
||||
for (int i = 0; i < nodeTypes.Length; i++) {
|
||||
Type type = nodeTypes[i];
|
||||
|
||||
//Get node context menu path
|
||||
string path = GetNodeMenuName(type);
|
||||
if (string.IsNullOrEmpty(path)) continue;
|
||||
|
||||
menu.AddItem(new GUIContent(path), false, () => {
|
||||
// Check if user is allowed to add more of given node type
|
||||
XNode.Node.DisallowMultipleNodesAttribute disallowAttrib;
|
||||
bool disallowed = false;
|
||||
if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) {
|
||||
int typeCount = target.nodes.Count(x => x.GetType() == type);
|
||||
if (typeCount >= disallowAttrib.max) disallowed = true;
|
||||
}
|
||||
|
||||
// Add node entry to context menu
|
||||
if (disallowed) menu.AddItem(new GUIContent(path), false, null);
|
||||
else menu.AddItem(new GUIContent(path), false, () => {
|
||||
XNode.Node node = CreateNode(type, pos);
|
||||
NodeEditorWindow.current.AutoConnect(node);
|
||||
});
|
||||
@ -133,7 +160,7 @@ namespace XNodeEditor {
|
||||
|
||||
/// <summary> Deal with objects dropped into the graph through DragAndDrop </summary>
|
||||
public virtual void OnDropObjects(UnityEngine.Object[] objects) {
|
||||
Debug.Log("No OnDropObjects override defined for " + GetType());
|
||||
if (GetType() != typeof(NodeGraphEditor)) Debug.Log("No OnDropObjects override defined for " + GetType());
|
||||
}
|
||||
|
||||
/// <summary> Create a node and save it in the graph asset </summary>
|
||||
@ -143,14 +170,14 @@ namespace XNodeEditor {
|
||||
Undo.RegisterCreatedObjectUndo(node, "Create Node");
|
||||
node.position = position;
|
||||
if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
|
||||
AssetDatabase.AddObjectToAsset(node, target);
|
||||
if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target);
|
||||
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
|
||||
NodeEditorWindow.RepaintAll();
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary> Creates a copy of the original node in the graph </summary>
|
||||
public XNode.Node CopyNode(XNode.Node original) {
|
||||
public virtual XNode.Node CopyNode(XNode.Node original) {
|
||||
Undo.RecordObject(target, "Duplicate Node");
|
||||
XNode.Node node = target.CopyNode(original);
|
||||
Undo.RegisterCreatedObjectUndo(node, "Duplicate Node");
|
||||
@ -160,8 +187,25 @@ namespace XNodeEditor {
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary> Return false for nodes that can't be removed </summary>
|
||||
public virtual bool CanRemove(XNode.Node node) {
|
||||
// Check graph attributes to see if this node is required
|
||||
Type graphType = target.GetType();
|
||||
XNode.NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll(
|
||||
graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute);
|
||||
if (attribs.Any(x => x.Requires(node.GetType()))) {
|
||||
if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary> Safely remove a node and all its connections. </summary>
|
||||
public virtual void RemoveNode(XNode.Node node) {
|
||||
if (!CanRemove(node)) return;
|
||||
|
||||
// Remove the node
|
||||
Undo.RecordObject(node, "Delete Node");
|
||||
Undo.RecordObject(target, "Delete Node");
|
||||
foreach (var port in node.Ports)
|
||||
@ -190,4 +234,4 @@ namespace XNodeEditor {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
Scripts/Editor/NodeGraphImporter.cs
Normal file
45
Scripts/Editor/NodeGraphImporter.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.AssetImporters;
|
||||
using UnityEngine;
|
||||
using XNode;
|
||||
|
||||
namespace XNodeEditor {
|
||||
/// <summary> Deals with modified assets </summary>
|
||||
class NodeGraphImporter : AssetPostprocessor {
|
||||
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
|
||||
foreach (string path in importedAssets) {
|
||||
// Skip processing anything without the .asset extension
|
||||
if (Path.GetExtension(path) != ".asset") continue;
|
||||
|
||||
// Get the object that is requested for deletion
|
||||
NodeGraph graph = AssetDatabase.LoadAssetAtPath<NodeGraph>(path);
|
||||
if (graph == null) continue;
|
||||
|
||||
// Get attributes
|
||||
Type graphType = graph.GetType();
|
||||
NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll(
|
||||
graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute);
|
||||
|
||||
Vector2 position = Vector2.zero;
|
||||
foreach (NodeGraph.RequireNodeAttribute attrib in attribs) {
|
||||
if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position);
|
||||
if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position);
|
||||
if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) {
|
||||
if (!graph.nodes.Any(x => x.GetType() == type)) {
|
||||
XNode.Node node = graph.AddNode(type);
|
||||
node.position = position;
|
||||
position.x += 200;
|
||||
if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
|
||||
if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Scripts/Editor/NodeGraphImporter.cs.meta
Normal file
11
Scripts/Editor/NodeGraphImporter.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a816f2790bf3da48a2d6d0035ebc9a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -1,9 +1,11 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XNodeEditor {
|
||||
/// <summary> Utility for renaming assets </summary>
|
||||
public class RenamePopup : EditorWindow {
|
||||
private const string inputControlName = "nameInput";
|
||||
|
||||
public static RenamePopup current { get; private set; }
|
||||
public Object target;
|
||||
public string input;
|
||||
@ -19,7 +21,6 @@ namespace XNodeEditor {
|
||||
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;
|
||||
}
|
||||
@ -43,26 +44,40 @@ namespace XNodeEditor {
|
||||
UpdatePositionToMouse();
|
||||
firstFrame = false;
|
||||
}
|
||||
GUI.SetNextControlName(inputControlName);
|
||||
input = EditorGUILayout.TextField(input);
|
||||
EditorGUI.FocusTextInControl(inputControlName);
|
||||
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());
|
||||
NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
|
||||
AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target));
|
||||
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
|
||||
Close();
|
||||
target.TriggerOnValidate();
|
||||
target.TriggerOnValidate();
|
||||
}
|
||||
}
|
||||
// Rename asset to input text
|
||||
else {
|
||||
if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) {
|
||||
target.name = input;
|
||||
NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
|
||||
AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target));
|
||||
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
|
||||
Close();
|
||||
target.TriggerOnValidate();
|
||||
target.TriggerOnValidate();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.isKey && e.keyCode == KeyCode.Escape) {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy() {
|
||||
EditorGUIUtility.editingTextField = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Scripts/Editor/SceneGraphEditor.cs
Normal file
77
Scripts/Editor/SceneGraphEditor.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using XNode;
|
||||
|
||||
namespace XNodeEditor {
|
||||
[CustomEditor(typeof(SceneGraph), true)]
|
||||
public class SceneGraphEditor : Editor {
|
||||
private SceneGraph sceneGraph;
|
||||
private bool removeSafely;
|
||||
private Type graphType;
|
||||
|
||||
public override void OnInspectorGUI() {
|
||||
if (sceneGraph.graph == null) {
|
||||
if (GUILayout.Button("New graph", GUILayout.Height(40))) {
|
||||
if (graphType == null) {
|
||||
Type[] graphTypes = NodeEditorReflection.GetDerivedTypes(typeof(NodeGraph));
|
||||
GenericMenu menu = new GenericMenu();
|
||||
for (int i = 0; i < graphTypes.Length; i++) {
|
||||
Type graphType = graphTypes[i];
|
||||
menu.AddItem(new GUIContent(graphType.Name), false, () => CreateGraph(graphType));
|
||||
}
|
||||
menu.ShowAsContext();
|
||||
} else {
|
||||
CreateGraph(graphType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (GUILayout.Button("Open graph", GUILayout.Height(40))) {
|
||||
NodeEditorWindow.Open(sceneGraph.graph);
|
||||
}
|
||||
if (removeSafely) {
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("Really remove graph?");
|
||||
GUI.color = new Color(1, 0.8f, 0.8f);
|
||||
if (GUILayout.Button("Remove")) {
|
||||
removeSafely = false;
|
||||
Undo.RecordObject(sceneGraph, "Removed graph");
|
||||
sceneGraph.graph = null;
|
||||
}
|
||||
GUI.color = Color.white;
|
||||
if (GUILayout.Button("Cancel")) {
|
||||
removeSafely = false;
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
} else {
|
||||
GUI.color = new Color(1, 0.8f, 0.8f);
|
||||
if (GUILayout.Button("Remove graph")) {
|
||||
removeSafely = true;
|
||||
}
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable() {
|
||||
sceneGraph = target as SceneGraph;
|
||||
Type sceneGraphType = sceneGraph.GetType();
|
||||
if (sceneGraphType == typeof(SceneGraph)) {
|
||||
graphType = null;
|
||||
} else {
|
||||
Type baseType = sceneGraphType.BaseType;
|
||||
if (baseType.IsGenericType) {
|
||||
graphType = sceneGraphType = baseType.GetGenericArguments() [0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateGraph(Type type) {
|
||||
Undo.RecordObject(sceneGraph, "Create graph");
|
||||
sceneGraph.graph = ScriptableObject.CreateInstance(type) as NodeGraph;
|
||||
sceneGraph.graph.name = sceneGraph.name + "-graph";
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Scripts/Editor/SceneGraphEditor.cs.meta
Normal file
11
Scripts/Editor/SceneGraphEditor.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aea725adabc311f44b5ea8161360a915
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -65,7 +65,7 @@ namespace XNode {
|
||||
|
||||
[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);
|
||||
return AddDynamicInput(type, connectionType, typeConstraint, fieldName);
|
||||
}
|
||||
|
||||
[Obsolete("Use AddDynamicOutput instead")]
|
||||
@ -119,12 +119,12 @@ namespace XNode {
|
||||
protected void OnEnable() {
|
||||
if (graphHotfix != null) graph = graphHotfix;
|
||||
graphHotfix = null;
|
||||
UpdateStaticPorts();
|
||||
UpdatePorts();
|
||||
Init();
|
||||
}
|
||||
|
||||
/// <summary> Update static ports to reflect class fields. This happens automatically on enable. </summary>
|
||||
public void UpdateStaticPorts() {
|
||||
/// <summary> Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list. </summary>
|
||||
public void UpdatePorts() {
|
||||
NodeDataCache.UpdatePorts(this, ports);
|
||||
}
|
||||
|
||||
@ -262,7 +262,7 @@ namespace XNode {
|
||||
|
||||
#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)]
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class InputAttribute : Attribute {
|
||||
public ShowBackingValue backingValue;
|
||||
public ConnectionType connectionType;
|
||||
@ -285,7 +285,7 @@ namespace XNode {
|
||||
}
|
||||
|
||||
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
|
||||
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class OutputAttribute : Attribute {
|
||||
public ShowBackingValue backingValue;
|
||||
public ConnectionType connectionType;
|
||||
@ -314,16 +314,41 @@ namespace XNode {
|
||||
public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { }
|
||||
}
|
||||
|
||||
/// <summary> Manually supply node class with a context menu path </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class CreateNodeMenuAttribute : Attribute {
|
||||
public string menuName;
|
||||
public int order;
|
||||
/// <summary> Manually supply node class with a context menu path </summary>
|
||||
/// <param name="menuName"> Path to this node in the context menu. Null or empty hides it. </param>
|
||||
public CreateNodeMenuAttribute(string menuName) {
|
||||
this.menuName = menuName;
|
||||
this.order = 0;
|
||||
}
|
||||
|
||||
/// <summary> Manually supply node class with a context menu path </summary>
|
||||
/// <param name="menuName"> Path to this node in the context menu. Null or empty hides it. </param>
|
||||
/// <param name="order"> The order by which the menu items are displayed. </param>
|
||||
public CreateNodeMenuAttribute(string menuName, int order) {
|
||||
this.menuName = menuName;
|
||||
this.order = order;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Prevents Node of the same type to be added more than once (configurable) to a NodeGraph </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class DisallowMultipleNodesAttribute : Attribute {
|
||||
// TODO: Make inheritance work in such a way that applying [DisallowMultipleNodes(1)] to type NodeBar : Node
|
||||
// while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both.
|
||||
public int max;
|
||||
/// <summary> Prevents Node of the same type to be added more than once (configurable) to a NodeGraph </summary>
|
||||
/// <param name="max"> How many nodes to allow. Defaults to 1. </param>
|
||||
public DisallowMultipleNodesAttribute(int max = 1) {
|
||||
this.max = max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Specify a color for this node type </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class NodeTintAttribute : Attribute {
|
||||
public Color color;
|
||||
@ -350,6 +375,7 @@ namespace XNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Specify a width for this node type </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class NodeWidthAttribute : Attribute {
|
||||
public int width;
|
||||
|
||||
@ -9,7 +9,7 @@ namespace XNode {
|
||||
private static PortDataCache portDataCache;
|
||||
private static bool Initialized { get { return portDataCache != null; } }
|
||||
|
||||
/// <summary> Update static ports to reflect class fields. </summary>
|
||||
/// <summary> Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. </summary>
|
||||
public static void UpdatePorts(Node node, Dictionary<string, NodePort> ports) {
|
||||
if (!Initialized) BuildCache();
|
||||
|
||||
@ -17,6 +17,8 @@ namespace XNode {
|
||||
Dictionary<string, List<NodePort>> removedPorts = new Dictionary<string, List<NodePort>>();
|
||||
System.Type nodeType = node.GetType();
|
||||
|
||||
List<NodePort> dynamicListPorts = new List<NodePort>();
|
||||
|
||||
List<NodePort> typePortCache;
|
||||
if (portDataCache.TryGetValue(nodeType, out typePortCache)) {
|
||||
for (int i = 0; i < typePortCache.Count; i++) {
|
||||
@ -25,6 +27,7 @@ namespace XNode {
|
||||
}
|
||||
|
||||
// Cleanup port dict - Remove nonexisting static ports - update static port types
|
||||
// AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation.
|
||||
// Loop through current node ports
|
||||
foreach (NodePort port in ports.Values.ToList()) {
|
||||
// If port still exists, check it it has been changed
|
||||
@ -43,6 +46,10 @@ namespace XNode {
|
||||
port.ClearConnections();
|
||||
ports.Remove(port.fieldName);
|
||||
}
|
||||
// If the port is dynamic and is managed by a dynamic port list, flag it for reference updates
|
||||
else if (IsDynamicListPort(port)) {
|
||||
dynamicListPorts.Add(port);
|
||||
}
|
||||
}
|
||||
// Add missing ports
|
||||
foreach (NodePort staticPort in staticPorts.Values) {
|
||||
@ -60,8 +67,57 @@ namespace XNode {
|
||||
ports.Add(staticPort.fieldName, port);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, make sure dynamic list port settings correspond to the settings of their "backing port"
|
||||
foreach (NodePort listPort in dynamicListPorts) {
|
||||
// At this point we know that ports here are dynamic list ports
|
||||
// which have passed name/"backing port" checks, ergo we can proceed more safely.
|
||||
string backingPortName = listPort.fieldName.Split(' ')[0];
|
||||
NodePort backingPort = staticPorts[backingPortName];
|
||||
|
||||
// Update port constraints. Creating a new port instead will break the editor, mandating the need for setters.
|
||||
listPort.ValueType = GetBackingValueType(backingPort.ValueType);
|
||||
listPort.direction = backingPort.direction;
|
||||
listPort.connectionType = backingPort.connectionType;
|
||||
listPort.typeConstraint = backingPort.typeConstraint;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists
|
||||
/// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not
|
||||
/// defined as an array or a list), returns the given type itself.
|
||||
/// </summary>
|
||||
private static System.Type GetBackingValueType(System.Type portValType) {
|
||||
if (portValType.HasElementType) {
|
||||
return portValType.GetElementType();
|
||||
}
|
||||
if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) {
|
||||
return portValType.GetGenericArguments()[0];
|
||||
}
|
||||
return portValType;
|
||||
}
|
||||
|
||||
/// <summary>Returns true if the given port is in a dynamic port list.</summary>
|
||||
private static bool IsDynamicListPort(NodePort port) {
|
||||
// Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have
|
||||
// no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port.
|
||||
// Thus, we need to check for attributes... (but at least we don't need to look at all fields this time)
|
||||
string[] fieldNameParts = port.fieldName.Split(' ');
|
||||
if (fieldNameParts.Length != 2) return false;
|
||||
|
||||
FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]);
|
||||
if (backingPortInfo == null) return false;
|
||||
|
||||
object[] attribs = backingPortInfo.GetCustomAttributes(true);
|
||||
return attribs.Any(x => {
|
||||
Node.InputAttribute inputAttribute = x as Node.InputAttribute;
|
||||
Node.OutputAttribute outputAttribute = x as Node.OutputAttribute;
|
||||
return inputAttribute != null && inputAttribute.dynamicPortList ||
|
||||
outputAttribute != null && outputAttribute.dynamicPortList;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary> Cache node types </summary>
|
||||
private static void BuildCache() {
|
||||
portDataCache = new PortDataCache();
|
||||
@ -81,6 +137,7 @@ namespace XNode {
|
||||
case "UnityEngine":
|
||||
case "System":
|
||||
case "mscorlib":
|
||||
case "Microsoft":
|
||||
continue;
|
||||
default:
|
||||
nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
|
||||
@ -99,7 +156,14 @@ namespace XNode {
|
||||
// GetFields doesnt return inherited private fields, so walk through base types and pick those up
|
||||
System.Type tempType = nodeType;
|
||||
while ((tempType = tempType.BaseType) != typeof(XNode.Node)) {
|
||||
fieldInfo.AddRange(tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance));
|
||||
FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
for (int i = 0; i < parentFields.Length; i++) {
|
||||
// Ensure that we do not already have a member with this type and name
|
||||
FieldInfo parentField = parentFields[i];
|
||||
if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) {
|
||||
fieldInfo.Add(parentField);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fieldInfo;
|
||||
}
|
||||
|
||||
@ -81,5 +81,44 @@ namespace XNode {
|
||||
// Remove all nodes prior to graph destruction
|
||||
Clear();
|
||||
}
|
||||
|
||||
#region Attributes
|
||||
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted. </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
public class RequireNodeAttribute : Attribute {
|
||||
public Type type0;
|
||||
public Type type1;
|
||||
public Type type2;
|
||||
|
||||
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary>
|
||||
public RequireNodeAttribute(Type type) {
|
||||
this.type0 = type;
|
||||
this.type1 = null;
|
||||
this.type2 = null;
|
||||
}
|
||||
|
||||
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary>
|
||||
public RequireNodeAttribute(Type type, Type type2) {
|
||||
this.type0 = type;
|
||||
this.type1 = type2;
|
||||
this.type2 = null;
|
||||
}
|
||||
|
||||
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary>
|
||||
public RequireNodeAttribute(Type type, Type type2, Type type3) {
|
||||
this.type0 = type;
|
||||
this.type1 = type2;
|
||||
this.type2 = type3;
|
||||
}
|
||||
|
||||
public bool Requires(Type type) {
|
||||
if (type == null) return false;
|
||||
if (type == type0) return true;
|
||||
else if (type == type1) return true;
|
||||
else if (type == type2) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -19,9 +19,18 @@ namespace XNode {
|
||||
}
|
||||
}
|
||||
|
||||
public IO direction { get { return _direction; } }
|
||||
public Node.ConnectionType connectionType { get { return _connectionType; } }
|
||||
public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } }
|
||||
public IO direction {
|
||||
get { return _direction; }
|
||||
internal set { _direction = value; }
|
||||
}
|
||||
public Node.ConnectionType connectionType {
|
||||
get { return _connectionType; }
|
||||
internal set { _connectionType = value; }
|
||||
}
|
||||
public Node.TypeConstraint typeConstraint {
|
||||
get { return _typeConstraint; }
|
||||
internal set { _typeConstraint = value; }
|
||||
}
|
||||
|
||||
/// <summary> Is this port connected to anytihng? </summary>
|
||||
public bool IsConnected { get { return connections.Count != 0; } }
|
||||
|
||||
23
Scripts/SceneGraph.cs
Normal file
23
Scripts/SceneGraph.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using XNode;
|
||||
|
||||
namespace XNode {
|
||||
/// <summary> Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects. </summary>
|
||||
public class SceneGraph : MonoBehaviour {
|
||||
public NodeGraph graph;
|
||||
}
|
||||
|
||||
/// <summary> Derive from this class to create a SceneGraph with a specific graph type. </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public class MySceneGraph : SceneGraph<MyGraph> {
|
||||
///
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class SceneGraph<T> : SceneGraph where T : NodeGraph {
|
||||
public new T graph { get { return base.graph as T; } set { base.graph = value; } }
|
||||
}
|
||||
}
|
||||
11
Scripts/SceneGraph.cs.meta
Normal file
11
Scripts/SceneGraph.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7915171fc13472a40a0162003052d2db
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -1,7 +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",
|
||||
"version": "1.8.0",
|
||||
"unity": "2018.1",
|
||||
"displayName": "xNode"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user