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

refactor project

convert init on load method to wait until editor is done updating before attempting to reconnect loose nodes
This commit is contained in:
Stephen Hodgson 2023-07-27 13:04:45 -04:00
parent 82f7887931
commit bad4cc5c17
No known key found for this signature in database
GPG Key ID: 9A5CBE13747461CA
129 changed files with 6996 additions and 3581 deletions

View File

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

38
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: build
on:
pull_request:
branches:
- '*'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: windows
build-target: Android
steps:
- uses: actions/checkout@v3
with:
clean: false
lfs: true
# Installs the Unity Editor based on your project version text file
# sets -> env.UNITY_EDITOR_PATH
# sets -> env.UNITY_PROJECT_PATH
# https://github.com/XRTK/unity-setup
- uses: xrtk/unity-setup@v7.2
with:
build-targets: ${{ matrix.build-target }}
- name: Unity Build (${{ matrix.build-target }})
uses: RageAgainstThePixel/unity-build@v5
with:
build-target: ${{ matrix.build-target }}
publish-artifacts: false

35
.github/workflows/upm-subtree-split.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: upm-subtree-split
on:
push:
branches:
- main
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
upm-subtree-split:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.CI_TOKEN }}
fetch-depth: 0
- name: upm subtree split
run: |
# upm subtree split
git config user.name github-actions
git config user.email github-actions@github.com
git fetch --all --tags
$packageDir = Get-Item -Path "**/Packages/com.*" | Select-Object -ExpandProperty FullName
$packageDir = $packageDir.replace('${{ github.workspace }}/','')
Write-Host $packageDir
git subtree split --prefix="$packageDir" -b upm
git checkout upm
git fetch origin upm
git rebase origin/upm --reapply-cherry-picks
git push origin upm --force-with-lease --tags --set-upstream --verbose
working-directory: ${{ github.workspace }}
shell: pwsh

102
.gitignore vendored
View File

