Add VRChat and GitHub packages to Unity project

This commit is contained in:
gunnbr committed 2021-01-18 09:14:18 -08:00
1 parent 0334a52b21
commit 85538aaccc
3297 files changed
+847877

No files matched your search

@@ -0,0 +1,79 @@
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Experimental.UIElements;
using VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView;
namespace UnityEditor.Experimental.UIElements.GraphView
{
public static class GraphElementExtension
{
public static void Reserialize(this GraphElement element)
{
MarkDirty();
var evt = new Event()
{
type = EventType.ExecuteCommand,
commandName = UdonGraphCommands.Reserialize,
};
using (var e = ExecuteCommandEvent.GetPooled(evt))
{
element.SendEvent(e);
}
}
public static void Reload(this GraphElement element)
{
var evt = new Event()
{
type = EventType.ExecuteCommand,
commandName = UdonGraphCommands.Reload
};
using (var e = ExecuteCommandEvent.GetPooled(evt))
{
element.SendEvent(e);
}
}
public static void SaveNewData(this GraphElement element)
{
var evt = new Event()
{
type = EventType.ExecuteCommand,
commandName = UdonGraphCommands.SaveNewData
};
using (var e = ExecuteCommandEvent.GetPooled(evt))
{
element.SendEvent(e);
}
MarkDirty();
}
public static void MarkDirty()
{
if (!EditorApplication.isPlaying)
{
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
}
public static Vector2 GetSnappedPosition(Vector2 position)
{
// don't snap at 0 size
var snap = Settings.GridSnapSize;
if (snap == 0) return position;
position.x = (float)System.Math.Round(position.x / snap) * snap;
position.y = (float)System.Math.Round(position.y / snap) * snap;
return position;
}
public static Rect GetSnappedRect(Rect rect)
{
rect.position = GetSnappedPosition(rect.position);
return rect;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 469db50616185d04e8a46dcd75db12d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,73 @@
using UnityEngine.Experimental.UIElements;
using System;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
public class UdonArrayEditor : VisualElement
{
private IArrayProvider _inspector;
private Button _editArrayButton;
private System.Action<object> _setValueCallback;
private Type _type;
private object _value;
private bool _inspectorOpen = false;
public UdonArrayEditor(Type t, System.Action<object> valueChangedAction, object value)
{
_setValueCallback = valueChangedAction;
_value = value;
_type = t.GetElementType();
_editArrayButton = new Button(EditArray)
{
text = "Edit",
name = "array-editor",
};
Add(_editArrayButton);
}
private void EditArray()
{
if (_inspector == null)
{
// Create it new
Type typedArrayInspector = (typeof(UdonArrayInspector<>)).MakeGenericType(_type);
_inspector = (Activator.CreateInstance(typedArrayInspector, null, _value) as IArrayProvider);
Add(_inspector as VisualElement);
_inspectorOpen = true;
_editArrayButton.text = "Save";
return;
}
else
{
// Update Values when 'Save' is clicked
if(_inspectorOpen)
{
// Update Values
var values = _inspector.GetValues();
_setValueCallback(values);
// Remove Inspector
_inspector.RemoveFromHierarchy();
// Update Button Text
_editArrayButton.text = "Edit";
_inspectorOpen = false;
return;
}
else
{
// Inspector exists, it's just removed
_inspectorOpen = true;
Add(_inspector as VisualElement);
_editArrayButton.text = "Save";
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f257a6eeae213a4db991d486cace003
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,150 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Experimental.UIElements;
using UnityEditor.Experimental.UIElements;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
public class UdonArrayInspector<T> : VisualElement, IArrayProvider
{
private ScrollView _scroller;
private VisualContainer _container;
private List<VisualElement> _fields = new List<VisualElement>();
private IntegerField _sizeField;
private System.Action<object> _setValueCallback;
public UdonArrayInspector(System.Action<object> valueChangedAction, object value)
{
_setValueCallback = valueChangedAction;
AddToClassList("input-inspector");
var resizeContainer = new VisualElement()
{
name = "resize-container",
};
resizeContainer.Add(new Label("size"));
_sizeField = new IntegerField();
_sizeField.isDelayed = true;
_sizeField.OnValueChanged((evt) =>
{
ResizeTo(evt.newValue);
});
resizeContainer.Add(_sizeField);
Add(resizeContainer);
_scroller = new ScrollView()
{
name = "array-scroll",
};
Add(_scroller);
_container = new VisualContainer();
_scroller.SetContents(_container);
if (value == null)
{
// Can't show if array is empty
_sizeField.value = 0;
return;
}
else
{
IEnumerable<T> values = (value as IEnumerable<T>);
if (values == null)
{
Debug.LogError($"Couldn't convert {value} to {typeof(T).ToString()} Array");
return;
}
// Populate fields and their values from passed-in array
_fields = new List<VisualElement>();
foreach (var item in values)
{
var field = GetValueField(item);
_fields.Add(field);
_container.Add(field);
}
_sizeField.value = values.Count();
}
}
private void ResizeTo(int newValue)
{
_sizeField.value = newValue;
// Create from scratch if currentFields are null
if(_fields == null)
{
Debug.Log($"Creating from Scratch");
_fields = new List<VisualElement>();
for (int i = 0; i < newValue; i++)
{
var field = GetValueField(null);
_fields.Add(field);
_container.Add(field as VisualElement);
}
return;
}
// Shrink list if new value is less than old one
if(newValue < _fields.Count)
{
for (int i = _fields.Count - 1; i >= newValue; i--)
{
(_fields[i] as VisualElement).RemoveFromHierarchy();
_fields.RemoveAt(i);
}
MarkDirtyRepaint();
return;
}
// Expand list if new value is more than old one.
if(newValue > _fields.Count)
{
int numberToAdd = newValue - _fields.Count;
for (int i = 0; i < numberToAdd; i++)
{
var field = GetValueField(null);
if (field == null)
{
Debug.LogWarning($"Sorry, can't edit object of type {typeof(T).ToString()} yet.");
return;
}
_fields.Add(field);
_container.Add(field as VisualElement);
}
MarkDirtyRepaint();
return;
}
}
private VisualElement GetValueField(object value)
{
return UdonFieldFactory.CreateField(typeof(T), value, _setValueCallback);
}
public object GetValues()
{
var result = new List<T>();
for (int i = 0; i < _fields.Count; i++)
{
var f = (_fields[i] as INotifyValueChanged<T>);
result.Add(f.value);
}
return result.ToArray();
}
}
public interface IArrayProvider
{
object GetValues();
void RemoveFromHierarchy(); // in VisualElement
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f4f0ade55ae13b6468a765826f1f2540
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,192 @@
using System;
using UnityEngine;
using UnityEditor.Experimental.UIElements.GraphView;
using UnityEngine.Experimental.UIElements;
using UnityEngine.Experimental.UIElements.StyleSheets;
using UnityEditor;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
public class UdonComment : UdonGraphElement, IUdonGraphElementDataProvider
{
private VisualElement _mainContainer;
private Label _label;
private TextField _textField;
private CustomData _customData = new CustomData();
private UdonGraph _graph;
public UdonGroup group;
// Called from Context menu
public static UdonComment Create(string value, Rect position, UdonGraph graph)
{
var comment = new UdonComment("", graph);
// make sure rect size is not 0
position.width = position.width > 0 ? position.width : 128;
position.height = position.height > 0 ? position.height : 40;
comment._customData.layout = position;
comment._customData.title = value;
comment.UpdateFromData();
graph.MarkSceneDirty();
return comment;
}
public static UdonComment Create(UdonGraphElementData elementData, UdonGraph graph)
{
var comment = new UdonComment(elementData.jsonData, graph);
comment.UpdateFromData();
graph.MarkSceneDirty();
return comment;
}
private UdonComment(string jsonData, UdonGraph graph)
{
title = "Comment";
name = "comment";
_graph = graph;
capabilities |= Capabilities.Selectable | Capabilities.Movable | Capabilities.Deletable | Capabilities.Resizable;
pickingMode = PickingMode.Ignore;
type = UdonGraphElementType.UdonComment;
if(!string.IsNullOrEmpty(jsonData))
{
EditorJsonUtility.FromJsonOverwrite(jsonData, _customData);
}
_mainContainer = new VisualElement();
_mainContainer.StretchToParentSize();
_mainContainer.AddToClassList("mainContainer");
Add(_mainContainer);
_label = new Label();
_label.RegisterCallback<MouseDownEvent>(OnLabelClick);
_mainContainer.Add(_label);
_textField = new TextField(1000, true, false, '*');
_textField.isDelayed = true;
// Support IME
_textField.RegisterCallback<FocusInEvent>(evt =>{ Input.imeCompositionMode = IMECompositionMode.On;});
_textField.RegisterCallback<FocusOutEvent>(evt => {
SetText(_textField.text);
Input.imeCompositionMode = IMECompositionMode.Auto;
SwitchToEditMode(false);
});
_textField.OnValueChanged((evt) =>
{
SetText(evt.newValue);
SwitchToEditMode(false);
});
}
private void UpdateFromData()
{
if(_customData != null)
{
layer = _customData.layer;
if(string.IsNullOrEmpty(_customData.uid))
{
_customData.uid = Guid.NewGuid().ToString();
}
uid = _customData.uid;
SetPosition(_customData.layout);
SetText(_customData.title);
}
}
protected override void OnStyleResolved(ICustomStyle style)
{
base.OnStyleResolved(style);
// Something is forcing style! Resetting a few things here, grrr.
this.style.borderBottomWidth = 1;
var resizer = this.Q(null, "resizer");
if(resizer != null)
{
resizer.style.paddingTop = 0;
resizer.style.paddingLeft = 0;
}
}
public override void SetPosition(Rect newPos)
{
newPos = GraphElementExtension.GetSnappedRect(newPos);
base.SetPosition(newPos);
}
public override void UpdatePresenterPosition()
{
base.UpdatePresenterPosition();
_customData.layout = GraphElementExtension.GetSnappedRect(GetPosition());
this.SaveNewData();
}
private double lastClickTime;
private const double doubleClickSpeed = 0.5;
private void OnLabelClick(MouseDownEvent evt)
{
var newTime = UnityEditor.EditorApplication.timeSinceStartup;
if(newTime - lastClickTime < doubleClickSpeed)
{
SwitchToEditMode(true);
}
lastClickTime = newTime;
}
private void SwitchToEditMode(bool switchingToEdit)
{
if (switchingToEdit)
{
_mainContainer.Remove(_label);
_textField.value = _label.text;
_mainContainer.Add(_textField);
_textField.Focus();
_textField.SelectAll();
}
else
{
_mainContainer.Remove(_textField);
_mainContainer.Add(_label);
}
MarkDirtyRepaint();
}
public void SetText(string value)
{
Undo.RecordObject(_graph.graphProgramAsset, "Rename Comment");
value = value.TrimEnd();
_customData.title = value;
_label.text = value;
_graph.SaveNewData();
MarkDirtyRepaint();
}
public UdonGraphElementData GetData()
{
return new UdonGraphElementData(UdonGraphElementType.UdonComment, uid, EditorJsonUtility.ToJson(_customData));
}
public class CustomData
{
public string uid;
public Rect layout;
public string title = "Comment";
public int layer;
public Color elementTypeColor;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e5916b8dd19e4445a9156a457b82ee4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,51 @@
using System;
using UnityEngine;
using UnityEditor.Experimental.UIElements.GraphView;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
public class UdonGraphElement : GraphElement
{
// Upgrade note - persistenceKey turns into viewDataKey in Unity 2019, this getter will make that transition easier
public string uid { get => persistenceKey; set => persistenceKey = value; }
internal UdonGraphElementType type = UdonGraphElementType.GraphElement;
internal UdonGraphElement()
{
}
// save this update to the asset
public override void UpdatePresenterPosition()
{
base.UpdatePresenterPosition();
this.Reserialize();
}
}
public interface IUdonGraphElementDataProvider
{
UdonGraphElementData GetData();
}
[Serializable]
public class GraphRect
{
public float x;
public float y;
public float width;
public float height;
public GraphRect(Rect input)
{
this.x = Mathf.Round(input.x);
this.y = Mathf.Round(input.y);
this.width = Mathf.Round(input.width);
this.height = Mathf.Round(input.height);
}
public Rect rect => new Rect(this.x, this.y, this.width, this.height);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba3ecc4c46929404d8c2ec920743b823
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,191 @@
using System;
using UnityEngine;
using UnityEditor.Experimental.UIElements.GraphView;
using System.Collections.Generic;
using UnityEditor;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
public class UdonGroup : Group, IUdonGraphElementDataProvider
{
public string uid;
private CustomData _customData = new CustomData();
private UdonGraph _graph;
public static UdonGroup Create(string value, Rect position, UdonGraph graph)
{
var group = new UdonGroup("", graph);
group.uid = Guid.NewGuid().ToString();
// make sure rect size is not 0
position.width = position.width > 0 ? position.width : 128;
position.height = position.height > 0 ? position.height : 128;
group._customData.uid = group.uid;
group._customData.layout = position;
group._customData.title = value;
return group;
}
public static UdonGroup Create(UdonGraphElementData elementData, UdonGraph graph)
{
return new UdonGroup(elementData.jsonData, graph);
}
// current order of operations issue when creating a group from the context menu means this isn't set until first save. This allows us to force it.
public void UpdateDataId()
{
_customData.uid = uid;
}
// Build a Group from jsonData, save to userData
private UdonGroup(string jsonData, UdonGraph graph)
{
title = "Group";
_graph = graph;
if (!string.IsNullOrEmpty(jsonData))
{
EditorJsonUtility.FromJsonOverwrite(jsonData, _customData);
}
}
public override void OnPersistentDataReady()
{
base.OnPersistentDataReady();
if(_customData != null)
{
// Propagate data to useful places
title = _customData.title;
layer = _customData.layer;
if (string.IsNullOrEmpty(_customData.uid))
{
_customData.uid = Guid.NewGuid().ToString();
}
uid = _customData.uid;
SetPosition(_customData.layout);
// Add all elements from graph to self
var childUIDs = _customData.containedElements;
if (childUIDs != null)
{
foreach (var item in childUIDs)
{
var childNode = _graph.GetElementByGuid(item);
if (childNode != null)
{
if (!ContainsElement(childNode))
{
AddElement(childNode);
// nodes can wind up behind the group since they're added first
childNode.BringToFront();
}
}
}
}
}
}
public bool containsElement(string uid)
{
return _customData.containedElements.Contains(uid);
}
public override void SetPosition(Rect newPos)
{
newPos = GraphElementExtension.GetSnappedRect(newPos);
base.SetPosition(newPos);
}
// Save data to asset after new position set
public override void UpdatePresenterPosition()
{
base.UpdatePresenterPosition();
_customData.layout = GraphElementExtension.GetSnappedRect(GetPosition());
this.SaveNewData();
}
// Save data to asset after rename
protected override void OnGroupRenamed(string oldName, string newName)
{
// limit name to 100 characters
title = newName.Substring(0, Mathf.Min(newName.Length, 100));
_customData.title = title;
this.SaveNewData();
}
protected override void OnElementsAdded(IEnumerable<GraphElement> elements)
{
base.OnElementsAdded(elements);
foreach (var element in elements)
{
if (!_customData.containedElements.Contains(element.persistenceKey))
{
// don't add StackNode children directly to group, messes up reload
if(!(element.parent is UdonStackNode))
{
_customData.containedElements.Add(element.persistenceKey);
}
}
// Set group variable on UdonNodes
if (element is UdonNode)
{
(element as UdonNode).group = this;
}
if (element is UdonComment)
{
(element as UdonComment).group = this;
}
}
this.SaveNewData();
}
protected override void OnElementsRemoved(IEnumerable<GraphElement> elements)
{
base.OnElementsRemoved(elements);
foreach (var element in elements)
{
if (_customData.containedElements.Contains(element.persistenceKey))
{
_customData.containedElements.Remove(element.persistenceKey);
if(element is UdonNode)
{
(element as UdonNode).group = null;
}
else if (element is UdonComment)
{
(element as UdonComment).group = null;
}
}
}
this.SaveNewData();
}
public override bool AcceptsElement(GraphElement element, ref string reasonWhyNotAccepted)
{
return base.AcceptsElement(element, ref reasonWhyNotAccepted);
}
public UdonGraphElementData GetData()
{
return new UdonGraphElementData(UdonGraphElementType.UdonGroup, uid, EditorJsonUtility.ToJson(_customData));
}
public class CustomData
{
public string uid;
public Rect layout;
public List<string> containedElements = new List<string>();
public string title;
public int layer;
public Color elementTypeColor;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1b8045222a10ce04b815642b9cd5ca17
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,63 @@
using System;
using UnityEditor.Experimental.UIElements.GraphView;
using UnityEngine;
using UnityEngine.Experimental.UIElements;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
public class UdonMinimap : MiniMap
{
private CustomData _customData = new CustomData();
private UdonGraph _graph;
public UdonMinimap(UdonGraph graph)
{
_graph = graph;
name = "UdonMap";
maxWidth = 200;
maxHeight = 100;
anchored = false;
SetPosition(_customData.layout);
}
private void SaveNewData()
{
if (!_graph.IsReloading)
{
_graph.SaveNewData();
}
}
public void SetVisible(bool value)
{
visible = value;
_customData.visible = value;
SaveNewData();
}
public override void UpdatePresenterPosition()
{
_customData.layout = GetPosition();
SaveNewData();
}
public UdonGraphElementData GetData()
{
return new UdonGraphElementData(UdonGraphElementType.Minimap, persistenceKey, JsonUtility.ToJson(_customData));
}
internal void LoadData(UdonGraphElementData data)
{
JsonUtility.FromJsonOverwrite(data.jsonData, _customData);
SetPosition(_customData.layout);
visible = _customData.visible;
}
public class CustomData
{
public bool visible = true;
public Rect layout = new Rect(new Vector2(10, 20), Vector2.zero);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b006d67642298f04e895b6709ef12429
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,877 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor.Experimental.UIElements.GraphView;
using EditorUI = UnityEditor.Experimental.UIElements;
using EngineUI = UnityEngine.Experimental.UIElements;
using VRC.Udon.Graph.Interfaces;
using VRC.Udon.Graph;
using VRC.Udon.Serialization;
using UnityEditor;
using UnityEngine.Experimental.UIElements;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
public class UdonNode : Node, IEdgeConnectorListener
{
// name is inherited from parent VisualElement class
public Type type;
public GameObject gameObject;
private UdonGraph _graphView;
private EditorUI.PopupField<string> _popup;
public UdonNodeDefinition definition;
public UdonNodeData data;
public Dictionary<int, UdonPort> portsIn;
public Dictionary<int, UdonPort> portsOut;
public List<UdonPort> portsFlowIn;
public List<UdonPort> portsFlowOut;
private INodeRegistry _registry;
public UdonGroup group;
// Overload handling
private IList<UdonNodeDefinition> overloadDefinitions;
private readonly Dictionary<UdonNodeDefinition, string> _optionNameCache = new Dictionary<UdonNodeDefinition, string>();
private readonly Dictionary<UdonNodeDefinition, string> _cleanerOptionNameCache = new Dictionary<UdonNodeDefinition, string>();
public UdonGraph Graph { get => _graphView; private set { } }
public INodeRegistry Registry { get => _registry; private set { } }
private readonly string[] _specialFlows =
{
"Block",
"Branch",
"For",
"Foreach",
"While",
};
// Upgrade note - persistenceKey turns into viewDataKey in Unity 2019, this getter will make that transition easier
public string uid { get => persistenceKey; set => persistenceKey = value; }
// Called when creating from Asset
public static UdonNode CreateNode(UdonNodeData nodeData, UdonGraph view)
{
UdonNodeDefinition definition = UdonEditorManager.Instance.GetNodeDefinition(nodeData.fullName);
if (definition == null)
{
Debug.LogError($"Cannot create node {nodeData.fullName} because there is no matching Node Definition");
return null;
}
return CreateNode(definition, view, nodeData);
}
// Called when creating from scratch
public static UdonNode CreateNode(UdonNodeDefinition definition, UdonGraph view, UdonNodeData nodeData = null)
{
return new UdonNode(definition, view, nodeData);
}
// Constructor is private to force all paths through Static factory method
private UdonNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null)
{
_graphView = view;
definition = nodeDefinition;
var registry = UdonGraphExtensions.GetRegistryForDefinition(nodeDefinition);
if(registry != null)
{
this._registry = registry;
}
else
{
Debug.LogWarning($"Couldn't find registry for {nodeDefinition.fullName}");
}
VisualContainer titleContainer = new VisualContainer()
{
name = "title-container",
};
this.Q("title").Insert(0, titleContainer);
titleContainer.Add(this.Q("title-label"));
var subtitle = new EngineUI.Label("")
{
name = "subtitle",
};
bool skipSubtitle = (
_specialFlows.Contains(nodeDefinition.fullName)
|| nodeDefinition.fullName.EndsWith("et_Variable")
|| nodeDefinition.fullName.StartsWithCached("Const_")
);
if (!skipSubtitle)
{
titleContainer.Insert(0, subtitle);
}
// Set Title
var displayTitle = UdonGraphExtensions.PrettyString(nodeDefinition.name).FriendlyNameify();
if (displayTitle == "Const_VRCUdonCommonInterfacesIUdonEventReceiver")
{
displayTitle = "UdonBehaviour";
}
else if(displayTitle == "==" || displayTitle == "!=" || displayTitle == "+")
{
displayTitle = $"{nodeDefinition.type.Name} {displayTitle}";
}
if (displayTitle.StartsWith("Op "))
displayTitle = displayTitle.Replace("Op ", "");
title = displayTitle;
name = nodeDefinition.fullName;
elementTypeColor = UnityEngine.Random.ColorHSV(0.5f, 0.6f, 0.1f, 0.2f, 0.8f, 0.9f);
string className = nodeDefinition.name.Split(' ').FirstOrDefault().Split('_').FirstOrDefault();
AddToClassList(className);
// Null is a type here, so handle it special
if(nodeDefinition.type == null)
{
AddToClassList("null");
}
else
{
AddToClassList(nodeDefinition.type.Namespace);
AddToClassList(nodeDefinition.type.Name);
}
AddToClassList(displayTitle.Replace(" ", "").ToLowerFirstChar());
if (nodeDefinition.fullName.Contains('_'))
{
AddToClassList(nodeDefinition.fullName.Substring(0, nodeDefinition.fullName.IndexOf('_')));
}
if (!skipSubtitle)
{
if (nodeDefinition.fullName.StartsWith("Event_"))
{
subtitle.text = "Event";
}
else
{
subtitle.text = className;
// temp title shenanigans
int firstSplit = nodeDefinition.fullName.IndexOf("__")+2;
if (firstSplit > 1)
{
int lastSplit = nodeDefinition.fullName.IndexOf("__", firstSplit);
int stringLength = (lastSplit > -1) ? lastSplit - firstSplit : nodeDefinition.fullName.Length - firstSplit;
string line2 = nodeDefinition.fullName.Substring(firstSplit, stringLength).Replace("_", " ").UppercaseFirst();
if (line2.StartsWith("Op "))
{
line2 = line2.Replace("Op ", "");
subtitle.text = nodeDefinition.type.Name;
}
title = line2;
}
else
{
Debug.Log($"Couldn't find classname for {nodeDefinition.fullName}");
}
}
}
// Create or validate nodeData
if (nodeData == null)
{
data = _graphView.graphData.AddNode(nodeDefinition.fullName);
PopulateDefaultValues();
ValidateNodeData();
}
else
{
data = nodeData;
ValidateNodeData();
SetPosition(new Rect(data.position.x, data.position.y, 0, 0));
}
uid = data.uid;
// Fill in all fields, etc and add to the graph view
if (UdonGraphExtensions.ShouldShowDocumentationLink(definition))
{
DrawHelpButton();
}
// Show overloads for nodes EXCEPT type, those have too many entries and break Unity UI
if (!nodeDefinition.fullName.StartsWith("Type_"))
{
RefreshPopup();
}
LayoutPorts(nodeDefinition);
view.MarkSceneDirty();
}
private void DrawHelpButton()
{
EngineUI.Button helpButton = new EngineUI.Button(ShowNodeDocs)
{
name = "help-button",
};
helpButton.Add(new EngineUI.TextElement()
{
name = "icon",
text = "?"
});
titleButtonContainer.Add(helpButton);
}
private void ShowNodeDocs()
{
string url = UdonGraphExtensions.GetDocumentationLink(definition);
if (!string.IsNullOrEmpty(url))
{
Help.BrowseURL(url);
}
}
public override void SetPosition(Rect newPos)
{
newPos.position = GraphElementExtension.GetSnappedPosition(newPos.position);
base.SetPosition(newPos);
data.position = newPos.position;
}
public override void UpdatePresenterPosition()
{
base.UpdatePresenterPosition();
this.Reserialize();
}
private void RefreshPopup()
{
// Get overloads, draw them if we have more than one signature for this method
overloadDefinitions = CacheOverloads();
if (overloadDefinitions != null && overloadDefinitions.Count > 1)
{
// Get index of currently selected (could cache this on node instead)
// TODO: switch to just reading this from Popup, which probably stores it
int currentIndex = 0;
for (int i = 0; i < overloadDefinitions.Count; i++)
{
if (overloadDefinitions.ElementAt(i).fullName != name)
{
continue;
}
currentIndex = i;
break;
}
// Build dropdown list
List<string> options = new List<string>();
for (int i = 0; i < overloadDefinitions.Count; i++)
{
UdonNodeDefinition nodeDefinition = overloadDefinitions.ElementAt(i);
if (!_optionNameCache.TryGetValue(nodeDefinition, out string optionName))
{
optionName = nodeDefinition.fullName;
// don't add overload types that take pointers, not supported
string[] splitOptionName = optionName.Split(new[] { "__" }, StringSplitOptions.None);
if (splitOptionName.Length >= 3)
{
optionName = $"({splitOptionName[2].Replace("_", ", ")})";
}
optionName = optionName.FriendlyNameify();
_optionNameCache.Add(nodeDefinition, optionName);
}
if (!_cleanerOptionNameCache.TryGetValue(nodeDefinition, out string cleanerOptionName))
{
cleanerOptionName =
optionName.Replace("UnityEngine", "").Replace("System", "").Replace("Variable_", "");
_cleanerOptionNameCache.Add(nodeDefinition, cleanerOptionName);
}
options.Add(cleanerOptionName);
// optionName is what was used as the tooltip. Do we need the tooltip?
}
// Clear out old one
if (inputContainer.Contains(_popup))
{
inputContainer.Remove(_popup);
}
_popup = new EditorUI.PopupField<string>(options, currentIndex);
_popup.OnValueChanged((e) =>
{
// TODO - store data in the dropdown and use formatListItemCallback?
data.fullName = overloadDefinitions.ElementAt(_popup.index).fullName;
_graphView.Reload();
});
inputContainer.Add(_popup);
}
}
private List<UdonNodeDefinition> CacheOverloads()
{
string baseIdentifier = name;
string[] splitBaseIdentifier = baseIdentifier.Split(new[] { "__" }, StringSplitOptions.None);
if (splitBaseIdentifier.Length >= 2)
{
baseIdentifier = $"{splitBaseIdentifier[0]}__{splitBaseIdentifier[1]}__";
}
if (baseIdentifier.StartsWithCached("Const_"))
{
return null;
}
if (baseIdentifier.StartsWithCached("Type_"))
{
baseIdentifier = "Type_";
}
if (baseIdentifier.StartsWithCached("Variable_"))
{
baseIdentifier = "Variable_";
}
// This used to be cached on graph instead of calculated per-node
// TODO: cache this somewhere, maybe UdonEditorManager? Is that worth it for performance?
IEnumerable<UdonNodeDefinition> matchingNodeDefinitions =
UdonEditorManager.Instance.GetNodeDefinitions(baseIdentifier);
var result = new List<UdonNodeDefinition>();
foreach (var definition in matchingNodeDefinitions)
{
// don't add definitions with pointer parameters, not supported in Udon
if (!definition.fullName.Contains('*'))
{
result.Add(definition);
}
}
return result;
}
internal void RestoreConnections()
{
RestoreInputs();
RestoreFlows();
}
private void RestoreFlows()
{
for (int i = 0; i < data.flowUIDs.Length; i++)
{
// skip if flow uid is empty
string nodeUID = data.flowUIDs[i];
if (string.IsNullOrEmpty(nodeUID))
{
continue;
}
// Find connected node via Graph
UdonNode connectedNode = _graphView.GetNodeByGuid(nodeUID) as UdonNode;
if (connectedNode == null)
{
Debug.Log($"Couldn't find node with GUID {nodeUID}");
}
// Trying to move a Block's flow that was left at the end to the beginning
if (portsFlowOut != null && i >= portsFlowOut.Count)
{
Debug.LogWarning($"Trying to restore flow to {connectedNode.name} from a non-existent port, skipping");
for (int j = 0; j < data.flowUIDs.Length; j++)
{
bool didRestoreFlow = false;
if (string.IsNullOrEmpty(data.flowUIDs[j]))
{
data.flowUIDs[j] = data.flowUIDs[i];
data.flowUIDs[i] = "";
didRestoreFlow = true;
}
if (didRestoreFlow)
{
RestoreFlows();
}
}
continue;
}
UdonPort sourcePort = null;
// Edge case, but its possible that this is null in broken graphs
// Skip if we can't find the source port
if (portsFlowOut != null)
{
sourcePort = portsFlowOut.Count > 1 ? portsFlowOut[i] : portsFlowOut.FirstOrDefault();
if (sourcePort == null)
{
Debug.LogError($"Failed to find output flow port for node {uid}");
// clear the flow uid, user will have to reconnect by hand
data.flowUIDs[i] = "";
continue;
}
}
else
{
Debug.LogError($"Failed to find output flow port for node {uid}");
// clear the flow uid, user will have to reconnect by hand
data.flowUIDs[i] = "";
continue;
}
UdonPort destPort = null;
// Edge case, but its possible that this is null in broken graphs
if(connectedNode.portsFlowIn != null)
{
destPort = connectedNode.portsFlowIn.FirstOrDefault();
if (destPort == null)
{
Debug.LogError($"Failed to find input flow port node node {nodeUID}");
// clear the flow uid, user will have to reconnect by hand
data.flowUIDs[i] = "";
continue;
}
}
else
{
Debug.LogError($"Failed to find input flow port node node {nodeUID}");
// clear the flow uid, user will have to reconnect by hand
data.flowUIDs[i] = "";
continue;
}
// Passed the tests! ready to connect
var edge = sourcePort.ConnectTo(destPort);
edge.AddToClassList("flow");
_graphView.AddElement(edge);
}
}
private void RestoreInputs()
{
for (int i = 0; i < definition.Inputs.Count; i++)
{
// Skip to next input if we don't have a node to check at this index
if (data.nodeUIDs.Length <= i)
{
continue;
}
// Skip to next input if we have a bad node reference
if (string.IsNullOrEmpty(data.nodeUIDs[i]))
{
continue;
}
// get otherIndex. not 100% sure what this refers to yet, maybe a port index?
string[] splitUID = data.nodeUIDs[i].Split('|');
string nodeUID = splitUID[0];
int otherIndex = 0;
if (splitUID.Length > 1)
{
otherIndex = int.Parse(splitUID[1]);
}
// Skip if we don't have a good uid for the other node
if (string.IsNullOrEmpty(nodeUID))
{
continue;
}
// Find connected node via Graph
UdonNode connectedNode = _graphView.GetNodeByGuid(nodeUID) as UdonNode;
if (connectedNode == null)
{
Debug.Log($"Couldn't find node with GUID {nodeUID}");
}
// No matching port for this data, skip
if (!portsIn.TryGetValue(i, out UdonPort destPort))
{
Debug.LogError($"Failed to find input data slot (index {i}) for node {uid} {data.fullName}");
continue;
}
// Copied from Legacy, not sure what conditions would cause this
if (otherIndex < 0 || connectedNode.portsOut.Keys.Count <= otherIndex)
{
otherIndex = 0;
}
// skip if we can't find the sourcePort - comment better once you understand what this is exactly
if (!connectedNode.portsOut.TryGetValue(otherIndex, out UdonPort sourcePort))
{
Debug.LogError($"Failed to find output data slot for node {nodeUID}");
continue;
}
// Passed the tests! ready to connect
var edge = sourcePort.ConnectTo(destPort);
_graphView.AddElement(edge);
}
}
// Legacy, haven't gone through yet
void ValidateNodeData()
{
bool modifiedData = false;
for (int i = 0; i < data.nodeValues.Length; i++)
{
if (definition.Inputs.Count <= i)
{
continue;
}
Type expectedType = definition.Inputs[i].type;
if (data.nodeValues[i] == null)
{
continue;
}
object value = data.nodeValues[i].Deserialize();
if (value == null)
{
continue;
}
if (!expectedType.IsInstanceOfType(value))
{
data.nodeValues[i] = SerializableObjectContainer.Serialize(null, expectedType);
modifiedData = true;
}
}
if (modifiedData)
{
_graphView.ReSerializeData();
}
}
void PopulateDefaultValues()
{
// No default values so I'm just...making them?
int count = definition.Inputs.Count;
data.nodeValues = new SerializableObjectContainer[count];
data.nodeUIDs = new string[count];
for (int i = 0; i < count; i++)
{
object value = definition.defaultValues.Count > i ? definition.defaultValues[i] : default;
data.nodeValues[i] = SerializableObjectContainer.Serialize(value, definition.Inputs[i].type);
}
}
private enum VariableNodeType { Get, Set};
// renamed from MakePorts to match Legacy implementation for now
private void LayoutPorts(UdonNodeDefinition udonNodeDefinition)
{
SetupFlowPorts(udonNodeDefinition);
// Don't setup in ports for Get_Variable node types, instead add variable popup
if (name.CompareTo("Get_Variable") == 0)
{
AddVariablePopup(VariableNodeType.Get);
}
else
{
// Add Variable popup and in-ports for Set_Variable
if (name.CompareTo("Set_Variable") == 0)
{
AddVariablePopup(VariableNodeType.Set);
}
SetupInPorts(udonNodeDefinition);
}
SetupOutPorts(udonNodeDefinition);
RefreshExpandedState();
RefreshPorts();
}
// TODO: Test this again after we have the new graph serializing the addition of nodes
private void AddVariablePopup(VariableNodeType varType)
{
// Legacy method of determining currently selected index
// TODO: upgrade this logic path from the legacy method of determining Variable indices
// Get Variable nodes only have one value, get it and deserialize
var value = data.nodeValues[0].Deserialize();
// Make copy of options so we can add the Create New Variable option
List<string> options = new List<string>(_graphView.GetVariableNames)
{
"Create New Variable"
};
// Get value of selected node in rather roundabout way
int currentIndex = _graphView.GetVariableNodes
.IndexOf(_graphView.GetVariableNodes.FirstOrDefault(v => v.uid == (string)value));
if (currentIndex < 0)
{
Debug.LogWarning($"Node {name} didn't have a variable assigned, removing");
_graphView.graphData.RemoveNode(data);
_graphView.RemoveElement(this);
_graphView.Reload();
return;
}
// Create popup, set current value and set function to update data when it's changed.
var popup = new EditorUI.PopupField<string>(options, currentIndex);
popup.OnValueChanged((e) => {
// Test whether we've selected an existing variable or the 'Create New Variable' option
if(popup.index < _graphView.GetVariableNames.Count)
{
// not currently using event value, which is variable name. Instead using legacy method of comparing index to graph variable nodes array index
string newUid = _graphView.GetVariableNodes[popup.index].uid;
// Get Variable nodes only have one entry, so index is 0 below
SetNewValue(newUid, 0);
// include 'Set' in title for Set Variable, just name for Get Variable
title = (varType == VariableNodeType.Set ? "Set " : "") + e.newValue;
_graphView.Reload();
}
else
{
// User selected 'Create New Variable'
string newUid = _graphView.AddNewVariable();
SetNewValue(newUid, 0, typeof(string));
// TODO: see if we can remove the need for a second reload here
_graphView.Reload();
}
});
string startingUid = _graphView.GetVariableNodes[currentIndex].uid;
SetNewValue(startingUid, 0);
// include 'Set' in title for Set Variable, just name for Get Variable
title = (varType == VariableNodeType.Set ? "Set " : "") + _graphView.GetVariableNames[currentIndex];
// Add newly created popup to node
inputContainer.Add(popup);
}
public void SetNewValue(object newValue, int index, Type inType = null)
{
data.nodeValues[index] = SerializableObjectContainer.Serialize(newValue, inType);
this.Reserialize();
}
private void SetupOutPorts(UdonNodeDefinition udonNodeDefinition)
{
portsOut = new Dictionary<int, UdonPort>();
for (int i = 0; i < udonNodeDefinition.Outputs.Count; i++)
{
var item = udonNodeDefinition.Outputs[i];
// Convert object type to variable type for Get_Variable nodes, or run them through the SlotTypeConverter for all other nodes
Type type = (udonNodeDefinition.fullName.Contains("Get_Variable")) ?
GetTypeForDefinition(udonNodeDefinition) :
UdonGraphExtensions.SlotTypeConverter(item.type, udonNodeDefinition.fullName);
string label = UdonGraphExtensions.FriendlyTypeName(type).FriendlyNameify();
if (label == "IUdonEventReceiver")
{
label = "UdonBehaviour";
}
if (item.name != null) label = $"{label} {item.name}";
UdonPort port = (UdonPort) UdonPort.Create(label, Direction.Output, this, type, data, i);
outputContainer.Add(port);
portsOut[i] = port;
}
}
private void SetupInPorts(UdonNodeDefinition udonNodeDefinition)
{
portsIn = new Dictionary<int, UdonPort>();
// Expand node data to hold values for all inputs
data.Resize(udonNodeDefinition.Inputs.Count);
int startIndex = 0;
// Skip first input for Set_Variable since that's the eventName which is set via dropdown
if (name.CompareTo("Set_Variable") == 0)
{
startIndex = 1;
}
// Skip inputs for Null and This nodes
if (name.Contains("Const_Null") || name.Contains("Const_This"))
{
return;
}
for (int index = startIndex; index < udonNodeDefinition.Inputs.Count; index++)
{
UdonNodeParameter input = udonNodeDefinition.Inputs[index];
string label = "";
// TODO: Ask Cubed what this does? Or figure it out.
if (udonNodeDefinition.Inputs.Count > index && index >= 0)
{
label = udonNodeDefinition.Inputs[index].name;
}
if (label == "IUdonEventReceiver")
{
label = "UdonBehaviour";
}
label = label.FriendlyNameify();
string typeName = UdonGraphExtensions.FriendlyTypeName(input.type);
// skip over types with pointers. Should remove these from included overloads in the first place!
if (typeName.Contains('*'))
{
continue;
}
// Convert object type to variable type for Set_Variable nodes, or run them through the SlotTypeConverter for all other nodes
Type type = (udonNodeDefinition.fullName.Contains("Set_Variable")) ?
type = GetTypeForDefinition(udonNodeDefinition) :
UdonGraphExtensions.SlotTypeConverter(input.type, udonNodeDefinition.fullName);
// not 100% sure if I should use label or typeName here
UdonPort p = UdonPort.Create(label, Direction.Input, this, type, data, index) as UdonPort;
inputContainer.Add(p);
portsIn.Add(index, p);
}
}
private Type GetTypeForDefinition(UdonNodeDefinition udonNodeDefinition)
{
string targetUid = data.nodeValues[0].Deserialize().ToString();
UdonNodeData varData = _graphView.GetVariableNodes.Where(n => n.uid == targetUid).FirstOrDefault();
if (varData != null)
{
var targetDefinition = UdonEditorManager.Instance.GetNodeDefinition(varData.fullName);
if (targetDefinition != null)
{
return UdonGraphExtensions.SlotTypeConverter(targetDefinition.type, udonNodeDefinition.fullName);
}
}
// if we fail, return generic object type
return typeof(object);
}
private void SetupFlowPorts(UdonNodeDefinition udonNodeDefinition)
{
if (udonNodeDefinition.flow)
{
portsFlowIn = new List<UdonPort>();
portsFlowOut = new List<UdonPort>();
string label = "";
int inFlowIndex = -1;
int outFlowIndex = -1;
// don't add input flow for events, they're called from above
if (!udonNodeDefinition.fullName.StartsWith("Event_"))
{
label = udonNodeDefinition.inputFlowNames.Count > 0 ? udonNodeDefinition.inputFlowNames[0] : "";
AddFlowPort(Direction.Input, label, ++inFlowIndex);
}
// add output flow
label = udonNodeDefinition.outputFlowNames.Count > 0 ? udonNodeDefinition.outputFlowNames[0] : "";
AddFlowPort(Direction.Output, label, ++outFlowIndex);
if (_specialFlows.Contains(udonNodeDefinition.fullName))
{
label = udonNodeDefinition.outputFlowNames.Count > 1 ? udonNodeDefinition.outputFlowNames[1] : "";
AddFlowPort(Direction.Output, label, ++outFlowIndex);
}
// Add the number of output flows we need for a Block
if (udonNodeDefinition.fullName == "Block")
{
data.flowUIDs = data.flowUIDs.Where(f => !string.IsNullOrEmpty(f)).ToArray();
int connectedFlows = data.flowUIDs.Length;
if (connectedFlows > 1)
{
for (int i = 0; i < connectedFlows - 1; i++)
{
AddFlowPort(Direction.Output, "", ++outFlowIndex);
}
}
}
}
}
private void AddFlowPort(Direction d, string label, int index)
{
UdonPort p = (UdonPort) UdonPort.Create(label, d, this, null, data, index);
p.AddToClassList("flow");
if(d == Direction.Input)
{
inputContainer.Add(p);
portsFlowIn.Add(p);
}
else
{
outputContainer.Add(p);
portsFlowOut.Add(p);
}
}
private bool HasRecursiveFlow(Port fromSlot, Port toSlot)
{
// No need to check connections to value slots
if (toSlot.portType != null) return false;
// Check out ports of node being connected TO
foreach (var port in (toSlot.node as UdonNode).portsFlowOut)
{
// if any of its ports connect to fromSlot, it's recursive. using foreach for convenience, should be just one edge
foreach (var edge in port.connections)
{
// if this connection goes to the node that started this all, then it's recursion
if(edge.input.node == fromSlot.node)
{
return true;
}
// Need to run this recursively to check all ports
if(HasRecursiveFlow(fromSlot, edge.input))
{
return true;
}
}
}
return false;
}
#region IEdgeConnectorListener
public void OnDrop(UnityEditor.Experimental.UIElements.GraphView.GraphView graphView, Edge edge)
{
if (edge.output != null && edge.input != null && !HasRecursiveFlow(edge.output, edge.input))
{
edge.output.Connect(edge);
edge.input.Connect(edge);
graphView.AddElement(edge);
// Reload block nodes after new connections
if(definition.fullName == "Block")
{
this.Reload();
}
}
}
public void OnDropOutsidePort(Edge edge, Vector2 position)
{
if (!Settings.SearchOnNoodleDrop) return;
if (edge.output != null && edge.output.portType != null)
{
_graphView.OpenPortSearch(edge.output.portType, position, edge.output, Direction.Input);
}
else if (edge.input != null && edge.input.portType != null)
{
_graphView.OpenPortSearch(edge.input.portType, position, edge.input, Direction.Output);
}
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dcd657bc1dcf357448d27bcfa8c5dc36
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,431 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEditor.Experimental.UIElements.GraphView;
using UnityEngine;
using UnityEngine.Experimental.UIElements;
using UnityEngine.UI;
using VRC.Udon.Graph;
using VRC.Udon.Serialization;
using EditorUI = UnityEditor.Experimental.UIElements;
using EngineUI = UnityEngine.Experimental.UIElements;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
[Serializable]
public class UdonPort : Port
{
public string FullName;
private UdonNodeData _udonNodeData;
private int _nodeValueIndex;
private VisualElement _inputField;
private VisualElement _inputFieldTypeLabel;
private IArrayProvider _inspector;
private bool _waitToReserialize = false;
protected UdonPort(Orientation portOrientation, Direction portDirection, Capacity portCapacity, Type type) : base(portOrientation, portDirection, portCapacity, type)
{
}
public static Port Create(string portName, Direction portDirection, IEdgeConnectorListener connectorListener, Type type, UdonNodeData data, int index, Orientation orientation = Orientation.Horizontal)
{
Capacity capacity = Capacity.Single;
if(portDirection == Direction.Input && type == null || portDirection == Direction.Output && type != null)
{
capacity = Capacity.Multi;
}
var port = new UdonPort(orientation, portDirection, capacity, type)
{
m_EdgeConnector = new EdgeConnector<Edge>(connectorListener),
};
port.portName = portName;
port._udonNodeData = data;
port._nodeValueIndex = index;
port.SetupPort();
return port;
}
public int GetIndex()
{
return _nodeValueIndex;
}
private void SetupPort()
{
this.AddManipulator(m_EdgeConnector);
tooltip = UdonGraphExtensions.FriendlyTypeName(portType);
if (portType == null || direction == Direction.Output)
{
return;
}
var value = TryGetValueObject(out object result);
var field = UdonFieldFactory.CreateField(
portType,
result,
newValue => SetNewValue(newValue)
);
if(field != null)
{
SetupField(field);
}
if (_udonNodeData.fullName.StartsWith("Const"))
{
RemoveConnector();
}
UpdateLabel(connected);
}
// Made its own method for now as we have issues auto-converting between string and char in a TextField
// TODO: refactor SetupField so we can do just the field.value part separately to combine with this
private VisualElement SetupCharField()
{
TextField field = new TextField();
field.AddToClassList("portField");
if (TryGetValueObject(out object result))
{
field.value = UdonGraphExtensions.UnescapeLikeALiteral((char)result);
}
field.isDelayed = true;
// Special handling for escaping char value
field.OnValueChanged((e) =>
{
if(e.newValue[0] == '\\' && e.newValue.Length > 1)
{
SetNewValue(UdonGraphExtensions.EscapeLikeALiteral(e.newValue.Substring(0, 2)));
}
else
{
SetNewValue(e.newValue[0]);
}
});
_inputField = field;
// Add label, shown when input is connected. Not shown by default
var friendlyName = UdonGraphExtensions.FriendlyTypeName(typeof(char)).FriendlyNameify();
var label = new EngineUI.Label(friendlyName);
_inputFieldTypeLabel = label;
return _inputField;
}
private void SetupField(VisualElement field)
{
// Delay color fields so they don't break UI
if(portType.IsAssignableFrom(typeof(EditorUI.ColorField)))
{
_waitToReserialize = true;
}
// Custom Event fields need their event names sanitized after input and their connectors removed
if (_udonNodeData.fullName.CompareTo("Event_Custom") == 0)
{
var tfield = (TextField)field;
tfield.OnValueChanged((e) =>
{
string newValue = e.newValue.SanitizeVariableName();
tfield.value = newValue;
SetNewValue(newValue);
});
RemoveConnector();
}
// Add label, shown when input is connected. Not shown by default
var friendlyName = UdonGraphExtensions.FriendlyTypeName(portType).FriendlyNameify();
var label = new EngineUI.Label(friendlyName);
_inputFieldTypeLabel = label;
field.AddToClassList("portField");
_inputField = field;
Add(_inputField);
}
private void RemoveConnector()
{
this.Q("connector")?.RemoveFromHierarchy();
this.Q(null, "connectorText")?.RemoveFromHierarchy();
}
#pragma warning disable 0649 // variable never assigned
private EngineUI.Button _editArrayButton;
private void EditArray(Type elementType)
{
// Update Values when 'Save' is clicked
if(_inspector != null)
{
// Update Values
SetNewValue(_inspector.GetValues());
// Remove Inspector
_inspector.RemoveFromHierarchy();
_inspector = null;
// Update Button Text
_editArrayButton.text = "Edit";
return;
}
// Otherwise set up the inspector
_editArrayButton.text = "Save";
// Get value object, null is ok
TryGetValueObject(out object value);
// Create it new
Type typedArrayInspector = (typeof(UdonArrayInspector<>)).MakeGenericType(elementType);
_inspector = (Activator.CreateInstance(typedArrayInspector, value) as IArrayProvider);
parent.Add(_inspector as VisualElement);
}
// Update elements on connect
public override void Connect(Edge edge)
{
base.Connect(edge);
// The below logic is just for Output ports
if (edge.input.Equals(this)) return;
// hide field, show label
var input = ((UdonPort)edge.input);
input.UpdateLabel(true);
if (IsReloading())
{
return;
}
// update data
if (portType == null)
{
// We are a flow port
SetFlowUID(((UdonNode)input.node).uid);
}
else
{
// We are a value port, we need to send our info over to the OTHER node
string myNodeUid = ((UdonNode)node).uid;
input.SetDataFromNewConnection($"{myNodeUid}|{_nodeValueIndex}", input.GetIndex());
}
// in this case, we catch the method on the left node, with input as the right node
SendReserializeEvent();
}
public override void OnStopEdgeDragging()
{
base.OnStopEdgeDragging();
if (edgeConnector.edgeDragHelper.draggedPort == this)
{
if (capacity == Capacity.Single && connections.Count() > 0)
{
// This port could only have one connection. Fixed in Reserialize, need to reload to show the change
this.Reload();
}
}
}
private void SetFlowUID(string newValue)
{
if (_udonNodeData.flowUIDs.Length <= _nodeValueIndex)
{
// If we don't have space for this flow value, create a new array
// TODO: handle this elsewhere?
var newFlowArray = new string[_nodeValueIndex + 1];
for (int i = 0; i < _udonNodeData.flowUIDs.Length; i++)
{
newFlowArray[i] = _udonNodeData.flowUIDs[i];
}
_udonNodeData.flowUIDs = newFlowArray;
_udonNodeData.flowUIDs.SetValue(newValue, _nodeValueIndex);
}
else
{
_udonNodeData.flowUIDs.SetValue(newValue, _nodeValueIndex);
}
}
public bool IsReloading()
{
if(node is UdonNode)
{
return ((UdonNode)node).Graph.IsReloading;
}
else if(node is UdonStackNode)
{
return ((UdonStackNode)node).Graph.IsReloading;
}
else
{
return false;
}
}
public void SetDataFromNewConnection(string uidAndPort, int index)
{
// can't do this for Reg stack nodes yet so skipping for demo
if (_udonNodeData == null) return;
if (_udonNodeData.nodeUIDs.Length <= _nodeValueIndex)
{
Debug.Log("Couldn't set it");
}
else
{
_udonNodeData.nodeUIDs.SetValue(uidAndPort, index);
}
}
// Update elements on disconnect
public override void Disconnect(Edge edge)
{
base.Disconnect(edge);
// hide label, show field
if(direction == Direction.Input)
{
UpdateLabel(false);
}
if (IsReloading())
{
return;
}
// update data
if (direction == Direction.Output && portType == null)
{
// We are a flow port
SetFlowUID("");
}
else if (direction == Direction.Input && portType != null)
{
// Direction is input
// We are a value port
SetDataFromNewConnection("", GetIndex());
}
// in this case, we catch the method on the left node, with input as the right node
SendReserializeEvent();
}
public void UpdateLabel(bool isConnected)
{
// Port has a 'connected' bool but it doesn't seem to update, so passing 'isConnected' for now
if (isConnected)
{
if (_inputField != null && Contains(_inputField))
{
_inputField.RemoveFromHierarchy();
}
if (_inputFieldTypeLabel != null && !Contains(_inputFieldTypeLabel))
{
Add(_inputFieldTypeLabel);
}
if(_editArrayButton != null && Contains(_editArrayButton))
{
_editArrayButton.RemoveFromHierarchy();
}
}
else
{
if (_inputField != null && !Contains(_inputField))
{
Add(_inputField);
}
if (_inputFieldTypeLabel != null && Contains(_inputFieldTypeLabel))
{
_inputFieldTypeLabel.RemoveFromHierarchy();
}
if(_editArrayButton != null && !Contains(_editArrayButton))
{
Add(_editArrayButton);
}
}
}
private bool TryGetValueObject(out object result)
{
result = null;
SerializableObjectContainer container = null;
// For Get and Set variable nodes, reach into the actual value of the linked variable
if (_udonNodeData.fullName.Contains("et_Variable"))
{
string targetVariableUid = (string)_udonNodeData.nodeValues[0].Deserialize();
if (targetVariableUid == null) return false;
var data = _udonNodeData.GetGraph().FindNode(targetVariableUid);
if (data == null) return false;
container = data.nodeValues[0];
}
else
{
container = _udonNodeData.nodeValues[_nodeValueIndex];
}
if (container == null)
{
return false;
}
result = container.Deserialize();
if(result == null)
{
return false;
}
return true;
}
private void SetNewValue(object newValue)
{
var container = SerializableObjectContainer.Serialize(newValue, portType);
if (_udonNodeData.fullName.Contains("Set_Variable"))
{
// Variables need to be traced back to their actual data points
string targetVariableUid = (string)_udonNodeData.nodeValues[0].Deserialize();
if (targetVariableUid == null) return;
var data = _udonNodeData.GetGraph().FindNode(targetVariableUid);
if (data == null) return;
data.nodeValues[0] = container;
}
else
{
// regular value setting
_udonNodeData.nodeValues[_nodeValueIndex] = container;
}
if (!_waitToReserialize)
{
SendReserializeEvent();
}
}
private void SendReserializeEvent()
{
if (!IsReloading())
{
this.Reserialize();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f83d1d3578dd28498c71a980bca86dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor.Experimental.UIElements.GraphView;
using EditorUI = UnityEditor.Experimental.UIElements;
using VRC.Udon.Graph.Interfaces;
using VRC.Udon.Graph;
using UnityEngine.Experimental.UIElements;
using UnityEditor;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
public class UdonStackNode : StackNode, IEdgeConnectorListener, IUdonGraphElementDataProvider
{
private UdonGraph _graph;
private INodeRegistry _registry;
public UdonPort refPort;
private CustomData _customData = new CustomData();
public UdonGraph Graph { get => _graph; private set { } }
// Upgrade note - persistenceKey turns into viewDataKey in Unity 2019, this getter will make that transition easier
public string uid { get => persistenceKey; set => persistenceKey = value; }
// Called when restoring from asset
public static UdonStackNode Create(UdonGraphElementData elementData, UdonGraph graph)
{
return new UdonStackNode(elementData.jsonData, graph);
}
// Called when creating from Search
public static UdonStackNode Create(INodeRegistry registry, Vector2 position, UdonGraph graph)
{
var stackNode = new UdonStackNode("", graph);
string registryName = registry.ToString();
registryName = registryName.Substring(registryName.LastIndexOf('.') + 1);
stackNode._customData.registryName = registryName;
stackNode._customData.layout = new Rect(position, Vector2.zero);
return stackNode;
}
private string GetSimpleNameForRegistry(INodeRegistry registry)
{
string registryName = registry.ToString().Replace("NodeRegistry", "").FriendlyNameify();
registryName = registryName.Substring(registryName.LastIndexOf(".") + 1);
registryName = registryName.Replace("UnityEngine", "");
return registryName;
}
private UdonStackNode(string jsonData, UdonGraph graph)
{
_graph = graph;
if (!string.IsNullOrEmpty(jsonData))
{
EditorJsonUtility.FromJsonOverwrite(jsonData, _customData);
}
}
private bool _initialized = false;
public override void OnPersistentDataReady()
{
if (_initialized) return;
if(_customData != null)
{
// Exit early if we can't find the registry
if (!UdonEditorManager.Instance.TryGetRegistry(_customData.registryName, out _registry))
{
Debug.LogError($"Couldn't make Stack Node from Registry {_customData.registryName}");
return;
}
_customData.title = GetSimpleNameForRegistry(_registry);
layer = _customData.layer;
if (string.IsNullOrEmpty(_customData.uid))
{
_customData.uid = Guid.NewGuid().ToString();
}
uid = _customData.uid;
name = _customData.title;
headerContainer.Add(new Label() { text = _customData.title });
MakePorts(_registry);
SetPosition(_customData.layout);
UseDefaultStyling();
// add all contained elements from graph to self
var childUIDs = _customData.containedElements;
if (childUIDs != null)
{
foreach (var item in childUIDs)
{
var childNode = _graph.GetElementByGuid(item);
if (childNode != null)
{
AddElement(childNode);
// nodes can wind up behind the stacknode since they're added first
childNode.BringToFront();
}
}
}
}
_initialized = true;
this.SaveNewData();
}
private void MakePorts(INodeRegistry registry)
{
Dictionary<string, UdonNodeDefinition> objectMethods = new Dictionary<string, UdonNodeDefinition>();
RefreshExpandedState();
RefreshPorts();
}
public INodeRegistry GetRegistry()
{
return _registry;
}
public void OnElementsAdded(IEnumerable<GraphElement> elements)
{
foreach (var element in elements)
{
if (!_customData.containedElements.Contains(element.persistenceKey))
{
_customData.containedElements.Add(element.persistenceKey);
Debug.Log($"StackNode {name} adding {element.name}");
}
}
this.SaveNewData();
}
public void OnElementsRemoved(IEnumerable<GraphElement> elements)
{
foreach (var element in elements)
{
if (_customData.containedElements.Contains(element.persistenceKey))
{
Debug.Log($"StackNode {name} removing {element.name}");
_customData.containedElements.Remove(element.persistenceKey);
}
}
this.SaveNewData();
}
protected override bool AcceptsElement(GraphElement element, ref int proposedIndex, int maxIndex)
{
return true;
}
public override void UpdatePresenterPosition()
{
base.UpdatePresenterPosition();
_customData.layout = GetPosition();
this.SaveNewData();
}
#region IEdgeConnectorListener
public void OnDropOutsidePort(Edge edge, Vector2 position)
{
Debug.Log($"OnDropOutside from {name}");
}
public void OnDrop(EditorUI.GraphView.GraphView graphView, Edge edge)
{
Debug.Log($"OnDrop from {name}");
}
#endregion
public UdonGraphElementData GetData()
{
return new UdonGraphElementData(UdonGraphElementType.UdonStackNode, uid, EditorJsonUtility.ToJson(_customData));
}
public class CustomData
{
public string uid;
public Rect layout;
public List<string> containedElements = new List<string>();
public string registryName;
public string title;
public int layer;
public Color elementTypeColor;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c5cfbbfcd7bb5ad4487f1f9388a3a168
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,79 @@
using System;
using UnityEditor.Experimental.UIElements.GraphView;
using UnityEngine;
using UnityEngine.Experimental.UIElements;
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
{
public class UdonVariablesBlackboard : Blackboard, IUdonGraphElementDataProvider
{
private CustomData _customData = new CustomData();
private UdonGraph _graph;
public UdonVariablesBlackboard(UdonGraph graph)
{
_graph = graph;
title = "Variables";
name = "Parameters";
scrollable = true;
// Remove subtitle
var subtitle = this.Query<Label>("subTitleLabel").AtIndex(0);
if (subtitle != null)
{
subtitle.RemoveFromHierarchy();
}
// Improve resizer UI
style.borderBottomWidth = 1;
var resizer = this.Q(null, "resizer");
if (resizer != null)
{
resizer.style.paddingTop = 0;
resizer.style.paddingLeft = 0;
}
SetPosition(_customData.layout);
}
public void SetVisible(bool value)
{
visible = value;
_customData.visible = value;
SaveNewData();
}
public override void UpdatePresenterPosition()
{
_customData.layout = GetPosition();
SaveNewData();
}
private void SaveNewData()
{
if (!_graph.IsReloading)
{
_graph.SaveNewData();
}
}
public UdonGraphElementData GetData()
{
return new UdonGraphElementData(UdonGraphElementType.VariablesWindow, persistenceKey, JsonUtility.ToJson(_customData));
}
public class CustomData {
public bool visible = true;
public Rect layout = new Rect(10, 130, 200, 150);
}
internal void LoadData(UdonGraphElementData data)
{
JsonUtility.FromJsonOverwrite(data.jsonData, _customData);
SetPosition(_customData.layout);
this.visible = _customData.visible;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2d0a4730c5f61b247b27b54f280300b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: