Add VRChat and GitHub packages to Unity project
This commit is contained in:
1 parent
0334a52b21
commit
85538aaccc
3297 files changed
+847877
No files matched your search
+79
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 469db50616185d04e8a46dcd75db12d2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+73
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f257a6eeae213a4db991d486cace003
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+150
@@ -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
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4f0ade55ae13b6468a765826f1f2540
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+192
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e5916b8dd19e4445a9156a457b82ee4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+51
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba3ecc4c46929404d8c2ec920743b823
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+191
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b8045222a10ce04b815642b9cd5ca17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+63
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b006d67642298f04e895b6709ef12429
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+877
@@ -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
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dcd657bc1dcf357448d27bcfa8c5dc36
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+431
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f83d1d3578dd28498c71a980bca86dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+195
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5cfbbfcd7bb5ad4487f1f9388a3a168
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+79
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d0a4730c5f61b247b27b54f280300b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in new issue
Block a user