@ -1,30 +1,84 @@
/[Ll]ibrary/ # ============ #
/[Tt]emp/ # System Files #
/[Oo]bj/ # ============ #
/[Bb]uild/ .DS_Store
._*
# Autogenerated VS/MD solution and project files # =============== #
*.csproj # Unity generated #
*.unityproj # =============== #
*.sln [Aa]pp/
*.suo [Aa]pp.meta
*.tmp [Bb]in/
*.user [Bb]uilds/
[Bb]uild/
[Ll]ibrary/
[Ll]ogs/
[Oo]bj/
[Tt]emp/
UserSettings/
UWP/
WindowsStoreApp/
UnityGenerated/
UnityPackageManager/
.out/
.gradle/
project.json
project.lock.json
*.package
TextMesh Pro.meta
TextMesh Pro/
UIElementsSchema/
*packages-lock.json
# ============ #
# Certificates #
# ============ #
*.cert
*.privkey
*.pfx
*.pfx.meta
# ===================================== #
# Visual Studio / MonoDevelop generated #
# ===================================== #
.vs/
ExportedObj/
obj/
*.svd
*.userprefs *.userprefs
/*.csproj
*.csproj
*.pidb *.pidb
*.booproj *.suo
/*.sln
*.sln
*.user
*.unityproj
*.ipch
*.opensdf
*.sdf
*.tlog
*.log
*.idb
*.opendb
*.vsconfig
# Unity3D generated meta files # ============================ #
*.pidb.meta # Visual Studio Code Generated #
# ============================ #
.vscode/
# Unity3D Generated File On Crash Reports # ========================= #
sysinfo.txt # Jetbrains Rider Generated #
# ========================= #
.idea/
_ReSharper.Caches
/Examples/ # ===================== #
# Project Specific List #
.git.meta # ===================== #
.gitignore.meta --Version/
.gitattributes.meta artifacts/
StreamingAssets/
# OS X only: StreamingAssets.meta
.DS_Store

View File

@ -1,40 +0,0 @@
## Contributing to xNode
💙Thank you for taking the time to contribute💙
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.
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
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
* Explicit usage of brackets when doing multiple math operations on the same line
## Formatting
I use VSCode with the C# FixFormat extension and the following setting overrides:
```json
"csharpfixformat.style.spaces.beforeParenthesis": false,
"csharpfixformat.style.indent.regionIgnored": true
```
* Open braces on same line as condition
* 4 spaces for indentation.

View File

@ -1,66 +0,0 @@
using UnityEditor;
using UnityEngine;
using System.IO;
namespace XNodeEditor {
/// <summary> Deals with modified assets </summary>
class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor {
/// <summary> Automatically delete Node sub-assets before deleting their script.
/// 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) {
// Skip processing anything without the .cs extension
if (Path.GetExtension(path) != ".cs") return AssetDeleteResult.DidNotDelete;
// Get the object that is requested for deletion
UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object> (path);
// If we aren't deleting a script, return
if (!(obj is UnityEditor.MonoScript)) return AssetDeleteResult.DidNotDelete;
// Check script type. Return if deleting a non-node script
UnityEditor.MonoScript script = obj as UnityEditor.MonoScript;
System.Type scriptType = script.GetClass ();
if (scriptType == null || (scriptType != typeof (XNode.Node) && !scriptType.IsSubclassOf (typeof (XNode.Node)))) return AssetDeleteResult.DidNotDelete;
// Find all ScriptableObjects using this script
string[] guids = AssetDatabase.FindAssets ("t:" + scriptType);
for (int i = 0; i < guids.Length; i++) {
string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]);
Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath);
for (int k = 0; k < objs.Length; k++) {
XNode.Node node = objs[k] as XNode.Node;
if (node.GetType () == scriptType) {
if (node != null && node.graph != null) {
// Delete the node and notify the user
Debug.LogWarning (node.name + " of " + node.graph + " depended on deleted script and has been removed automatically.", node.graph);
node.graph.RemoveNode (node);
}
}
}
}
// We didn't actually delete the script. Tell the internal system to carry on with normal deletion procedure
return AssetDeleteResult.DidNotDelete;
}
/// <summary> Automatically re-add loose node assets to the Graph node list </summary>
[InitializeOnLoadMethod]
private static void OnReloadEditor () {
// Find all NodeGraph assets
string[] guids = AssetDatabase.FindAssets ("t:" + typeof (XNode.NodeGraph));
for (int i = 0; i < guids.Length; i++) {
string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]);
XNode.NodeGraph graph = AssetDatabase.LoadAssetAtPath (assetpath, typeof (XNode.NodeGraph)) as XNode.NodeGraph;
graph.nodes.RemoveAll(x => x == null); //Remove null items
Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath);
// Ensure that all sub node assets are present in the graph node list
for (int u = 0; u < objs.Length; u++) {
// Ignore null sub assets
if (objs[u] == null) continue;
if (!graph.nodes.Contains (objs[u] as XNode.Node)) graph.nodes.Add(objs[u] as XNode.Node);
}
}
}
}
}

View File

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

View File

@ -1,7 +0,0 @@
{
"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.8.0",
"unity": "2018.1",
"displayName": "xNode"
}

View File

@ -1,9 +1,8 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 657b15cb3ec32a24ca80faebf094d0f4 guid: 999581c3922a8f942959bcb0a0fc7d18
folderAsset: yes folderAsset: yes
timeCreated: 1505418321
licenseType: Free
DefaultImporter: DefaultImporter:
externalObjects: {}
userData: userData:
assetBundleName: assetBundleName:
assetBundleVariant: assetBundleVariant:

View File

@ -0,0 +1,267 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 705507994}
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 500
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 500
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 2
m_PVRDenoiserTypeDirect: 0
m_PVRDenoiserTypeIndirect: 0
m_PVRDenoiserTypeAO: 0
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 0
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 2
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
accuratePlacement: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &705507993
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 705507995}
- component: {fileID: 705507994}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &705507994
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 705507993}
m_Enabled: 1
serializedVersion: 8
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_CookieSize: 10
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_Lightmapping: 1
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_ShadowRadius: 0
m_ShadowAngle: 0
--- !u!4 &705507995
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 705507993}
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!1 &963194225
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 963194228}
- component: {fileID: 963194227}
- component: {fileID: 963194226}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &963194226
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 1
--- !u!20 &963194227
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_GateFitMode: 2
m_FocalLength: 50
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &963194228
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

View File

@ -1,6 +1,6 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: bc1db8b29c76d44648c9c86c2dfade6d guid: 9fc0d4010bbf28b4594072e72b8655ab
TextScriptImporter: DefaultImporter:
externalObjects: {} externalObjects: {}
userData: userData:
assetBundleName: assetBundleName:

View File

@ -0,0 +1,75 @@
# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_open_brace = all
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:error
# Code-block preferences
csharp_prefer_braces = true:error
# Use language keywords for types
dotnet_style_predefined_type_for_member_access = true
dotnet_style_predefined_type_for_locals_parameters_members = true
# Code Style
csharp_style_var_when_type_is_apparent = true
#### Resharper/Rider Rules ####
# https://www.jetbrains.com/help/resharper/EditorConfig_Properties.html
resharper_csharp_force_attribute_style=separate
csharp_place_field_attribute_on_same_line=false
csharp_place_accessorholder_attribute_on_same_line=false
csharp_trailing_comma_in_multiline_lists=false
csharp_trailing_comma_in_singleline_lists=false
csharp_keep_existing_attribute_arrangement=false
csharp_blank_lines_around_region=1
csharp_blank_lines_inside_region=1
csharp_keep_blank_lines_in_code=false
csharp_remove_blank_lines_near_braces_in_code=true
csharp_blank_lines_before_control_transfer_statements=1
csharp_blank_lines_after_control_transfer_statements=1
csharp_blank_lines_before_block_statements=1
csharp_blank_lines_after_block_statements=1
csharp_blank_lines_before_multiline_statements=1
csharp_blank_lines_after_multiline_statements=1
csharp_blank_lines_around_block_case_section=0
csharp_blank_lines_around_multiline_case_section=0
csharp_blank_lines_before_case=0
csharp_blank_lines_after_case=0
resharper_unity_duplicate_event_function_highlighting=error
resharper_unity_duplicate_shortcut_highlighting=error
resharper_unity_expected_component_highlighting=error
resharper_unity_explicit_tag_comparison_highlighting=error
resharper_unity_incorrect_mono_behaviour_instantiation_highlighting=error
resharper_unity_incorrect_scriptable_object_instantiation_highlighting=error
resharper_unity_no_null_coalescing_highlighting=error
resharper_unity_performance_critical_code_camera_main_highlighting=error
resharper_unity_performance_critical_code_invocation_highlighting=warning
resharper_unity_performance_critical_code_null_comparison_highlighting=warning
resharper_unity_possible_misapplication_of_attribute_to_multiple_fields_highlighting=error
resharper_unity_redundant_attribute_on_target_highlighting=error
resharper_unity_redundant_formerly_serialized_as_attribute_highlighting=error
resharper_unity_unresolved_component_or_scriptable_object_highlighting=error
resharper_use_name_of_instead_of_type_of_highlighting=error
resharper_wrong_public_modifier_specification_highlighting=error

View File

@ -0,0 +1,31 @@
name: publish
on:
push:
branches:
- upm
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
token: ${{ secrets.CI_TOKEN }}
ref: upm
clean: true
lfs: true
- uses: xrtk/upm-release@development
with:
upm-username: 'pillow-build-bot'
upm-email: 'hello@pillow.social'
upm-server-address: 'http://upm.pillow.social:4873'
upm-auth-token: '${{ secrets.UPM_AUTH_TOKEN }}'
github-username: 'TogetherXR'
github-pat: '${{ secrets.CI_TOKEN }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
package-root: '${{ github.workspace }}'

View File

@ -0,0 +1,2 @@
.npmrc
.github/

View File

@ -0,0 +1 @@
strict-ssl=false

View File

@ -1,6 +1,7 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 243efae3a6b7941ad8f8e54dcf38ce8c guid: 809e2193de010ae4b9fa476efbe41cd4
TextScriptImporter: folderAsset: yes
DefaultImporter:
externalObjects: {} externalObjects: {}
userData: userData:
assetBundleName: assetBundleName:

View File

@ -1,12 +1,12 @@
using System; using System;
/// <summary> Overrides the ValueType of the Port, to have a ValueType different from the type of its serializable field </summary> /// <summary> Overrides the ValueType of the Port, to have a ValueType different from the type of its serializable field </summary>
/// <remarks> Especially useful in Dynamic Port Lists to create Value-Port Pairs with different type. </remarks> /// <remarks> Especially useful in Dynamic Port Lists to create Value-Port Pairs with different type. </remarks>
[AttributeUsage(AttributeTargets.Field)] [AttributeUsage(AttributeTargets.Field)]
public class PortTypeOverrideAttribute : Attribute { public class PortTypeOverrideAttribute : Attribute {
public Type type; public Type type;
/// <summary> Overrides the ValueType of the Port </summary> /// <summary> Overrides the ValueType of the Port </summary>
/// <param name="type">ValueType of the Port</param> /// <param name="type">ValueType of the Port</param>
public PortTypeOverrideAttribute(Type type) { public PortTypeOverrideAttribute(Type type) {
this.type = type; this.type = type;
} }
} }

View File

@ -51,9 +51,13 @@ namespace XNodeEditor
public void Run() public void Run()
{ {
if ( func2 != null ) if ( func2 != null )
{
func2( userData ); func2( userData );
}
else if ( func != null ) else if ( func != null )
{
func(); func();
}
} }
} }
@ -62,33 +66,41 @@ namespace XNodeEditor
private AdvancedGenericMenuItem FindOrCreateItem( string name, AdvancedGenericMenuItem currentRoot = null ) private AdvancedGenericMenuItem FindOrCreateItem( string name, AdvancedGenericMenuItem currentRoot = null )
{ {
if ( string.IsNullOrWhiteSpace( name ) ) if ( string.IsNullOrWhiteSpace( name ) )
{
return null; return null;
}
AdvancedGenericMenuItem item = null; AdvancedGenericMenuItem item = null;
string[] paths = name.Split( '/' ); var paths = name.Split( '/' );
if ( currentRoot == null ) if ( currentRoot == null )
{ {
item = items.FirstOrDefault( x => x != null && x.name == paths[0] ); item = items.FirstOrDefault( x => x != null && x.name == paths[0] );
if ( item == null ) if ( item == null )
{
items.Add( item = new AdvancedGenericMenuItem( paths[0] ) ); items.Add( item = new AdvancedGenericMenuItem( paths[0] ) );
}
} }
else else
{ {
item = currentRoot.children.OfType<AdvancedGenericMenuItem>().FirstOrDefault( x => x.name == paths[0] ); item = currentRoot.children.OfType<AdvancedGenericMenuItem>().FirstOrDefault( x => x.name == paths[0] );
if ( item == null ) if ( item == null )
{
currentRoot.AddChild( item = new AdvancedGenericMenuItem( paths[0] ) ); currentRoot.AddChild( item = new AdvancedGenericMenuItem( paths[0] ) );
}
} }
if ( paths.Length > 1 ) if ( paths.Length > 1 )
{
return FindOrCreateItem( string.Join( "/", paths, 1, paths.Length - 1 ), item ); return FindOrCreateItem( string.Join( "/", paths, 1, paths.Length - 1 ), item );
}
return item; return item;
} }
private AdvancedGenericMenuItem FindParent( string name ) private AdvancedGenericMenuItem FindParent( string name )
{ {
string[] paths = name.Split( '/' ); var paths = name.Split( '/' );
return FindOrCreateItem( string.Join( "/", paths, 0, paths.Length - 1 ) ); return FindOrCreateItem( string.Join( "/", paths, 0, paths.Length - 1 ) );
} }
@ -169,9 +181,13 @@ namespace XNodeEditor
{ {
var parent = string.IsNullOrWhiteSpace( path ) ? null : FindParent( path ); var parent = string.IsNullOrWhiteSpace( path ) ? null : FindParent( path );
if ( parent == null ) if ( parent == null )
{
items.Add( null ); items.Add( null );
}
else else
{
parent.AddSeparator(); parent.AddSeparator();
}
} }
// //
@ -195,9 +211,13 @@ namespace XNodeEditor
foreach ( var m in items ) foreach ( var m in items )
{ {
if ( m == null ) if ( m == null )
{
root.AddSeparator(); root.AddSeparator();
}
else else
{
root.AddChild( m ); root.AddChild( m );
}
} }
return root; return root;
@ -206,8 +226,10 @@ namespace XNodeEditor
protected override void ItemSelected( AdvancedDropdownItem item ) protected override void ItemSelected( AdvancedDropdownItem item )
{ {
if ( item is AdvancedGenericMenuItem gmItem ) if ( item is AdvancedGenericMenuItem gmItem )
{
gmItem.Run(); gmItem.Run();
}
} }
} }
} }
#endif #endif

View File

@ -27,8 +27,11 @@ namespace XNodeEditor {
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
// Get current enum name // Get current enum name
string enumName = ""; var enumName = "";
if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length) enumName = property.enumDisplayNames[property.enumValueIndex]; if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length)
{
enumName = property.enumDisplayNames[property.enumValueIndex];
}
#if UNITY_2017_1_OR_NEWER #if UNITY_2017_1_OR_NEWER
// Display dropdown // Display dropdown
@ -49,16 +52,16 @@ namespace XNodeEditor {
public static void ShowContextMenuAtMouse(SerializedProperty property) { public static void ShowContextMenuAtMouse(SerializedProperty property) {
// Initialize menu // Initialize menu
GenericMenu menu = new GenericMenu(); var menu = new GenericMenu();
// Add all enum display names to menu // Add all enum display names to menu
for (int i = 0; i < property.enumDisplayNames.Length; i++) { for (var i = 0; i < property.enumDisplayNames.Length; i++) {
int index = i; var index = i;
menu.AddItem(new GUIContent(property.enumDisplayNames[i]), false, () => SetEnum(property, index)); menu.AddItem(new GUIContent(property.enumDisplayNames[i]), false, () => SetEnum(property, index));
} }
// Display at cursor position // Display at cursor position
Rect r = new Rect(Event.current.mousePosition, new Vector2(0, 0)); var r = new Rect(Event.current.mousePosition, new Vector2(0, 0));
menu.DropDown(r); menu.DropDown(r);
} }
@ -68,4 +71,4 @@ namespace XNodeEditor {
property.serializedObject.Update(); property.serializedObject.Update();
} }
} }
} }

View File

@ -57,8 +57,8 @@ namespace XNodeEditor {
serializedObject.Update(); serializedObject.Update();
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
SerializedProperty graphProp = serializedObject.FindProperty("graph"); var graphProp = serializedObject.FindProperty("graph");
NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph); var w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
w.Home(); // Focus selected node w.Home(); // Focus selected node
} }
@ -72,4 +72,4 @@ namespace XNodeEditor {
} }
} }
#endif #endif
} }

View File

@ -16,8 +16,8 @@ namespace XNodeEditor {
string[] deletedAssets, string[] deletedAssets,
string[] movedAssets, string[] movedAssets,
string[] movedFromAssetPaths) { string[] movedFromAssetPaths) {
for (int i = 0; i < movedAssets.Length; i++) { for (var i = 0; i < movedAssets.Length; i++) {
Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node; var 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 // 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 // main asset, reset the node graph to be the main asset and rename the node asset back to its default
@ -32,4 +32,4 @@ namespace XNodeEditor {
} }
} }
} }
} }

View File

@ -1,187 +1,217 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
#if ODIN_INSPECTOR #if ODIN_INSPECTOR
using Sirenix.OdinInspector.Editor; using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities; using Sirenix.Utilities;
using Sirenix.Utilities.Editor; using Sirenix.Utilities.Editor;
#endif #endif
#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU
using GenericMenu = XNodeEditor.AdvancedGenericMenu; using GenericMenu = XNodeEditor.AdvancedGenericMenu;
#endif #endif
namespace XNodeEditor { namespace XNodeEditor {
/// <summary> Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. </summary> /// <summary> Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. </summary>
[CustomNodeEditor(typeof(XNode.Node))] [CustomNodeEditor(typeof(XNode.Node))]
public class NodeEditor : XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node> { public class NodeEditor : XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node> {
/// <summary> Fires every whenever a node was modified through the editor </summary> /// <summary> Fires every whenever a node was modified through the editor </summary>
public static Action<XNode.Node> onUpdateNode; public static Action<XNode.Node> onUpdateNode;
public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>(); public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>();
#if ODIN_INSPECTOR #if ODIN_INSPECTOR
protected internal static bool inNodeEditor = false; protected internal static bool inNodeEditor = false;
#endif #endif
public virtual void OnHeaderGUI() { public virtual void OnHeaderGUI() {
GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
} }
/// <summary> Draws standard field editors for all public fields </summary> /// <summary> Draws standard field editors for all public fields </summary>
public virtual void OnBodyGUI() { public virtual void OnBodyGUI() {
#if ODIN_INSPECTOR #if ODIN_INSPECTOR
inNodeEditor = true; inNodeEditor = true;
#endif #endif
// Unity specifically requires this to save/update any serial object. // Unity specifically requires this to save/update any serial object.
// serializedObject.Update(); must go at the start of an inspector gui, and // serializedObject.Update(); must go at the start of an inspector gui, and
// serializedObject.ApplyModifiedProperties(); goes at the end. // serializedObject.ApplyModifiedProperties(); goes at the end.
serializedObject.Update(); serializedObject.Update();
string[] excludes = { "m_Script", "graph", "position", "ports" }; string[] excludes = { "m_Script", "graph", "position", "ports" };
#if ODIN_INSPECTOR #if ODIN_INSPECTOR
try try
{ {
#if ODIN_INSPECTOR_3 #if ODIN_INSPECTOR_3
objectTree.BeginDraw( true ); objectTree.BeginDraw( true );
#else #else
InspectorUtilities.BeginDrawPropertyTree(objectTree, true); InspectorUtilities.BeginDrawPropertyTree(objectTree, true);
#endif #endif
} }
catch ( ArgumentNullException ) catch ( ArgumentNullException )
{ {
#if ODIN_INSPECTOR_3 #if ODIN_INSPECTOR_3
objectTree.EndDraw(); objectTree.EndDraw();
#else #else
InspectorUtilities.EndDrawPropertyTree(objectTree); InspectorUtilities.EndDrawPropertyTree(objectTree);
#endif #endif
NodeEditor.DestroyEditor(this.target); NodeEditor.DestroyEditor(this.target);
return; return;
} }
GUIHelper.PushLabelWidth( 84 ); GUIHelper.PushLabelWidth( 84 );
objectTree.Draw( true ); objectTree.Draw( true );
#if ODIN_INSPECTOR_3 #if ODIN_INSPECTOR_3
objectTree.EndDraw(); objectTree.EndDraw();
#else #else
InspectorUtilities.EndDrawPropertyTree(objectTree); InspectorUtilities.EndDrawPropertyTree(objectTree);
#endif #endif
GUIHelper.PopLabelWidth(); GUIHelper.PopLabelWidth();
#else #else
// Iterate through serialized properties and draw them like the Inspector (But with ports) // Iterate through serialized properties and draw them like the Inspector (But with ports)
SerializedProperty iterator = serializedObject.GetIterator(); var iterator = serializedObject.GetIterator();
bool enterChildren = true; var enterChildren = true;
while (iterator.NextVisible(enterChildren)) { while (iterator.NextVisible(enterChildren)) {
enterChildren = false; enterChildren = false;
if (excludes.Contains(iterator.name)) continue; if (excludes.Contains(iterator.name))
NodeEditorGUILayout.PropertyField(iterator, true); {
} continue;
#endif }
// Iterate through dynamic ports and draw them in the order in which they are serialized NodeEditorGUILayout.PropertyField(iterator, true);
foreach (XNode.NodePort dynamicPort in target.DynamicPorts) { }
if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue; #endif
NodeEditorGUILayout.PortField(dynamicPort);
} // Iterate through dynamic ports and draw them in the order in which they are serialized
foreach (var dynamicPort in target.DynamicPorts) {
serializedObject.ApplyModifiedProperties(); if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort))
{
#if ODIN_INSPECTOR continue;
// Call repaint so that the graph window elements respond properly to layout changes coming from Odin }
if (GUIHelper.RepaintRequested) {
GUIHelper.ClearRepaintRequest(); NodeEditorGUILayout.PortField(dynamicPort);
window.Repaint(); }
}
#endif serializedObject.ApplyModifiedProperties();
#if ODIN_INSPECTOR #if ODIN_INSPECTOR
inNodeEditor = false; // Call repaint so that the graph window elements respond properly to layout changes coming from Odin
#endif if (GUIHelper.RepaintRequested) {
} GUIHelper.ClearRepaintRequest();
window.Repaint();
public virtual int GetWidth() { }
Type type = target.GetType(); #endif
int width;
if (type.TryGetAttributeWidth(out width)) return width; #if ODIN_INSPECTOR
else return 208; inNodeEditor = false;
} #endif
}
/// <summary> Returns color for target node </summary>
public virtual Color GetTint() { public virtual int GetWidth() {
// Try get color from [NodeTint] attribute var type = target.GetType();
Type type = target.GetType(); int width;
Color color; if (type.TryGetAttributeWidth(out width))
if (type.TryGetAttributeTint(out color)) return color; {
// Return default color (grey) return width;
else return NodeEditorPreferences.GetSettings().tintColor; }
} else
{
public virtual GUIStyle GetBodyStyle() { return 208;
return NodeEditorResources.styles.nodeBody; }
} }
public virtual GUIStyle GetBodyHighlightStyle() { /// <summary> Returns color for target node </summary>
return NodeEditorResources.styles.nodeHighlight; public virtual Color GetTint() {
} // Try get color from [NodeTint] attribute
var type = target.GetType();
/// <summary> Override to display custom node header tooltips </summary> Color color;
public virtual string GetHeaderTooltip() { if (type.TryGetAttributeTint(out color))
return null; {
} return color;
}
/// <summary> Add items for the context menu when right-clicking this node. Override to add custom menu items. </summary> // Return default color (grey)
public virtual void AddContextMenuItems(GenericMenu menu) { else
bool canRemove = true; {
// Actions if only one node is selected return NodeEditorPreferences.GetSettings().tintColor;
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); public virtual GUIStyle GetBodyStyle() {
return NodeEditorResources.styles.nodeBody;
canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node); }
}
public virtual GUIStyle GetBodyHighlightStyle() {
// Add actions to any number of selected nodes return NodeEditorResources.styles.nodeHighlight;
menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes); }
menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes);
/// <summary> Override to display custom node header tooltips </summary>
if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); public virtual string GetHeaderTooltip() {
else menu.AddItem(new GUIContent("Remove"), false, null); return null;
}
// Custom sctions if only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { /// <summary> Add items for the context menu when right-clicking this node. Override to add custom menu items. </summary>
XNode.Node node = Selection.activeObject as XNode.Node; public virtual void AddContextMenuItems(GenericMenu menu) {
menu.AddCustomContextMenuItems(node); var canRemove = true;
} // Actions if only one node is selected
} if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
var node = Selection.activeObject as XNode.Node;
/// <summary> Rename the node asset. This will trigger a reimport of the node. </summary> menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node));
public void Rename(string newName) { menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode);
if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType());
target.name = newName; canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node);
OnRename(); }
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
} // Add actions to any number of selected nodes
menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes);
/// <summary> Called after this node's name has changed. </summary> menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes);
public virtual void OnRename() { }
if (canRemove)
[AttributeUsage(AttributeTargets.Class)] {
public class CustomNodeEditorAttribute : Attribute, menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
XNodeEditor.Internal.NodeEditorBase<NodeEditor, NodeEditor.CustomNodeEditorAttribute, XNode.Node>.INodeEditorAttrib { }
private Type inspectedType; else
/// <summary> Tells a NodeEditor which Node type it is an editor for </summary> {
/// <param name="inspectedType">Type that this editor can edit</param> menu.AddItem(new GUIContent("Remove"), false, null);
public CustomNodeEditorAttribute(Type inspectedType) { }
this.inspectedType = inspectedType;
} // Custom sctions if only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
public Type GetInspectedType() { var node = Selection.activeObject as XNode.Node;
return inspectedType; 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 = 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 {
private Type inspectedType;
/// <summary> Tells a NodeEditor which Node type it is an editor for </summary>
/// <param name="inspectedType">Type that this editor can edit</param>
public CustomNodeEditorAttribute(Type inspectedType) {
this.inspectedType = inspectedType;
}
public Type GetInspectedType() {
return inspectedType;
}
}
}
}

View File

@ -0,0 +1,103 @@
using UnityEditor;
using UnityEngine;
using System.IO;
namespace XNodeEditor
{
/// <summary> Deals with modified assets </summary>
internal class NodeEditorAssetModProcessor : AssetModificationProcessor
{
/// <summary> Automatically delete Node sub-assets before deleting their script.
/// 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)
{
// Skip processing anything without the .cs extension
if (Path.GetExtension(path) != ".cs")
{
return AssetDeleteResult.DidNotDelete;
}
// Get the object that is requested for deletion
var obj = AssetDatabase.LoadAssetAtPath<Object>(path);
// If we aren't deleting a script, return
if (obj is MonoScript script)
{
var scriptType = script.GetClass();
if (scriptType == null ||
(scriptType != typeof(XNode.Node) && !scriptType.IsSubclassOf(typeof(XNode.Node))))
{
return AssetDeleteResult.DidNotDelete;
}
// Find all ScriptableObjects using this script
var guids = AssetDatabase.FindAssets($"t:{scriptType}");
for (var i = 0; i < guids.Length; i++)
{
var assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
var objects = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath);
for (var k = 0; k < objects.Length; k++)
{
if (objects[k] is XNode.Node node &&
node.GetType() == scriptType &&
node != null && node.graph != null)
{
// Delete the node and notify the user
Debug.LogWarning($"{node.name} of {node.graph} depended on deleted script and has been removed automatically.", node.graph);
node.graph.RemoveNode(node);
}
}
}
// We didn't actually delete the script. Tell the internal system to carry on with normal deletion procedure
return AssetDeleteResult.DidNotDelete;
}
// Check script type. Return if deleting a non-node script
return AssetDeleteResult.DidNotDelete;
}
[InitializeOnLoadMethod]
private static void Init() => OnReloadEditor();
/// <summary> Automatically re-add loose node assets to the Graph node list </summary>
private static void OnReloadEditor() => EditorApplication.delayCall += () =>
{
if (EditorApplication.isUpdating)
{
OnReloadEditor();
return;
}
// Find all NodeGraph assets
var guids = AssetDatabase.FindAssets($"t:{typeof(XNode.NodeGraph)}");
for (var i = 0; i < guids.Length; i++)
{
var assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
if (AssetDatabase.LoadAssetAtPath(assetPath, typeof(XNode.NodeGraph)) is XNode.NodeGraph graph)
{
graph.nodes.RemoveAll(x => x == null); //Remove null items
var objects = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath);
// Ensure that all sub node assets are present in the graph node list
for (var u = 0; u < objects.Length; u++)
{
// Ignore null sub assets
if (objects[u] == null) { continue; }
if (!graph.nodes.Contains(objects[u] as XNode.Node))
{
graph.nodes.Add(objects[u] as XNode.Node);
}
}
}
}
};
}
}

View File

@ -40,11 +40,15 @@ namespace XNodeEditor.Internal {
#endif #endif
public static T GetEditor(K target, NodeEditorWindow window) { public static T GetEditor(K target, NodeEditorWindow window) {
if (target == null) return null; if (target == null)
T editor; {
return null;
}
T editor;
if (!editors.TryGetValue(target, out editor)) { if (!editors.TryGetValue(target, out editor)) {
Type type = target.GetType(); var type = target.GetType();
Type editorType = GetEditorType(type); var editorType = GetEditorType(type);
editor = Activator.CreateInstance(editorType) as T; editor = Activator.CreateInstance(editorType) as T;
editor.target = target; editor.target = target;
editor.serializedObject = new SerializedObject(target); editor.serializedObject = new SerializedObject(target);
@ -52,15 +56,31 @@ namespace XNodeEditor.Internal {
editor.OnCreate(); editor.OnCreate();
editors.Add(target, editor); editors.Add(target, editor);
} }
if (editor.target == null) editor.target = target; if (editor.target == null)
if (editor.window != window) editor.window = window; {
if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target); editor.target = target;
return editor; }
if (editor.window != window)
{
editor.window = window;
}
if (editor.serializedObject == null)
{
editor.serializedObject = new SerializedObject(target);
}
return editor;
} }
public static void DestroyEditor( K target ) public static void DestroyEditor( K target )
{ {
if ( target == null ) return; if ( target == null )
{
return;
}
T editor; T editor;
if ( editors.TryGetValue( target, out editor ) ) if ( editors.TryGetValue( target, out editor ) )
{ {
@ -69,11 +89,23 @@ namespace XNodeEditor.Internal {
} }
private static Type GetEditorType(Type type) { private static Type GetEditorType(Type type) {
if (type == null) return null; if (type == null)
if (editorTypes == null) CacheCustomEditors(); {
Type result; return null;
if (editorTypes.TryGetValue(type, out result)) return result; }
//If type isn't found, try base type
if (editorTypes == null)
{
CacheCustomEditors();
}
Type result;
if (editorTypes.TryGetValue(type, out result))
{
return result;
}
//If type isn't found, try base type
return GetEditorType(type.BaseType); return GetEditorType(type.BaseType);
} }
@ -81,12 +113,20 @@ namespace XNodeEditor.Internal {
editorTypes = new Dictionary<Type, Type>(); editorTypes = new Dictionary<Type, Type>();
//Get all classes deriving from NodeEditor via reflection //Get all classes deriving from NodeEditor via reflection
Type[] nodeEditors = typeof(T).GetDerivedTypes(); var nodeEditors = typeof(T).GetDerivedTypes();
for (int i = 0; i < nodeEditors.Length; i++) { for (var i = 0; i < nodeEditors.Length; i++) {
if (nodeEditors[i].IsAbstract) continue; if (nodeEditors[i].IsAbstract)
var attribs = nodeEditors[i].GetCustomAttributes(typeof(A), false); {
if (attribs == null || attribs.Length == 0) continue; continue;
A attrib = attribs[0] as A; }
var attribs = nodeEditors[i].GetCustomAttributes(typeof(A), false);
if (attribs == null || attribs.Length == 0)
{
continue;
}
var attrib = attribs[0] as A;
editorTypes.Add(attrib.GetInspectedType(), nodeEditors[i]); editorTypes.Add(attrib.GetInspectedType(), nodeEditors[i]);
} }
} }
@ -98,4 +138,4 @@ namespace XNodeEditor.Internal {
Type GetInspectedType(); Type GetInspectedType();
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -21,9 +21,13 @@ namespace XNodeEditor {
/// <summary> Make a field for a serialized property. Automatically displays relevant node port. </summary> /// <summary> Make a field for a serialized property. Automatically displays relevant node port. </summary>
public static void PropertyField(SerializedProperty property, GUIContent label, bool includeChildren = true, params GUILayoutOption[] options) { public static void PropertyField(SerializedProperty property, GUIContent label, bool includeChildren = true, params GUILayoutOption[] options) {
if (property == null) throw new NullReferenceException(); if (property == null)
XNode.Node node = property.serializedObject.targetObject as XNode.Node; {
XNode.NodePort port = node.GetPort(property.name); throw new NullReferenceException();
}
var node = property.serializedObject.targetObject as XNode.Node;
var port = node.GetPort(property.name);
PropertyField(property, label, port, includeChildren); PropertyField(property, label, port, includeChildren);
} }
@ -34,61 +38,83 @@ namespace XNodeEditor {
/// <summary> Make a field for a serialized property. Manual node port override. </summary> /// <summary> Make a field for a serialized property. Manual node port override. </summary>
public static void PropertyField(SerializedProperty property, GUIContent label, XNode.NodePort port, bool includeChildren = true, params GUILayoutOption[] options) { public static void PropertyField(SerializedProperty property, GUIContent label, XNode.NodePort port, bool includeChildren = true, params GUILayoutOption[] options) {
if (property == null) throw new NullReferenceException(); if (property == null)
{
throw new NullReferenceException();
}
// If property is not a port, display a regular property field // If property is not a port, display a regular property field
if (port == null) EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); if (port == null)
{
EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
}
else { else {
Rect rect = new Rect(); var rect = new Rect();
List<PropertyAttribute> propertyAttributes = NodeEditorUtilities.GetCachedPropertyAttribs(port.node.GetType(), property.name); var propertyAttributes = NodeEditorUtilities.GetCachedPropertyAttribs(port.node.GetType(), property.name);
// If property is an input, display a regular property field and put a port handle on the left side // If property is an input, display a regular property field and put a port handle on the left side
if (port.direction == XNode.NodePort.IO.Input) { if (port.direction == XNode.NodePort.IO.Input) {
// Get data from [Input] attribute // Get data from [Input] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected; var showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.InputAttribute inputAttribute; XNode.Node.InputAttribute inputAttribute;
bool dynamicPortList = false; var dynamicPortList = false;
if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) { if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) {
dynamicPortList = inputAttribute.dynamicPortList; dynamicPortList = inputAttribute.dynamicPortList;
showBacking = inputAttribute.backingValue; showBacking = inputAttribute.backingValue;
} }
bool usePropertyAttributes = dynamicPortList || var usePropertyAttributes = dynamicPortList ||
showBacking == XNode.Node.ShowBackingValue.Never || showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected); (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
float spacePadding = 0; float spacePadding = 0;
string tooltip = null; string tooltip = null;
foreach (var attr in propertyAttributes) { foreach (var attr in propertyAttributes) {
if (attr is SpaceAttribute) { if (attr is SpaceAttribute) {
if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height); if (usePropertyAttributes)
else spacePadding += (attr as SpaceAttribute).height; {
GUILayout.Space((attr as SpaceAttribute).height);
}
else
{
spacePadding += (attr as SpaceAttribute).height;
}
} else if (attr is HeaderAttribute) { } else if (attr is HeaderAttribute) {
if (usePropertyAttributes) { if (usePropertyAttributes) {
//GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs
Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it. var position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it.
position.yMin += EditorGUIUtility.singleLineHeight * 0.5f; position.yMin += EditorGUIUtility.singleLineHeight * 0.5f;
position = EditorGUI.IndentedRect(position); position = EditorGUI.IndentedRect(position);
GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel); GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel);
} else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f; } else
{
spacePadding += EditorGUIUtility.singleLineHeight * 1.5f;
}
} else if (attr is TooltipAttribute) { } else if (attr is TooltipAttribute) {
tooltip = (attr as TooltipAttribute).tooltip; tooltip = (attr as TooltipAttribute).tooltip;
} }
} }
if (dynamicPortList) { if (dynamicPortList) {
Type type = GetType(property); var type = GetType(property);
XNode.Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple; var connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType); DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
return; return;
} }
switch (showBacking) { switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected: case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected // Display a label if port is connected
if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip)); if (port.IsConnected)
{
EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip));
}
// Display an editable property field if port is not connected // Display an editable property field if port is not connected
else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); else
{
EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
}
break; break;
case XNode.Node.ShowBackingValue.Never: case XNode.Node.ShowBackingValue.Never:
// Display a label // Display a label
@ -106,49 +132,65 @@ namespace XNodeEditor {
// If property is an output, display a text label and put a port handle on the right side // If property is an output, display a text label and put a port handle on the right side
} else if (port.direction == XNode.NodePort.IO.Output) { } else if (port.direction == XNode.NodePort.IO.Output) {
// Get data from [Output] attribute // Get data from [Output] attribute
XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected; var showBacking = XNode.Node.ShowBackingValue.Unconnected;
XNode.Node.OutputAttribute outputAttribute; XNode.Node.OutputAttribute outputAttribute;
bool dynamicPortList = false; var dynamicPortList = false;
if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) { if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) {
dynamicPortList = outputAttribute.dynamicPortList; dynamicPortList = outputAttribute.dynamicPortList;
showBacking = outputAttribute.backingValue; showBacking = outputAttribute.backingValue;
} }
bool usePropertyAttributes = dynamicPortList || var usePropertyAttributes = dynamicPortList ||
showBacking == XNode.Node.ShowBackingValue.Never || showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected); (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
float spacePadding = 0; float spacePadding = 0;
string tooltip = null; string tooltip = null;
foreach (var attr in propertyAttributes) { foreach (var attr in propertyAttributes) {
if (attr is SpaceAttribute) { if (attr is SpaceAttribute) {
if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height); if (usePropertyAttributes)
else spacePadding += (attr as SpaceAttribute).height; {
GUILayout.Space((attr as SpaceAttribute).height);
}
else
{
spacePadding += (attr as SpaceAttribute).height;
}
} else if (attr is HeaderAttribute) { } else if (attr is HeaderAttribute) {
if (usePropertyAttributes) { if (usePropertyAttributes) {
//GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs
Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it. var position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it.
position.yMin += EditorGUIUtility.singleLineHeight * 0.5f; position.yMin += EditorGUIUtility.singleLineHeight * 0.5f;
position = EditorGUI.IndentedRect(position); position = EditorGUI.IndentedRect(position);
GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel); GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel);
} else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f; } else
{
spacePadding += EditorGUIUtility.singleLineHeight * 1.5f;
}
} else if (attr is TooltipAttribute) { } else if (attr is TooltipAttribute) {
tooltip = (attr as TooltipAttribute).tooltip; tooltip = (attr as TooltipAttribute).tooltip;
} }
} }
if (dynamicPortList) { if (dynamicPortList) {
Type type = GetType(property); var type = GetType(property);
XNode.Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple; var connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType); DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
return; return;
} }
switch (showBacking) { switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected: case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected // Display a label if port is connected
if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); if (port.IsConnected)
{
EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
}
// Display an editable property field if port is not connected // Display an editable property field if port is not connected
else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); else
{
EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
}
break; break;
case XNode.Node.ShowBackingValue.Never: case XNode.Node.ShowBackingValue.Never:
// Display a label // Display a label
@ -167,20 +209,20 @@ namespace XNodeEditor {
rect.size = new Vector2(16, 16); rect.size = new Vector2(16, 16);
Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port); var backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port);
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); var col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port); var portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port);
DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background); DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background);
// Register the handle position // Register the handle position
Vector2 portPos = rect.center; var portPos = rect.center;
NodeEditor.portPositions[port] = portPos; NodeEditor.portPositions[port] = portPos;
} }
} }
private static System.Type GetType(SerializedProperty property) { private static System.Type GetType(SerializedProperty property) {
System.Type parentType = property.serializedObject.targetObject.GetType(); var parentType = property.serializedObject.targetObject.GetType();
System.Reflection.FieldInfo fi = parentType.GetFieldInfo(property.name); var fi = parentType.GetFieldInfo(property.name);
return fi.FieldType; return fi.FieldType;
} }
@ -191,17 +233,25 @@ namespace XNodeEditor {
/// <summary> Make a simple port field. </summary> /// <summary> Make a simple port field. </summary>
public static void PortField(GUIContent label, XNode.NodePort port, params GUILayoutOption[] options) { public static void PortField(GUIContent label, XNode.NodePort port, params GUILayoutOption[] options) {
if (port == null) return; if (port == null)
if (options == null) options = new GUILayoutOption[] { GUILayout.MinWidth(30) }; {
return;
}
if (options == null)
{
options = new GUILayoutOption[] { GUILayout.MinWidth(30) };
}
Vector2 position = Vector3.zero; Vector2 position = Vector3.zero;
GUIContent content = label != null ? label : new GUIContent(ObjectNames.NicifyVariableName(port.fieldName)); var content = label != null ? label : new GUIContent(ObjectNames.NicifyVariableName(port.fieldName));
// If property is an input, display a regular property field and put a port handle on the left side // If property is an input, display a regular property field and put a port handle on the left side
if (port.direction == XNode.NodePort.IO.Input) { if (port.direction == XNode.NodePort.IO.Input) {
// Display a label // Display a label
EditorGUILayout.LabelField(content, options); EditorGUILayout.LabelField(content, options);
Rect rect = GUILayoutUtility.GetLastRect(); var rect = GUILayoutUtility.GetLastRect();
float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left; float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left;
position = rect.position - new Vector2(16 + paddingLeft, 0); position = rect.position - new Vector2(16 + paddingLeft, 0);
} }
@ -210,7 +260,7 @@ namespace XNodeEditor {
// Display a label // Display a label
EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options); EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options);
Rect rect = GUILayoutUtility.GetLastRect(); var rect = GUILayoutUtility.GetLastRect();
rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right; rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right;
position = rect.position + new Vector2(rect.width, 0); position = rect.position + new Vector2(rect.width, 0);
} }
@ -219,25 +269,32 @@ namespace XNodeEditor {
/// <summary> Make a simple port field. </summary> /// <summary> Make a simple port field. </summary>
public static void PortField(Vector2 position, XNode.NodePort port) { public static void PortField(Vector2 position, XNode.NodePort port) {
if (port == null) return; if (port == null)
{
return;
}
Rect rect = new Rect(position, new Vector2(16, 16)); var rect = new Rect(position, new Vector2(16, 16));
Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port); var backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port);
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); var col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port); var portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port);
DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background); DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background);
// Register the handle position // Register the handle position
Vector2 portPos = rect.center; var portPos = rect.center;
NodeEditor.portPositions[port] = portPos; NodeEditor.portPositions[port] = portPos;
} }
/// <summary> Add a port field to previous layout element. </summary> /// <summary> Add a port field to previous layout element. </summary>
public static void AddPortField(XNode.NodePort port) { public static void AddPortField(XNode.NodePort port) {
if (port == null) return; if (port == null)
Rect rect = new Rect(); {
return;
}
var rect = new Rect();
// If property is an input, display a regular property field and put a port handle on the left side // If property is an input, display a regular property field and put a port handle on the left side
if (port.direction == XNode.NodePort.IO.Input) { if (port.direction == XNode.NodePort.IO.Input) {
@ -253,14 +310,14 @@ namespace XNodeEditor {
rect.size = new Vector2(16, 16); rect.size = new Vector2(16, 16);
Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port); var backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port);
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); var col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port); var portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port);
DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background); DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background);
// Register the handle position // Register the handle position
Vector2 portPos = rect.center; var portPos = rect.center;
NodeEditor.portPositions[port] = portPos; NodeEditor.portPositions[port] = portPos;
} }
@ -281,7 +338,7 @@ namespace XNodeEditor {
/// <param name="border">texture for border of the dot port</param> /// <param name="border">texture for border of the dot port</param>
/// <param name="dot">texture for the dot port</param> /// <param name="dot">texture for the dot port</param>
public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor, Texture2D border, Texture2D dot) { public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor, Texture2D border, Texture2D dot) {
Color col = GUI.color; var col = GUI.color;
GUI.color = backgroundColor; GUI.color = backgroundColor;
GUI.DrawTexture(rect, border); GUI.DrawTexture(rect, border);
GUI.color = typeColor; GUI.color = typeColor;
@ -304,12 +361,19 @@ namespace XNodeEditor {
/// <summary> Is this port part of a DynamicPortList? </summary> /// <summary> Is this port part of a DynamicPortList? </summary>
public static bool IsDynamicPortListPort(XNode.NodePort port) { public static bool IsDynamicPortListPort(XNode.NodePort port) {
string[] parts = port.fieldName.Split(' '); var parts = port.fieldName.Split(' ');
if (parts.Length != 2) return false; if (parts.Length != 2)
{
return false;
}
Dictionary<string, ReorderableList> cache; Dictionary<string, ReorderableList> cache;
if (reorderableListCache.TryGetValue(port.node, out cache)) { if (reorderableListCache.TryGetValue(port.node, out cache)) {
ReorderableList list; ReorderableList list;
if (cache.TryGetValue(parts[0], out list)) return true; if (cache.TryGetValue(parts[0], out list))
{
return true;
}
} }
return false; return false;
} }
@ -321,33 +385,42 @@ namespace XNodeEditor {
/// <param name="connectionType">Connection type of added dynamic ports</param> /// <param name="connectionType">Connection type of added dynamic ports</param>
/// <param name="onCreation">Called on the list on creation. Use this if you want to customize the created ReorderableList</param> /// <param name="onCreation">Called on the list on creation. Use this if you want to customize the created ReorderableList</param>
public static void DynamicPortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action<ReorderableList> onCreation = null) { public static void DynamicPortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action<ReorderableList> onCreation = null) {
XNode.Node node = serializedObject.targetObject as XNode.Node; var node = serializedObject.targetObject as XNode.Node;
var indexedPorts = node.DynamicPorts.Select(x => { var indexedPorts = node.DynamicPorts.Select(x => {
string[] split = x.fieldName.Split(' '); var split = x.fieldName.Split(' ');
if (split != null && split.Length == 2 && split[0] == fieldName) { if (split != null && split.Length == 2 && split[0] == fieldName) {
int i = -1; var i = -1;
if (int.TryParse(split[1], out i)) { if (int.TryParse(split[1], out i)) {
return new { index = i, port = x }; return new { index = i, port = x };
} }
} }
return new { index = -1, port = (XNode.NodePort)null }; return new { index = -1, port = (XNode.NodePort)null };
}).Where(x => x.port != null); }).Where(x => x.port != null);
List<XNode.NodePort> dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); var dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
node.UpdatePorts(); node.UpdatePorts();
ReorderableList list = null; ReorderableList list = null;
Dictionary<string, ReorderableList> rlc; Dictionary<string, ReorderableList> rlc;
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) { if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
if (!rlc.TryGetValue(fieldName, out list)) list = null; if (!rlc.TryGetValue(fieldName, out list))
{
list = null;
}
} }
// If a ReorderableList isn't cached for this array, do so. // If a ReorderableList isn't cached for this array, do so.
if (list == null) { if (list == null) {
SerializedProperty arrayData = serializedObject.FindProperty(fieldName); var arrayData = serializedObject.FindProperty(fieldName);
list = CreateReorderableList(fieldName, dynamicPorts, arrayData, type, serializedObject, io, connectionType, typeConstraint, onCreation); list = CreateReorderableList(fieldName, dynamicPorts, arrayData, type, serializedObject, io, connectionType, typeConstraint, onCreation);
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) rlc.Add(fieldName, list); if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc))
else reorderableListCache.Add(serializedObject.targetObject, new Dictionary<string, ReorderableList>() { { fieldName, list } }); {
rlc.Add(fieldName, list);
}
else
{
reorderableListCache.Add(serializedObject.targetObject, new Dictionary<string, ReorderableList>() { { fieldName, list } });
}
} }
list.list = dynamicPorts; list.list = dynamicPorts;
list.DoLayoutList(); list.DoLayoutList();
@ -355,34 +428,45 @@ namespace XNodeEditor {
} }
private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) { private static ReorderableList CreateReorderableList(string fieldName, List<XNode.NodePort> dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action<ReorderableList> onCreation) {
bool hasArrayData = arrayData != null && arrayData.isArray; var hasArrayData = arrayData != null && arrayData.isArray;
XNode.Node node = serializedObject.targetObject as XNode.Node; var node = serializedObject.targetObject as XNode.Node;
ReorderableList list = new ReorderableList(dynamicPorts, null, true, true, true, true); var list = new ReorderableList(dynamicPorts, null, true, true, true, true);
string label = arrayData != null ? arrayData.displayName : ObjectNames.NicifyVariableName(fieldName); var label = arrayData != null ? arrayData.displayName : ObjectNames.NicifyVariableName(fieldName);
list.drawElementCallback = list.drawElementCallback =
(Rect rect, int index, bool isActive, bool isFocused) => { (Rect rect, int index, bool isActive, bool isFocused) => {
XNode.NodePort port = node.GetPort(fieldName + " " + index); var port = node.GetPort(fieldName + " " + index);
if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) { if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
if (arrayData.arraySize <= index) { if (arrayData.arraySize <= index) {
EditorGUI.LabelField(rect, "Array[" + index + "] data out of range"); EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
return; return;
} }
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index); var itemData = arrayData.GetArrayElementAtIndex(index);
EditorGUI.PropertyField(rect, itemData, true); EditorGUI.PropertyField(rect, itemData, true);
} else EditorGUI.LabelField(rect, port != null ? port.fieldName : ""); } else
{
EditorGUI.LabelField(rect, port != null ? port.fieldName : "");
}
if (port != null) { if (port != null) {
Vector2 pos = rect.position + (port.IsOutput ? new Vector2(rect.width + 6, 0) : new Vector2(-36, 0)); var pos = rect.position + (port.IsOutput ? new Vector2(rect.width + 6, 0) : new Vector2(-36, 0));
NodeEditorGUILayout.PortField(pos, port); NodeEditorGUILayout.PortField(pos, port);
} }
}; };
list.elementHeightCallback = list.elementHeightCallback =
(int index) => { (int index) => {
if (hasArrayData) { if (hasArrayData) {
if (arrayData.arraySize <= index) return EditorGUIUtility.singleLineHeight; if (arrayData.arraySize <= index)
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index); {
return EditorGUIUtility.singleLineHeight;
}
var itemData = arrayData.GetArrayElementAtIndex(index);
return EditorGUI.GetPropertyHeight(itemData); return EditorGUI.GetPropertyHeight(itemData);
} else return EditorGUIUtility.singleLineHeight; } else
{
return EditorGUIUtility.singleLineHeight;
}
}; };
list.drawHeaderCallback = list.drawHeaderCallback =
(Rect rect) => { (Rect rect) => {
@ -395,15 +479,15 @@ namespace XNodeEditor {
list.onReorderCallback = list.onReorderCallback =
(ReorderableList rl) => { (ReorderableList rl) => {
serializedObject.Update(); serializedObject.Update();
bool hasRect = false; var hasRect = false;
bool hasNewRect = false; var hasNewRect = false;
Rect rect = Rect.zero; var rect = Rect.zero;
Rect newRect = Rect.zero; var newRect = Rect.zero;
// Move up // Move up
if (rl.index > reorderableListIndex) { if (rl.index > reorderableListIndex) {
for (int i = reorderableListIndex; i < rl.index; ++i) { for (var i = reorderableListIndex; i < rl.index; ++i) {
XNode.NodePort port = node.GetPort(fieldName + " " + i); var port = node.GetPort(fieldName + " " + i);
XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i + 1)); var nextPort = node.GetPort(fieldName + " " + (i + 1));
port.SwapConnections(nextPort); port.SwapConnections(nextPort);
// Swap cached positions to mitigate twitching // Swap cached positions to mitigate twitching
@ -415,9 +499,9 @@ namespace XNodeEditor {
} }
// Move down // Move down
else { else {
for (int i = reorderableListIndex; i > rl.index; --i) { for (var i = reorderableListIndex; i > rl.index; --i) {
XNode.NodePort port = node.GetPort(fieldName + " " + i); var port = node.GetPort(fieldName + " " + i);
XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i - 1)); var nextPort = node.GetPort(fieldName + " " + (i - 1));
port.SwapConnections(nextPort); port.SwapConnections(nextPort);
// Swap cached positions to mitigate twitching // Swap cached positions to mitigate twitching
@ -445,12 +529,22 @@ namespace XNodeEditor {
list.onAddCallback = list.onAddCallback =
(ReorderableList rl) => { (ReorderableList rl) => {
// Add dynamic port postfixed with an index number // Add dynamic port postfixed with an index number
string newName = fieldName + " 0"; var newName = fieldName + " 0";
int i = 0; var i = 0;
while (node.HasPort(newName)) newName = fieldName + " " + (++i); while (node.HasPort(newName))
{
newName = fieldName + " " + (++i);
}
if (io == XNode.NodePort.IO.Output)
{
node.AddDynamicOutput(type, connectionType, XNode.Node.TypeConstraint.None, newName);
}
else
{
node.AddDynamicInput(type, connectionType, typeConstraint, newName);
}
if (io == XNode.NodePort.IO.Output) node.AddDynamicOutput(type, connectionType, XNode.Node.TypeConstraint.None, newName);
else node.AddDynamicInput(type, connectionType, typeConstraint, newName);
serializedObject.Update(); serializedObject.Update();
EditorUtility.SetDirty(node); EditorUtility.SetDirty(node);
if (hasArrayData) { if (hasArrayData) {
@ -462,9 +556,9 @@ namespace XNodeEditor {
(ReorderableList rl) => { (ReorderableList rl) => {
var indexedPorts = node.DynamicPorts.Select(x => { var indexedPorts = node.DynamicPorts.Select(x => {
string[] split = x.fieldName.Split(' '); var split = x.fieldName.Split(' ');
if (split != null && split.Length == 2 && split[0] == fieldName) { if (split != null && split.Length == 2 && split[0] == fieldName) {
int i = -1; var i = -1;
if (int.TryParse(split[1], out i)) { if (int.TryParse(split[1], out i)) {
return new { index = i, port = x }; return new { index = i, port = x };
} }
@ -473,7 +567,7 @@ namespace XNodeEditor {
}).Where(x => x.port != null); }).Where(x => x.port != null);
dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
int index = rl.index; var index = rl.index;
if (dynamicPorts[index] == null) { if (dynamicPorts[index] == null) {
Debug.LogWarning("No port found at index " + index + " - Skipped"); Debug.LogWarning("No port found at index " + index + " - Skipped");
@ -484,9 +578,9 @@ namespace XNodeEditor {
// Clear the removed ports connections // Clear the removed ports connections
dynamicPorts[index].ClearConnections(); dynamicPorts[index].ClearConnections();
// Move following connections one step up to replace the missing connection // Move following connections one step up to replace the missing connection
for (int k = index + 1; k < dynamicPorts.Count(); k++) { for (var k = index + 1; k < dynamicPorts.Count(); k++) {
for (int j = 0; j < dynamicPorts[k].ConnectionCount; j++) { for (var j = 0; j < dynamicPorts[k].ConnectionCount; j++) {
XNode.NodePort other = dynamicPorts[k].GetConnection(j); var other = dynamicPorts[k].GetConnection(j);
dynamicPorts[k].Disconnect(other); dynamicPorts[k].Disconnect(other);
dynamicPorts[k - 1].Connect(other); dynamicPorts[k - 1].Connect(other);
} }
@ -517,14 +611,25 @@ namespace XNodeEditor {
}; };
if (hasArrayData) { if (hasArrayData) {
int dynamicPortCount = dynamicPorts.Count; var dynamicPortCount = dynamicPorts.Count;
while (dynamicPortCount < arrayData.arraySize) { while (dynamicPortCount < arrayData.arraySize) {
// Add dynamic port postfixed with an index number // Add dynamic port postfixed with an index number
string newName = arrayData.name + " 0"; var newName = arrayData.name + " 0";
int i = 0; var i = 0;
while (node.HasPort(newName)) newName = arrayData.name + " " + (++i); while (node.HasPort(newName))
if (io == XNode.NodePort.IO.Output) node.AddDynamicOutput(type, connectionType, typeConstraint, newName); {
else node.AddDynamicInput(type, connectionType, typeConstraint, newName); newName = arrayData.name + " " + (++i);
}
if (io == XNode.NodePort.IO.Output)
{
node.AddDynamicOutput(type, connectionType, typeConstraint, newName);
}
else
{
node.AddDynamicInput(type, connectionType, typeConstraint, newName);
}
EditorUtility.SetDirty(node); EditorUtility.SetDirty(node);
dynamicPortCount++; dynamicPortCount++;
} }
@ -534,8 +639,12 @@ namespace XNodeEditor {
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
serializedObject.Update(); serializedObject.Update();
} }
if (onCreation != null) onCreation(list); if (onCreation != null)
{
onCreation(list);
}
return list; return list;
} }
} }
} }

View File

@ -51,14 +51,22 @@ namespace XNodeEditor {
private Texture2D _gridTexture; private Texture2D _gridTexture;
public Texture2D gridTexture { public Texture2D gridTexture {
get { get {
if (_gridTexture == null) _gridTexture = NodeEditorResources.GenerateGridTexture(gridLineColor, gridBgColor); if (_gridTexture == null)
{
_gridTexture = NodeEditorResources.GenerateGridTexture(gridLineColor, gridBgColor);
}
return _gridTexture; return _gridTexture;
} }
} }
private Texture2D _crossTexture; private Texture2D _crossTexture;
public Texture2D crossTexture { public Texture2D crossTexture {
get { get {
if (_crossTexture == null) _crossTexture = NodeEditorResources.GenerateCrossTexture(gridLineColor); if (_crossTexture == null)
{
_crossTexture = NodeEditorResources.GenerateCrossTexture(gridLineColor);
}
return _crossTexture; return _crossTexture;
} }
} }
@ -66,8 +74,8 @@ namespace XNodeEditor {
public void OnAfterDeserialize() { public void OnAfterDeserialize() {
// Deserialize typeColorsData // Deserialize typeColorsData
typeColors = new Dictionary<string, Color>(); typeColors = new Dictionary<string, Color>();
string[] data = typeColorsData.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var data = typeColorsData.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < data.Length; i += 2) { for (var i = 0; i < data.Length; i += 2) {
Color col; Color col;
if (ColorUtility.TryParseHtmlString("#" + data[i + 1], out col)) { if (ColorUtility.TryParseHtmlString("#" + data[i + 1], out col)) {
typeColors.Add(data[i], col); typeColors.Add(data[i], col);
@ -86,24 +94,34 @@ namespace XNodeEditor {
/// <summary> Get settings of current active editor </summary> /// <summary> Get settings of current active editor </summary>
public static Settings GetSettings() { public static Settings GetSettings() {
if (XNodeEditor.NodeEditorWindow.current == null) return new Settings(); if (XNodeEditor.NodeEditorWindow.current == null)
{
return new Settings();
}
if (lastEditor != XNodeEditor.NodeEditorWindow.current.graphEditor) { if (lastEditor != XNodeEditor.NodeEditorWindow.current.graphEditor) {
object[] attribs = XNodeEditor.NodeEditorWindow.current.graphEditor.GetType().GetCustomAttributes(typeof(XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute), true); var attribs = XNodeEditor.NodeEditorWindow.current.graphEditor.GetType().GetCustomAttributes(typeof(XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute), true);
if (attribs.Length == 1) { if (attribs.Length == 1) {
XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute attrib = attribs[0] as XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute; var attrib = attribs[0] as XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute;
lastEditor = XNodeEditor.NodeEditorWindow.current.graphEditor; lastEditor = XNodeEditor.NodeEditorWindow.current.graphEditor;
lastKey = attrib.editorPrefsKey; lastKey = attrib.editorPrefsKey;
} else return null; } else
{
return null;
}
} }
if (!settings.ContainsKey(lastKey)) VerifyLoaded(); if (!settings.ContainsKey(lastKey))
{
VerifyLoaded();
}
return settings[lastKey]; return settings[lastKey];
} }
#if UNITY_2019_1_OR_NEWER #if UNITY_2019_1_OR_NEWER
[SettingsProvider] [SettingsProvider]
public static SettingsProvider CreateXNodeSettingsProvider() { public static SettingsProvider CreateXNodeSettingsProvider() {
SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) { var provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) {
guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); }, guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); },
keywords = new HashSet<string>(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" }) keywords = new HashSet<string>(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" })
}; };
@ -116,9 +134,13 @@ namespace XNodeEditor {
#endif #endif
private static void PreferencesGUI() { private static void PreferencesGUI() {
VerifyLoaded(); VerifyLoaded();
Settings settings = NodeEditorPreferences.settings[lastKey]; var 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");
}
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(); EditorGUILayout.Space();
NodeSettingsGUI(lastKey, settings); NodeSettingsGUI(lastKey, settings);
@ -155,7 +177,11 @@ namespace XNodeEditor {
EditorGUILayout.LabelField("System", EditorStyles.boldLabel); EditorGUILayout.LabelField("System", EditorStyles.boldLabel);
settings.autoSave = EditorGUILayout.Toggle(new GUIContent("Autosave", "Disable for better editor performance"), settings.autoSave); settings.autoSave = EditorGUILayout.Toggle(new GUIContent("Autosave", "Disable for better editor performance"), settings.autoSave);
settings.openOnCreate = EditorGUILayout.Toggle(new GUIContent("Open Editor on Create", "Disable to prevent openening the editor when creating a new graph"), settings.openOnCreate); settings.openOnCreate = EditorGUILayout.Toggle(new GUIContent("Open Editor on Create", "Disable to prevent openening the editor when creating a new graph"), settings.openOnCreate);
if (GUI.changed) SavePrefs(key, settings); if (GUI.changed)
{
SavePrefs(key, settings);
}
EditorGUILayout.Space(); EditorGUILayout.Space();
} }
@ -188,16 +214,23 @@ namespace XNodeEditor {
//Display type colors. Save them if they are edited by the user //Display type colors. Save them if they are edited by the user
foreach (var type in typeColorKeys) { foreach (var type in typeColorKeys) {
string typeColorKey = NodeEditorUtilities.PrettyName(type); var typeColorKey = NodeEditorUtilities.PrettyName(type);
Color col = typeColors[type]; var col = typeColors[type];
EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck();
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
col = EditorGUILayout.ColorField(typeColorKey, col); col = EditorGUILayout.ColorField(typeColorKey, col);
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
if (EditorGUI.EndChangeCheck()) { if (EditorGUI.EndChangeCheck()) {
typeColors[type] = col; typeColors[type] = col;
if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col; if (settings.typeColors.ContainsKey(typeColorKey))
else settings.typeColors.Add(typeColorKey, col); {
settings.typeColors[typeColorKey] = col;
}
else
{
settings.typeColors.Add(typeColorKey, col);
}
SavePrefs(key, settings); SavePrefs(key, settings);
NodeEditorWindow.RepaintAll(); NodeEditorWindow.RepaintAll();
} }
@ -208,16 +241,30 @@ namespace XNodeEditor {
private static Settings LoadPrefs() { private static Settings LoadPrefs() {
// Create settings if it doesn't exist // Create settings if it doesn't exist
if (!EditorPrefs.HasKey(lastKey)) { if (!EditorPrefs.HasKey(lastKey)) {
if (lastEditor != null) EditorPrefs.SetString(lastKey, JsonUtility.ToJson(lastEditor.GetDefaultPreferences())); if (lastEditor != null)
else EditorPrefs.SetString(lastKey, JsonUtility.ToJson(new Settings())); {
EditorPrefs.SetString(lastKey, JsonUtility.ToJson(lastEditor.GetDefaultPreferences()));
}
else
{
EditorPrefs.SetString(lastKey, JsonUtility.ToJson(new Settings()));
}
} }
return JsonUtility.FromJson<Settings>(EditorPrefs.GetString(lastKey)); return JsonUtility.FromJson<Settings>(EditorPrefs.GetString(lastKey));
} }
/// <summary> Delete all prefs </summary> /// <summary> Delete all prefs </summary>
public static void ResetPrefs() { public static void ResetPrefs() {
if (EditorPrefs.HasKey(lastKey)) EditorPrefs.DeleteKey(lastKey); if (EditorPrefs.HasKey(lastKey))
if (settings.ContainsKey(lastKey)) settings.Remove(lastKey); {
EditorPrefs.DeleteKey(lastKey);
}
if (settings.ContainsKey(lastKey))
{
settings.Remove(lastKey);
}
typeColors = new Dictionary<Type, Color>(); typeColors = new Dictionary<Type, Color>();
VerifyLoaded(); VerifyLoaded();
NodeEditorWindow.RepaintAll(); NodeEditorWindow.RepaintAll();
@ -230,20 +277,30 @@ namespace XNodeEditor {
/// <summary> Check if we have loaded settings for given key. If not, load them </summary> /// <summary> Check if we have loaded settings for given key. If not, load them </summary>
private static void VerifyLoaded() { private static void VerifyLoaded() {
if (!settings.ContainsKey(lastKey)) settings.Add(lastKey, LoadPrefs()); if (!settings.ContainsKey(lastKey))
{
settings.Add(lastKey, LoadPrefs());
}
} }
/// <summary> Return color based on type </summary> /// <summary> Return color based on type </summary>
public static Color GetTypeColor(System.Type type) { public static Color GetTypeColor(System.Type type) {
VerifyLoaded(); VerifyLoaded();
if (type == null) return Color.gray; if (type == null)
{
return Color.gray;
}
Color col; Color col;
if (!typeColors.TryGetValue(type, out col)) { if (!typeColors.TryGetValue(type, out col)) {
string typeName = type.PrettyName(); var typeName = type.PrettyName();
if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]); if (settings[lastKey].typeColors.ContainsKey(typeName))
{
typeColors.Add(type, settings[lastKey].typeColors[typeName]);
}
else { else {
#if UNITY_5_4_OR_NEWER #if UNITY_5_4_OR_NEWER
UnityEngine.Random.State oldState = UnityEngine.Random.state; var oldState = UnityEngine.Random.state;
UnityEngine.Random.InitState(typeName.GetHashCode()); UnityEngine.Random.InitState(typeName.GetHashCode());
#else #else
int oldSeed = UnityEngine.Random.seed; int oldSeed = UnityEngine.Random.seed;
@ -261,4 +318,4 @@ namespace XNodeEditor {
return col; return col;
} }
} }
} }

View File

@ -1,183 +1,198 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU
using GenericMenu = XNodeEditor.AdvancedGenericMenu; using GenericMenu = XNodeEditor.AdvancedGenericMenu;
#endif #endif
namespace XNodeEditor { namespace XNodeEditor {
/// <summary> Contains reflection-related extensions built for xNode </summary> /// <summary> Contains reflection-related extensions built for xNode </summary>
public static class NodeEditorReflection { public static class NodeEditorReflection {
[NonSerialized] private static Dictionary<Type, Color> nodeTint; [NonSerialized] private static Dictionary<Type, Color> nodeTint;
[NonSerialized] private static Dictionary<Type, int> nodeWidth; [NonSerialized] private static Dictionary<Type, int> nodeWidth;
/// <summary> All available node types </summary> /// <summary> All available node types </summary>
public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } }
[NonSerialized] private static Type[] _nodeTypes = null; [NonSerialized] private static Type[] _nodeTypes = null;
/// <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> /// <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) { public static Func<bool> GetIsDockedDelegate(this EditorWindow window) {
BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; var fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); var isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true);
return (Func<bool>) Delegate.CreateDelegate(typeof(Func<bool>), window, isDockedMethod); return (Func<bool>) Delegate.CreateDelegate(typeof(Func<bool>), window, isDockedMethod);
} }
public static Type[] GetNodeTypes() { public static Type[] GetNodeTypes() {
//Get all classes deriving from Node via reflection //Get all classes deriving from Node via reflection
return GetDerivedTypes(typeof(XNode.Node)); return GetDerivedTypes(typeof(XNode.Node));
} }
/// <summary> Custom node tint colors defined with [NodeColor(r, g, b)] </summary> /// <summary> Custom node tint colors defined with [NodeColor(r, g, b)] </summary>
public static bool TryGetAttributeTint(this Type nodeType, out Color tint) { public static bool TryGetAttributeTint(this Type nodeType, out Color tint) {
if (nodeTint == null) { if (nodeTint == null) {
CacheAttributes<Color, XNode.Node.NodeTintAttribute>(ref nodeTint, x => x.color); CacheAttributes<Color, XNode.Node.NodeTintAttribute>(ref nodeTint, x => x.color);
} }
return nodeTint.TryGetValue(nodeType, out tint); return nodeTint.TryGetValue(nodeType, out tint);
} }
/// <summary> Get custom node widths defined with [NodeWidth(width)] </summary> /// <summary> Get custom node widths defined with [NodeWidth(width)] </summary>
public static bool TryGetAttributeWidth(this Type nodeType, out int width) { public static bool TryGetAttributeWidth(this Type nodeType, out int width) {
if (nodeWidth == null) { if (nodeWidth == null) {
CacheAttributes<int, XNode.Node.NodeWidthAttribute>(ref nodeWidth, x => x.width); CacheAttributes<int, XNode.Node.NodeWidthAttribute>(ref nodeWidth, x => x.width);
} }
return nodeWidth.TryGetValue(nodeType, out 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 { private static void CacheAttributes<V, A>(ref Dictionary<Type, V> dict, Func<A, V> getter) where A : Attribute {
dict = new Dictionary<Type, V>(); dict = new Dictionary<Type, V>();
for (int i = 0; i < nodeTypes.Length; i++) { for (var i = 0; i < nodeTypes.Length; i++) {
object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true); var attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true);
if (attribs == null || attribs.Length == 0) continue; if (attribs == null || attribs.Length == 0)
A attrib = attribs[0] as A; {
dict.Add(nodeTypes[i], getter(attrib)); continue;
} }
}
var attrib = attribs[0] as A;
/// <summary> Get FieldInfo of a field, including those that are private and/or inherited </summary> dict.Add(nodeTypes[i], getter(attrib));
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 /// <summary> Get FieldInfo of a field, including those that are private and/or inherited </summary>
while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); public static FieldInfo GetFieldInfo(this Type type, string fieldName) {
return field; // If we can't find field in the first run, it's probably a private field in a base class.
} var field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
// Search base classes for private fields only. Public fields are found above
/// <summary> Get all classes deriving from baseType via reflection </summary> while (field == null && (type = type.BaseType) != typeof(XNode.Node))
public static Type[] GetDerivedTypes(this Type baseType) { {
List<System.Type> types = new List<System.Type>(); field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); }
foreach (Assembly assembly in assemblies) {
try { return field;
types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); }
} catch (ReflectionTypeLoadException) { }
} /// <summary> Get all classes deriving from baseType via reflection </summary>
return types.ToArray(); public static Type[] GetDerivedTypes(this Type baseType) {
} var types = new List<System.Type>();
var assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
/// <summary> Find methods marked with the [ContextMenu] attribute and add them to the context menu </summary> foreach (var assembly in assemblies) {
public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) { try {
KeyValuePair<ContextMenu, MethodInfo>[] items = GetContextMenuMethods(obj); types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
if (items.Length != 0) { } catch (ReflectionTypeLoadException) { }
contextMenu.AddSeparator(""); }
List<string> invalidatedEntries = new List<string>(); return types.ToArray();
foreach (KeyValuePair<ContextMenu, MethodInfo> checkValidate in items) { }
if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) {
invalidatedEntries.Add(checkValidate.Key.menuItem); /// <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) {
} var items = GetContextMenuMethods(obj);
for (int i = 0; i < items.Length; i++) { if (items.Length != 0) {
KeyValuePair<ContextMenu, MethodInfo> kvp = items[i]; contextMenu.AddSeparator("");
if (invalidatedEntries.Contains(kvp.Key.menuItem)) { var invalidatedEntries = new List<string>();
contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem)); foreach (var checkValidate in items) {
} else { if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) {
contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); invalidatedEntries.Add(checkValidate.Key.menuItem);
} }
} }
} for (var i = 0; i < items.Length; i++) {
} var kvp = items[i];
if (invalidatedEntries.Contains(kvp.Key.menuItem)) {
/// <summary> Call OnValidate on target </summary> contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem));
public static void TriggerOnValidate(this UnityEngine.Object target) { } else {
System.Reflection.MethodInfo onValidate = null; contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null));
if (target != null) { }
onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); }
if (onValidate != null) onValidate.Invoke(target, null); }
} }
}
/// <summary> Call OnValidate on target </summary>
public static KeyValuePair<ContextMenu, MethodInfo>[] GetContextMenuMethods(object obj) { public static void TriggerOnValidate(this UnityEngine.Object target) {
Type type = obj.GetType(); System.Reflection.MethodInfo onValidate = null;
MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (target != null) {
List<KeyValuePair<ContextMenu, MethodInfo>> kvp = new List<KeyValuePair<ContextMenu, MethodInfo>>(); onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
for (int i = 0; i < methods.Length; i++) { if (onValidate != null)
ContextMenu[] attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray(); {
if (attribs == null || attribs.Length == 0) continue; onValidate.Invoke(target, null);
if (methods[i].GetParameters().Length != 0) { }
Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands."); }
continue; }
}
if (methods[i].IsStatic) { public static KeyValuePair<ContextMenu, MethodInfo>[] GetContextMenuMethods(object obj) {
Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands."); var type = obj.GetType();
continue; var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
} var kvp = new List<KeyValuePair<ContextMenu, MethodInfo>>();
for (var i = 0; i < methods.Length; i++) {
for (int k = 0; k < attribs.Length; k++) { var attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray();
kvp.Add(new KeyValuePair<ContextMenu, MethodInfo>(attribs[k], methods[i])); if (attribs == null || attribs.Length == 0)
} {
} continue;
#if UNITY_5_5_OR_NEWER }
//Sort menu items
kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority)); if (methods[i].GetParameters().Length != 0) {
#endif Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands.");
return kvp.ToArray(); continue;
} }
if (methods[i].IsStatic) {
/// <summary> Very crude. Uses a lot of reflection. </summary> Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands.");
public static void OpenPreferences() { continue;
try { }
#if UNITY_2018_3_OR_NEWER
SettingsService.OpenUserPreferences("Preferences/Node Editor"); for (var k = 0; k < attribs.Length; k++) {
#else kvp.Add(new KeyValuePair<ContextMenu, MethodInfo>(attribs[k], methods[i]));
//Open preferences window }
Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow)); }
Type type = assembly.GetType("UnityEditor.PreferencesWindow"); #if UNITY_5_5_OR_NEWER
type.GetMethod("ShowPreferencesWindow", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null); //Sort menu items
kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority));
//Get the window #endif
EditorWindow window = EditorWindow.GetWindow(type); return kvp.ToArray();
}
//Make sure custom sections are added (because waiting for it to happen automatically is too slow)
FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance); /// <summary> Very crude. Uses a lot of reflection. </summary>
if ((bool) refreshField.GetValue(window)) { public static void OpenPreferences() {
type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null); try {
refreshField.SetValue(window, false); #if UNITY_2018_3_OR_NEWER
} SettingsService.OpenUserPreferences("Preferences/Node Editor");
#else
//Get sections //Open preferences window
FieldInfo sectionsField = type.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic); Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow));
IList sections = sectionsField.GetValue(window) as IList; Type type = assembly.GetType("UnityEditor.PreferencesWindow");
type.GetMethod("ShowPreferencesWindow", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null);
//Iterate through sections and check contents
Type sectionType = sectionsField.FieldType.GetGenericArguments() [0]; //Get the window
FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public); EditorWindow window = EditorWindow.GetWindow(type);
for (int i = 0; i < sections.Count; i++) {
GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent; //Make sure custom sections are added (because waiting for it to happen automatically is too slow)
if (sectionContent.text == "Node Editor") { FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance);
//Found contents - Set index if ((bool) refreshField.GetValue(window)) {
FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic); type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null);
sectionIndexField.SetValue(window, i); refreshField.SetValue(window, false);
return; }
}
} //Get sections
#endif FieldInfo sectionsField = type.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic);
} catch (Exception e) { IList sections = sectionsField.GetValue(window) as IList;
Debug.LogError(e);
Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number."); //Iterate through sections and check contents
} Type sectionType = sectionsField.FieldType.GetGenericArguments() [0];
} FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public);
} for (int i = 0; i < sections.Count; i++) {
} GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent;
if (sectionContent.text == "Node Editor") {
//Found contents - Set index
FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic);
sectionIndexField.SetValue(window, i);
return;
}
}
#endif
} catch (Exception e) {
Debug.LogError(e);
Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number.");
}
}
}
}

View File

@ -1,95 +1,107 @@
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
namespace XNodeEditor { namespace XNodeEditor {
public static class NodeEditorResources { public static class NodeEditorResources {
// Textures // Textures
public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load<Texture2D>("xnode_dot"); } } public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load<Texture2D>("xnode_dot"); } }
private static Texture2D _dot; private static Texture2D _dot;
public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load<Texture2D>("xnode_dot_outer"); } } public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load<Texture2D>("xnode_dot_outer"); } }
private static Texture2D _dotOuter; private static Texture2D _dotOuter;
public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load<Texture2D>("xnode_node"); } } public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load<Texture2D>("xnode_node"); } }
private static Texture2D _nodeBody; private static Texture2D _nodeBody;
public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load<Texture2D>("xnode_node_highlight"); } } public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load<Texture2D>("xnode_node_highlight"); } }
private static Texture2D _nodeHighlight; private static Texture2D _nodeHighlight;
// Styles // Styles
public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } }
public static Styles _styles = null; public static Styles _styles = null;
public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } } public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } }
public class Styles { public class Styles {
public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight;
public Styles() { public Styles() {
GUIStyle baseStyle = new GUIStyle("Label"); var baseStyle = new GUIStyle("Label");
baseStyle.fixedHeight = 18; baseStyle.fixedHeight = 18;
inputPort = new GUIStyle(baseStyle); inputPort = new GUIStyle(baseStyle);
inputPort.alignment = TextAnchor.UpperLeft; inputPort.alignment = TextAnchor.UpperLeft;
inputPort.padding.left = 0; inputPort.padding.left = 0;
inputPort.active.background = dot; inputPort.active.background = dot;
inputPort.normal.background = dotOuter; inputPort.normal.background = dotOuter;
outputPort = new GUIStyle(baseStyle); outputPort = new GUIStyle(baseStyle);
outputPort.alignment = TextAnchor.UpperRight; outputPort.alignment = TextAnchor.UpperRight;
outputPort.padding.right = 0; outputPort.padding.right = 0;
outputPort.active.background = dot; outputPort.active.background = dot;
outputPort.normal.background = dotOuter; outputPort.normal.background = dotOuter;
nodeHeader = new GUIStyle(); nodeHeader = new GUIStyle();
nodeHeader.alignment = TextAnchor.MiddleCenter; nodeHeader.alignment = TextAnchor.MiddleCenter;
nodeHeader.fontStyle = FontStyle.Bold; nodeHeader.fontStyle = FontStyle.Bold;
nodeHeader.normal.textColor = Color.white; nodeHeader.normal.textColor = Color.white;
nodeBody = new GUIStyle(); nodeBody = new GUIStyle();
nodeBody.normal.background = NodeEditorResources.nodeBody; nodeBody.normal.background = NodeEditorResources.nodeBody;
nodeBody.border = new RectOffset(32, 32, 32, 32); nodeBody.border = new RectOffset(32, 32, 32, 32);
nodeBody.padding = new RectOffset(16, 16, 4, 16); nodeBody.padding = new RectOffset(16, 16, 4, 16);
nodeHighlight = new GUIStyle(); nodeHighlight = new GUIStyle();
nodeHighlight.normal.background = NodeEditorResources.nodeHighlight; nodeHighlight.normal.background = NodeEditorResources.nodeHighlight;
nodeHighlight.border = new RectOffset(32, 32, 32, 32); nodeHighlight.border = new RectOffset(32, 32, 32, 32);
tooltip = new GUIStyle("helpBox"); tooltip = new GUIStyle("helpBox");
tooltip.alignment = TextAnchor.MiddleCenter; tooltip.alignment = TextAnchor.MiddleCenter;
} }
} }
public static Texture2D GenerateGridTexture(Color line, Color bg) { public static Texture2D GenerateGridTexture(Color line, Color bg) {
Texture2D tex = new Texture2D(64, 64); var tex = new Texture2D(64, 64);
Color[] cols = new Color[64 * 64]; var cols = new Color[64 * 64];
for (int y = 0; y < 64; y++) { for (var y = 0; y < 64; y++) {
for (int x = 0; x < 64; x++) { for (var x = 0; x < 64; x++) {
Color col = bg; var col = bg;
if (y % 16 == 0 || x % 16 == 0) col = Color.Lerp(line, bg, 0.65f); if (y % 16 == 0 || x % 16 == 0)
if (y == 63 || x == 63) col = Color.Lerp(line, bg, 0.35f); {
cols[(y * 64) + x] = col; col = Color.Lerp(line, bg, 0.65f);
} }
}
tex.SetPixels(cols); if (y == 63 || x == 63)
tex.wrapMode = TextureWrapMode.Repeat; {
tex.filterMode = FilterMode.Bilinear; col = Color.Lerp(line, bg, 0.35f);
tex.name = "Grid"; }
tex.Apply();
return tex; cols[(y * 64) + x] = col;
} }
}
public static Texture2D GenerateCrossTexture(Color line) { tex.SetPixels(cols);
Texture2D tex = new Texture2D(64, 64); tex.wrapMode = TextureWrapMode.Repeat;
Color[] cols = new Color[64 * 64]; tex.filterMode = FilterMode.Bilinear;
for (int y = 0; y < 64; y++) { tex.name = "Grid";
for (int x = 0; x < 64; x++) { tex.Apply();
Color col = line; return tex;
if (y != 31 && x != 31) col.a = 0; }
cols[(y * 64) + x] = col;
} public static Texture2D GenerateCrossTexture(Color line) {
} var tex = new Texture2D(64, 64);
tex.SetPixels(cols); var cols = new Color[64 * 64];
tex.wrapMode = TextureWrapMode.Clamp; for (var y = 0; y < 64; y++) {
tex.filterMode = FilterMode.Bilinear; for (var x = 0; x < 64; x++) {
tex.name = "Grid"; var col = line;
tex.Apply(); if (y != 31 && x != 31)
return tex; {
} col.a = 0;
} }
}
cols[(y * 64) + x] = col;
}
}
tex.SetPixels(cols);
tex.wrapMode = TextureWrapMode.Clamp;
tex.filterMode = FilterMode.Bilinear;
tex.name = "Grid";
tex.Apply();
return tex;
}
}
}

View File

@ -1,317 +1,372 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace XNodeEditor { namespace XNodeEditor {
/// <summary> A set of editor-only utilities and extensions for xNode </summary> /// <summary> A set of editor-only utilities and extensions for xNode </summary>
public static class NodeEditorUtilities { public static class NodeEditorUtilities {
/// <summary>C#'s Script Icon [The one MonoBhevaiour Scripts have].</summary> /// <summary>C#'s Script Icon [The one MonoBhevaiour Scripts have].</summary>
private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D); private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D);
/// Saves Attribute from Type+Field for faster lookup. Resets on recompiles. /// Saves Attribute from Type+Field for faster lookup. Resets on recompiles.
private static Dictionary<Type, Dictionary<string, Dictionary<Type, Attribute>>> typeAttributes = new Dictionary<Type, Dictionary<string, Dictionary<Type, Attribute>>>(); private static Dictionary<Type, Dictionary<string, Dictionary<Type, Attribute>>> typeAttributes = new Dictionary<Type, Dictionary<string, Dictionary<Type, Attribute>>>();
/// Saves ordered PropertyAttribute from Type+Field for faster lookup. Resets on recompiles. /// Saves ordered PropertyAttribute from Type+Field for faster lookup. Resets on recompiles.
private static Dictionary<Type, Dictionary<string, List<PropertyAttribute>>> typeOrderedPropertyAttributes = new Dictionary<Type, Dictionary<string, List<PropertyAttribute>>>(); private static Dictionary<Type, Dictionary<string, List<PropertyAttribute>>> typeOrderedPropertyAttributes = new Dictionary<Type, Dictionary<string, List<PropertyAttribute>>>();
public static bool GetAttrib<T>(Type classType, out T attribOut) where T : Attribute { public static bool GetAttrib<T>(Type classType, out T attribOut) where T : Attribute {
object[] attribs = classType.GetCustomAttributes(typeof(T), false); var attribs = classType.GetCustomAttributes(typeof(T), false);
return GetAttrib(attribs, out attribOut); return GetAttrib(attribs, out attribOut);
} }
public static bool GetAttrib<T>(object[] attribs, out T attribOut) where T : Attribute { public static bool GetAttrib<T>(object[] attribs, out T attribOut) where T : Attribute {
for (int i = 0; i < attribs.Length; i++) { for (var i = 0; i < attribs.Length; i++) {
if (attribs[i] is T) { if (attribs[i] is T) {
attribOut = attribs[i] as T; attribOut = attribs[i] as T;
return true; return true;
} }
} }
attribOut = null; attribOut = null;
return false; return false;
} }
public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute { public static bool GetAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute {
// If we can't find field in the first run, it's probably a private field in a base class. // If we can't find field in the first run, it's probably a private field in a base class.
FieldInfo field = classType.GetFieldInfo(fieldName); var field = classType.GetFieldInfo(fieldName);
// This shouldn't happen. Ever. // This shouldn't happen. Ever.
if (field == null) { if (field == null) {
Debug.LogWarning("Field " + fieldName + " couldnt be found"); Debug.LogWarning("Field " + fieldName + " couldnt be found");
attribOut = null; attribOut = null;
return false; return false;
} }
object[] attribs = field.GetCustomAttributes(typeof(T), true); var attribs = field.GetCustomAttributes(typeof(T), true);
return GetAttrib(attribs, out attribOut); return GetAttrib(attribs, out attribOut);
} }
public static bool HasAttrib<T>(object[] attribs) where T : Attribute { public static bool HasAttrib<T>(object[] attribs) where T : Attribute {
for (int i = 0; i < attribs.Length; i++) { for (var i = 0; i < attribs.Length; i++) {
if (attribs[i].GetType() == typeof(T)) { if (attribs[i].GetType() == typeof(T)) {
return true; return true;
} }
} }
return false; return false;
} }
public static bool GetCachedAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute { public static bool GetCachedAttrib<T>(Type classType, string fieldName, out T attribOut) where T : Attribute {
Dictionary<string, Dictionary<Type, Attribute>> typeFields; Dictionary<string, Dictionary<Type, Attribute>> typeFields;
if (!typeAttributes.TryGetValue(classType, out typeFields)) { if (!typeAttributes.TryGetValue(classType, out typeFields)) {
typeFields = new Dictionary<string, Dictionary<Type, Attribute>>(); typeFields = new Dictionary<string, Dictionary<Type, Attribute>>();
typeAttributes.Add(classType, typeFields); typeAttributes.Add(classType, typeFields);
} }
Dictionary<Type, Attribute> typeTypes; Dictionary<Type, Attribute> typeTypes;
if (!typeFields.TryGetValue(fieldName, out typeTypes)) { if (!typeFields.TryGetValue(fieldName, out typeTypes)) {
typeTypes = new Dictionary<Type, Attribute>(); typeTypes = new Dictionary<Type, Attribute>();
typeFields.Add(fieldName, typeTypes); typeFields.Add(fieldName, typeTypes);
} }
Attribute attr; Attribute attr;
if (!typeTypes.TryGetValue(typeof(T), out attr)) { if (!typeTypes.TryGetValue(typeof(T), out attr)) {
if (GetAttrib<T>(classType, fieldName, out attribOut)) { if (GetAttrib<T>(classType, fieldName, out attribOut)) {
typeTypes.Add(typeof(T), attribOut); typeTypes.Add(typeof(T), attribOut);
return true; return true;
} else typeTypes.Add(typeof(T), null); } else
} {
typeTypes.Add(typeof(T), null);
if (attr == null) { }
attribOut = null; }
return false;
} if (attr == null) {
attribOut = null;
attribOut = attr as T; return false;
return true; }
}
attribOut = attr as T;
public static List<PropertyAttribute> GetCachedPropertyAttribs(Type classType, string fieldName) { return true;
Dictionary<string, List<PropertyAttribute>> typeFields; }
if (!typeOrderedPropertyAttributes.TryGetValue(classType, out typeFields)) {
typeFields = new Dictionary<string, List<PropertyAttribute>>(); public static List<PropertyAttribute> GetCachedPropertyAttribs(Type classType, string fieldName) {
typeOrderedPropertyAttributes.Add(classType, typeFields); Dictionary<string, List<PropertyAttribute>> typeFields;
} if (!typeOrderedPropertyAttributes.TryGetValue(classType, out typeFields)) {
typeFields = new Dictionary<string, List<PropertyAttribute>>();
List<PropertyAttribute> typeAttributes; typeOrderedPropertyAttributes.Add(classType, typeFields);
if (!typeFields.TryGetValue(fieldName, out typeAttributes)) { }
FieldInfo field = classType.GetFieldInfo(fieldName);
object[] attribs = field.GetCustomAttributes(typeof(PropertyAttribute), true); List<PropertyAttribute> typeAttributes;
typeAttributes = attribs.Cast<PropertyAttribute>().Reverse().ToList(); //Unity draws them in reverse if (!typeFields.TryGetValue(fieldName, out typeAttributes)) {
typeFields.Add(fieldName, typeAttributes); var field = classType.GetFieldInfo(fieldName);
} var attribs = field.GetCustomAttributes(typeof(PropertyAttribute), true);
typeAttributes = attribs.Cast<PropertyAttribute>().Reverse().ToList(); //Unity draws them in reverse
return typeAttributes; typeFields.Add(fieldName, typeAttributes);
} }
public static bool IsMac() { return typeAttributes;
#if UNITY_2017_1_OR_NEWER }
return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
#else public static bool IsMac() {
return SystemInfo.operatingSystem.StartsWith("Mac"); #if UNITY_2017_1_OR_NEWER
#endif return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
} #else
return SystemInfo.operatingSystem.StartsWith("Mac");
/// <summary> Returns true if this can be casted to <see cref="Type"/></summary> #endif
public static bool IsCastableTo(this Type from, Type to) { }
if (to.IsAssignableFrom(from)) return true;
var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static) /// <summary> Returns true if this can be casted to <see cref="Type"/></summary>
.Where( public static bool IsCastableTo(this Type from, Type to) {
m => m.ReturnType == to && if (to.IsAssignableFrom(from))
(m.Name == "op_Implicit" || {
m.Name == "op_Explicit") return true;
); }
return methods.Count() > 0;
} var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(
/// <summary> m => m.ReturnType == to &&
/// Looking for ports with value Type compatible with a given type. (m.Name == "op_Implicit" ||
/// </summary> m.Name == "op_Explicit")
/// <param name="nodeType">Node to search</param> );
/// <param name="compatibleType">Type to find compatiblities</param> return methods.Count() > 0;
/// <param name="direction"></param> }
/// <returns>True if NodeType has some port with value type compatible</returns>
public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { /// <summary>
Type findType = typeof(XNode.Node.InputAttribute); /// Looking for ports with value Type compatible with a given type.
if (direction == XNode.NodePort.IO.Output) /// </summary>
findType = typeof(XNode.Node.OutputAttribute); /// <param name="nodeType">Node to search</param>
/// <param name="compatibleType">Type to find compatiblities</param>
//Get All fields from node type and we go filter only field with portAttribute. /// <param name="direction"></param>
//This way is possible to know the values of the all ports and if have some with compatible value tue /// <returns>True if NodeType has some port with value type compatible</returns>
foreach (FieldInfo f in XNode.NodeDataCache.GetNodeFields(nodeType)) { public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) {
var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault(); var findType = typeof(XNode.Node.InputAttribute);
if (portAttribute != null) { if (direction == XNode.NodePort.IO.Output)
if (IsCastableTo(f.FieldType, compatibleType)) { {
return true; findType = typeof(XNode.Node.OutputAttribute);
} }
}
} //Get All fields from node type and we go filter only field with portAttribute.
//This way is possible to know the values of the all ports and if have some with compatible value tue
return false; foreach (var f in XNode.NodeDataCache.GetNodeFields(nodeType)) {
} var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault();
if (portAttribute != null) {
/// <summary> if (IsCastableTo(f.FieldType, compatibleType)) {
/// Filter only node types that contains some port value type compatible with an given type return true;
/// </summary> }
/// <param name="nodeTypes">List with all nodes type to filter</param> }
/// <param name="compatibleType">Compatible Type to Filter</param> }
/// <returns>Return Only Node Types with ports compatible, or an empty list</returns>
public static List<Type> GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { return false;
//Result List }
List<Type> filteredTypes = new List<Type>();
/// <summary>
//Return empty list /// Filter only node types that contains some port value type compatible with an given type
if (nodeTypes == null) { return filteredTypes; } /// </summary>
if (compatibleType == null) { return filteredTypes; } /// <param name="nodeTypes">List with all nodes type to filter</param>
/// <param name="compatibleType">Compatible Type to Filter</param>
//Find compatiblity /// <returns>Return Only Node Types with ports compatible, or an empty list</returns>
foreach (Type findType in nodeTypes) { public static List<Type> GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) {
if (HasCompatiblePortType(findType, compatibleType, direction)) { //Result List
filteredTypes.Add(findType); var filteredTypes = new List<Type>();
}
} //Return empty list
if (nodeTypes == null) { return filteredTypes; }
return filteredTypes; if (compatibleType == null) { return filteredTypes; }
}
//Find compatiblity
foreach (var findType in nodeTypes) {
/// <summary> Return a prettiefied type name. </summary> if (HasCompatiblePortType(findType, compatibleType, direction)) {
public static string PrettyName(this Type type) { filteredTypes.Add(findType);
if (type == null) return "null"; }
if (type == typeof(System.Object)) return "object"; }
if (type == typeof(float)) return "float";
else if (type == typeof(int)) return "int"; return filteredTypes;
else if (type == typeof(long)) return "long"; }
else if (type == typeof(double)) return "double";
else if (type == typeof(string)) return "string";
else if (type == typeof(bool)) return "bool"; /// <summary> Return a prettiefied type name. </summary>
else if (type.IsGenericType) { public static string PrettyName(this Type type) {
string s = ""; if (type == null)
Type genericType = type.GetGenericTypeDefinition(); {
if (genericType == typeof(List<>)) s = "List"; return "null";
else s = type.GetGenericTypeDefinition().ToString(); }
Type[] types = type.GetGenericArguments(); if (type == typeof(System.Object))
string[] stypes = new string[types.Length]; {
for (int i = 0; i < types.Length; i++) { return "object";
stypes[i] = types[i].PrettyName(); }
}
return s + "<" + string.Join(", ", stypes) + ">"; if (type == typeof(float))
} else if (type.IsArray) { {
string rank = ""; return "float";
for (int i = 1; i < type.GetArrayRank(); i++) { }
rank += ","; else if (type == typeof(int))
} {
Type elementType = type.GetElementType(); return "int";
if (!elementType.IsArray) return elementType.PrettyName() + "[" + rank + "]"; }
else { else if (type == typeof(long))
string s = elementType.PrettyName(); {
int i = s.IndexOf('['); return "long";
return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i); }
} else if (type == typeof(double))
} else return type.ToString(); {
} return "double";
}
/// <summary> Returns the default name for the node type. </summary> else if (type == typeof(string))
public static string NodeDefaultName(Type type) { {
string typeName = type.Name; return "string";
// Automatically remove redundant 'Node' postfix }
if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); else if (type == typeof(bool))
typeName = UnityEditor.ObjectNames.NicifyVariableName(typeName); {
return typeName; return "bool";
} }
else if (type.IsGenericType) {
/// <summary> Returns the default creation path for the node type. </summary> var s = "";
public static string NodeDefaultPath(Type type) { var genericType = type.GetGenericTypeDefinition();
string typePath = type.ToString().Replace('.', '/'); if (genericType == typeof(List<>))
// Automatically remove redundant 'Node' postfix {
if (typePath.EndsWith("Node")) typePath = typePath.Substring(0, typePath.LastIndexOf("Node")); s = "List";
typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath); }
return typePath; else
} {
s = type.GetGenericTypeDefinition().ToString();
/// <summary>Creates a new C# Class.</summary> }
[MenuItem("Assets/Create/xNode/Node C# Script", false, 89)]
private static void CreateNode() { var types = type.GetGenericArguments();
string[] guids = AssetDatabase.FindAssets("xNode_NodeTemplate.cs"); var stypes = new string[types.Length];
if (guids.Length == 0) { for (var i = 0; i < types.Length; i++) {
Debug.LogWarning("xNode_NodeTemplate.cs.txt not found in asset database"); stypes[i] = types[i].PrettyName();
return; }
} return s + "<" + string.Join(", ", stypes) + ">";
string path = AssetDatabase.GUIDToAssetPath(guids[0]); } else if (type.IsArray) {
CreateFromTemplate( var rank = "";
"NewNode.cs", for (var i = 1; i < type.GetArrayRank(); i++) {
path rank += ",";
); }
} var elementType = type.GetElementType();
if (!elementType.IsArray)
/// <summary>Creates a new C# Class.</summary> {
[MenuItem("Assets/Create/xNode/NodeGraph C# Script", false, 89)] return elementType.PrettyName() + "[" + rank + "]";
private static void CreateGraph() { }
string[] guids = AssetDatabase.FindAssets("xNode_NodeGraphTemplate.cs"); else {
if (guids.Length == 0) { string s = elementType.PrettyName();
Debug.LogWarning("xNode_NodeGraphTemplate.cs.txt not found in asset database"); var i = s.IndexOf('[');
return; return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i);
} }
string path = AssetDatabase.GUIDToAssetPath(guids[0]); } else
CreateFromTemplate( {
"NewNodeGraph.cs", return type.ToString();
path }
); }
}
/// <summary> Returns the default name for the node type. </summary>
public static void CreateFromTemplate(string initialName, string templatePath) { public static string NodeDefaultName(Type type) {
ProjectWindowUtil.StartNameEditingIfProjectWindowExists( var typeName = type.Name;
0, // Automatically remove redundant 'Node' postfix
ScriptableObject.CreateInstance<DoCreateCodeFile>(), if (typeName.EndsWith("Node"))
initialName, {
scriptIcon, typeName = typeName.Substring(0, typeName.LastIndexOf("Node"));
templatePath }
);
} typeName = UnityEditor.ObjectNames.NicifyVariableName(typeName);
return typeName;
/// Inherits from EndNameAction, must override EndNameAction.Action }
public class DoCreateCodeFile : UnityEditor.ProjectWindowCallback.EndNameEditAction {
public override void Action(int instanceId, string pathName, string resourceFile) { /// <summary> Returns the default creation path for the node type. </summary>
Object o = CreateScript(pathName, resourceFile); public static string NodeDefaultPath(Type type) {
ProjectWindowUtil.ShowCreatedAsset(o); var typePath = type.ToString().Replace('.', '/');
} // Automatically remove redundant 'Node' postfix
} if (typePath.EndsWith("Node"))
{
/// <summary>Creates Script from Template's path.</summary> typePath = typePath.Substring(0, typePath.LastIndexOf("Node"));
internal static UnityEngine.Object CreateScript(string pathName, string templatePath) { }
string className = Path.GetFileNameWithoutExtension(pathName).Replace(" ", string.Empty);
string templateText = string.Empty; typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath);
return typePath;
UTF8Encoding encoding = new UTF8Encoding(true, false); }
if (File.Exists(templatePath)) { /// <summary>Creates a new C# Class.</summary>
/// Read procedures. [MenuItem("Assets/Create/xNode/Node C# Script", false, 89)]
StreamReader reader = new StreamReader(templatePath); private static void CreateNode() {
templateText = reader.ReadToEnd(); var guids = AssetDatabase.FindAssets("xNode_NodeTemplate.cs");
reader.Close(); if (guids.Length == 0) {
Debug.LogWarning("xNode_NodeTemplate.cs.txt not found in asset database");
templateText = templateText.Replace("#SCRIPTNAME#", className); return;
templateText = templateText.Replace("#NOTRIM#", string.Empty); }
/// You can replace as many tags you make on your templates, just repeat Replace function var path = AssetDatabase.GUIDToAssetPath(guids[0]);
/// e.g.: CreateFromTemplate(
/// templateText = templateText.Replace("#NEWTAG#", "MyText"); "NewNode.cs",
path
/// Write procedures. );
}
StreamWriter writer = new StreamWriter(Path.GetFullPath(pathName), false, encoding);
writer.Write(templateText); /// <summary>Creates a new C# Class.</summary>
writer.Close(); [MenuItem("Assets/Create/xNode/NodeGraph C# Script", false, 89)]
private static void CreateGraph() {
AssetDatabase.ImportAsset(pathName); var guids = AssetDatabase.FindAssets("xNode_NodeGraphTemplate.cs");
return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object)); if (guids.Length == 0) {
} else { Debug.LogWarning("xNode_NodeGraphTemplate.cs.txt not found in asset database");
Debug.LogError(string.Format("The template file was not found: {0}", templatePath)); return;
return null; }
} var path = AssetDatabase.GUIDToAssetPath(guids[0]);
} CreateFromTemplate(
} "NewNodeGraph.cs",
} path
);
}
public static void CreateFromTemplate(string initialName, string templatePath) {
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(
0,
ScriptableObject.CreateInstance<DoCreateCodeFile>(),
initialName,
scriptIcon,
templatePath
);
}
/// Inherits from EndNameAction, must override EndNameAction.Action
public class DoCreateCodeFile : UnityEditor.ProjectWindowCallback.EndNameEditAction {
public override void Action(int instanceId, string pathName, string resourceFile) {
var o = CreateScript(pathName, resourceFile);
ProjectWindowUtil.ShowCreatedAsset(o);
}
}
/// <summary>Creates Script from Template's path.</summary>
internal static UnityEngine.Object CreateScript(string pathName, string templatePath) {
var className = Path.GetFileNameWithoutExtension(pathName).Replace(" ", string.Empty);
var templateText = string.Empty;
var encoding = new UTF8Encoding(true, false);
if (File.Exists(templatePath)) {
/// Read procedures.
var reader = new StreamReader(templatePath);
templateText = reader.ReadToEnd();
reader.Close();
templateText = templateText.Replace("#SCRIPTNAME#", className);
templateText = templateText.Replace("#NOTRIM#", string.Empty);
/// You can replace as many tags you make on your templates, just repeat Replace function
/// e.g.:
/// templateText = templateText.Replace("#NEWTAG#", "MyText");
/// Write procedures.
var writer = new StreamWriter(Path.GetFullPath(pathName), false, encoding);
writer.Write(templateText);
writer.Close();
AssetDatabase.ImportAsset(pathName);
return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object));
} else {
Debug.LogError(string.Format("The template file was not found: {0}", templatePath));
return null;
}
}
}
}

View File

@ -1,216 +1,253 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEditor; using UnityEditor;
using UnityEditor.Callbacks; using UnityEditor.Callbacks;
using UnityEngine; using UnityEngine;
using System; using System;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
namespace XNodeEditor { namespace XNodeEditor {
[InitializeOnLoad] [InitializeOnLoad]
public partial class NodeEditorWindow : EditorWindow { public partial class NodeEditorWindow : EditorWindow {
public static NodeEditorWindow current; public static NodeEditorWindow current;
/// <summary> Stores node positions for all nodePorts. </summary> /// <summary> Stores node positions for all nodePorts. </summary>
public Dictionary<XNode.NodePort, Rect> portConnectionPoints { get { return _portConnectionPoints; } } public Dictionary<XNode.NodePort, Rect> portConnectionPoints { get { return _portConnectionPoints; } }
private Dictionary<XNode.NodePort, Rect> _portConnectionPoints = new Dictionary<XNode.NodePort, Rect>(); private Dictionary<XNode.NodePort, Rect> _portConnectionPoints = new Dictionary<XNode.NodePort, Rect>();
[SerializeField] private NodePortReference[] _references = new NodePortReference[0]; [SerializeField] private NodePortReference[] _references = new NodePortReference[0];
[SerializeField] private Rect[] _rects = new Rect[0]; [SerializeField] private Rect[] _rects = new Rect[0];
private Func<bool> isDocked { private Func<bool> isDocked {
get { get {
if (_isDocked == null) _isDocked = this.GetIsDockedDelegate(); if (_isDocked == null)
return _isDocked; {
} _isDocked = this.GetIsDockedDelegate();
} }
private Func<bool> _isDocked;
return _isDocked;
[System.Serializable] private class NodePortReference { }
[SerializeField] private XNode.Node _node; }
[SerializeField] private string _name; private Func<bool> _isDocked;
public NodePortReference(XNode.NodePort nodePort) { [System.Serializable] private class NodePortReference {
_node = nodePort.node; [SerializeField] private XNode.Node _node;
_name = nodePort.fieldName; [SerializeField] private string _name;
}
public NodePortReference(XNode.NodePort nodePort) {
public XNode.NodePort GetNodePort() { _node = nodePort.node;
if (_node == null) { _name = nodePort.fieldName;
return null; }
}
return _node.GetPort(_name); public XNode.NodePort GetNodePort() {
} if (_node == null) {
} return null;
}
private void OnDisable() { return _node.GetPort(_name);
// Cache portConnectionPoints before serialization starts }
int count = portConnectionPoints.Count; }
_references = new NodePortReference[count];
_rects = new Rect[count]; private void OnDisable() {
int index = 0; // Cache portConnectionPoints before serialization starts
foreach (var portConnectionPoint in portConnectionPoints) { var count = portConnectionPoints.Count;
_references[index] = new NodePortReference(portConnectionPoint.Key); _references = new NodePortReference[count];
_rects[index] = portConnectionPoint.Value; _rects = new Rect[count];
index++; var index = 0;
} foreach (var portConnectionPoint in portConnectionPoints) {
} _references[index] = new NodePortReference(portConnectionPoint.Key);
_rects[index] = portConnectionPoint.Value;
private void OnEnable() { index++;
// Reload portConnectionPoints if there are any }
int length = _references.Length; }
if (length == _rects.Length) {
for (int i = 0; i < length; i++) { private void OnEnable() {
XNode.NodePort nodePort = _references[i].GetNodePort(); // Reload portConnectionPoints if there are any
if (nodePort != null) var length = _references.Length;
_portConnectionPoints.Add(nodePort, _rects[i]); if (length == _rects.Length) {
} for (var i = 0; i < length; i++) {
} var nodePort = _references[i].GetNodePort();
} if (nodePort != null)
{
public Dictionary<XNode.Node, Vector2> nodeSizes { get { return _nodeSizes; } } _portConnectionPoints.Add(nodePort, _rects[i]);
private Dictionary<XNode.Node, Vector2> _nodeSizes = new Dictionary<XNode.Node, Vector2>(); }
public XNode.NodeGraph graph; }
public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } } }
private Vector2 _panOffset; }
public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom); Repaint(); } }
private float _zoom = 1; public Dictionary<XNode.Node, Vector2> nodeSizes { get { return _nodeSizes; } }
private Dictionary<XNode.Node, Vector2> _nodeSizes = new Dictionary<XNode.Node, Vector2>();
void OnFocus() { public XNode.NodeGraph graph;
current = this; public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } }
ValidateGraphEditor(); private Vector2 _panOffset;
if (graphEditor != null) { public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom); Repaint(); } }
graphEditor.OnWindowFocus(); private float _zoom = 1;
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
} private void OnFocus() {
current = this;
dragThreshold = Math.Max(1f, Screen.width / 1000f); ValidateGraphEditor();
} if (graphEditor != null) {
graphEditor.OnWindowFocus();
void OnLostFocus() { if (NodeEditorPreferences.GetSettings().autoSave)
if (graphEditor != null) graphEditor.OnWindowFocusLost(); {
} AssetDatabase.SaveAssets();
}
[InitializeOnLoadMethod] }
private static void OnLoad() {
Selection.selectionChanged -= OnSelectionChanged; dragThreshold = Math.Max(1f, Screen.width / 1000f);
Selection.selectionChanged += OnSelectionChanged; }
}
private void OnLostFocus() {
/// <summary> Handle Selection Change events</summary> if (graphEditor != null)
private static void OnSelectionChanged() { {
XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph; graphEditor.OnWindowFocusLost();
if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { }
if (NodeEditorPreferences.GetSettings().openOnCreate) Open(nodeGraph); }
}
} [InitializeOnLoadMethod]
private static void OnLoad() {
/// <summary> Make sure the graph editor is assigned and to the right object </summary> Selection.selectionChanged -= OnSelectionChanged;
private void ValidateGraphEditor() { Selection.selectionChanged += OnSelectionChanged;
NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); }
if (this.graphEditor != graphEditor && graphEditor != null) {
this.graphEditor = graphEditor; /// <summary> Handle Selection Change events</summary>
graphEditor.OnOpen(); private static void OnSelectionChanged() {
} var nodeGraph = Selection.activeObject as XNode.NodeGraph;
} if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) {
if (NodeEditorPreferences.GetSettings().openOnCreate)
/// <summary> Create editor window </summary> {
public static NodeEditorWindow Init() { Open(nodeGraph);
NodeEditorWindow w = CreateInstance<NodeEditorWindow>(); }
w.titleContent = new GUIContent("xNode"); }
w.wantsMouseMove = true; }
w.Show();
return w; /// <summary> Make sure the graph editor is assigned and to the right object </summary>
} private void ValidateGraphEditor() {
var graphEditor = NodeGraphEditor.GetEditor(graph, this);
public void Save() { if (this.graphEditor != graphEditor && graphEditor != null) {
if (AssetDatabase.Contains(graph)) { this.graphEditor = graphEditor;
EditorUtility.SetDirty(graph); graphEditor.OnOpen();
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); }
} else SaveAs(); }
}
/// <summary> Create editor window </summary>
public void SaveAs() { public static NodeEditorWindow Init() {
string path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", ""); var w = CreateInstance<NodeEditorWindow>();
if (string.IsNullOrEmpty(path)) return; w.titleContent = new GUIContent("xNode");
else { w.wantsMouseMove = true;
XNode.NodeGraph existingGraph = AssetDatabase.LoadAssetAtPath<XNode.NodeGraph>(path); w.Show();
if (existingGraph != null) AssetDatabase.DeleteAsset(path); return w;
AssetDatabase.CreateAsset(graph, path); }
EditorUtility.SetDirty(graph);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); public void Save() {
} if (AssetDatabase.Contains(graph)) {
} EditorUtility.SetDirty(graph);
if (NodeEditorPreferences.GetSettings().autoSave)
private void DraggableWindow(int windowID) { {
GUI.DragWindow(); AssetDatabase.SaveAssets();
} }
} else
public Vector2 WindowToGridPosition(Vector2 windowPosition) { {
return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom; SaveAs();
} }
}
public Vector2 GridToWindowPosition(Vector2 gridPosition) {
return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom); public void SaveAs() {
} var path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", "");
if (string.IsNullOrEmpty(path))
public Rect GridToWindowRectNoClipped(Rect gridRect) { {
gridRect.position = GridToWindowPositionNoClipped(gridRect.position); return;
return gridRect; }
} else {
var existingGraph = AssetDatabase.LoadAssetAtPath<XNode.NodeGraph>(path);
public Rect GridToWindowRect(Rect gridRect) { if (existingGraph != null)
gridRect.position = GridToWindowPosition(gridRect.position); {
gridRect.size /= zoom; AssetDatabase.DeleteAsset(path);
return gridRect; }
}
AssetDatabase.CreateAsset(graph, path);
public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) { EditorUtility.SetDirty(graph);
Vector2 center = position.size * 0.5f; if (NodeEditorPreferences.GetSettings().autoSave)
// UI Sharpness complete fix - Round final offset not panOffset {
float xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x)); AssetDatabase.SaveAssets();
float yOffset = Mathf.Round(center.y * zoom + (panOffset.y + gridPosition.y)); }
return new Vector2(xOffset, yOffset); }
} }
public void SelectNode(XNode.Node node, bool add) { private void DraggableWindow(int windowID) {
if (add) { GUI.DragWindow();
List<Object> selection = new List<Object>(Selection.objects); }
selection.Add(node);
Selection.objects = selection.ToArray(); public Vector2 WindowToGridPosition(Vector2 windowPosition) {
} else Selection.objects = new Object[] { node }; return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom;
} }
public void DeselectNode(XNode.Node node) { public Vector2 GridToWindowPosition(Vector2 gridPosition) {
List<Object> selection = new List<Object>(Selection.objects); return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom);
selection.Remove(node); }
Selection.objects = selection.ToArray();
} public Rect GridToWindowRectNoClipped(Rect gridRect) {
gridRect.position = GridToWindowPositionNoClipped(gridRect.position);
[OnOpenAsset(0)] return gridRect;
public static bool OnOpen(int instanceID, int line) { }
XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph;
if (nodeGraph != null) { public Rect GridToWindowRect(Rect gridRect) {
Open(nodeGraph); gridRect.position = GridToWindowPosition(gridRect.position);
return true; gridRect.size /= zoom;
} return gridRect;
return false; }
}
public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) {
/// <summary>Open the provided graph in the NodeEditor</summary> var center = position.size * 0.5f;
public static NodeEditorWindow Open(XNode.NodeGraph graph) { // UI Sharpness complete fix - Round final offset not panOffset
if (!graph) return null; var xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x));
var yOffset = Mathf.Round(center.y * zoom + (panOffset.y + gridPosition.y));
NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; return new Vector2(xOffset, yOffset);
w.wantsMouseMove = true; }
w.graph = graph;
return w; public void SelectNode(XNode.Node node, bool add) {
} if (add) {
var selection = new List<Object>(Selection.objects);
/// <summary> Repaint all open NodeEditorWindows. </summary> selection.Add(node);
public static void RepaintAll() { Selection.objects = selection.ToArray();
NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll<NodeEditorWindow>(); } else
for (int i = 0; i < windows.Length; i++) { {
windows[i].Repaint(); Selection.objects = new Object[] { node };
} }
} }
}
} public void DeselectNode(XNode.Node node) {
var selection = new List<Object>(Selection.objects);
selection.Remove(node);
Selection.objects = selection.ToArray();
}
[OnOpenAsset(0)]
public static bool OnOpen(int instanceID, int line) {
var nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph;
if (nodeGraph != null) {
Open(nodeGraph);
return true;
}
return false;
}
/// <summary>Open the provided graph in the NodeEditor</summary>
public static NodeEditorWindow Open(XNode.NodeGraph graph) {
if (!graph)
{
return null;
}
var w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
w.wantsMouseMove = true;
w.graph = graph;
return w;
}
/// <summary> Repaint all open NodeEditorWindows. </summary>
public static void RepaintAll() {
var windows = Resources.FindObjectsOfTypeAll<NodeEditorWindow>();
for (var i = 0; i < windows.Length; i++) {
windows[i].Repaint();
}
}
}
}

View File

@ -44,9 +44,13 @@ namespace XNodeEditor {
//Check if type has the CreateNodeMenuAttribute //Check if type has the CreateNodeMenuAttribute
XNode.Node.CreateNodeMenuAttribute attrib; XNode.Node.CreateNodeMenuAttribute attrib;
if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path
{
return attrib.menuName; return attrib.menuName;
}
else // Return generated path else // Return generated path
{
return NodeEditorUtilities.NodeDefaultPath(type); return NodeEditorUtilities.NodeDefaultPath(type);
}
} }
/// <summary> The order by which the menu items are displayed. </summary> /// <summary> The order by which the menu items are displayed. </summary>
@ -54,9 +58,13 @@ namespace XNodeEditor {
//Check if type has the CreateNodeMenuAttribute //Check if type has the CreateNodeMenuAttribute
XNode.Node.CreateNodeMenuAttribute attrib; XNode.Node.CreateNodeMenuAttribute attrib;
if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path
{
return attrib.order; return attrib.order;
}
else else
{
return 0; return 0;
}
} }
/// <summary> /// <summary>
@ -74,7 +82,7 @@ namespace XNodeEditor {
/// <param name="compatibleType">Use it to filter only nodes with ports value type, compatible with this type</param> /// <param name="compatibleType">Use it to filter only nodes with ports value type, compatible with this type</param>
/// <param name="direction">Direction of the compatiblity</param> /// <param name="direction">Direction of the compatiblity</param>
public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) {
Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); var pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition);
Type[] nodeTypes; Type[] nodeTypes;
@ -84,31 +92,53 @@ namespace XNodeEditor {
nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(GetNodeMenuOrder).ToArray(); nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(GetNodeMenuOrder).ToArray();
} }
for (int i = 0; i < nodeTypes.Length; i++) { for (var i = 0; i < nodeTypes.Length; i++) {
Type type = nodeTypes[i]; var type = nodeTypes[i];
//Get node context menu path //Get node context menu path
string path = GetNodeMenuName(type); var path = GetNodeMenuName(type);
if (string.IsNullOrEmpty(path)) continue; if (string.IsNullOrEmpty(path))
{
continue;
}
// Check if user is allowed to add more of given node type // Check if user is allowed to add more of given node type
XNode.Node.DisallowMultipleNodesAttribute disallowAttrib; XNode.Node.DisallowMultipleNodesAttribute disallowAttrib;
bool disallowed = false; var disallowed = false;
if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) { if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) {
int typeCount = target.nodes.Count(x => x.GetType() == type); var typeCount = target.nodes.Count(x => x.GetType() == type);
if (typeCount >= disallowAttrib.max) disallowed = true; if (typeCount >= disallowAttrib.max)
{
disallowed = true;
}
} }
// Add node entry to context menu // Add node entry to context menu
if (disallowed) menu.AddItem(new GUIContent(path), false, null); if (disallowed)
else menu.AddItem(new GUIContent(path), false, () => { {
XNode.Node node = CreateNode(type, pos); menu.AddItem(new GUIContent(path), false, null);
if (node != null) NodeEditorWindow.current.AutoConnect(node); // handle null nodes to avoid nullref exceptions }
}); else
{
menu.AddItem(new GUIContent(path), false, () => {
var node = CreateNode(type, pos);
if (node != null)
{
NodeEditorWindow.current.AutoConnect(node); // handle null nodes to avoid nullref exceptions
}
});
}
} }
menu.AddSeparator(""); menu.AddSeparator("");
if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos)); if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0)
else menu.AddDisabledItem(new GUIContent("Paste")); {
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.AddItem(new GUIContent("Preferences"), false, () => NodeEditorReflection.OpenPreferences());
menu.AddCustomContextMenuItems(target); menu.AddCustomContextMenuItems(target);
} }
@ -117,11 +147,11 @@ namespace XNodeEditor {
/// <param name="output"> The output this noodle comes from. Never null. </param> /// <param name="output"> The output this noodle comes from. Never null. </param>
/// <param name="input"> The output this noodle comes from. Can be null if we are dragging the noodle. </param> /// <param name="input"> The output this noodle comes from. Can be null if we are dragging the noodle. </param>
public virtual Gradient GetNoodleGradient(XNode.NodePort output, XNode.NodePort input) { public virtual Gradient GetNoodleGradient(XNode.NodePort output, XNode.NodePort input) {
Gradient grad = new Gradient(); var grad = new Gradient();
// If dragging the noodle, draw solid, slightly transparent // If dragging the noodle, draw solid, slightly transparent
if (input == null) { if (input == null) {
Color a = GetTypeColor(output.ValueType); var a = GetTypeColor(output.ValueType);
grad.SetKeys( grad.SetKeys(
new GradientColorKey[] { new GradientColorKey(a, 0f) }, new GradientColorKey[] { new GradientColorKey(a, 0f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f) } new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f) }
@ -129,8 +159,8 @@ namespace XNodeEditor {
} }
// If normal, draw gradient fading from one input color to the other // If normal, draw gradient fading from one input color to the other
else { else {
Color a = GetTypeColor(output.ValueType); var a = GetTypeColor(output.ValueType);
Color b = GetTypeColor(input.ValueType); var b = GetTypeColor(input.ValueType);
// If any port is hovered, tint white // If any port is hovered, tint white
if (window.hoveredPort == output || window.hoveredPort == input) { if (window.hoveredPort == output || window.hoveredPort == input) {
a = Color.Lerp(a, Color.white, 0.8f); a = Color.Lerp(a, Color.white, 0.8f);
@ -176,7 +206,9 @@ namespace XNodeEditor {
/// <returns></returns> /// <returns></returns>
public virtual GUIStyle GetPortStyle(XNode.NodePort port) { public virtual GUIStyle GetPortStyle(XNode.NodePort port) {
if (port.direction == XNode.NodePort.IO.Input) if (port.direction == XNode.NodePort.IO.Input)
{
return NodeEditorResources.styles.inputPort; return NodeEditorResources.styles.inputPort;
}
return NodeEditorResources.styles.outputPort; return NodeEditorResources.styles.outputPort;
} }
@ -194,11 +226,11 @@ namespace XNodeEditor {
/// <summary> Override to display custom tooltips </summary> /// <summary> Override to display custom tooltips </summary>
public virtual string GetPortTooltip(XNode.NodePort port) { public virtual string GetPortTooltip(XNode.NodePort port) {
Type portType = port.ValueType; var portType = port.ValueType;
string tooltip = ""; var tooltip = "";
tooltip = portType.PrettyName(); tooltip = portType.PrettyName();
if (port.IsOutput) { if (port.IsOutput) {
object obj = port.node.GetValue(port); var obj = port.node.GetValue(port);
tooltip += " = " + (obj != null ? obj.ToString() : "null"); tooltip += " = " + (obj != null ? obj.ToString() : "null");
} }
return tooltip; return tooltip;
@ -206,19 +238,38 @@ namespace XNodeEditor {
/// <summary> Deal with objects dropped into the graph through DragAndDrop </summary> /// <summary> Deal with objects dropped into the graph through DragAndDrop </summary>
public virtual void OnDropObjects(UnityEngine.Object[] objects) { public virtual void OnDropObjects(UnityEngine.Object[] objects) {
if (GetType() != typeof(NodeGraphEditor)) Debug.Log("No OnDropObjects override defined for " + GetType()); if (GetType() != typeof(NodeGraphEditor))
{
Debug.Log("No OnDropObjects override defined for " + GetType());
}
} }
/// <summary> Create a node and save it in the graph asset </summary> /// <summary> Create a node and save it in the graph asset </summary>
public virtual XNode.Node CreateNode(Type type, Vector2 position) { public virtual XNode.Node CreateNode(Type type, Vector2 position) {
Undo.RecordObject(target, "Create Node"); Undo.RecordObject(target, "Create Node");
XNode.Node node = target.AddNode(type); var node = target.AddNode(type);
if (node == null) return null; // handle null nodes to avoid nullref exceptions if (node == null)
{
return null; // handle null nodes to avoid nullref exceptions
}
Undo.RegisterCreatedObjectUndo(node, "Create Node"); Undo.RegisterCreatedObjectUndo(node, "Create Node");
node.position = position; node.position = position;
if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); if (node.name == null || node.name.Trim() == "")
if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); {
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); node.name = NodeEditorUtilities.NodeDefaultName(type);
}
if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target)))
{
AssetDatabase.AddObjectToAsset(node, target);
}
if (NodeEditorPreferences.GetSettings().autoSave)
{
AssetDatabase.SaveAssets();
}
NodeEditorWindow.RepaintAll(); NodeEditorWindow.RepaintAll();
return node; return node;
} }
@ -226,19 +277,27 @@ namespace XNodeEditor {
/// <summary> Creates a copy of the original node in the graph </summary> /// <summary> Creates a copy of the original node in the graph </summary>
public virtual XNode.Node CopyNode(XNode.Node original) { public virtual XNode.Node CopyNode(XNode.Node original) {
Undo.RecordObject(target, "Duplicate Node"); Undo.RecordObject(target, "Duplicate Node");
XNode.Node node = target.CopyNode(original); var node = target.CopyNode(original);
Undo.RegisterCreatedObjectUndo(node, "Duplicate Node"); Undo.RegisterCreatedObjectUndo(node, "Duplicate Node");
node.name = original.name; node.name = original.name;
if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target)))
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); {
AssetDatabase.AddObjectToAsset(node, target);
}
if (NodeEditorPreferences.GetSettings().autoSave)
{
AssetDatabase.SaveAssets();
}
return node; return node;
} }
/// <summary> Return false for nodes that can't be removed </summary> /// <summary> Return false for nodes that can't be removed </summary>
public virtual bool CanRemove(XNode.Node node) { public virtual bool CanRemove(XNode.Node node) {
// Check graph attributes to see if this node is required // Check graph attributes to see if this node is required
Type graphType = target.GetType(); var graphType = target.GetType();
XNode.NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( var attribs = Array.ConvertAll(
graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute); graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute);
if (attribs.Any(x => x.Requires(node.GetType()))) { if (attribs.Any(x => x.Requires(node.GetType()))) {
if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) { if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) {
@ -250,17 +309,26 @@ namespace XNodeEditor {
/// <summary> Safely remove a node and all its connections. </summary> /// <summary> Safely remove a node and all its connections. </summary>
public virtual void RemoveNode(XNode.Node node) { public virtual void RemoveNode(XNode.Node node) {
if (!CanRemove(node)) return; if (!CanRemove(node))
{
return;
}
// Remove the node // Remove the node
Undo.RecordObject(node, "Delete Node"); Undo.RecordObject(node, "Delete Node");
Undo.RecordObject(target, "Delete Node"); Undo.RecordObject(target, "Delete Node");
foreach (var port in node.Ports) foreach (var port in node.Ports)
foreach (var conn in port.GetConnections()) foreach (var conn in port.GetConnections())
{
Undo.RecordObject(conn.node, "Delete Node"); Undo.RecordObject(conn.node, "Delete Node");
}
target.RemoveNode(node); target.RemoveNode(node);
Undo.DestroyObjectImmediate(node); Undo.DestroyObjectImmediate(node);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); if (NodeEditorPreferences.GetSettings().autoSave)
{
AssetDatabase.SaveAssets();
}
} }
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
@ -281,4 +349,4 @@ namespace XNodeEditor {
} }
} }
} }
} }

View File

@ -0,0 +1,69 @@
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>
internal class NodeGraphImporter : AssetPostprocessor {
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
foreach (var path in importedAssets) {
// Skip processing anything without the .asset extension
if (Path.GetExtension(path) != ".asset")
{
continue;
}
// Get the object that is requested for deletion
var graph = AssetDatabase.LoadAssetAtPath<NodeGraph>(path);
if (graph == null)
{
continue;
}
// Get attributes
var graphType = graph.GetType();
var attribs = Array.ConvertAll(
graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute);
var position = Vector2.zero;
foreach (var 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)) {
var node = graph.AddNode(type);
node.position = position;
position.x += 200;
if (node.name == null || node.name.Trim() == "")
{
node.name = NodeEditorUtilities.NodeDefaultName(type);
}
if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph)))
{
AssetDatabase.AddObjectToAsset(node, graph);
}
}
}
}
}

View File

@ -14,8 +14,12 @@ namespace XNodeEditor {
/// <summary> Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply. /// <summary> Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply.
public static RenamePopup Show(Object target, float width = 200) { public static RenamePopup Show(Object target, float width = 200) {
RenamePopup window = EditorWindow.GetWindow<RenamePopup>(true, "Rename " + target.name, true); var window = EditorWindow.GetWindow<RenamePopup>(true, "Rename " + target.name, true);
if (current != null) current.Close(); if (current != null)
{
current.Close();
}
current = window; current = window;
window.target = target; window.target = target;
window.input = target.name; window.input = target.name;
@ -26,9 +30,13 @@ namespace XNodeEditor {
} }
private void UpdatePositionToMouse() { private void UpdatePositionToMouse() {
if (Event.current == null) return; if (Event.current == null)
{
return;
}
Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
Rect pos = position; var pos = position;
pos.x = mousePoint.x - position.width * 0.5f; pos.x = mousePoint.x - position.width * 0.5f;
pos.y = mousePoint.y - 10; pos.y = mousePoint.y - 10;
position = pos; position = pos;
@ -47,7 +55,7 @@ namespace XNodeEditor {
GUI.SetNextControlName(inputControlName); GUI.SetNextControlName(inputControlName);
input = EditorGUILayout.TextField(input); input = EditorGUILayout.TextField(input);
EditorGUI.FocusTextInControl(inputControlName); EditorGUI.FocusTextInControl(inputControlName);
Event e = Event.current; var e = Event.current;
// If input is empty, revert name to default instead // If input is empty, revert name to default instead
if (input == null || input.Trim() == "") { if (input == null || input.Trim() == "") {
if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) { if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) {
@ -84,4 +92,4 @@ namespace XNodeEditor {
EditorGUIUtility.editingTextField = false; EditorGUIUtility.editingTextField = false;
} }
} }
} }

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -16,10 +16,10 @@ namespace XNodeEditor {
if (sceneGraph.graph == null) { if (sceneGraph.graph == null) {
if (GUILayout.Button("New graph", GUILayout.Height(40))) { if (GUILayout.Button("New graph", GUILayout.Height(40))) {
if (graphType == null) { if (graphType == null) {
Type[] graphTypes = NodeEditorReflection.GetDerivedTypes(typeof(NodeGraph)); var graphTypes = NodeEditorReflection.GetDerivedTypes(typeof(NodeGraph));
GenericMenu menu = new GenericMenu(); var menu = new GenericMenu();
for (int i = 0; i < graphTypes.Length; i++) { for (var i = 0; i < graphTypes.Length; i++) {
Type graphType = graphTypes[i]; var graphType = graphTypes[i];
menu.AddItem(new GUIContent(graphType.Name), false, () => CreateGraph(graphType)); menu.AddItem(new GUIContent(graphType.Name), false, () => CreateGraph(graphType));
} }
menu.ShowAsContext(); menu.ShowAsContext();
@ -58,11 +58,11 @@ namespace XNodeEditor {
private void OnEnable() { private void OnEnable() {
sceneGraph = target as SceneGraph; sceneGraph = target as SceneGraph;
Type sceneGraphType = sceneGraph.GetType(); var sceneGraphType = sceneGraph.GetType();
if (sceneGraphType == typeof(SceneGraph)) { if (sceneGraphType == typeof(SceneGraph)) {
graphType = null; graphType = null;
} else { } else {
Type baseType = sceneGraphType.BaseType; var baseType = sceneGraphType.BaseType;
if (baseType.IsGenericType) { if (baseType.IsGenericType) {
graphType = sceneGraphType = baseType.GetGenericArguments() [0]; graphType = sceneGraphType = baseType.GetGenericArguments() [0];
} }
@ -75,4 +75,4 @@ namespace XNodeEditor {
sceneGraph.graph.name = sceneGraph.name + "-graph"; sceneGraph.graph.name = sceneGraph.name + "-graph";
} }
} }
} }

View File

@ -1,421 +1,503 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace XNode { namespace XNode {
/// <summary> /// <summary>
/// Base class for all nodes /// Base class for all nodes
/// </summary> /// </summary>
/// <example> /// <example>
/// Classes extending this class will be considered as valid nodes by xNode. /// Classes extending this class will be considered as valid nodes by xNode.
/// <code> /// <code>
/// [System.Serializable] /// [System.Serializable]
/// public class Adder : Node { /// public class Adder : Node {
/// [Input] public float a; /// [Input] public float a;
/// [Input] public float b; /// [Input] public float b;
/// [Output] public float result; /// [Output] public float result;
/// ///
/// // GetValue should be overridden to return a value for any specified output port /// // GetValue should be overridden to return a value for any specified output port
/// public override object GetValue(NodePort port) { /// public override object GetValue(NodePort port) {
/// return a + b; /// return a + b;
/// } /// }
/// } /// }
/// </code> /// </code>
/// </example> /// </example>
[Serializable] [Serializable]
public abstract class Node : ScriptableObject { public abstract class Node : ScriptableObject {
/// <summary> Used by <see cref="InputAttribute"/> and <see cref="OutputAttribute"/> to determine when to display the field value associated with a <see cref="NodePort"/> </summary> /// <summary> Used by <see cref="InputAttribute"/> and <see cref="OutputAttribute"/> to determine when to display the field value associated with a <see cref="NodePort"/> </summary>
public enum ShowBackingValue { public enum ShowBackingValue {
/// <summary> Never show the backing value </summary> /// <summary> Never show the backing value </summary>
Never, Never,
/// <summary> Show the backing value only when the port does not have any active connections </summary> /// <summary> Show the backing value only when the port does not have any active connections </summary>
Unconnected, Unconnected,
/// <summary> Always show the backing value </summary> /// <summary> Always show the backing value </summary>
Always Always
} }
public enum ConnectionType { public enum ConnectionType {
/// <summary> Allow multiple connections</summary> /// <summary> Allow multiple connections</summary>
Multiple, Multiple,
/// <summary> always override the current connection </summary> /// <summary> always override the current connection </summary>
Override, Override,
} }
/// <summary> Tells which types of input to allow </summary> /// <summary> Tells which types of input to allow </summary>
public enum TypeConstraint { public enum TypeConstraint {
/// <summary> Allow all types of input</summary> /// <summary> Allow all types of input</summary>
None, None,
/// <summary> Allow connections where input value type is assignable from output value type (eg. ScriptableObject --> Object)</summary> /// <summary> Allow connections where input value type is assignable from output value type (eg. ScriptableObject --> Object)</summary>
Inherited, Inherited,
/// <summary> Allow only similar types </summary> /// <summary> Allow only similar types </summary>
Strict, Strict,
/// <summary> Allow connections where output value type is assignable from input value type (eg. Object --> ScriptableObject)</summary> /// <summary> Allow connections where output value type is assignable from input value type (eg. Object --> ScriptableObject)</summary>
InheritedInverse, InheritedInverse,
/// <summary> Allow connections where output value type is assignable from input value or input value type is assignable from output value type</summary> /// <summary> Allow connections where output value type is assignable from input value or input value type is assignable from output value type</summary>
InheritedAny InheritedAny
} }
#region Obsolete #region Obsolete
[Obsolete("Use DynamicPorts instead")] [Obsolete("Use DynamicPorts instead")]
public IEnumerable<NodePort> InstancePorts { get { return DynamicPorts; } } public IEnumerable<NodePort> InstancePorts { get { return DynamicPorts; } }
[Obsolete("Use DynamicOutputs instead")] [Obsolete("Use DynamicOutputs instead")]
public IEnumerable<NodePort> InstanceOutputs { get { return DynamicOutputs; } } public IEnumerable<NodePort> InstanceOutputs { get { return DynamicOutputs; } }
[Obsolete("Use DynamicInputs instead")] [Obsolete("Use DynamicInputs instead")]
public IEnumerable<NodePort> InstanceInputs { get { return DynamicInputs; } } public IEnumerable<NodePort> InstanceInputs { get { return DynamicInputs; } }
[Obsolete("Use AddDynamicInput instead")] [Obsolete("Use AddDynamicInput instead")]
public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicInput(type, connectionType, typeConstraint, fieldName); return AddDynamicInput(type, connectionType, typeConstraint, fieldName);
} }
[Obsolete("Use AddDynamicOutput instead")] [Obsolete("Use AddDynamicOutput instead")]
public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicOutput(type, connectionType, typeConstraint, fieldName); return AddDynamicOutput(type, connectionType, typeConstraint, fieldName);
} }
[Obsolete("Use AddDynamicPort instead")] [Obsolete("Use AddDynamicPort instead")]
private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName); return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName);
} }
[Obsolete("Use RemoveDynamicPort instead")] [Obsolete("Use RemoveDynamicPort instead")]
public void RemoveInstancePort(string fieldName) { public void RemoveInstancePort(string fieldName) {
RemoveDynamicPort(fieldName); RemoveDynamicPort(fieldName);
} }
[Obsolete("Use RemoveDynamicPort instead")] [Obsolete("Use RemoveDynamicPort instead")]
public void RemoveInstancePort(NodePort port) { public void RemoveInstancePort(NodePort port) {
RemoveDynamicPort(port); RemoveDynamicPort(port);
} }
[Obsolete("Use ClearDynamicPorts instead")] [Obsolete("Use ClearDynamicPorts instead")]
public void ClearInstancePorts() { public void ClearInstancePorts() {
ClearDynamicPorts(); ClearDynamicPorts();
} }
#endregion #endregion
/// <summary> Iterate over all ports on this node. </summary> /// <summary> Iterate over all ports on this node. </summary>
public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values) yield return port; } } public IEnumerable<NodePort> Ports { get { foreach (NodePort port in ports.Values)
/// <summary> Iterate over all outputs on this node. </summary> {
public IEnumerable<NodePort> Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } } yield return port;
/// <summary> Iterate over all inputs on this node. </summary> }
public IEnumerable<NodePort> Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } } } }
/// <summary> Iterate over all dynamic ports on this node. </summary> /// <summary> Iterate over all outputs on this node. </summary>
public IEnumerable<NodePort> DynamicPorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } } public IEnumerable<NodePort> Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput)
/// <summary> Iterate over all dynamic outputs on this node. </summary> {
public IEnumerable<NodePort> DynamicOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } } yield return port;
/// <summary> Iterate over all dynamic inputs on this node. </summary> }
public IEnumerable<NodePort> DynamicInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } } } } }
/// <summary> Parent <see cref="NodeGraph"/> </summary> /// <summary> Iterate over all inputs on this node. </summary>
[SerializeField] public NodeGraph graph; public IEnumerable<NodePort> Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput)
/// <summary> Position on the <see cref="NodeGraph"/> </summary> {
[SerializeField] public Vector2 position; yield return port;
/// <summary> It is recommended not to modify these at hand. Instead, see <see cref="InputAttribute"/> and <see cref="OutputAttribute"/> </summary> }
[SerializeField] private NodePortDictionary ports = new NodePortDictionary(); } } }
/// <summary> Iterate over all dynamic ports on this node. </summary>
/// <summary> Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable </summary> public IEnumerable<NodePort> DynamicPorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic)
public static NodeGraph graphHotfix; {
yield return port;
protected void OnEnable() { }
if (graphHotfix != null) graph = graphHotfix; } } }
graphHotfix = null; /// <summary> Iterate over all dynamic outputs on this node. </summary>
UpdatePorts(); public IEnumerable<NodePort> DynamicOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput)
Init(); {
} yield return port;
}
/// <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() { /// <summary> Iterate over all dynamic inputs on this node. </summary>
NodeDataCache.UpdatePorts(this, ports); public IEnumerable<NodePort> DynamicInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput)
} {
yield return port;
/// <summary> Initialize node. Called on enable. </summary> }
protected virtual void Init() { } } } }
/// <summary> Parent <see cref="NodeGraph"/> </summary>
/// <summary> Checks all connections for invalid references, and removes them. </summary> [SerializeField] public NodeGraph graph;
public void VerifyConnections() { /// <summary> Position on the <see cref="NodeGraph"/> </summary>
foreach (NodePort port in Ports) port.VerifyConnections(); [SerializeField] public Vector2 position;
} /// <summary> It is recommended not to modify these at hand. Instead, see <see cref="InputAttribute"/> and <see cref="OutputAttribute"/> </summary>
[SerializeField] private NodePortDictionary ports = new NodePortDictionary();
#region Dynamic Ports
/// <summary> Convenience function. </summary> /// <summary> Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable </summary>
/// <seealso cref="AddInstancePort"/> public static NodeGraph graphHotfix;
/// <seealso cref="AddInstanceOutput"/>
public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { protected void OnEnable() {
return AddDynamicPort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName); if (graphHotfix != null)
} {
graph = graphHotfix;
/// <summary> Convenience function. </summary> }
/// <seealso cref="AddInstancePort"/>
/// <seealso cref="AddInstanceInput"/> graphHotfix = null;
public NodePort AddDynamicOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { UpdatePorts();
return AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName); Init();
} }
/// <summary> Add a dynamic, serialized port to this node. </summary> /// <summary> Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list. </summary>
/// <seealso cref="AddDynamicInput"/> public void UpdatePorts() {
/// <seealso cref="AddDynamicOutput"/> NodeDataCache.UpdatePorts(this, ports);
private NodePort AddDynamicPort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { }
if (fieldName == null) {
fieldName = "dynamicInput_0"; /// <summary> Initialize node. Called on enable. </summary>
int i = 0; protected virtual void Init() { }
while (HasPort(fieldName)) fieldName = "dynamicInput_" + (++i);
} else if (HasPort(fieldName)) { /// <summary> Checks all connections for invalid references, and removes them. </summary>
Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); public void VerifyConnections() {
return ports[fieldName]; foreach (NodePort port in Ports)
} {
NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this); port.VerifyConnections();
ports.Add(fieldName, port); }
return port; }
}
#region Dynamic Ports
/// <summary> Remove an dynamic port from the node </summary> /// <summary> Convenience function. </summary>
public void RemoveDynamicPort(string fieldName) { /// <seealso cref="AddInstancePort"/>
NodePort dynamicPort = GetPort(fieldName); /// <seealso cref="AddInstanceOutput"/>
if (dynamicPort == null) throw new ArgumentException("port " + fieldName + " doesn't exist"); public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
RemoveDynamicPort(GetPort(fieldName)); return AddDynamicPort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName);
} }
/// <summary> Remove an dynamic port from the node </summary> /// <summary> Convenience function. </summary>
public void RemoveDynamicPort(NodePort port) { /// <seealso cref="AddInstancePort"/>
if (port == null) throw new ArgumentNullException("port"); /// <seealso cref="AddInstanceInput"/>
else if (port.IsStatic) throw new ArgumentException("cannot remove static port"); public NodePort AddDynamicOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
port.ClearConnections(); return AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName);
ports.Remove(port.fieldName); }
}
/// <summary> Add a dynamic, serialized port to this node. </summary>
/// <summary> Removes all dynamic ports from the node </summary> /// <seealso cref="AddDynamicInput"/>
[ContextMenu("Clear Dynamic Ports")] /// <seealso cref="AddDynamicOutput"/>
public void ClearDynamicPorts() { private NodePort AddDynamicPort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
List<NodePort> dynamicPorts = new List<NodePort>(DynamicPorts); if (fieldName == null) {
foreach (NodePort port in dynamicPorts) { fieldName = "dynamicInput_0";
RemoveDynamicPort(port); var i = 0;
} while (HasPort(fieldName))
} {
#endregion fieldName = "dynamicInput_" + (++i);
}
#region Ports } else if (HasPort(fieldName)) {
/// <summary> Returns output port which matches fieldName </summary> Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this);
public NodePort GetOutputPort(string fieldName) { return ports[fieldName];
NodePort port = GetPort(fieldName); }
if (port == null || port.direction != NodePort.IO.Output) return null; NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this);
else return port; ports.Add(fieldName, port);
} return port;
}
/// <summary> Returns input port which matches fieldName </summary>
public NodePort GetInputPort(string fieldName) { /// <summary> Remove an dynamic port from the node </summary>
NodePort port = GetPort(fieldName); public void RemoveDynamicPort(string fieldName) {
if (port == null || port.direction != NodePort.IO.Input) return null; NodePort dynamicPort = GetPort(fieldName);
else return port; if (dynamicPort == null)
} {
throw new ArgumentException("port " + fieldName + " doesn't exist");
/// <summary> Returns port which matches fieldName </summary> }
public NodePort GetPort(string fieldName) {
NodePort port; RemoveDynamicPort(GetPort(fieldName));
if (ports.TryGetValue(fieldName, out port)) return port; }
else return null;
} /// <summary> Remove an dynamic port from the node </summary>
public void RemoveDynamicPort(NodePort port) {
public bool HasPort(string fieldName) { if (port == null)
return ports.ContainsKey(fieldName); {
} throw new ArgumentNullException("port");
#endregion }
else if (port.IsStatic)
#region Inputs/Outputs {
/// <summary> Return input value for a specified port. Returns fallback value if no ports are connected </summary> throw new ArgumentException("cannot remove static port");
/// <param name="fieldName">Field name of requested input port</param> }
/// <param name="fallback">If no ports are connected, this value will be returned</param>
public T GetInputValue<T>(string fieldName, T fallback = default(T)) { port.ClearConnections();
NodePort port = GetPort(fieldName); ports.Remove(port.fieldName);
if (port != null && port.IsConnected) return port.GetInputValue<T>(); }
else return fallback;
} /// <summary> Removes all dynamic ports from the node </summary>
[ContextMenu("Clear Dynamic Ports")]
/// <summary> Return all input values for a specified port. Returns fallback value if no ports are connected </summary> public void ClearDynamicPorts() {
/// <param name="fieldName">Field name of requested input port</param> List<NodePort> dynamicPorts = new List<NodePort>(DynamicPorts);
/// <param name="fallback">If no ports are connected, this value will be returned</param> foreach (NodePort port in dynamicPorts) {
public T[] GetInputValues<T>(string fieldName, params T[] fallback) { RemoveDynamicPort(port);
NodePort port = GetPort(fieldName); }
if (port != null && port.IsConnected) return port.GetInputValues<T>(); }
else return fallback; #endregion
}
#region Ports
/// <summary> Returns a value based on requested port output. Should be overridden in all derived nodes with outputs. </summary> /// <summary> Returns output port which matches fieldName </summary>
/// <param name="port">The requested port.</param> public NodePort GetOutputPort(string fieldName) {
public virtual object GetValue(NodePort port) { NodePort port = GetPort(fieldName);
Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType()); if (port == null || port.direction != NodePort.IO.Output)
return null; {
} return null;
#endregion }
else
/// <summary> Called after a connection between two <see cref="NodePort"/>s is created </summary> {
/// <param name="from">Output</param> <param name="to">Input</param> return port;
public virtual void OnCreateConnection(NodePort from, NodePort to) { } }
}
/// <summary> Called after a connection is removed from this port </summary>
/// <param name="port">Output or Input</param> /// <summary> Returns input port which matches fieldName </summary>
public virtual void OnRemoveConnection(NodePort port) { } public NodePort GetInputPort(string fieldName) {
NodePort port = GetPort(fieldName);
/// <summary> Disconnect everything from this node </summary> if (port == null || port.direction != NodePort.IO.Input)
public void ClearConnections() { {
foreach (NodePort port in Ports) port.ClearConnections(); return null;
} }
else
#region Attributes {
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary> return port;
[AttributeUsage(AttributeTargets.Field)] }
public class InputAttribute : Attribute { }
public ShowBackingValue backingValue;
public ConnectionType connectionType; /// <summary> Returns port which matches fieldName </summary>
[Obsolete("Use dynamicPortList instead")] public NodePort GetPort(string fieldName) {
public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } NodePort port;
public bool dynamicPortList; if (ports.TryGetValue(fieldName, out port))
public TypeConstraint typeConstraint; {
return port;
/// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary> }
/// <param name="backingValue">Should we display the backing value for this port as an editor field? </param> else
/// <param name="connectionType">Should we allow multiple connections? </param> {
/// <param name="typeConstraint">Constrains which input connections can be made to this port </param> return null;
/// <param name="dynamicPortList">If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays </param> }
public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) { }
this.backingValue = backingValue;
this.connectionType = connectionType; public bool HasPort(string fieldName) {
this.dynamicPortList = dynamicPortList; return ports.ContainsKey(fieldName);
this.typeConstraint = typeConstraint; }
} #endregion
}
#region Inputs/Outputs
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary> /// <summary> Return input value for a specified port. Returns fallback value if no ports are connected </summary>
[AttributeUsage(AttributeTargets.Field)] /// <param name="fieldName">Field name of requested input port</param>
public class OutputAttribute : Attribute { /// <param name="fallback">If no ports are connected, this value will be returned</param>
public ShowBackingValue backingValue; public T GetInputValue<T>(string fieldName, T fallback = default(T)) {
public ConnectionType connectionType; NodePort port = GetPort(fieldName);
[Obsolete("Use dynamicPortList instead")] if (port != null && port.IsConnected)
public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } {
public bool dynamicPortList; return port.GetInputValue<T>();
public TypeConstraint typeConstraint; }
else
/// <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> return fallback;
/// <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) { /// <summary> Return all input values for a specified port. Returns fallback value if no ports are connected </summary>
this.backingValue = backingValue; /// <param name="fieldName">Field name of requested input port</param>
this.connectionType = connectionType; /// <param name="fallback">If no ports are connected, this value will be returned</param>
this.dynamicPortList = dynamicPortList; public T[] GetInputValues<T>(string fieldName, params T[] fallback) {
this.typeConstraint = typeConstraint; NodePort port = GetPort(fieldName);
} if (port != null && port.IsConnected)
{
/// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary> return port.GetInputValues<T>();
/// <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> else
/// <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> {
[Obsolete("Use constructor with TypeConstraint")] return fallback;
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> /// <summary> Returns a value based on requested port output. Should be overridden in all derived nodes with outputs. </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] /// <param name="port">The requested port.</param>
public class CreateNodeMenuAttribute : Attribute { public virtual object GetValue(NodePort port) {
public string menuName; Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType());
public int order; return null;
/// <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> #endregion
public CreateNodeMenuAttribute(string menuName) {
this.menuName = menuName; /// <summary> Called after a connection between two <see cref="NodePort"/>s is created </summary>
this.order = 0; /// <param name="from">Output</param> <param name="to">Input</param>
} public virtual void OnCreateConnection(NodePort from, NodePort to) { }
/// <summary> Manually supply node class with a context menu path </summary> /// <summary> Called after a connection is removed from this port </summary>
/// <param name="menuName"> Path to this node in the context menu. Null or empty hides it. </param> /// <param name="port">Output or Input</param>
/// <param name="order"> The order by which the menu items are displayed. </param> public virtual void OnRemoveConnection(NodePort port) { }
public CreateNodeMenuAttribute(string menuName, int order) {
this.menuName = menuName; /// <summary> Disconnect everything from this node </summary>
this.order = order; public void ClearConnections() {
} foreach (NodePort port in Ports)
} {
port.ClearConnections();
/// <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 #region Attributes
// while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both. /// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
public int max; [AttributeUsage(AttributeTargets.Field)]
/// <summary> Prevents Node of the same type to be added more than once (configurable) to a NodeGraph </summary> public class InputAttribute : Attribute {
/// <param name="max"> How many nodes to allow. Defaults to 1. </param> public ShowBackingValue backingValue;
public DisallowMultipleNodesAttribute(int max = 1) { public ConnectionType connectionType;
this.max = max; [Obsolete("Use dynamicPortList instead")]
} public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } }
} public bool dynamicPortList;
public TypeConstraint typeConstraint;
/// <summary> Specify a color for this node type </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] /// <summary> Mark a serializable field as an input port. You can access this through <see cref="GetInputPort(string)"/> </summary>
public class NodeTintAttribute : Attribute { /// <param name="backingValue">Should we display the backing value for this port as an editor field? </param>
public Color color; /// <param name="connectionType">Should we allow multiple connections? </param>
/// <summary> Specify a color for this node type </summary> /// <param name="typeConstraint">Constrains which input connections can be made to this port </param>
/// <param name="r"> Red [0.0f .. 1.0f] </param> /// <param name="dynamicPortList">If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays </param>
/// <param name="g"> Green [0.0f .. 1.0f] </param> public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) {
/// <param name="b"> Blue [0.0f .. 1.0f] </param> this.backingValue = backingValue;
public NodeTintAttribute(float r, float g, float b) { this.connectionType = connectionType;
color = new Color(r, g, b); this.dynamicPortList = dynamicPortList;
} this.typeConstraint = typeConstraint;
}
/// <summary> Specify a color for this node type </summary> }
/// <param name="hex"> HEX color value </param>
public NodeTintAttribute(string hex) { /// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
ColorUtility.TryParseHtmlString(hex, out color); [AttributeUsage(AttributeTargets.Field)]
} public class OutputAttribute : Attribute {
public ShowBackingValue backingValue;
/// <summary> Specify a color for this node type </summary> public ConnectionType connectionType;
/// <param name="r"> Red [0 .. 255] </param> [Obsolete("Use dynamicPortList instead")]
/// <param name="g"> Green [0 .. 255] </param> public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } }
/// <param name="b"> Blue [0 .. 255] </param> public bool dynamicPortList;
public NodeTintAttribute(byte r, byte g, byte b) { public TypeConstraint typeConstraint;
color = new Color32(r, g, b, byte.MaxValue);
} /// <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>
/// <summary> Specify a width for this node type </summary> /// <param name="typeConstraint">Constrains which input connections can be made from this port </param>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] /// <param name="dynamicPortList">If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays </param>
public class NodeWidthAttribute : Attribute { public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) {
public int width; this.backingValue = backingValue;
/// <summary> Specify a width for this node type </summary> this.connectionType = connectionType;
/// <param name="width"> Width </param> this.dynamicPortList = dynamicPortList;
public NodeWidthAttribute(int width) { this.typeConstraint = typeConstraint;
this.width = width; }
}
} /// <summary> Mark a serializable field as an output port. You can access this through <see cref="GetOutputPort(string)"/> </summary>
#endregion /// <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>
[Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver { /// <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>
[SerializeField] private List<string> keys = new List<string>(); [Obsolete("Use constructor with TypeConstraint")]
[SerializeField] private List<NodePort> values = new List<NodePort>(); public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { }
}
public void OnBeforeSerialize() {
keys.Clear(); /// <summary> Manually supply node class with a context menu path </summary>
values.Clear(); [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
keys.Capacity = this.Count; public class CreateNodeMenuAttribute : Attribute {
values.Capacity = this.Count; public string menuName;
foreach (KeyValuePair<string, NodePort> pair in this) { public int order;
keys.Add(pair.Key); /// <summary> Manually supply node class with a context menu path </summary>
values.Add(pair.Value); /// <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;
public void OnAfterDeserialize() { }
this.Clear();
#if UNITY_2021_3_OR_NEWER /// <summary> Manually supply node class with a context menu path </summary>
this.EnsureCapacity(keys.Count); /// <param name="menuName"> Path to this node in the context menu. Null or empty hides it. </param>
#endif /// <param name="order"> The order by which the menu items are displayed. </param>
public CreateNodeMenuAttribute(string menuName, int order) {
if (keys.Count != values.Count) this.menuName = menuName;
throw new System.Exception("there are " + keys.Count + " keys and " + values.Count + " values after deserialization. Make sure that both key and value types are serializable."); this.order = order;
}
for (int i = 0; i < keys.Count; i++) }
this.Add(keys[i], values[i]);
} /// <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;
/// <summary> Specify a color for this node type </summary>
/// <param name="r"> Red [0.0f .. 1.0f] </param>
/// <param name="g"> Green [0.0f .. 1.0f] </param>
/// <param name="b"> Blue [0.0f .. 1.0f] </param>
public NodeTintAttribute(float r, float g, float b) {
color = new Color(r, g, b);
}
/// <summary> Specify a color for this node type </summary>
/// <param name="hex"> HEX color value </param>
public NodeTintAttribute(string hex) {
ColorUtility.TryParseHtmlString(hex, out color);
}
/// <summary> Specify a color for this node type </summary>
/// <param name="r"> Red [0 .. 255] </param>
/// <param name="g"> Green [0 .. 255] </param>
/// <param name="b"> Blue [0 .. 255] </param>
public NodeTintAttribute(byte r, byte g, byte b) {
color = new Color32(r, g, b, byte.MaxValue);
}
}
/// <summary> Specify a width for this node type </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeWidthAttribute : Attribute {
public int width;
/// <summary> Specify a width for this node type </summary>
/// <param name="width"> Width </param>
public NodeWidthAttribute(int width) {
this.width = width;
}
}
#endregion
[Serializable] private class NodePortDictionary : Dictionary<string, NodePort>, ISerializationCallbackReceiver {
[SerializeField] private List<string> keys = new List<string>();
[SerializeField] private List<NodePort> values = new List<NodePort>();
public void OnBeforeSerialize() {
keys.Clear();
values.Clear();
keys.Capacity = this.Count;
values.Capacity = this.Count;
foreach (KeyValuePair<string, NodePort> pair in this) {
keys.Add(pair.Key);
values.Add(pair.Value);
}
}
public void OnAfterDeserialize() {
this.Clear();
#if UNITY_2021_3_OR_NEWER
this.EnsureCapacity(keys.Count);
#endif
if (keys.Count != values.Count)
{
throw new System.Exception("there are " + keys.Count + " keys and " + values.Count + " values after deserialization. Make sure that both key and value types are serializable.");
}
for (var i = 0; i < keys.Count; i++)
{
this.Add(keys[i], values[i]);
}
}
}
}
}

View File

@ -12,8 +12,11 @@ namespace XNode {
private static bool Initialized { get { return portDataCache != null; } } private static bool Initialized { get { return portDataCache != null; } }
public static string GetTypeQualifiedName(System.Type type) { public static string GetTypeQualifiedName(System.Type type) {
if(typeQualifiedNameCache == null) typeQualifiedNameCache = new Dictionary<System.Type, string>(); if(typeQualifiedNameCache == null)
{
typeQualifiedNameCache = new Dictionary<System.Type, string>();
}
string name; string name;
if (!typeQualifiedNameCache.TryGetValue(type, out name)) { if (!typeQualifiedNameCache.TryGetValue(type, out name)) {
name = type.AssemblyQualifiedName; name = type.AssemblyQualifiedName;
@ -24,13 +27,19 @@ namespace XNode {
/// <summary> Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. </summary> /// <summary> Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. </summary>
public static void UpdatePorts(Node node, Dictionary<string, NodePort> ports) { public static void UpdatePorts(Node node, Dictionary<string, NodePort> ports) {
if (!Initialized) BuildCache(); if (!Initialized)
{
BuildCache();
}
Dictionary<string, List<NodePort>> removedPorts = new Dictionary<string, List<NodePort>>(); Dictionary<string, List<NodePort>> removedPorts = new Dictionary<string, List<NodePort>>();
System.Type nodeType = node.GetType(); System.Type nodeType = node.GetType();
Dictionary<string, string> formerlySerializedAs = null; Dictionary<string, string> formerlySerializedAs = null;
if (formerlySerializedAsCache != null) formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs); if (formerlySerializedAsCache != null)
{
formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs);
}
List<NodePort> dynamicListPorts = new List<NodePort>(); List<NodePort> dynamicListPorts = new List<NodePort>();
@ -49,17 +58,27 @@ namespace XNode {
// If port exists but with wrong settings, remove it. Re-add it later. // If port exists but with wrong settings, remove it. Re-add it later.
if (port.IsDynamic || port.direction != staticPort.direction || port.connectionType != staticPort.connectionType || port.typeConstraint != staticPort.typeConstraint) { 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 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()); if (!port.IsDynamic && port.direction == staticPort.direction)
{
removedPorts.Add(port.fieldName, port.GetConnections());
}
port.ClearConnections(); port.ClearConnections();
ports.Remove(port.fieldName); ports.Remove(port.fieldName);
} else port.ValueType = staticPort.ValueType; } else
{
port.ValueType = staticPort.ValueType;
}
} }
// If port doesn't exist anymore, remove it // If port doesn't exist anymore, remove it
else if (port.IsStatic) { else if (port.IsStatic) {
//See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts //See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts
// so it can be reconnected in missing ports stage. // so it can be reconnected in missing ports stage.
string newName = null; string newName = null;
if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName)) removedPorts.Add(newName, port.GetConnections()); if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName))
{
removedPorts.Add(newName, port.GetConnections());
}
port.ClearConnections(); port.ClearConnections();
ports.Remove(port.fieldName); ports.Remove(port.fieldName);
@ -76,13 +95,20 @@ namespace XNode {
//If we just removed the port, try re-adding the connections //If we just removed the port, try re-adding the connections
List<NodePort> reconnectConnections; List<NodePort> reconnectConnections;
if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) { if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) {
for (int i = 0; i < reconnectConnections.Count; i++) { for (var i = 0; i < reconnectConnections.Count; i++) {
NodePort connection = reconnectConnections[i]; NodePort connection = reconnectConnections[i];
if (connection == null) continue; if (connection == null)
{
continue;
}
// CAVEAT: Ports connected under special conditions defined in graphEditor.CanConnect overrides will not auto-connect. // CAVEAT: Ports connected under special conditions defined in graphEditor.CanConnect overrides will not auto-connect.
// To fix this, this code would need to be moved to an editor script and call graphEditor.CanConnect instead of port.CanConnectTo. // To fix this, this code would need to be moved to an editor script and call graphEditor.CanConnect instead of port.CanConnectTo.
// This is only a problem in the rare edge case where user is using non-standard CanConnect overrides and changes port type of an already connected port // This is only a problem in the rare edge case where user is using non-standard CanConnect overrides and changes port type of an already connected port
if (port.CanConnectTo(connection)) port.Connect(connection); if (port.CanConnectTo(connection))
{
port.Connect(connection);
}
} }
} }
ports.Add(staticPort.fieldName, port); ports.Add(staticPort.fieldName, port);
@ -93,7 +119,7 @@ namespace XNode {
foreach (NodePort listPort in dynamicListPorts) { foreach (NodePort listPort in dynamicListPorts) {
// At this point we know that ports here are dynamic list ports // 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. // which have passed name/"backing port" checks, ergo we can proceed more safely.
string backingPortName = listPort.fieldName.Substring(0, listPort.fieldName.IndexOf(' ')); var backingPortName = listPort.fieldName.Substring(0, listPort.fieldName.IndexOf(' '));
NodePort backingPort = staticPorts[backingPortName]; NodePort backingPort = staticPorts[backingPortName];
// Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters.
@ -124,13 +150,19 @@ namespace XNode {
// Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have // 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. // 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) // 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(' '); var fieldNameParts = port.fieldName.Split(' ');
if (fieldNameParts.Length != 2) return false; if (fieldNameParts.Length != 2)
{
return false;
}
FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]); FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]);
if (backingPortInfo == null) return false; if (backingPortInfo == null)
{
return false;
}
object[] attribs = backingPortInfo.GetCustomAttributes(true); var attribs = backingPortInfo.GetCustomAttributes(true);
return attribs.Any(x => { return attribs.Any(x => {
Node.InputAttribute inputAttribute = x as Node.InputAttribute; Node.InputAttribute inputAttribute = x as Node.InputAttribute;
Node.OutputAttribute outputAttribute = x as Node.OutputAttribute; Node.OutputAttribute outputAttribute = x as Node.OutputAttribute;
@ -149,9 +181,13 @@ namespace XNode {
// Loop through assemblies and add node types to list // Loop through assemblies and add node types to list
foreach (Assembly assembly in assemblies) { foreach (Assembly assembly in assemblies) {
// Skip certain dlls to improve performance // Skip certain dlls to improve performance
string assemblyName = assembly.GetName().Name; var assemblyName = assembly.GetName().Name;
int index = assemblyName.IndexOf('.'); var index = assemblyName.IndexOf('.');
if (index != -1) assemblyName = assemblyName.Substring(0, index); if (index != -1)
{
assemblyName = assemblyName.Substring(0, index);
}
switch (assemblyName) { switch (assemblyName) {
// The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped
case "UnityEditor": case "UnityEditor":
@ -167,7 +203,7 @@ namespace XNode {
} }
} }
for (int i = 0; i < nodeTypes.Count; i++) { for (var i = 0; i < nodeTypes.Count; i++) {
CachePorts(nodeTypes[i]); CachePorts(nodeTypes[i]);
} }
} }
@ -179,7 +215,7 @@ namespace XNode {
System.Type tempType = nodeType; System.Type tempType = nodeType;
while ((tempType = tempType.BaseType) != typeof(XNode.Node)) { while ((tempType = tempType.BaseType) != typeof(XNode.Node)) {
FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
for (int i = 0; i < parentFields.Length; i++) { for (var i = 0; i < parentFields.Length; i++) {
// Ensure that we do not already have a member with this type and name // Ensure that we do not already have a member with this type and name
FieldInfo parentField = parentFields[i]; FieldInfo parentField = parentFields[i];
if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) { if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) {
@ -193,29 +229,52 @@ namespace XNode {
private static void CachePorts(System.Type nodeType) { private static void CachePorts(System.Type nodeType) {
List<System.Reflection.FieldInfo> fieldInfo = GetNodeFields(nodeType); List<System.Reflection.FieldInfo> fieldInfo = GetNodeFields(nodeType);
for (int i = 0; i < fieldInfo.Count; i++) { for (var i = 0; i < fieldInfo.Count; i++) {
//Get InputAttribute and OutputAttribute //Get InputAttribute and OutputAttribute
object[] attribs = fieldInfo[i].GetCustomAttributes(true); var attribs = fieldInfo[i].GetCustomAttributes(true);
Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute;
Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute;
UnityEngine.Serialization.FormerlySerializedAsAttribute formerlySerializedAsAttribute = attribs.FirstOrDefault(x => x is UnityEngine.Serialization.FormerlySerializedAsAttribute) as UnityEngine.Serialization.FormerlySerializedAsAttribute; UnityEngine.Serialization.FormerlySerializedAsAttribute formerlySerializedAsAttribute = attribs.FirstOrDefault(x => x is UnityEngine.Serialization.FormerlySerializedAsAttribute) as UnityEngine.Serialization.FormerlySerializedAsAttribute;
if (inputAttrib == null && outputAttrib == null) continue; if (inputAttrib == null && outputAttrib == null)
{
continue;
}
if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output."); if (inputAttrib != null && outputAttrib != null)
{
Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output.");
}
else { else {
if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new Dictionary<string, NodePort>()); if (!portDataCache.ContainsKey(nodeType))
NodePort port = new NodePort(fieldInfo[i]); {
portDataCache.Add(nodeType, new Dictionary<string, NodePort>());
}
NodePort port = new NodePort(fieldInfo[i]);
portDataCache[nodeType].Add(port.fieldName, port); portDataCache[nodeType].Add(port.fieldName, port);
} }
if (formerlySerializedAsAttribute != null) { if (formerlySerializedAsAttribute != null) {
if (formerlySerializedAsCache == null) formerlySerializedAsCache = new Dictionary<System.Type, Dictionary<string, string>>(); if (formerlySerializedAsCache == null)
if (!formerlySerializedAsCache.ContainsKey(nodeType)) formerlySerializedAsCache.Add(nodeType, new Dictionary<string, string>()); {
formerlySerializedAsCache = new Dictionary<System.Type, Dictionary<string, string>>();
}
if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName)) Debug.LogError("Another FormerlySerializedAs with value '" + formerlySerializedAsAttribute.oldName + "' already exist on this node."); if (!formerlySerializedAsCache.ContainsKey(nodeType))
else formerlySerializedAsCache[nodeType].Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name); {
formerlySerializedAsCache.Add(nodeType, new Dictionary<string, string>());
}
if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName))
{
Debug.LogError("Another FormerlySerializedAs with value '" + formerlySerializedAsAttribute.oldName + "' already exist on this node.");
}
else
{
formerlySerializedAsCache[nodeType].Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name);
}
} }
} }
} }

View File

@ -1,124 +1,152 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace XNode { namespace XNode {
/// <summary> Base class for all node graphs </summary> /// <summary> Base class for all node graphs </summary>
[Serializable] [Serializable]
public abstract class NodeGraph : ScriptableObject { public abstract class NodeGraph : ScriptableObject {
/// <summary> All nodes in the graph. <para/> /// <summary> All nodes in the graph. <para/>
/// See: <see cref="AddNode{T}"/> </summary> /// See: <see cref="AddNode{T}"/> </summary>
[SerializeField] public List<Node> nodes = new List<Node>(); [SerializeField] public List<Node> nodes = new List<Node>();
/// <summary> Add a node to the graph by type (convenience method - will call the System.Type version) </summary> /// <summary> Add a node to the graph by type (convenience method - will call the System.Type version) </summary>
public T AddNode<T>() where T : Node { public T AddNode<T>() where T : Node {
return AddNode(typeof(T)) as T; return AddNode(typeof(T)) as T;
} }
/// <summary> Add a node to the graph by type </summary> /// <summary> Add a node to the graph by type </summary>
public virtual Node AddNode(Type type) { public virtual Node AddNode(Type type) {
Node.graphHotfix = this; Node.graphHotfix = this;
Node node = ScriptableObject.CreateInstance(type) as Node; Node node = ScriptableObject.CreateInstance(type) as Node;
node.graph = this; node.graph = this;
nodes.Add(node); nodes.Add(node);
return node; return node;
} }
/// <summary> Creates a copy of the original node in the graph </summary> /// <summary> Creates a copy of the original node in the graph </summary>
public virtual Node CopyNode(Node original) { public virtual Node CopyNode(Node original) {
Node.graphHotfix = this; Node.graphHotfix = this;
Node node = ScriptableObject.Instantiate(original); Node node = ScriptableObject.Instantiate(original);
node.graph = this; node.graph = this;
node.ClearConnections(); node.ClearConnections();
nodes.Add(node); nodes.Add(node);
return node; return node;
} }
/// <summary> Safely remove a node and all its connections </summary> /// <summary> Safely remove a node and all its connections </summary>
/// <param name="node"> The node to remove </param> /// <param name="node"> The node to remove </param>
public virtual void RemoveNode(Node node) { public virtual void RemoveNode(Node node) {
node.ClearConnections(); node.ClearConnections();
nodes.Remove(node); nodes.Remove(node);
if (Application.isPlaying) Destroy(node); if (Application.isPlaying)
} {
Destroy(node);
/// <summary> Remove all nodes and connections from the graph </summary> }
public virtual void Clear() { }
if (Application.isPlaying) {
for (int i = 0; i < nodes.Count; i++) { /// <summary> Remove all nodes and connections from the graph </summary>
if (nodes[i] != null) Destroy(nodes[i]); public virtual void Clear() {
} if (Application.isPlaying) {
} for (var i = 0; i < nodes.Count; i++) {
nodes.Clear(); if (nodes[i] != null)
} {
Destroy(nodes[i]);
/// <summary> Create a new deep copy of this graph </summary> }
public virtual XNode.NodeGraph Copy() { }
// Instantiate a new nodegraph instance }
NodeGraph graph = Instantiate(this); nodes.Clear();
// Instantiate all nodes inside the graph }
for (int i = 0; i < nodes.Count; i++) {
if (nodes[i] == null) continue; /// <summary> Create a new deep copy of this graph </summary>
Node.graphHotfix = graph; public virtual XNode.NodeGraph Copy() {
Node node = Instantiate(nodes[i]) as Node; // Instantiate a new nodegraph instance
node.graph = graph; NodeGraph graph = Instantiate(this);
graph.nodes[i] = node; // Instantiate all nodes inside the graph
} for (var i = 0; i < nodes.Count; i++) {
if (nodes[i] == null)
// Redirect all connections {
for (int i = 0; i < graph.nodes.Count; i++) { continue;
if (graph.nodes[i] == null) continue; }
foreach (NodePort port in graph.nodes[i].Ports) {
port.Redirect(nodes, graph.nodes); Node.graphHotfix = graph;
} Node node = Instantiate(nodes[i]) as Node;
} node.graph = graph;
graph.nodes[i] = node;
return graph; }
}
// Redirect all connections
protected virtual void OnDestroy() { for (var i = 0; i < graph.nodes.Count; i++) {
// Remove all nodes prior to graph destruction if (graph.nodes[i] == null)
Clear(); {
} continue;
}
#region Attributes
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted. </summary> foreach (NodePort port in graph.nodes[i].Ports) {
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] port.Redirect(nodes, graph.nodes);
public class RequireNodeAttribute : Attribute { }
public Type type0; }
public Type type1;
public Type type2; return graph;
}
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary>
public RequireNodeAttribute(Type type) { protected virtual void OnDestroy() {
this.type0 = type; // Remove all nodes prior to graph destruction
this.type1 = null; Clear();
this.type2 = null; }
}
#region Attributes
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary> /// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted. </summary>
public RequireNodeAttribute(Type type, Type type2) { [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
this.type0 = type; public class RequireNodeAttribute : Attribute {
this.type1 = type2; public Type type0;
this.type2 = null; public Type type1;
} public Type type2;
/// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary> /// <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) { public RequireNodeAttribute(Type type) {
this.type0 = type; this.type0 = type;
this.type1 = type2; this.type1 = null;
this.type2 = type3; this.type2 = null;
} }
public bool Requires(Type type) { /// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary>
if (type == null) return false; public RequireNodeAttribute(Type type, Type type2) {
if (type == type0) return true; this.type0 = type;
else if (type == type1) return true; this.type1 = type2;
else if (type == type2) return true; this.type2 = null;
return false; }
}
} /// <summary> Automatically ensures the existance of a certain node type, and prevents it from being deleted </summary>
#endregion public RequireNodeAttribute(Type type, Type type2, Type type3) {
} this.type0 = type;
} this.type1 = type2;
this.type2 = type3;
}
public bool Requires(Type type) {
if (type == null)
{
return false;
}
if (type == type0)
{
return true;
}
else if (type == type1)
{
return true;
}
else if (type == type2)
{
return true;
}
return false;
}
}
#endregion
}
}

View File

@ -1,422 +1,552 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using UnityEngine; using UnityEngine;
namespace XNode { namespace XNode {
[Serializable] [Serializable]
public class NodePort { public class NodePort {
public enum IO { Input, Output } public enum IO { Input, Output }
public int ConnectionCount { get { return connections.Count; } } public int ConnectionCount { get { return connections.Count; } }
/// <summary> Return the first non-null connection </summary> /// <summary> Return the first non-null connection </summary>
public NodePort Connection { public NodePort Connection {
get { get {
for (int i = 0; i < connections.Count; i++) { for (var i = 0; i < connections.Count; i++) {
if (connections[i] != null) return connections[i].Port; if (connections[i] != null)
} {
return null; return connections[i].Port;
} }
} }
return null;
public IO direction { }
get { return _direction; } }
internal set { _direction = value; }
} public IO direction {
public Node.ConnectionType connectionType { get { return _direction; }
get { return _connectionType; } internal set { _direction = value; }
internal set { _connectionType = value; } }
} public Node.ConnectionType connectionType {
public Node.TypeConstraint typeConstraint { get { return _connectionType; }
get { return _typeConstraint; } internal set { _connectionType = value; }
internal set { _typeConstraint = value; } }
} public Node.TypeConstraint typeConstraint {
get { return _typeConstraint; }
/// <summary> Is this port connected to anytihng? </summary> internal set { _typeConstraint = value; }
public bool IsConnected { get { return connections.Count != 0; } } }
public bool IsInput { get { return direction == IO.Input; } }
public bool IsOutput { get { return direction == IO.Output; } } /// <summary> Is this port connected to anytihng? </summary>
public bool IsConnected { get { return connections.Count != 0; } }
public string fieldName { get { return _fieldName; } } public bool IsInput { get { return direction == IO.Input; } }
public Node node { get { return _node; } } public bool IsOutput { get { return direction == IO.Output; } }
public bool IsDynamic { get { return _dynamic; } }
public bool IsStatic { get { return !_dynamic; } } public string fieldName { get { return _fieldName; } }
public Type ValueType { public Node node { get { return _node; } }
get { public bool IsDynamic { get { return _dynamic; } }
if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) valueType = Type.GetType(_typeQualifiedName, false); public bool IsStatic { get { return !_dynamic; } }
return valueType; public Type ValueType {
} get {
set { if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName))
if (valueType == value) return; {
valueType = value; valueType = Type.GetType(_typeQualifiedName, false);
if (value != null) _typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value); }
}
} return valueType;
private Type valueType; }
set {
[SerializeField] private string _fieldName; if (valueType == value)
[SerializeField] private Node _node; {
[SerializeField] private string _typeQualifiedName; return;
[SerializeField] private List<PortConnection> connections = new List<PortConnection>(); }
[SerializeField] private IO _direction;
[SerializeField] private Node.ConnectionType _connectionType; valueType = value;
[SerializeField] private Node.TypeConstraint _typeConstraint; if (value != null)
[SerializeField] private bool _dynamic; {
_typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value);
/// <summary> Construct a static targetless nodeport. Used as a template. </summary> }
public NodePort(FieldInfo fieldInfo) { }
_fieldName = fieldInfo.Name; }
ValueType = fieldInfo.FieldType; private Type valueType;
_dynamic = false;
var attribs = fieldInfo.GetCustomAttributes(false); [SerializeField] private string _fieldName;
for (int i = 0; i < attribs.Length; i++) { [SerializeField] private Node _node;
if (attribs[i] is Node.InputAttribute) { [SerializeField] private string _typeQualifiedName;
_direction = IO.Input; [SerializeField] private List<PortConnection> connections = new List<PortConnection>();
_connectionType = (attribs[i] as Node.InputAttribute).connectionType; [SerializeField] private IO _direction;
_typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint; [SerializeField] private Node.ConnectionType _connectionType;
} else if (attribs[i] is Node.OutputAttribute) { [SerializeField] private Node.TypeConstraint _typeConstraint;
_direction = IO.Output; [SerializeField] private bool _dynamic;
_connectionType = (attribs[i] as Node.OutputAttribute).connectionType;
_typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; /// <summary> Construct a static targetless nodeport. Used as a template. </summary>
} public NodePort(FieldInfo fieldInfo) {
// Override ValueType of the Port _fieldName = fieldInfo.Name;
if(attribs[i] is PortTypeOverrideAttribute) { ValueType = fieldInfo.FieldType;
ValueType = (attribs[i] as PortTypeOverrideAttribute).type; _dynamic = false;
} var attribs = fieldInfo.GetCustomAttributes(false);
} for (var i = 0; i < attribs.Length; i++) {
} if (attribs[i] is Node.InputAttribute) {
_direction = IO.Input;
/// <summary> Copy a nodePort but assign it to another node. </summary> _connectionType = (attribs[i] as Node.InputAttribute).connectionType;
public NodePort(NodePort nodePort, Node node) { _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint;
_fieldName = nodePort._fieldName; } else if (attribs[i] is Node.OutputAttribute) {
ValueType = nodePort.valueType; _direction = IO.Output;
_direction = nodePort.direction; _connectionType = (attribs[i] as Node.OutputAttribute).connectionType;
_dynamic = nodePort._dynamic; _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint;
_connectionType = nodePort._connectionType; }
_typeConstraint = nodePort._typeConstraint; // Override ValueType of the Port
_node = node; if(attribs[i] is PortTypeOverrideAttribute) {
} ValueType = (attribs[i] as PortTypeOverrideAttribute).type;
}
/// <summary> Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. </summary> }
public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) { }
_fieldName = fieldName;
this.ValueType = type; /// <summary> Copy a nodePort but assign it to another node. </summary>
_direction = direction; public NodePort(NodePort nodePort, Node node) {
_node = node; _fieldName = nodePort._fieldName;
_dynamic = true; ValueType = nodePort.valueType;
_connectionType = connectionType; _direction = nodePort.direction;
_typeConstraint = typeConstraint; _dynamic = nodePort._dynamic;
} _connectionType = nodePort._connectionType;
_typeConstraint = nodePort._typeConstraint;
/// <summary> Checks all connections for invalid references, and removes them. </summary> _node = node;
public void VerifyConnections() { }
for (int i = connections.Count - 1; i >= 0; i--) {
if (connections[i].node != null && /// <summary> Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. </summary>
!string.IsNullOrEmpty(connections[i].fieldName) && public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) {
connections[i].node.GetPort(connections[i].fieldName) != null) _fieldName = fieldName;
continue; this.ValueType = type;
connections.RemoveAt(i); _direction = direction;
} _node = node;
} _dynamic = true;
_connectionType = connectionType;
/// <summary> Return the output value of this node through its parent nodes GetValue override method. </summary> _typeConstraint = typeConstraint;
/// <returns> <see cref="Node.GetValue(NodePort)"/> </returns> }
public object GetOutputValue() {
if (direction == IO.Input) return null; /// <summary> Checks all connections for invalid references, and removes them. </summary>
return node.GetValue(this); public void VerifyConnections() {
} for (var i = connections.Count - 1; i >= 0; i--) {
if (connections[i].node != null &&
/// <summary> Return the output value of the first connected port. Returns null if none found or invalid.</summary> !string.IsNullOrEmpty(connections[i].fieldName) &&
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns> connections[i].node.GetPort(connections[i].fieldName) != null)
public object GetInputValue() { {
NodePort connectedPort = Connection; continue;
if (connectedPort == null) return null; }
return connectedPort.GetOutputValue();
} connections.RemoveAt(i);
}
/// <summary> Return the output values of all connected ports. </summary> }
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public object[] GetInputValues() { /// <summary> Return the output value of this node through its parent nodes GetValue override method. </summary>
object[] objs = new object[ConnectionCount]; /// <returns> <see cref="Node.GetValue(NodePort)"/> </returns>
for (int i = 0; i < ConnectionCount; i++) { public object GetOutputValue() {
NodePort connectedPort = connections[i].Port; if (direction == IO.Input)
if (connectedPort == null) { // if we happen to find a null port, remove it and look again {
connections.RemoveAt(i); return null;
i--; }
continue;
} return node.GetValue(this);
objs[i] = connectedPort.GetOutputValue(); }
}
return objs; /// <summary> Return the output value of the first connected port. Returns null if none found or invalid.</summary>
} /// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public object GetInputValue() {
/// <summary> Return the output value of the first connected port. Returns null if none found or invalid. </summary> NodePort connectedPort = Connection;
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns> if (connectedPort == null)
public T GetInputValue<T>() { {
object obj = GetInputValue(); return null;
return obj is T ? (T) obj : default(T); }
}
return connectedPort.GetOutputValue();
/// <summary> Return the output values of all connected ports. </summary> }
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
public T[] GetInputValues<T>() { /// <summary> Return the output values of all connected ports. </summary>
object[] objs = GetInputValues(); /// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
T[] ts = new T[objs.Length]; public object[] GetInputValues() {
for (int i = 0; i < objs.Length; i++) { var objs = new object[ConnectionCount];
if (objs[i] is T) ts[i] = (T) objs[i]; for (var i = 0; i < ConnectionCount; i++) {
} NodePort connectedPort = connections[i].Port;
return ts; if (connectedPort == null) { // if we happen to find a null port, remove it and look again
} connections.RemoveAt(i);
i--;
/// <summary> Return true if port is connected and has a valid input. </summary> continue;
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns> }
public bool TryGetInputValue<T>(out T value) { objs[i] = connectedPort.GetOutputValue();
object obj = GetInputValue(); }
if (obj is T) { return objs;
value = (T) obj; }
return true;
} else { /// <summary> Return the output value of the first connected port. Returns null if none found or invalid. </summary>
value = default(T); /// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
return false; public T GetInputValue<T>() {
} var obj = GetInputValue();
} return obj is T ? (T) obj : default(T);
}
/// <summary> Return the sum of all inputs. </summary>
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns> /// <summary> Return the output values of all connected ports. </summary>
public float GetInputSum(float fallback) { /// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
object[] objs = GetInputValues(); public T[] GetInputValues<T>() {
if (objs.Length == 0) return fallback; var objs = GetInputValues();
float result = 0; T[] ts = new T[objs.Length];
for (int i = 0; i < objs.Length; i++) { for (var i = 0; i < objs.Length; i++) {
if (objs[i] is float) result += (float) objs[i]; if (objs[i] is T)
} {
return result; ts[i] = (T) objs[i];
} }
}
/// <summary> Return the sum of all inputs. </summary> return ts;
/// <returns> <see cref="NodePort.GetOutputValue"/> </returns> }
public int GetInputSum(int fallback) {
object[] objs = GetInputValues(); /// <summary> Return true if port is connected and has a valid input. </summary>
if (objs.Length == 0) return fallback; /// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
int result = 0; public bool TryGetInputValue<T>(out T value) {
for (int i = 0; i < objs.Length; i++) { var obj = GetInputValue();
if (objs[i] is int) result += (int) objs[i]; if (obj is T) {
} value = (T) obj;
return result; return true;
} } else {
value = default(T);
/// <summary> Connect this <see cref="NodePort"/> to another </summary> return false;
/// <param name="port">The <see cref="NodePort"/> to connect to</param> }
public void Connect(NodePort port) { }
if (connections == null) connections = new List<PortConnection>();
if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; } /// <summary> Return the sum of all inputs. </summary>
if (port == this) { Debug.LogWarning("Cannot connect port to self."); return; } /// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; } public float GetInputSum(float fallback) {
if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; } var objs = GetInputValues();
#if UNITY_EDITOR if (objs.Length == 0)
UnityEditor.Undo.RecordObject(node, "Connect Port"); {
UnityEditor.Undo.RecordObject(port.node, "Connect Port"); return fallback;
#endif }
if (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); }
if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); } float result = 0;
connections.Add(new PortConnection(port)); for (var i = 0; i < objs.Length; i++) {
if (port.connections == null) port.connections = new List<PortConnection>(); if (objs[i] is float)
if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this)); {
node.OnCreateConnection(this, port); result += (float) objs[i];
port.node.OnCreateConnection(this, port); }
} }
return result;
public List<NodePort> GetConnections() { }
List<NodePort> result = new List<NodePort>();
for (int i = 0; i < connections.Count; i++) { /// <summary> Return the sum of all inputs. </summary>
NodePort port = GetConnection(i); /// <returns> <see cref="NodePort.GetOutputValue"/> </returns>
if (port != null) result.Add(port); public int GetInputSum(int fallback) {
} var objs = GetInputValues();
return result; if (objs.Length == 0)
} {
return fallback;
public NodePort GetConnection(int i) { }
//If the connection is broken for some reason, remove it.
if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) { var result = 0;
connections.RemoveAt(i); for (var i = 0; i < objs.Length; i++) {
return null; if (objs[i] is int)
} {
NodePort port = connections[i].node.GetPort(connections[i].fieldName); result += (int) objs[i];
if (port == null) { }
connections.RemoveAt(i); }
return null; return result;
} }
return port;
} /// <summary> Connect this <see cref="NodePort"/> to another </summary>
/// <param name="port">The <see cref="NodePort"/> to connect to</param>
/// <summary> Get index of the connection connecting this and specified ports </summary> public void Connect(NodePort port) {
public int GetConnectionIndex(NodePort port) { if (connections == null)
for (int i = 0; i < ConnectionCount; i++) { {
if (connections[i].Port == port) return i; connections = new List<PortConnection>();
} }
return -1;
} if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; }
if (port == this) { Debug.LogWarning("Cannot connect port to self."); return; }
public bool IsConnectedTo(NodePort port) { if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; }
for (int i = 0; i < connections.Count; i++) { if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; }
if (connections[i].Port == port) return true; #if UNITY_EDITOR
} UnityEditor.Undo.RecordObject(node, "Connect Port");
return false; UnityEditor.Undo.RecordObject(port.node, "Connect Port");
} #endif
if (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); }
/// <summary> Returns true if this port can connect to specified port </summary> if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); }
public bool CanConnectTo(NodePort port) { connections.Add(new PortConnection(port));
// Figure out which is input and which is output if (port.connections == null)
NodePort input = null, output = null; {
if (IsInput) input = this; port.connections = new List<PortConnection>();
else output = this; }
if (port.IsInput) input = port;
else output = port; if (!port.IsConnectedTo(this))
// If there isn't one of each, they can't connect {
if (input == null || output == null) return false; port.connections.Add(new PortConnection(this));
// 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; node.OnCreateConnection(this, port);
if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; port.node.OnCreateConnection(this, port);
if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; }
// Check output type constraints
if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; public List<NodePort> GetConnections() {
if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; List<NodePort> result = new List<NodePort>();
if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; for (var i = 0; i < connections.Count; i++) {
if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; NodePort port = GetConnection(i);
// Success if (port != null)
return true; {
} result.Add(port);
}
/// <summary> Disconnect this port from another port </summary> }
public void Disconnect(NodePort port) { return result;
// Remove this ports connection to the other }
for (int i = connections.Count - 1; i >= 0; i--) {
if (connections[i].Port == port) { public NodePort GetConnection(int i) {
connections.RemoveAt(i); //If the connection is broken for some reason, remove it.
} if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) {
} connections.RemoveAt(i);
if (port != null) { return null;
// Remove the other ports connection to this port }
for (int i = 0; i < port.connections.Count; i++) { NodePort port = connections[i].node.GetPort(connections[i].fieldName);
if (port.connections[i].Port == this) { if (port == null) {
port.connections.RemoveAt(i); connections.RemoveAt(i);
// Trigger OnRemoveConnection from this side port return null;
port.node.OnRemoveConnection(port); }
} return port;
} }
}
// Trigger OnRemoveConnection /// <summary> Get index of the connection connecting this and specified ports </summary>
node.OnRemoveConnection(this); public int GetConnectionIndex(NodePort port) {
} for (var i = 0; i < ConnectionCount; i++) {
if (connections[i].Port == port)
/// <summary> Disconnect this port from another port </summary> {
public void Disconnect(int i) { return i;
// Remove the other ports connection to this port }
NodePort otherPort = connections[i].Port; }
if (otherPort != null) { return -1;
otherPort.connections.RemoveAll(it => { return it.Port == this; }); }
}
// Remove this ports connection to the other public bool IsConnectedTo(NodePort port) {
connections.RemoveAt(i); for (var i = 0; i < connections.Count; i++) {
if (connections[i].Port == port)
// Trigger OnRemoveConnection {
node.OnRemoveConnection(this); return true;
if (otherPort != null) otherPort.node.OnRemoveConnection(otherPort); }
} }
return false;
public void ClearConnections() { }
while (connections.Count > 0) {
Disconnect(connections[0].Port); /// <summary> Returns true if this port can connect to specified port </summary>
} public bool CanConnectTo(NodePort port) {
} // Figure out which is input and which is output
NodePort input = null, output = null;
/// <summary> Get reroute points for a given connection. This is used for organization </summary> if (IsInput)
public List<Vector2> GetReroutePoints(int index) { {
return connections[index].reroutePoints; input = this;
} }
else
/// <summary> Swap connections with another node </summary> {
public void SwapConnections(NodePort targetPort) { output = this;
int aConnectionCount = connections.Count; }
int bConnectionCount = targetPort.connections.Count;
if (port.IsInput)
List<NodePort> portConnections = new List<NodePort>(); {
List<NodePort> targetPortConnections = new List<NodePort>(); input = port;
}
// Cache port connections else
for (int i = 0; i < aConnectionCount; i++) {
portConnections.Add(connections[i].Port); output = port;
}
// Cache target port connections
for (int i = 0; i < bConnectionCount; i++) // If there isn't one of each, they can't connect
targetPortConnections.Add(targetPort.connections[i].Port); if (input == null || output == null)
{
ClearConnections(); return false;
targetPort.ClearConnections(); }
// Add port connections to targetPort // Check input type constraints
for (int i = 0; i < portConnections.Count; i++) if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType))
targetPort.Connect(portConnections[i]); {
return false;
// Add target port connections to this one }
for (int i = 0; i < targetPortConnections.Count; i++)
Connect(targetPortConnections[i]); if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType)
{
} return false;
}
/// <summary> Copy all connections pointing to a node and add them to this one </summary>
public void AddConnections(NodePort targetPort) { if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType))
int connectionCount = targetPort.ConnectionCount; {
for (int i = 0; i < connectionCount; i++) { return false;
PortConnection connection = targetPort.connections[i]; }
NodePort otherPort = connection.Port;
Connect(otherPort); if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType))
} {
} return false;
}
/// <summary> Move all connections pointing to this node, to another node </summary>
public void MoveConnections(NodePort targetPort) { // Check output type constraints
int connectionCount = connections.Count; if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType))
{
// Add connections to target port return false;
for (int i = 0; i < connectionCount; i++) { }
PortConnection connection = targetPort.connections[i];
NodePort otherPort = connection.Port; if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType)
Connect(otherPort); {
} return false;
ClearConnections(); }
}
if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType))
/// <summary> Swap connected nodes from the old list with nodes from the new list </summary> {
public void Redirect(List<Node> oldNodes, List<Node> newNodes) { return false;
foreach (PortConnection connection in connections) { }
int index = oldNodes.IndexOf(connection.node);
if (index >= 0) connection.node = newNodes[index]; if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType))
} {
} return false;
}
[Serializable]
private class PortConnection { // Success
[SerializeField] public string fieldName; return true;
[SerializeField] public Node node; }
public NodePort Port { get { return port != null ? port : port = GetPort(); } }
/// <summary> Disconnect this port from another port </summary>
[NonSerialized] private NodePort port; public void Disconnect(NodePort port) {
/// <summary> Extra connection path points for organization </summary> // Remove this ports connection to the other
[SerializeField] public List<Vector2> reroutePoints = new List<Vector2>(); for (var i = connections.Count - 1; i >= 0; i--) {
if (connections[i].Port == port) {
public PortConnection(NodePort port) { connections.RemoveAt(i);
this.port = port; }
node = port.node; }
fieldName = port.fieldName; if (port != null) {
} // Remove the other ports connection to this port
for (var i = 0; i < port.connections.Count; i++) {
/// <summary> Returns the port that this <see cref="PortConnection"/> points to </summary> if (port.connections[i].Port == this) {
private NodePort GetPort() { port.connections.RemoveAt(i);
if (node == null || string.IsNullOrEmpty(fieldName)) return null; // Trigger OnRemoveConnection from this side port
return node.GetPort(fieldName); port.node.OnRemoveConnection(port);
} }
} }
} }
} // Trigger OnRemoveConnection
node.OnRemoveConnection(this);
}
/// <summary> Disconnect this port from another port </summary>
public void Disconnect(int i) {
// Remove the other ports connection to this port
NodePort otherPort = connections[i].Port;
if (otherPort != null) {
otherPort.connections.RemoveAll(it => { return it.Port == this; });
}
// Remove this ports connection to the other
connections.RemoveAt(i);
// Trigger OnRemoveConnection
node.OnRemoveConnection(this);
if (otherPort != null)
{
otherPort.node.OnRemoveConnection(otherPort);
}
}
public void ClearConnections() {
while (connections.Count > 0) {
Disconnect(connections[0].Port);
}
}
/// <summary> Get reroute points for a given connection. This is used for organization </summary>
public List<Vector2> GetReroutePoints(int index) {
return connections[index].reroutePoints;
}
/// <summary> Swap connections with another node </summary>
public void SwapConnections(NodePort targetPort) {
var aConnectionCount = connections.Count;
var bConnectionCount = targetPort.connections.Count;
List<NodePort> portConnections = new List<NodePort>();
List<NodePort> targetPortConnections = new List<NodePort>();
// Cache port connections
for (var i = 0; i < aConnectionCount; i++)
{
portConnections.Add(connections[i].Port);
}
// Cache target port connections
for (var i = 0; i < bConnectionCount; i++)
{
targetPortConnections.Add(targetPort.connections[i].Port);
}
ClearConnections();
targetPort.ClearConnections();
// Add port connections to targetPort
for (var i = 0; i < portConnections.Count; i++)
{
targetPort.Connect(portConnections[i]);
}
// Add target port connections to this one
for (var i = 0; i < targetPortConnections.Count; i++)
{
Connect(targetPortConnections[i]);
}
}
/// <summary> Copy all connections pointing to a node and add them to this one </summary>
public void AddConnections(NodePort targetPort) {
var connectionCount = targetPort.ConnectionCount;
for (var i = 0; i < connectionCount; i++) {
PortConnection connection = targetPort.connections[i];
NodePort otherPort = connection.Port;
Connect(otherPort);
}
}
/// <summary> Move all connections pointing to this node, to another node </summary>
public void MoveConnections(NodePort targetPort) {
var connectionCount = connections.Count;
// Add connections to target port
for (var i = 0; i < connectionCount; i++) {
PortConnection connection = targetPort.connections[i];
NodePort otherPort = connection.Port;
Connect(otherPort);
}
ClearConnections();
}
/// <summary> Swap connected nodes from the old list with nodes from the new list </summary>
public void Redirect(List<Node> oldNodes, List<Node> newNodes) {
foreach (PortConnection connection in connections) {
var index = oldNodes.IndexOf(connection.node);
if (index >= 0)
{
connection.node = newNodes[index];
}
}
}
[Serializable]
private class PortConnection {
[SerializeField] public string fieldName;
[SerializeField] public Node node;
public NodePort Port { get { return port != null ? port : port = GetPort(); } }
[NonSerialized] private NodePort port;
/// <summary> Extra connection path points for organization </summary>
[SerializeField] public List<Vector2> reroutePoints = new List<Vector2>();
public PortConnection(NodePort port) {
this.port = port;
node = port.node;
fieldName = port.fieldName;
}
/// <summary> Returns the port that this <see cref="PortConnection"/> points to </summary>
private NodePort GetPort() {
if (node == null || string.IsNullOrEmpty(fieldName))
{
return null;
}
return node.GetPort(fieldName);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More