mirror of
https://github.com/Siccity/xNode.git
synced 2026-02-04 14:24:54 +08:00
Merge branch 'master' into examples
# Conflicts: # .gitignore
This commit is contained in:
commit
49e29a9f3e
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: thorbrigsted
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: thorbrigsted
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -20,10 +20,6 @@
|
||||
# Unity3D Generated File On Crash Reports
|
||||
sysinfo.txt
|
||||
|
||||
README.md.meta
|
||||
LICENSE.md.meta
|
||||
CONTRIBUTING.md.meta
|
||||
|
||||
.git.meta
|
||||
.gitignore.meta
|
||||
.gitattributes.meta
|
||||
|
||||
@ -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,7 +16,7 @@ xNode aims to be simple and extendible, not trying to fix all of Unity's shortco
|
||||
If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel.
|
||||
|
||||
## 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. Params described if not obvious
|
||||
|
||||
7
CONTRIBUTING.md.meta
Normal file
7
CONTRIBUTING.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc1db8b29c76d44648c9c86c2dfade6d
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
LICENSE.md.meta
Normal file
7
LICENSE.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77523c356ccf04f56b53e6527c6b12fd
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
21
README.md
21
README.md
@ -7,7 +7,7 @@
|
||||
|
||||
[Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki)
|
||||
|
||||
[Support Me on Ko-fi](https://ko-fi.com/Z8Z5DYWA)
|
||||
Support xNode on [Ko-fi](https://ko-fi.com/Z8Z5DYWA) or [Patreon](https://www.patreon.com/thorbrigsted)
|
||||
|
||||
### xNode
|
||||
Thinking of developing a node-based plugin? Then this is for you. You can download it as an archive and unpack to a new unity project, or connect it as git submodule.
|
||||
@ -32,6 +32,20 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
|
||||
* [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph
|
||||
* [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
|
||||
@ -70,8 +84,3 @@ public class MathNode : Node {
|
||||
|
||||
Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support.
|
||||
Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page.
|
||||
|
||||
Projects using xNode:
|
||||
* [Graphmesh](https://github.com/Siccity/Graphmesh "Go to github page")
|
||||
* [Dialogue](https://github.com/Siccity/Dialogue "Go to github page")
|
||||
* [qAI](https://github.com/jlreymendez/qAI "Go to github page")
|
||||
|
||||
7
README.md.meta
Normal file
7
README.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 243efae3a6b7941ad8f8e54dcf38ce8c
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -12,6 +12,12 @@ namespace XNodeEditor {
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
EnumPopup(position, property, label);
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
public static void EnumPopup(Rect position, SerializedProperty property, GUIContent label) {
|
||||
// Throw error on wrong type
|
||||
if (property.propertyType != SerializedPropertyType.Enum) {
|
||||
throw new ArgumentException("Parameter selected must be of type System.Enum");
|
||||
@ -39,10 +45,9 @@ namespace XNodeEditor {
|
||||
NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property);
|
||||
}
|
||||
#endif
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
private void ShowContextMenuAtMouse(SerializedProperty property) {
|
||||
public static void ShowContextMenuAtMouse(SerializedProperty property) {
|
||||
// Initialize menu
|
||||
GenericMenu menu = new GenericMenu();
|
||||
|
||||
@ -57,7 +62,7 @@ namespace XNodeEditor {
|
||||
menu.DropDown(r);
|
||||
}
|
||||
|
||||
private void SetEnum(SerializedProperty property, int index) {
|
||||
private static void SetEnum(SerializedProperty property, int index) {
|
||||
property.enumValueIndex = index;
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
property.serializedObject.Update();
|
||||
|
||||
8
Scripts/Editor/Drawers/Odin.meta
Normal file
8
Scripts/Editor/Drawers/Odin.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 327994a52f523b641898a39ff7500a02
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,48 @@
|
||||
#if UNITY_EDITOR && ODIN_INSPECTOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
using UnityEngine;
|
||||
using XNode;
|
||||
|
||||
namespace XNodeEditor {
|
||||
internal class OdinNodeInGraphAttributeProcessor<T> : OdinAttributeProcessor<T> where T : Node {
|
||||
public override bool CanProcessSelfAttributes(InspectorProperty property) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool CanProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member) {
|
||||
if (!NodeEditor.inNodeEditor)
|
||||
return false;
|
||||
|
||||
if (member.MemberType == MemberTypes.Field) {
|
||||
switch (member.Name) {
|
||||
case "graph":
|
||||
case "position":
|
||||
case "ports":
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void ProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member, List<Attribute> attributes) {
|
||||
switch (member.Name) {
|
||||
case "graph":
|
||||
case "position":
|
||||
case "ports":
|
||||
attributes.Add(new HideInInspector());
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3cf2561fbfea9a041ac81efbbb5b3e0d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
49
Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs
Normal file
49
Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs
Normal file
@ -0,0 +1,49 @@
|
||||
#if UNITY_EDITOR && ODIN_INSPECTOR
|
||||
using Sirenix.OdinInspector;
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
using Sirenix.Utilities.Editor;
|
||||
using UnityEngine;
|
||||
using XNode;
|
||||
|
||||
namespace XNodeEditor {
|
||||
public class InputAttributeDrawer : OdinAttributeDrawer<XNode.Node.InputAttribute> {
|
||||
protected override bool CanDrawAttributeProperty(InspectorProperty property) {
|
||||
Node node = property.Tree.WeakTargets[0] as Node;
|
||||
return node != null;
|
||||
}
|
||||
|
||||
protected override void DrawPropertyLayout(GUIContent label) {
|
||||
Node node = Property.Tree.WeakTargets[0] as Node;
|
||||
NodePort port = node.GetInputPort(Property.Name);
|
||||
|
||||
if (!NodeEditor.inNodeEditor) {
|
||||
if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected)
|
||||
CallNextDrawer(label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Property.Tree.WeakTargets.Count > 1) {
|
||||
SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (port != null) {
|
||||
var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath);
|
||||
if (portPropoerty == null) {
|
||||
SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath);
|
||||
return;
|
||||
} else {
|
||||
var labelWidth = Property.GetAttribute<LabelWidthAttribute>();
|
||||
if (labelWidth != null)
|
||||
GUIHelper.PushLabelWidth(labelWidth.Width);
|
||||
|
||||
NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30));
|
||||
|
||||
if (labelWidth != null)
|
||||
GUIHelper.PopLabelWidth();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta
Normal file
11
Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2fd590b2e9ea0bd49b6986a2ca9010ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
49
Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs
Normal file
49
Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs
Normal file
@ -0,0 +1,49 @@
|
||||
#if UNITY_EDITOR && ODIN_INSPECTOR
|
||||
using Sirenix.OdinInspector;
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
using Sirenix.Utilities.Editor;
|
||||
using UnityEngine;
|
||||
using XNode;
|
||||
|
||||
namespace XNodeEditor {
|
||||
public class OutputAttributeDrawer : OdinAttributeDrawer<XNode.Node.OutputAttribute> {
|
||||
protected override bool CanDrawAttributeProperty(InspectorProperty property) {
|
||||
Node node = property.Tree.WeakTargets[0] as Node;
|
||||
return node != null;
|
||||
}
|
||||
|
||||
protected override void DrawPropertyLayout(GUIContent label) {
|
||||
Node node = Property.Tree.WeakTargets[0] as Node;
|
||||
NodePort port = node.GetOutputPort(Property.Name);
|
||||
|
||||
if (!NodeEditor.inNodeEditor) {
|
||||
if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected)
|
||||
CallNextDrawer(label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Property.Tree.WeakTargets.Count > 1) {
|
||||
SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (port != null) {
|
||||
var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath);
|
||||
if (portPropoerty == null) {
|
||||
SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath);
|
||||
return;
|
||||
} else {
|
||||
var labelWidth = Property.GetAttribute<LabelWidthAttribute>();
|
||||
if (labelWidth != null)
|
||||
GUIHelper.PushLabelWidth(labelWidth.Width);
|
||||
|
||||
NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30));
|
||||
|
||||
if (labelWidth != null)
|
||||
GUIHelper.PopLabelWidth();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta
Normal file
11
Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7ebd8f2b42e2384aa109551dc46af88
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Scripts/Editor/Internal.meta
Normal file
8
Scripts/Editor/Internal.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6a1bbc054e282346a02e7bbddde3206
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Scripts/Editor/Internal/RerouteReference.cs
Normal file
20
Scripts/Editor/Internal/RerouteReference.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XNodeEditor.Internal {
|
||||
public struct RerouteReference {
|
||||
public XNode.NodePort port;
|
||||
public int connectionIndex;
|
||||
public int pointIndex;
|
||||
|
||||
public RerouteReference(XNode.NodePort port, int connectionIndex, int pointIndex) {
|
||||
this.port = port;
|
||||
this.connectionIndex = connectionIndex;
|
||||
this.pointIndex = pointIndex;
|
||||
}
|
||||
|
||||
public void InsertPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); }
|
||||
public void SetPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex) [pointIndex] = pos; }
|
||||
public void RemovePoint() { port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); }
|
||||
public Vector2 GetPoint() { return port.GetReroutePoints(connectionIndex) [pointIndex]; }
|
||||
}
|
||||
}
|
||||
11
Scripts/Editor/Internal/RerouteReference.cs.meta
Normal file
11
Scripts/Editor/Internal/RerouteReference.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 399f3c5fb717b2c458c3e9746f8959a3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -3,29 +3,51 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
#if ODIN_INSPECTOR
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
using Sirenix.Utilities;
|
||||
using Sirenix.Utilities.Editor;
|
||||
#endif
|
||||
|
||||
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 readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>();
|
||||
|
||||
#if ODIN_INSPECTOR
|
||||
internal static bool inNodeEditor = false;
|
||||
#endif
|
||||
|
||||
public virtual void OnHeaderGUI() {
|
||||
GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
|
||||
}
|
||||
|
||||
/// <summary> Draws standard field editors for all public fields </summary>
|
||||
public virtual void OnBodyGUI() {
|
||||
#if ODIN_INSPECTOR
|
||||
inNodeEditor = true;
|
||||
#endif
|
||||
|
||||
// Unity specifically requires this to save/update any serial object.
|
||||
// serializedObject.Update(); must go at the start of an inspector gui, and
|
||||
// serializedObject.ApplyModifiedProperties(); goes at the end.
|
||||
serializedObject.Update();
|
||||
string[] excludes = { "m_Script", "graph", "position", "ports" };
|
||||
|
||||
#if ODIN_INSPECTOR
|
||||
InspectorUtilities.BeginDrawPropertyTree(objectTree, true);
|
||||
GUIHelper.PushLabelWidth(84);
|
||||
objectTree.Draw(true);
|
||||
InspectorUtilities.EndDrawPropertyTree(objectTree);
|
||||
GUIHelper.PopLabelWidth();
|
||||
#else
|
||||
|
||||
// Iterate through serialized properties and draw them like the Inspector (But with ports)
|
||||
SerializedProperty iterator = serializedObject.GetIterator();
|
||||
bool enterChildren = true;
|
||||
@ -35,6 +57,7 @@ namespace XNodeEditor {
|
||||
if (excludes.Contains(iterator.name)) continue;
|
||||
NodeEditorGUILayout.PropertyField(iterator, true);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Iterate through dynamic ports and draw them in the order in which they are serialized
|
||||
foreach (XNode.NodePort dynamicPort in target.DynamicPorts) {
|
||||
@ -43,20 +66,37 @@ namespace XNodeEditor {
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
#if ODIN_INSPECTOR
|
||||
// Call repaint so that the graph window elements respond properly to layout changes coming from Odin
|
||||
if (GUIHelper.RepaintRequested) {
|
||||
GUIHelper.ClearRepaintRequest();
|
||||
window.Repaint();
|
||||
}
|
||||
#else
|
||||
window.Repaint();
|
||||
#endif
|
||||
|
||||
#if ODIN_INSPECTOR
|
||||
inNodeEditor = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
public virtual int GetWidth() {
|
||||
Type type = target.GetType();
|
||||
int width;
|
||||
if (NodeEditorWindow.nodeWidth.TryGetValue(type, out width)) return width;
|
||||
if (type.TryGetAttributeWidth(out width)) return width;
|
||||
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;
|
||||
if (type.TryGetAttributeTint(out color)) return color;
|
||||
// Return default color (grey)
|
||||
else return DEFAULTCOLOR;
|
||||
}
|
||||
|
||||
public virtual GUIStyle GetBodyStyle() {
|
||||
@ -73,19 +113,20 @@ 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);
|
||||
|
||||
// Custom sctions if only one node is selected
|
||||
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
|
||||
XNode.Node node = Selection.activeObject as XNode.Node;
|
||||
NodeEditorWindow.AddCustomContextMenuItems(menu, node);
|
||||
menu.AddCustomContextMenuItems(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Rename the node asset. This will trigger a reimport of the node. </summary>
|
||||
public void Rename(string newName) {
|
||||
if (newName == null || newName.Trim() == "") newName = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name);
|
||||
if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType());
|
||||
target.name = newName;
|
||||
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using XNodeEditor.Internal;
|
||||
|
||||
namespace XNodeEditor {
|
||||
public partial class NodeEditorWindow {
|
||||
@ -11,6 +12,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; } }
|
||||
@ -19,6 +22,7 @@ namespace XNodeEditor {
|
||||
[NonSerialized] private XNode.NodePort hoveredPort = null;
|
||||
[NonSerialized] private XNode.NodePort draggedOutput = null;
|
||||
[NonSerialized] private XNode.NodePort draggedOutputTarget = null;
|
||||
[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>();
|
||||
@ -27,29 +31,23 @@ namespace XNodeEditor {
|
||||
private RerouteReference[] preBoxSelectionReroute;
|
||||
private Rect selectionBox;
|
||||
private bool isDoubleClick = false;
|
||||
|
||||
private struct RerouteReference {
|
||||
public XNode.NodePort port;
|
||||
public int connectionIndex;
|
||||
public int pointIndex;
|
||||
|
||||
public RerouteReference(XNode.NodePort port, int connectionIndex, int pointIndex) {
|
||||
this.port = port;
|
||||
this.connectionIndex = connectionIndex;
|
||||
this.pointIndex = pointIndex;
|
||||
}
|
||||
|
||||
public void InsertPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); }
|
||||
public void SetPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex) [pointIndex] = pos; }
|
||||
public void RemovePoint() { port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); }
|
||||
public Vector2 GetPoint() { return port.GetReroutePoints(connectionIndex) [pointIndex]; }
|
||||
}
|
||||
private Vector2 lastMousePosition;
|
||||
|
||||
public void Controls() {
|
||||
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;
|
||||
@ -148,8 +146,10 @@ namespace XNodeEditor {
|
||||
if (IsHoveringPort) {
|
||||
if (hoveredPort.IsOutput) {
|
||||
draggedOutput = hoveredPort;
|
||||
autoConnectOutput = hoveredPort;
|
||||
} else {
|
||||
hoveredPort.VerifyConnections();
|
||||
autoConnectOutput = null;
|
||||
if (hoveredPort.IsConnected) {
|
||||
XNode.Node node = hoveredPort.node;
|
||||
XNode.NodePort output = hoveredPort.Connection;
|
||||
@ -217,6 +217,12 @@ namespace XNodeEditor {
|
||||
EditorUtility.SetDirty(graph);
|
||||
}
|
||||
}
|
||||
// Open context menu for auto-connection
|
||||
else if (NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
|
||||
GenericMenu menu = new GenericMenu();
|
||||
graphEditor.AddContextMenuItems(menu);
|
||||
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
|
||||
}
|
||||
//Release dragged connection
|
||||
draggedOutput = null;
|
||||
draggedOutputTarget = null;
|
||||
@ -268,11 +274,13 @@ namespace XNodeEditor {
|
||||
ShowPortContextMenu(hoveredPort);
|
||||
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
|
||||
if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false);
|
||||
autoConnectOutput = null;
|
||||
GenericMenu menu = new GenericMenu();
|
||||
NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu);
|
||||
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
|
||||
e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places.
|
||||
} else if (!IsHoveringNode) {
|
||||
autoConnectOutput = null;
|
||||
GenericMenu menu = new GenericMenu();
|
||||
graphEditor.AddContextMenuItems(menu);
|
||||
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
|
||||
@ -286,23 +294,40 @@ namespace XNodeEditor {
|
||||
case EventType.KeyDown:
|
||||
if (EditorGUIUtility.editingTextField) break;
|
||||
else if (e.keyCode == KeyCode.F) Home();
|
||||
if (IsMac()) {
|
||||
if (NodeEditorUtilities.IsMac()) {
|
||||
if (e.keyCode == KeyCode.Return) RenameSelectedNode();
|
||||
} else {
|
||||
if (e.keyCode == KeyCode.F2) RenameSelectedNode();
|
||||
}
|
||||
if (e.keyCode == KeyCode.A) {
|
||||
if (Selection.objects.Any(x => graph.nodes.Contains(x as XNode.Node))) {
|
||||
foreach (XNode.Node node in graph.nodes) {
|
||||
DeselectNode(node);
|
||||
}
|
||||
} else {
|
||||
foreach (XNode.Node node in graph.nodes) {
|
||||
SelectNode(node, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EventType.ValidateCommand:
|
||||
case EventType.ExecuteCommand:
|
||||
if (e.commandName == "SoftDelete") {
|
||||
if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
|
||||
e.Use();
|
||||
} else if (IsMac() && e.commandName == "Delete") {
|
||||
} else if (NodeEditorUtilities.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;
|
||||
@ -316,14 +341,6 @@ namespace XNodeEditor {
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMac() {
|
||||
#if UNITY_2017_1_OR_NEWER
|
||||
return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
|
||||
#else
|
||||
return SystemInfo.operatingSystem.StartsWith("Mac");
|
||||
#endif
|
||||
}
|
||||
|
||||
private void RecalculateDragOffsets(Event current) {
|
||||
dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count];
|
||||
// Selected nodes
|
||||
@ -340,10 +357,17 @@ namespace XNodeEditor {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Puts all nodes in focus. If no nodes are present, resets view to </summary>
|
||||
/// <summary> Puts all selected nodes in focus. If no nodes are present, resets view and zoom to to origin </summary>
|
||||
public void Home() {
|
||||
zoom = 2;
|
||||
panOffset = Vector2.zero;
|
||||
var nodes = Selection.objects.Where(o => o is XNode.Node).Cast<XNode.Node>().ToList();
|
||||
if (nodes.Count > 0) {
|
||||
Vector2 minPos = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y)));
|
||||
Vector2 maxPos = nodes.Select(x => x.position + (nodeSizes.ContainsKey(x) ? nodeSizes[x] : Vector2.zero)).Aggregate((x, y) => new Vector2(Mathf.Max(x.x, y.x), Mathf.Max(x.y, y.y)));
|
||||
panOffset = -(minPos + (maxPos - minPos) / 2f);
|
||||
} else {
|
||||
zoom = 2;
|
||||
panOffset = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Remove nodes in the graph in Selection.objects</summary>
|
||||
@ -386,41 +410,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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@ -470,5 +513,22 @@ namespace XNodeEditor {
|
||||
Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom));
|
||||
return windowRect.Contains(mousePos);
|
||||
}
|
||||
|
||||
/// <summary> Attempt to connect dragged output to target node </summary>
|
||||
public void AutoConnect(XNode.Node node) {
|
||||
if (autoConnectOutput == null) return;
|
||||
|
||||
// Find input port of same type
|
||||
XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && x.ValueType == autoConnectOutput.ValueType);
|
||||
// Fallback to input port
|
||||
if (inputPort == null) inputPort = node.Ports.FirstOrDefault(x => x.IsInput);
|
||||
// Autoconnect
|
||||
if (inputPort != null) autoConnectOutput.Connect(inputPort);
|
||||
|
||||
// Save changes
|
||||
EditorUtility.SetDirty(graph);
|
||||
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
|
||||
autoConnectOutput = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@ using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
#if ODIN_INSPECTOR
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
#endif
|
||||
|
||||
namespace XNodeEditor.Internal {
|
||||
/// <summary> Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) </summary>
|
||||
@ -17,6 +20,24 @@ namespace XNodeEditor.Internal {
|
||||
public NodeEditorWindow window;
|
||||
public K target;
|
||||
public SerializedObject serializedObject;
|
||||
#if ODIN_INSPECTOR
|
||||
private PropertyTree _objectTree;
|
||||
public PropertyTree objectTree {
|
||||
get {
|
||||
if (this._objectTree == null) {
|
||||
try {
|
||||
bool wasInEditor = NodeEditor.inNodeEditor;
|
||||
NodeEditor.inNodeEditor = true;
|
||||
this._objectTree = PropertyTree.Create(this.serializedObject);
|
||||
NodeEditor.inNodeEditor = wasInEditor;
|
||||
} catch (ArgumentException ex) {
|
||||
Debug.Log(ex);
|
||||
}
|
||||
}
|
||||
return this._objectTree;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public static T GetEditor(K target, NodeEditorWindow window) {
|
||||
if (target == null) return null;
|
||||
@ -50,7 +71,7 @@ namespace XNodeEditor.Internal {
|
||||
editorTypes = new Dictionary<Type, Type>();
|
||||
|
||||
//Get all classes deriving from NodeEditor via reflection
|
||||
Type[] nodeEditors = XNodeEditor.NodeEditorWindow.GetDerivedTypes(typeof(T));
|
||||
Type[] nodeEditors = typeof(T).GetDerivedTypes();
|
||||
for (int i = 0; i < nodeEditors.Length; i++) {
|
||||
if (nodeEditors[i].IsAbstract) continue;
|
||||
var attribs = nodeEditors[i].GetCustomAttributes(typeof(A), false);
|
||||
|
||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using XNodeEditor.Internal;
|
||||
|
||||
namespace XNodeEditor {
|
||||
/// <summary> Contains GUI methods </summary>
|
||||
@ -10,6 +11,7 @@ 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> Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run.</summary>
|
||||
public event Action onLateGUI;
|
||||
@ -200,11 +202,11 @@ namespace XNodeEditor {
|
||||
Rect fromRect;
|
||||
if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue;
|
||||
|
||||
Color connectionColor = graphEditor.GetPortColor(output);
|
||||
|
||||
for (int k = 0; k < output.ConnectionCount; k++) {
|
||||
XNode.NodePort input = output.GetConnection(k);
|
||||
|
||||
Color noodleColor = graphEditor.GetNoodleColor(output, input);
|
||||
|
||||
// Error handling
|
||||
if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return.
|
||||
if (!input.IsConnectedTo(output)) input.Connect(output);
|
||||
@ -217,7 +219,7 @@ namespace XNodeEditor {
|
||||
gridPoints.Add(fromRect.center);
|
||||
gridPoints.AddRange(reroutePoints);
|
||||
gridPoints.Add(toRect.center);
|
||||
DrawNoodle(connectionColor, gridPoints);
|
||||
DrawNoodle(noodleColor, gridPoints);
|
||||
|
||||
// Loop through reroute points again and draw the points
|
||||
for (int i = 0; i < reroutePoints.Count; i++) {
|
||||
@ -233,7 +235,7 @@ namespace XNodeEditor {
|
||||
GUI.DrawTexture(rect, NodeEditorResources.dotOuter);
|
||||
}
|
||||
|
||||
GUI.color = connectionColor;
|
||||
GUI.color = noodleColor;
|
||||
GUI.DrawTexture(rect, NodeEditorResources.dot);
|
||||
if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef);
|
||||
if (rect.Contains(mousePos)) hoveredReroute = rerouteRef;
|
||||
@ -411,15 +413,12 @@ 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);
|
||||
size.x += 8;
|
||||
Rect rect = new Rect(Event.current.mousePosition - (size), size);
|
||||
EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip);
|
||||
Repaint();
|
||||
|
||||
@ -139,9 +139,8 @@ namespace XNodeEditor {
|
||||
|
||||
rect.size = new Vector2(16, 16);
|
||||
|
||||
Color backgroundColor = new Color32(90, 97, 105, 255);
|
||||
Color tint;
|
||||
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
|
||||
NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current);
|
||||
Color backgroundColor = editor.GetTint();
|
||||
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
|
||||
DrawPortHandle(rect, backgroundColor, col);
|
||||
|
||||
@ -153,7 +152,7 @@ namespace XNodeEditor {
|
||||
|
||||
private static System.Type GetType(SerializedProperty property) {
|
||||
System.Type parentType = property.serializedObject.targetObject.GetType();
|
||||
System.Reflection.FieldInfo fi = NodeEditorWindow.GetFieldInfo(parentType, property.name);
|
||||
System.Reflection.FieldInfo fi = parentType.GetFieldInfo(property.name);
|
||||
return fi.FieldType;
|
||||
}
|
||||
|
||||
@ -176,7 +175,6 @@ namespace XNodeEditor {
|
||||
|
||||
Rect rect = GUILayoutUtility.GetLastRect();
|
||||
position = rect.position - new Vector2(16, 0);
|
||||
|
||||
}
|
||||
// If property is an output, display a text label and put a port handle on the right side
|
||||
else if (port.direction == XNode.NodePort.IO.Output) {
|
||||
@ -195,9 +193,8 @@ namespace XNodeEditor {
|
||||
|
||||
Rect rect = new Rect(position, new Vector2(16, 16));
|
||||
|
||||
Color backgroundColor = new Color32(90, 97, 105, 255);
|
||||
Color tint;
|
||||
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
|
||||
NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current);
|
||||
Color backgroundColor = editor.GetTint();
|
||||
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
|
||||
DrawPortHandle(rect, backgroundColor, col);
|
||||
|
||||
@ -223,9 +220,8 @@ namespace XNodeEditor {
|
||||
|
||||
rect.size = new Vector2(16, 16);
|
||||
|
||||
Color backgroundColor = new Color32(90, 97, 105, 255);
|
||||
Color tint;
|
||||
if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint;
|
||||
NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current);
|
||||
Color backgroundColor = editor.GetTint();
|
||||
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
|
||||
DrawPortHandle(rect, backgroundColor, col);
|
||||
|
||||
@ -293,7 +289,7 @@ namespace XNodeEditor {
|
||||
}
|
||||
}
|
||||
return new { index = -1, port = (XNode.NodePort) null };
|
||||
});
|
||||
}).Where(x => x.port != null);
|
||||
List<XNode.NodePort> dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
|
||||
|
||||
ReorderableList list = null;
|
||||
@ -323,12 +319,12 @@ namespace XNodeEditor {
|
||||
XNode.NodePort port = node.GetPort(fieldName + " " + index);
|
||||
if (hasArrayData) {
|
||||
if (arrayData.arraySize <= index) {
|
||||
EditorGUI.LabelField(rect, "Invalid element " + index);
|
||||
EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
|
||||
return;
|
||||
}
|
||||
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index);
|
||||
EditorGUI.PropertyField(rect, itemData, true);
|
||||
} else EditorGUI.LabelField(rect, port.fieldName);
|
||||
} 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);
|
||||
@ -422,12 +418,17 @@ namespace XNodeEditor {
|
||||
}
|
||||
}
|
||||
return new { index = -1, port = (XNode.NodePort) null };
|
||||
});
|
||||
}).Where(x => x.port != null);
|
||||
dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
|
||||
|
||||
int index = rl.index;
|
||||
|
||||
if (dynamicPorts.Count > index) {
|
||||
if (dynamicPorts[index] == null) {
|
||||
Debug.LogWarning("No port found at index " + index + " - Skipped");
|
||||
} else if (dynamicPorts.Count <= index) {
|
||||
Debug.LogWarning("DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + " - Skipped");
|
||||
} else {
|
||||
|
||||
// Clear the removed ports connections
|
||||
dynamicPorts[index].ClearConnections();
|
||||
// Move following connections one step up to replace the missing connection
|
||||
@ -442,11 +443,14 @@ namespace XNodeEditor {
|
||||
node.RemoveDynamicPort(dynamicPorts[dynamicPorts.Count() - 1].fieldName);
|
||||
serializedObject.Update();
|
||||
EditorUtility.SetDirty(node);
|
||||
} else {
|
||||
Debug.LogWarning("DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + ". Skipping.");
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -32,7 +32,9 @@ namespace XNodeEditor {
|
||||
public Color32 highlightColor = new Color32(255, 255, 255, 255);
|
||||
public bool gridSnap = true;
|
||||
public bool autoSave = true;
|
||||
public bool dragToCreate = 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;
|
||||
@ -105,6 +107,9 @@ namespace XNodeEditor {
|
||||
VerifyLoaded();
|
||||
Settings settings = NodeEditorPreferences.settings[lastKey];
|
||||
|
||||
if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), GUILayout.Width(100))) Application.OpenURL("https://github.com/Siccity/xNode/wiki");
|
||||
EditorGUILayout.Space();
|
||||
|
||||
NodeSettingsGUI(lastKey, settings);
|
||||
GridSettingsGUI(lastKey, settings);
|
||||
SystemSettingsGUI(lastKey, settings);
|
||||
@ -147,6 +152,8 @@ 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);
|
||||
settings.dragToCreate = EditorGUILayout.Toggle(new GUIContent("Drag to Create", "Drag a port connection anywhere on the grid to create and connect a node"), settings.dragToCreate);
|
||||
if (GUI.changed) {
|
||||
SavePrefs(key, settings);
|
||||
NodeEditorWindow.RepaintAll();
|
||||
@ -218,12 +225,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;
|
||||
|
||||
@ -7,62 +7,55 @@ using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XNodeEditor {
|
||||
/// <summary> Contains reflection-related info </summary>
|
||||
public partial class NodeEditorWindow {
|
||||
/// <summary> Custom node tint colors defined with [NodeColor(r, g, b)] </summary>
|
||||
public static Dictionary<Type, Color> nodeTint { get { return _nodeTint != null ? _nodeTint : _nodeTint = GetNodeTint(); } }
|
||||
|
||||
[NonSerialized] private static Dictionary<Type, Color> _nodeTint;
|
||||
/// <summary> Custom node widths defined with [NodeWidth(width)] </summary>
|
||||
public static Dictionary<Type, int> nodeWidth { get { return _nodeWidth != null ? _nodeWidth : _nodeWidth = GetNodeWidth(); } }
|
||||
|
||||
[NonSerialized] private static Dictionary<Type, int> _nodeWidth;
|
||||
/// <summary> Contains reflection-related extensions built for xNode </summary>
|
||||
public static class NodeEditorReflection {
|
||||
[NonSerialized] private static Dictionary<Type, Color> nodeTint;
|
||||
[NonSerialized] private static Dictionary<Type, int> nodeWidth;
|
||||
/// <summary> All available node types </summary>
|
||||
public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } }
|
||||
|
||||
[NonSerialized] private static Type[] _nodeTypes = null;
|
||||
|
||||
private Func<bool> isDocked {
|
||||
get {
|
||||
if (_isDocked == null) {
|
||||
BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
||||
MethodInfo isDockedMethod = typeof(NodeEditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true);
|
||||
_isDocked = (Func<bool>) Delegate.CreateDelegate(typeof(Func<bool>), this, isDockedMethod);
|
||||
}
|
||||
return _isDocked;
|
||||
}
|
||||
/// <summary> Return a delegate used to determine whether window is docked or not. It is faster to cache this delegate than run the reflection required each time. </summary>
|
||||
public static Func<bool> GetIsDockedDelegate(this EditorWindow window) {
|
||||
BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
||||
MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true);
|
||||
return (Func<bool>) Delegate.CreateDelegate(typeof(Func<bool>), window, isDockedMethod);
|
||||
}
|
||||
private Func<bool> _isDocked;
|
||||
|
||||
public static Type[] GetNodeTypes() {
|
||||
//Get all classes deriving from Node via reflection
|
||||
return GetDerivedTypes(typeof(XNode.Node));
|
||||
}
|
||||
|
||||
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.NodeTintAttribute), true);
|
||||
if (attribs == null || attribs.Length == 0) continue;
|
||||
XNode.Node.NodeTintAttribute attrib = attribs[0] as XNode.Node.NodeTintAttribute;
|
||||
tints.Add(nodeTypes[i], attrib.color);
|
||||
/// <summary> Custom node tint colors defined with [NodeColor(r, g, b)] </summary>
|
||||
public static bool TryGetAttributeTint(this Type nodeType, out Color tint) {
|
||||
if (nodeTint == null) {
|
||||
CacheAttributes<Color, XNode.Node.NodeTintAttribute>(ref nodeTint, x => x.color);
|
||||
}
|
||||
return tints;
|
||||
return nodeTint.TryGetValue(nodeType, out tint);
|
||||
}
|
||||
|
||||
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.NodeWidthAttribute), true);
|
||||
if (attribs == null || attribs.Length == 0) continue;
|
||||
XNode.Node.NodeWidthAttribute attrib = attribs[0] as XNode.Node.NodeWidthAttribute;
|
||||
widths.Add(nodeTypes[i], attrib.width);
|
||||
/// <summary> Get custom node widths defined with [NodeWidth(width)] </summary>
|
||||
public static bool TryGetAttributeWidth(this Type nodeType, out int width) {
|
||||
if (nodeWidth == null) {
|
||||
CacheAttributes<int, XNode.Node.NodeWidthAttribute>(ref nodeWidth, x => x.width);
|
||||
}
|
||||
return nodeWidth.TryGetValue(nodeType, out width);
|
||||
}
|
||||
|
||||
private static void CacheAttributes<V, A>(ref Dictionary<Type, V> dict, Func<A, V> getter) where A : Attribute {
|
||||
dict = new Dictionary<Type, V>();
|
||||
for (int i = 0; i < nodeTypes.Length; i++) {
|
||||
object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true);
|
||||
if (attribs == null || attribs.Length == 0) continue;
|
||||
A attrib = attribs[0] as A;
|
||||
dict.Add(nodeTypes[i], getter(attrib));
|
||||
}
|
||||
return widths;
|
||||
}
|
||||
|
||||
/// <summary> Get FieldInfo of a field, including those that are private and/or inherited </summary>
|
||||
public static FieldInfo GetFieldInfo(Type type, string fieldName) {
|
||||
public static FieldInfo GetFieldInfo(this Type type, string fieldName) {
|
||||
// If we can't find field in the first run, it's probably a private field in a base class.
|
||||
FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
// Search base classes for private fields only. Public fields are found above
|
||||
@ -71,25 +64,45 @@ namespace XNodeEditor {
|
||||
}
|
||||
|
||||
/// <summary> Get all classes deriving from baseType via reflection </summary>
|
||||
public static Type[] GetDerivedTypes(Type baseType) {
|
||||
public static Type[] GetDerivedTypes(this Type baseType) {
|
||||
List<System.Type> types = new List<System.Type>();
|
||||
System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
|
||||
foreach (Assembly assembly in assemblies) {
|
||||
try {
|
||||
types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
|
||||
} catch(ReflectionTypeLoadException) {}
|
||||
} catch (ReflectionTypeLoadException) { }
|
||||
}
|
||||
return types.ToArray();
|
||||
}
|
||||
|
||||
public static void AddCustomContextMenuItems(GenericMenu contextMenu, object obj) {
|
||||
KeyValuePair<ContextMenu, System.Reflection.MethodInfo>[] items = GetContextMenuMethods(obj);
|
||||
/// <summary> Find methods marked with the [ContextMenu] attribute and add them to the context menu </summary>
|
||||
public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object 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(this UnityEngine.Object target) {
|
||||
System.Reflection.MethodInfo onValidate = null;
|
||||
if (target != null) {
|
||||
onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (onValidate != null) onValidate.Invoke(target, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace XNodeEditor {
|
||||
/// <summary> A set of editor-only utilities and extensions for UnityNodeEditorBase </summary>
|
||||
/// <summary> A set of editor-only utilities and extensions for xNode </summary>
|
||||
public static class NodeEditorUtilities {
|
||||
|
||||
/// <summary>C#'s Script Icon [The one MonoBhevaiour Scripts have].</summary>
|
||||
@ -25,7 +25,7 @@ namespace XNodeEditor {
|
||||
|
||||
public static bool GetAttrib<T>(object[] attribs, out T attribOut) where T : Attribute {
|
||||
for (int i = 0; i < attribs.Length; i++) {
|
||||
if (attribs[i] is T){
|
||||
if (attribs[i] is T) {
|
||||
attribOut = attribs[i] as T;
|
||||
return true;
|
||||
}
|
||||
@ -36,7 +36,7 @@ namespace XNodeEditor {
|
||||
|
||||
public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute {
|
||||
// If we can't find field in the first run, it's probably a private field in a base class.
|
||||
FieldInfo field = NodeEditorWindow.GetFieldInfo(classType, fieldName);
|
||||
FieldInfo field = classType.GetFieldInfo(fieldName);
|
||||
// This shouldn't happen. Ever.
|
||||
if (field == null) {
|
||||
Debug.LogWarning("Field " + fieldName + " couldnt be found");
|
||||
@ -84,6 +84,14 @@ namespace XNodeEditor {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsMac() {
|
||||
#if UNITY_2017_1_OR_NEWER
|
||||
return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
|
||||
#else
|
||||
return SystemInfo.operatingSystem.StartsWith("Mac");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary> Returns true if this can be casted to <see cref="Type"/></summary>
|
||||
public static bool IsCastableTo(this Type from, Type to) {
|
||||
if (to.IsAssignableFrom(from)) return true;
|
||||
@ -133,6 +141,24 @@ 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> Returns the default creation path for the node type. </summary>
|
||||
public static string NodeDefaultPath(Type type) {
|
||||
string typePath = type.ToString().Replace('.', '/');
|
||||
// Automatically remove redundant 'Node' postfix
|
||||
if (typePath.EndsWith("Node")) typePath = typePath.Substring(0, typePath.LastIndexOf("Node"));
|
||||
typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath);
|
||||
return typePath;
|
||||
}
|
||||
|
||||
/// <summary>Creates a new C# Class.</summary>
|
||||
[MenuItem("Assets/Create/xNode/Node C# Script", false, 89)]
|
||||
private static void CreateNode() {
|
||||
|
||||
@ -2,6 +2,8 @@ using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Callbacks;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace XNodeEditor {
|
||||
[InitializeOnLoad]
|
||||
@ -14,6 +16,14 @@ namespace XNodeEditor {
|
||||
[SerializeField] private NodePortReference[] _references = new NodePortReference[0];
|
||||
[SerializeField] private Rect[] _rects = new Rect[0];
|
||||
|
||||
private Func<bool> isDocked {
|
||||
get {
|
||||
if (_isDocked == null) _isDocked = this.GetIsDockedDelegate();
|
||||
return _isDocked;
|
||||
}
|
||||
}
|
||||
private Func<bool> _isDocked;
|
||||
|
||||
[System.Serializable] private class NodePortReference {
|
||||
[SerializeField] private XNode.Node _node;
|
||||
[SerializeField] private string _name;
|
||||
|
||||
@ -38,49 +38,72 @@ namespace XNodeEditor {
|
||||
if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path
|
||||
return attrib.menuName;
|
||||
else // Return generated path
|
||||
return ObjectNames.NicifyVariableName(type.ToString().Replace('.', '/'));
|
||||
return NodeEditorUtilities.NodeDefaultPath(type);
|
||||
}
|
||||
|
||||
/// <summary> Add items for the context menu when right-clicking this node. Override to add custom menu items. </summary>
|
||||
public virtual void AddContextMenuItems(GenericMenu menu) {
|
||||
Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition);
|
||||
for (int i = 0; i < NodeEditorWindow.nodeTypes.Length; i++) {
|
||||
Type type = NodeEditorWindow.nodeTypes[i];
|
||||
for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++) {
|
||||
Type type = NodeEditorReflection.nodeTypes[i];
|
||||
|
||||
//Get node context menu path
|
||||
string path = GetNodeMenuName(type);
|
||||
if (string.IsNullOrEmpty(path)) continue;
|
||||
|
||||
menu.AddItem(new GUIContent(path), false, () => {
|
||||
CreateNode(type, pos);
|
||||
XNode.Node node = CreateNode(type, pos);
|
||||
NodeEditorWindow.current.AutoConnect(node);
|
||||
});
|
||||
}
|
||||
menu.AddSeparator("");
|
||||
menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorWindow.OpenPreferences());
|
||||
NodeEditorWindow.AddCustomContextMenuItems(menu, target);
|
||||
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, () => NodeEditorReflection.OpenPreferences());
|
||||
menu.AddCustomContextMenuItems(target);
|
||||
}
|
||||
|
||||
/// <summary> Returned color is used to color noodles </summary>
|
||||
public virtual Color GetNoodleColor(XNode.NodePort output, XNode.NodePort input) {
|
||||
return GetTypeColor(output.ValueType);
|
||||
}
|
||||
|
||||
/// <summary> Returned color is used to color ports </summary>
|
||||
public virtual Color GetPortColor(XNode.NodePort port) {
|
||||
return GetTypeColor(port.ValueType);
|
||||
}
|
||||
|
||||
/// <summary> Returns generated color for a type. This color is editable in preferences </summary>
|
||||
public virtual Color GetTypeColor(Type type) {
|
||||
return NodeEditorPreferences.GetTypeColor(type);
|
||||
}
|
||||
|
||||
/// <summary> Override to display custom tooltips </summary>
|
||||
public virtual string GetPortTooltip(XNode.NodePort port) {
|
||||
Type portType = port.ValueType;
|
||||
string tooltip = "";
|
||||
tooltip = portType.PrettyName();
|
||||
if (port.IsOutput) {
|
||||
object obj = port.node.GetValue(port);
|
||||
tooltip += " = " + (obj != null ? obj.ToString() : "null");
|
||||
}
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/// <summary> Deal with objects dropped into the graph through DragAndDrop </summary>
|
||||
public virtual void OnDropObjects(UnityEngine.Object[] objects) {
|
||||
Debug.Log("No OnDropItems override defined for " + GetType());
|
||||
}
|
||||
|
||||
/// <summary> Create a node and save it in the graph asset </summary>
|
||||
public virtual void CreateNode(Type type, Vector2 position) {
|
||||
public virtual XNode.Node CreateNode(Type type, Vector2 position) {
|
||||
XNode.Node node = target.AddNode(type);
|
||||
node.position = position;
|
||||
if (string.IsNullOrEmpty(node.name)) {
|
||||
// Automatically remove redundant 'Node' postfix
|
||||
string typeName = type.Name;
|
||||
if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node"));
|
||||
node.name = UnityEditor.ObjectNames.NicifyVariableName(typeName);
|
||||
}
|
||||
if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
|
||||
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>
|
||||
@ -93,15 +116,15 @@ 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();
|
||||
}
|
||||
|
||||
[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>
|
||||
|
||||
@ -2,65 +2,67 @@
|
||||
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;
|
||||
/// <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;
|
||||
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;
|
||||
}
|
||||
/// <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 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 OnLostFocus() {
|
||||
// Make the popup close on lose focus
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
if (firstFrame) {
|
||||
UpdatePositionToMouse();
|
||||
firstFrame = false;
|
||||
}
|
||||
input = EditorGUILayout.TextField(input);
|
||||
Event e = Event.current;
|
||||
// If input is empty, revert name to default instead
|
||||
if (input == null || input.Trim() == "") {
|
||||
if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) {
|
||||
target.name = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name);
|
||||
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
|
||||
Close();
|
||||
}
|
||||
}
|
||||
// Rename asset to input text
|
||||
else {
|
||||
if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) {
|
||||
target.name = input;
|
||||
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
target.TriggerOnValidate();
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
target.TriggerOnValidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Binary file not shown.
17
Scripts/Editor/XNodeEditor.asmdef
Normal file
17
Scripts/Editor/XNodeEditor.asmdef
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "XNodeEditor",
|
||||
"references": [
|
||||
"XNode"
|
||||
],
|
||||
"optionalUnityReferences": [],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": []
|
||||
}
|
||||
7
Scripts/Editor/XNodeEditor.asmdef.meta
Normal file
7
Scripts/Editor/XNodeEditor.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 002c1bbed08fa44d282ef34fd5edb138
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -290,16 +290,26 @@ namespace XNode {
|
||||
[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 output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
|
||||
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
|
||||
/// <param name="connectionType">Should we allow multiple connections? </param>
|
||||
/// <param name="typeConstraint">Constrains which input connections can be made from this port </param>
|
||||
/// <param name="dynamicPortList">If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays </param>
|
||||
public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) {
|
||||
this.backingValue = backingValue;
|
||||
this.connectionType = connectionType;
|
||||
this.dynamicPortList = dynamicPortList;
|
||||
this.typeConstraint = typeConstraint;
|
||||
}
|
||||
|
||||
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
|
||||
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
|
||||
/// <param name="connectionType">Should we allow multiple connections? </param>
|
||||
/// <param name="dynamicPortList">If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays </param>
|
||||
public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool dynamicPortList = false) {
|
||||
this.backingValue = backingValue;
|
||||
this.connectionType = connectionType;
|
||||
this.dynamicPortList = dynamicPortList;
|
||||
}
|
||||
[Obsolete("Use constructor with TypeConstraint")]
|
||||
public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
|
||||
@ -14,6 +14,7 @@ namespace XNode {
|
||||
if (!Initialized) BuildCache();
|
||||
|
||||
Dictionary<string, NodePort> staticPorts = new Dictionary<string, NodePort>();
|
||||
Dictionary<string, List<NodePort>> removedPorts = new Dictionary<string, List<NodePort>>();
|
||||
System.Type nodeType = node.GetType();
|
||||
|
||||
List<NodePort> typePortCache;
|
||||
@ -30,39 +31,63 @@ 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 || port.typeConstraint != staticPort.typeConstraint) ports.Remove(port.fieldName);
|
||||
else port.ValueType = staticPort.ValueType;
|
||||
if (port.IsDynamic || port.direction != staticPort.direction || port.connectionType != staticPort.connectionType || port.typeConstraint != staticPort.typeConstraint) {
|
||||
// If port is not dynamic and direction hasn't changed, add it to the list so we can try reconnecting the ports connections.
|
||||
if (!port.IsDynamic && port.direction == staticPort.direction) removedPorts.Add(port.fieldName, port.GetConnections());
|
||||
port.ClearConnections();
|
||||
ports.Remove(port.fieldName);
|
||||
} else port.ValueType = staticPort.ValueType;
|
||||
}
|
||||
// If port doesn't exist anymore, remove it
|
||||
else if (port.IsStatic) ports.Remove(port.fieldName);
|
||||
else if (port.IsStatic) {
|
||||
port.ClearConnections();
|
||||
ports.Remove(port.fieldName);
|
||||
}
|
||||
}
|
||||
// Add missing ports
|
||||
foreach (NodePort staticPort in staticPorts.Values) {
|
||||
if (!ports.ContainsKey(staticPort.fieldName)) {
|
||||
ports.Add(staticPort.fieldName, new NodePort(staticPort, node));
|
||||
NodePort port = new NodePort(staticPort, node);
|
||||
//If we just removed the port, try re-adding the connections
|
||||
List<NodePort> reconnectConnections;
|
||||
if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) {
|
||||
for (int i = 0; i < reconnectConnections.Count; i++) {
|
||||
NodePort connection = reconnectConnections[i];
|
||||
if (connection == null) continue;
|
||||
if (port.CanConnectTo(connection)) port.Connect(connection);
|
||||
}
|
||||
}
|
||||
ports.Add(staticPort.fieldName, port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Cache node types </summary>
|
||||
private static void BuildCache() {
|
||||
portDataCache = new PortDataCache();
|
||||
System.Type baseType = typeof(Node);
|
||||
List<System.Type> nodeTypes = new List<System.Type>();
|
||||
System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
|
||||
Assembly selfAssembly = Assembly.GetAssembly(baseType);
|
||||
if (selfAssembly.FullName.StartsWith("Assembly-CSharp") && !selfAssembly.FullName.Contains("-firstpass")) {
|
||||
// If xNode is not used as a DLL, check only CSharp (fast)
|
||||
nodeTypes.AddRange(selfAssembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)));
|
||||
} else {
|
||||
// Else, check all relevant DDLs (slower)
|
||||
// ignore all unity related assemblies
|
||||
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;
|
||||
nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
|
||||
|
||||
// Loop through assemblies and add node types to list
|
||||
foreach (Assembly assembly in assemblies) {
|
||||
// Skip certain dlls to improve performance
|
||||
string assemblyName = assembly.GetName().Name;
|
||||
int index = assemblyName.IndexOf('.');
|
||||
if (index != -1) assemblyName = assemblyName.Substring(0, index);
|
||||
switch (assemblyName) {
|
||||
// The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped
|
||||
case "UnityEditor":
|
||||
case "UnityEngine":
|
||||
case "System":
|
||||
case "mscorlib":
|
||||
continue;
|
||||
default:
|
||||
nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < nodeTypes.Count; i++) {
|
||||
CachePorts(nodeTypes[i]);
|
||||
}
|
||||
|
||||
@ -67,6 +67,7 @@ namespace XNode {
|
||||
} else if (attribs[i] is Node.OutputAttribute) {
|
||||
_direction = IO.Output;
|
||||
_connectionType = (attribs[i] as Node.OutputAttribute).connectionType;
|
||||
_typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -255,9 +256,12 @@ namespace XNode {
|
||||
else output = port;
|
||||
// If there isn't one of each, they can't connect
|
||||
if (input == null || output == null) return false;
|
||||
// Check type constraints
|
||||
// Check input type constraints
|
||||
if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false;
|
||||
if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false;
|
||||
// Check output type constraints
|
||||
if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !output.ValueType.IsAssignableFrom(input.ValueType)) return false;
|
||||
if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && output.ValueType != input.ValueType) return false;
|
||||
// Success
|
||||
return true;
|
||||
}
|
||||
|
||||
13
Scripts/XNode.asmdef
Normal file
13
Scripts/XNode.asmdef
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "XNode",
|
||||
"references": [],
|
||||
"optionalUnityReferences": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": []
|
||||
}
|
||||
7
Scripts/XNode.asmdef.meta
Normal file
7
Scripts/XNode.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8e24fd1eb19b4226afebb2810e3c19b
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
package.json
Normal file
7
package.json
Normal 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
7
package.json.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9869d68f06b74538a01e9b8e406159e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
x
Reference in New Issue
Block a user