diff options
Diffstat (limited to 'VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources')
122 files changed, 10628 insertions, 0 deletions
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes.meta new file mode 100644 index 00000000..1e05c23e --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 564cb755fc823d94d98948f3bb3f95cd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs new file mode 100644 index 00000000..17be4606 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace VRC.Udon.Editor.ProgramSources.Attributes +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public class UdonProgramSourceNewMenuAttribute : Attribute + { + public Type Type { get; } + public string DisplayName { get; } + + public UdonProgramSourceNewMenuAttribute(Type type, string displayName) + { + Type = type; + DisplayName = displayName; + } + } +} + diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs.meta new file mode 100644 index 00000000..ea45c1be --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0e5ced9511d591140b191bbd9e948e61 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs new file mode 100644 index 00000000..33ecd07f --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using UnityEditor; +using VRC.Udon.Common.Interfaces; +using VRC.Udon.ProgramSources; +using VRC.Udon.Serialization.OdinSerializer; + +namespace VRC.Udon.Editor.ProgramSources +{ + [CustomEditor(typeof(SerializedUdonProgramAsset))] + public class SerializedUdonProgramAssetEditor : UnityEditor.Editor + { + private SerializedProperty _serializedProgramBytesStringSerializedProperty; + private SerializedProperty _serializationDataFormatSerializedProperty; + + private void OnEnable() + { + _serializedProgramBytesStringSerializedProperty = serializedObject.FindProperty("serializedProgramBytesString"); + _serializationDataFormatSerializedProperty = serializedObject.FindProperty("serializationDataFormat"); + } + + public override void OnInspectorGUI() + { + DrawSerializationDebug(); + } + + [Conditional("UDON_DEBUG")] + private void DrawSerializationDebug() + { + EditorGUILayout.LabelField($"DataFormat: {(DataFormat)_serializationDataFormatSerializedProperty.enumValueIndex}"); + + if(string.IsNullOrEmpty(_serializedProgramBytesStringSerializedProperty.stringValue)) + { + return; + } + + if(_serializationDataFormatSerializedProperty.enumValueIndex == (int)DataFormat.JSON) + { + using(new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.TextArea(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(_serializedProgramBytesStringSerializedProperty.stringValue))); + } + } + else + { + using(new EditorGUI.DisabledScope(true)) + { + SerializedUdonProgramAsset serializedUdonProgramAsset = (SerializedUdonProgramAsset)target; + IUdonProgram udonProgram = serializedUdonProgramAsset.RetrieveProgram(); + byte[] serializedBytes = SerializationUtility.SerializeValue(udonProgram, DataFormat.JSON, out List<UnityEngine.Object> _); + EditorGUILayout.TextArea(System.Text.Encoding.UTF8.GetString(serializedBytes)); + } + } + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs.meta new file mode 100644 index 00000000..e72c03a7 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e1b5b45f24b268b42826fc5c5497dc15 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram.meta new file mode 100644 index 00000000..6fccff45 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2811cc384b684c04f9b647c597b55ff8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs new file mode 100644 index 00000000..0045931e --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEditor; +using UnityEngine; +using VRC.Udon.Editor; +using VRC.Udon.Editor.ProgramSources; +using VRC.Udon.Editor.ProgramSources.Attributes; + +[assembly: UdonProgramSourceNewMenu(typeof(UdonAssemblyProgramAsset), "Udon Assembly Program Asset")] + +namespace VRC.Udon.Editor.ProgramSources +{ + [CreateAssetMenu(menuName = "VRChat/Udon/Udon Assembly Program Asset", fileName = "New Udon Assembly Program Asset")] + public class UdonAssemblyProgramAsset : UdonProgramAsset + { + [SerializeField] + protected string udonAssembly = ""; + + [SerializeField] + protected string assemblyError = null; + + public delegate void AssembleDelegate(bool success, string assembly); + public event AssembleDelegate OnAssemble; + + protected override void DrawProgramSourceGUI(UdonBehaviour udonBehaviour, ref bool dirty) + { + DrawAssemblyTextArea(!Application.isPlaying, ref dirty); + DrawAssemblyErrorTextArea(); + + base.DrawProgramSourceGUI(udonBehaviour, ref dirty); + } + + protected override void RefreshProgramImpl() + { + AssembleProgram(); + } + + [PublicAPI] + protected virtual void DrawAssemblyTextArea(bool allowEditing, ref bool dirty) + { + EditorGUILayout.LabelField("Assembly Code", EditorStyles.boldLabel); + if(GUILayout.Button("Copy Assembly To Clipboard")) + { + EditorGUIUtility.systemCopyBuffer = udonAssembly; + } + + EditorGUI.BeginChangeCheck(); + using(new EditorGUI.DisabledScope(!allowEditing)) + { + string newAssembly = EditorGUILayout.TextArea(udonAssembly); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + Undo.RecordObject(this, "Edit Assembly Program Code"); + udonAssembly = newAssembly; + UdonEditorManager.Instance.QueueAndRefreshProgram(this); + } + } + } + + [PublicAPI] + protected void DrawAssemblyErrorTextArea() + { + if(string.IsNullOrEmpty(assemblyError)) + { + return; + } + + EditorGUILayout.LabelField("Assembly Error", EditorStyles.boldLabel); + using(new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.TextArea(assemblyError); + } + } + + [PublicAPI] + protected void AssembleProgram() + { + try + { + program = UdonEditorManager.Instance.Assemble(udonAssembly); + assemblyError = null; + OnAssemble?.Invoke(true, udonAssembly); + } + catch(Exception e) + { + program = null; + assemblyError = e.Message; + Debug.LogException(e); + OnAssemble?.Invoke(false, assemblyError); + } + } + + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs.meta new file mode 100644 index 00000000..c2d8a2b6 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 22203902d63dec94194fefc3e155c43b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs new file mode 100644 index 00000000..a6b6c856 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs @@ -0,0 +1,9 @@ +using UnityEditor; + +namespace VRC.Udon.Editor.ProgramSources +{ + [CustomEditor(typeof(UdonAssemblyProgramAsset))] + public class UdonAssemblyProgramAssetEditor : UdonProgramAssetEditor + { + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs.meta new file mode 100644 index 00000000..e7fb7fee --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3df823f3ab561fc43bcb81286e14b91d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs new file mode 100644 index 00000000..409c2c68 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs @@ -0,0 +1,27 @@ +using System.IO; +using JetBrains.Annotations; +using UnityEditor; +using UnityEditor.Experimental.AssetImporters; +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources +{ + [ScriptedImporter(1, "uasm")] + [UsedImplicitly] + public class UdonAssemblyProgramAssetImporter : ScriptedImporter + { + public override void OnImportAsset(AssetImportContext ctx) + { + UdonAssemblyProgramAsset udonAssemblyProgramAsset = ScriptableObject.CreateInstance<UdonAssemblyProgramAsset>(); + SerializedObject serializedUdonAssemblyProgramAsset = new SerializedObject(udonAssemblyProgramAsset); + SerializedProperty udonAssemblyProperty = serializedUdonAssemblyProgramAsset.FindProperty("udonAssembly"); + udonAssemblyProperty.stringValue = File.ReadAllText(ctx.assetPath); + serializedUdonAssemblyProgramAsset.ApplyModifiedProperties(); + + udonAssemblyProgramAsset.RefreshProgram(); + + ctx.AddObjectToAsset("Imported Udon Assembly Program", udonAssemblyProgramAsset); + ctx.SetMainObject(udonAssemblyProgramAsset); + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs.meta new file mode 100644 index 00000000..f3951da2 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c0638314c289c24193b47d1c53c9fca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram.meta new file mode 100644 index 00000000..1f5f980a --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fc78ce0a8f47f2642a2ed5fd39f05017 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI.meta new file mode 100644 index 00000000..61c37e70 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d162e94e3ec124e4398f173057d3715b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView.meta new file mode 100644 index 00000000..38782780 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 18fcefe274099824baeb479f629c1999 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields.meta new file mode 100644 index 00000000..70a6e016 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3963ddfd2d404d449ab36d09da1a92bc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs new file mode 100644 index 00000000..76ab6a34 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs @@ -0,0 +1,24 @@ +using System; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class ByteField : BaseField<byte> + { + public ByteField():base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Char Editor and listen for changes + IntegerField field = new IntegerField(); + + field.RegisterValueChangedCallback( + e => + value = Convert.ToByte(e.newValue)); + + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs.meta new file mode 100644 index 00000000..06d12e3c --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c66f97d5a5a38cb478fd4df11ece7be7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs new file mode 100644 index 00000000..1850cbfc --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs @@ -0,0 +1,24 @@ +using UnityEngine.UIElements; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class CharField : BaseField<char> + { + public CharField():base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Char Editor and listen for changes + TextField field = new TextField + { + maxLength = 1 + }; + field.RegisterValueChangedCallback( + e => + value = e.newValue.ToCharArray()[0]); + + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs.meta new file mode 100644 index 00000000..f4388e8c --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 602ffcf431b3e4f41a18bd868751439a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs new file mode 100644 index 00000000..d5d708b0 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs @@ -0,0 +1,23 @@ +using System; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class DecimalField : BaseField<decimal> + { + public DecimalField():base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Char Editor and listen for changes + DoubleField field = new DoubleField(); + field.RegisterValueChangedCallback( + e => + value = Convert.ToDecimal(e.newValue)); + + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs.meta new file mode 100644 index 00000000..feae7a68 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e79144dad56140a7bcd0d9f945153784 +timeCreated: 1632419615
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs new file mode 100644 index 00000000..51349917 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs @@ -0,0 +1,24 @@ +using UnityEngine.UIElements; +using UIElements = UnityEditor.UIElements; +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class LayerMaskField : BaseField<LayerMask> + { + public LayerMaskField() : base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create LayerMask Editor and listen for changes + UIElements.LayerMaskField field = new UIElements.LayerMaskField(); + field.RegisterValueChangedCallback(e => + { + this.value = e.newValue; + }); + + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs.meta new file mode 100644 index 00000000..67438a23 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42d9183d7c7ce67448d1e010456e36f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs new file mode 100644 index 00000000..13561b7c --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs @@ -0,0 +1,24 @@ +using UnityEngine.UIElements; +using UnityEditor.UIElements; +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class QuaternionField : BaseField<Quaternion> + { + public QuaternionField() :base(null, null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Vector4 Editor and listen for changes + Vector4Field field = new Vector4Field(); + field.RegisterValueChangedCallback( + e => + value = new Quaternion(e.newValue.x, e.newValue.y, e.newValue.z, e.newValue.w) + ); + Add(field); + } + + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs.meta new file mode 100644 index 00000000..fefc9cf3 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1c96407d2b7698c4c8a0476efa2c765d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs new file mode 100644 index 00000000..f0fa54df --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs @@ -0,0 +1,23 @@ +using System; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class SByteField : BaseField<sbyte> + { + public SByteField():base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Char Editor and listen for changes + IntegerField field = new IntegerField(); + field.RegisterValueChangedCallback( + e => + value = Convert.ToSByte(e.newValue)); + + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs.meta new file mode 100644 index 00000000..2e4769b0 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 42747856e1884be6b1112c8838963662 +timeCreated: 1632419007
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs new file mode 100644 index 00000000..9d3a1140 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs @@ -0,0 +1,23 @@ +using System; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class ShortField : BaseField<short> + { + public ShortField():base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Char Editor and listen for changes + IntegerField field = new IntegerField(); + field.RegisterValueChangedCallback( + e => + value = Convert.ToInt16(e.newValue)); + + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs.meta new file mode 100644 index 00000000..72da7dfd --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bcda1561abdb40c69f9eeb9211bd3e3d +timeCreated: 1632419462
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs new file mode 100644 index 00000000..77f5d696 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs @@ -0,0 +1,23 @@ +using System; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class UnsignedIntegerField : BaseField<uint> + { + public UnsignedIntegerField():base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Char Editor and listen for changes + IntegerField field = new IntegerField(); + field.RegisterValueChangedCallback( + e => + value = Convert.ToUInt32(e.newValue)); + + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs.meta new file mode 100644 index 00000000..516eafbb --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0b91b14ff1e24276863f173d0e9c760c +timeCreated: 1632419145
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs new file mode 100644 index 00000000..81298eb1 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs @@ -0,0 +1,23 @@ +using System; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class UnsignedLongField : BaseField<ulong> + { + public UnsignedLongField():base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Char Editor and listen for changes + IntegerField field = new IntegerField(); + field.RegisterValueChangedCallback( + e => + value = Convert.ToUInt64(e.newValue)); + + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs.meta new file mode 100644 index 00000000..653f1a3b --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1d1c8af690a94ef0a670e5d321733414 +timeCreated: 1632419341
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs new file mode 100644 index 00000000..6eac945f --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs @@ -0,0 +1,23 @@ +using System; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class UnsignedShortField : BaseField<ushort> + { + public UnsignedShortField():base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Char Editor and listen for changes + IntegerField field = new IntegerField(); + field.RegisterValueChangedCallback( + e => + value = Convert.ToUInt16(e.newValue)); + + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs.meta new file mode 100644 index 00000000..058ae560 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1387a4616a8f4c87bd0d55d2ffc021c8 +timeCreated: 1632419528
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs new file mode 100644 index 00000000..f6fb8307 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs @@ -0,0 +1,23 @@ +using UnityEngine.UIElements; +using System; +using VRC.SDKBase; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public class VRCUrlField : BaseField<VRCUrl> + { + public VRCUrlField():base(null,null) + { + // Set up styling + AddToClassList("UdonValueField"); + + // Create Text Editor and listen for changes + TextField field = new TextField(50, false, false, Char.MinValue); + field.RegisterValueChangedCallback( + e => + value = new VRCUrl(e.newValue) + ); + Add(field); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs.meta new file mode 100644 index 00000000..28d6b842 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eeb751ae1c234a04291c5039626f3470 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements.meta new file mode 100644 index 00000000..4c7d4a8f --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: efe54a45d15688542911081f5876ca68 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs new file mode 100644 index 00000000..55179e41 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs @@ -0,0 +1,81 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEngine.UIElements; +#else +using UnityEngine.Experimental.UIElements; +#endif +using System; +using UnityEditor.SceneManagement; +using UnityEngine; +using VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView; + +#if UNITY_2019_3_OR_NEWER +namespace UnityEditor.Experimental.GraphView +#else +namespace UnityEditor.Experimental.UIElements.GraphView +#endif +{ + public static class GraphElementExtension + { + + 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 Compile(this GraphElement element) + { + var evt = new Event() + { + type = EventType.ExecuteCommand, + commandName = UdonGraphCommands.Compile + }; + using (var e = ExecuteCommandEvent.GetPooled(evt)) + { + element.SendEvent(e); + } + } + + public static string GetUid(this GraphElement element) + { +#if UNITY_2019_3_OR_NEWER + return element.viewDataKey; +#else + return element.persistenceKey; +#endif + } + + 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)Math.Round(position.x / snap) * snap; + position.y = (float)Math.Round(position.y / snap) * snap; + + return position; + } + + public static Rect GetSnappedRect(Rect rect) + { + rect.position = GetSnappedPosition(rect.position); + return rect; + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs.meta new file mode 100644 index 00000000..ebc4db20 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 469db50616185d04e8a46dcd75db12d2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs new file mode 100644 index 00000000..437334c0 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs @@ -0,0 +1,86 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEngine.UIElements; +#else +using UnityEngine.Experimental.UIElements; +#endif +using System; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonArrayEditor : VisualElement + { + private IArrayProvider _inspector; + private Button _editArrayButton; + private Action<object> _setValueCallback; + private Type _type; + private object _value; + private bool _inspectorOpen = false; + + public UdonArrayEditor(Type t, 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); + + AddInspector(); + _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; + AddInspector(); + _editArrayButton.text = "Save"; + } + } + } + + private void AddInspector() + { + if (parent.GetType() == typeof(UdonPort)) + { + parent.parent.Add(_inspector as VisualElement); + } + else + { + Add(_inspector as VisualElement); + } + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs.meta new file mode 100644 index 00000000..135a415d --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f257a6eeae213a4db991d486cace003 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs new file mode 100644 index 00000000..446950cc --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs @@ -0,0 +1,156 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEngine.UIElements; +using UnityEditor.UIElements; +#else +using UnityEngine.Experimental.UIElements; +using UnityEditor.Experimental.UIElements; +#endif +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonArrayInspector<T> : VisualElement, IArrayProvider + { + private ScrollView _scroller; + private List<VisualElement> _fields = new List<VisualElement>(); + private IntegerField _sizeField; + private Action<object> _setValueCallback; + + public UdonArrayInspector(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; + #if UNITY_2019_3_OR_NEWER + _sizeField.RegisterValueChangedCallback((evt) => + #else + _sizeField.OnValueChanged((evt) => + #endif + { + ResizeTo(evt.newValue); + }); + resizeContainer.Add(_sizeField); + Add(resizeContainer); + + _scroller = new ScrollView() + { + name = "array-scroll", + }; + Add(_scroller); + + 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); + + _scroller.contentContainer.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); + _scroller.contentContainer.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); + + _scroller.contentContainer.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 + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs.meta new file mode 100644 index 00000000..1a535cb2 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f4f0ade55ae13b6468a765826f1f2540 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs new file mode 100644 index 00000000..78ff9926 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs @@ -0,0 +1,220 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +using UnityEngine.UIElements; +using UnityEditor.UIElements; +using UnityEngine.UIElements.StyleSheets; +#else +using UnityEditor.Experimental.UIElements.GraphView; +using UnityEngine.Experimental.UIElements; +using UnityEngine.Experimental.UIElements.StyleSheets; +#endif +using System; +using UnityEditor; +using UnityEngine; + +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 and Reload + 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); + }); + +#if UNITY_2019_3_OR_NEWER + _textField.RegisterValueChangedCallback((evt) => +#else + _textField.OnValueChanged((evt) => +#endif + { + SetText(evt.newValue); + SwitchToEditMode(false); + }); + } + + private void SaveNewData() + { + _graph.SaveGraphElementData(this); + } + + 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); + } + } +#if UNITY_2019_3_OR_NEWER + protected override void OnCustomStyleResolved(ICustomStyle style) + { + base.OnCustomStyleResolved(style); +#else + protected override void OnStyleResolved(ICustomStyle style) + { + base.OnStyleResolved(style); +#endif + // 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()); + SaveNewData(); + if (group != null) + { + group.SaveNewData(); + } + } + + private double lastClickTime; + private const double doubleClickSpeed = 0.5; + + private void OnLabelClick(MouseDownEvent evt) + { + var newTime = 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.delegatesFocus = true; + _textField.Focus(); + } + 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; + 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; + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs.meta new file mode 100644 index 00000000..2840e341 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e5916b8dd19e4445a9156a457b82ee4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs new file mode 100644 index 00000000..a6419359 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs @@ -0,0 +1,48 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +#else +using UnityEditor.Experimental.UIElements.GraphView; +#endif +using System; +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonGraphElement : GraphElement + { +#if UNITY_2019_3_OR_NEWER + public string uid { get => viewDataKey; set => viewDataKey = value; } +#else + public string uid { get => persistenceKey; set => persistenceKey = value; } +#endif + internal UdonGraphElementType type = UdonGraphElementType.GraphElement; + + internal UdonGraphElement() + { + } + } + + 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); + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs.meta new file mode 100644 index 00000000..1137de20 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba3ecc4c46929404d8c2ec920743b823 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs new file mode 100644 index 00000000..d6c91a73 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs @@ -0,0 +1,213 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +using UnityEngine.UIElements; +#else +using UnityEditor.Experimental.UIElements.GraphView; +using UnityEngine.Experimental.UIElements; +#endif +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonGroup : Group, IUdonGraphElementDataProvider + { + public string uid { get => _uid; set => _uid = value; } + private CustomData _customData = new CustomData(); + private UdonGraph _graph; + private string _uid; + private const int GROUP_LAYER = -1; + + 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; + } + + // Called in Reload > RestoreElementFromData + 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; + layer = GROUP_LAYER; + + if (!string.IsNullOrEmpty(jsonData)) + { + EditorJsonUtility.FromJsonOverwrite(jsonData, _customData); + } + + // listen for changes on child elements + RegisterCallback<GeometryChangedEvent>(OnGeometryChanged); + } + + private void OnGeometryChanged(GeometryChangedEvent evt) + { + _customData.layout = GraphElementExtension.GetSnappedRect(GetPosition()); + } + + public void Initialize() + { + 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; + + // Add all elements from graph to self + var childUIDs = _customData.containedElements; + if (childUIDs.Count > 0) + { + foreach (var item in childUIDs) + { + GraphElement element = _graph.GetElementByGuid(item); + if (element != null) + { + if (ContainsElement(element)) continue; + AddElement(element); + if (element is UdonComment c) + { + c.group = this; + } + else if (element is UdonNode n) + { + n.group = this; + } + } + } + } + else + { + // No children, so restore the saved position + SetPosition(_customData.layout); + } + } + } + + public override void SetPosition(Rect newPos) + { + newPos = GraphElementExtension.GetSnappedRect(newPos); + base.SetPosition(newPos); + } + + public void SaveNewData() + { + _graph.SaveGraphElementData(this); + } + + // Save data to asset after new position set + public override void UpdatePresenterPosition() + { + base.UpdatePresenterPosition(); + Rect layout = GraphElementExtension.GetSnappedRect(GetPosition()); + base.SetPosition(layout); + 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; + SaveNewData(); + } + + protected override void OnElementsAdded(IEnumerable<GraphElement> elements) + { + base.OnElementsAdded(elements); + foreach (var element in elements) + { + if (!_customData.containedElements.Contains(element.GetUid())) + { + _customData.containedElements.Add(element.GetUid()); + } + + // Set group variable on UdonNodes + if (element is UdonNode) + { + (element as UdonNode).group = this; + element.BringToFront(); + } + + if (element is UdonComment) + { + (element as UdonComment).group = this; + } + } + SaveNewData(); + } + + protected override void OnElementsRemoved(IEnumerable<GraphElement> elements) + { + base.OnElementsRemoved(elements); + foreach (var element in elements) + { + if (_customData.containedElements.Contains(element.GetUid())) + { + _customData.containedElements.Remove(element.GetUid()); + if (element is UdonNode) + { + (element as UdonNode).group = null; + } + else if (element is UdonComment) + { + (element as UdonComment).group = null; + } + } + } + + 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; + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs.meta new file mode 100644 index 00000000..291451b5 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b8045222a10ce04b815642b9cd5ca17 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs new file mode 100644 index 00000000..c6fec3e9 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs @@ -0,0 +1,57 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +#else +using UnityEditor.Experimental.UIElements.GraphView; +#endif +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonMinimap : MiniMap, IUdonGraphElementDataProvider + { + 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); + } + + public void SetVisible(bool value) + { + visible = value; + _customData.visible = value; + _graph.SaveGraphElementData(this); + } + + public override void UpdatePresenterPosition() + { + _customData.layout = GetPosition(); + _graph.SaveGraphElementData(this); + } + + public UdonGraphElementData GetData() + { + return new UdonGraphElementData(UdonGraphElementType.Minimap, this.GetUid(), 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); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs.meta new file mode 100644 index 00000000..dc27107b --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b006d67642298f04e895b6709ef12429 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs new file mode 100644 index 00000000..c690b6af --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs @@ -0,0 +1,1093 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +using EditorGV = UnityEditor.Experimental.GraphView; +using EngineUI = UnityEngine.UIElements; +using EditorUI = UnityEditor.UIElements; +using UnityEngine.UIElements; +#else +using UnityEditor.Experimental.UIElements.GraphView; +using EditorGV = UnityEditor.Experimental.UIElements.GraphView; +using EngineUI = UnityEngine.Experimental.UIElements; +using EditorUI = UnityEditor.Experimental.UIElements; +using UnityEngine.Experimental.UIElements; +#endif +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes; +using VRC.Udon.Graph; +using VRC.Udon.Graph.Interfaces; +using VRC.Udon.Serialization; +using VRC.Udon.Serialization.OdinSerializer.Utilities; +using Random = UnityEngine.Random; + +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; + protected 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 bool IsVariableNode => _variableNodeType != VariableNodeType.None; + + public UdonGraph Graph + { + get => _graphView; + private set { } + } + + public INodeRegistry Registry + { + get => _registry; + private set { } + } + + private readonly string[] _specialFlows = + { + "Block", + "Branch", + "For", + "Foreach", + "While", + "Is_Valid", + }; + + protected static readonly Dictionary<string, Type> DefinitionToTypeLookup = new Dictionary<string, Type>() + { + {"VRCUdonCommonInterfacesIUdonEventReceiver.__GetProgramVariable__SystemString__SystemObject", typeof(GetOrSetProgramVariableNode)}, + {"VRCUdonCommonInterfacesIUdonEventReceiver.__SetProgramVariable__SystemString_SystemObject__SystemVoid", typeof(GetOrSetProgramVariableNode)}, + {"VRCUdonCommonInterfacesIUdonEventReceiver.__GetProgramVariableType__SystemString__SystemType", typeof(GetOrSetProgramVariableNode)}, + {"VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEvent__SystemString__SystemVoid", typeof(SendCustomEventNode)}, + {"VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEventDelayedSeconds__SystemString_SystemSingle_VRCUdonCommonEnumsEventTiming__SystemVoid", typeof(SendCustomEventNode)}, + {"VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEventDelayedFrames__SystemString_SystemInt32_VRCUdonCommonEnumsEventTiming__SystemVoid", typeof(SendCustomEventNode)}, + {"VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomNetworkEvent__VRCUdonCommonInterfacesNetworkEventTarget_SystemString__SystemVoid", typeof(SendCustomEventNode)}, + {"Set_ReturnValue", typeof(SetReturnValueNode)}, + {"Set_Variable", typeof(SetVariableNode)}, + }; + +#if UNITY_2019_3_OR_NEWER + public string uid + { + get => viewDataKey; + set => viewDataKey = value; + } +#else + public string uid { get => persistenceKey; set => persistenceKey = value; } +#endif + + // Called when creating from Asset, calls the CreateNode method below + 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); + } + + // Always called when creating UdonNode + public static UdonNode CreateNode(UdonNodeDefinition definition, UdonGraph view, UdonNodeData nodeData = null) + { + Type type = typeof(UdonNode); + // overwrite type with target type if it exists + if (DefinitionToTypeLookup.TryGetValue(definition.fullName, out Type childType)) + { + type = childType; + } + UdonNode node = Activator.CreateInstance(type, definition, view, nodeData) as UdonNode; + node?.Initialize(); + return node; + } + + private bool skipSubtitle = false; + + private Label subtitle; + // Constructor is protected to force all paths through Static factory method except for child classes + public UdonNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null) + { + _graphView = view; + definition = nodeDefinition; + Undo.RecordObject(view.graphProgramAsset, "Add UdonNode"); + var registry = UdonGraphExtensions.GetRegistryForDefinition(nodeDefinition); + if(registry != null) + { + this._registry = registry; + } + else + { + Debug.LogWarning($"Couldn't find registry for {nodeDefinition.fullName}"); + } + + VisualElement titleContainer = new VisualElement() + { + name = "title-container", + }; + this.Q("title").Insert(0, titleContainer); + + titleContainer.Add(this.Q("title-label")); + + subtitle = new Label("") + { + name = "subtitle", + }; + skipSubtitle = ( + _specialFlows.Contains(definition.fullName) + || definition.fullName.EndsWith("et_Variable") + || definition.fullName.StartsWithCached("Const_") + ); + + if (!skipSubtitle) + { + titleContainer.Insert(0, subtitle); + } + + name = definition.fullName; + elementTypeColor = Random.ColorHSV(0.5f, 0.6f, 0.1f, 0.2f, 0.8f, 0.9f); + + // Null is a type here, so handle it special + if (nodeDefinition.type == null) + { + AddToClassList("null"); + } + else + { + AddToClassList(definition.type.Namespace); + AddToClassList(definition.type.Name); + } + + if (nodeDefinition.fullName.Contains('_')) + { + AddToClassList(definition.fullName.Substring(0, nodeDefinition.fullName.IndexOf('_'))); + } + + // Create or validate nodeData + if (nodeData == null) + { + data = _graphView.graphData.AddNode(definition.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_")) + { + RefreshOverloadPopup(); + } + + AddToClassList("UdonNode"); + + LayoutPorts(); + } + + public virtual void Initialize() + { + RefreshTitle(); + _graphView.MarkSceneDirty(); + } + + private string GetTargetVariableUid() + { + string result = ""; + if (IsVariableNode && this.data.nodeValues.Length > 0) + { + string[] parts = data.nodeValues[0].stringValue.Split('|'); + if (parts.Length > 1) + { + result = parts[1]; + } + } + return result; + } + + public void RefreshTitle() + { + if (IsVariableNode) + { + string uid = GetTargetVariableUid(); + if (!string.IsNullOrWhiteSpace(uid)) + { + string variableName = _graphView.GetVariableName(uid); + if (!string.IsNullOrWhiteSpace(variableName)) + { + switch (_variableNodeType) + { + case VariableNodeType.Set: + title = $"Set {variableName}"; + break; + case VariableNodeType.Change: + title = $"{variableName} Change"; + break; + case VariableNodeType.Get: + default: + title = variableName; + break; + } + return; + } + } + } + // Set Title + var displayTitle = UdonGraphExtensions.PrettyString(definition.name).FriendlyNameify(); + if (displayTitle == "Const_VRCUdonCommonInterfacesIUdonEventReceiver") + { + displayTitle = "UdonBehaviour"; + } + else if(displayTitle == "==" || displayTitle == "!=" || displayTitle == "+") + { + displayTitle = $"{definition.type.Name} {displayTitle}"; + } + + if (displayTitle.StartsWith("Op ")) + displayTitle = displayTitle.Replace("Op ", ""); + + title = displayTitle; + + AddToClassList(title.Replace(" ", "").ToLowerFirstChar()); + + if (definition == null) + { + Debug.LogWarning($"Definition for {this.name} is null"); + return; + } + + string className = definition.name.Split(' ').FirstOrDefault().Split('_').FirstOrDefault(); + AddToClassList(className); + + if (!skipSubtitle) + { + if (definition.fullName.StartsWith("Event_")) + { + subtitle.text = "Event"; + } + // make Constructor nodes readable + else if (definition.name == "ctor") + { + subtitle.text = definition.type.Name; + title = "Constructor"; + } + else + { + subtitle.text = className; + // temp title shenanigans + int firstSplit = definition.fullName.IndexOf("__") + 2; + if (firstSplit > 1) + { + int lastSplit = definition.fullName.IndexOf("__", firstSplit); + int stringLength = (lastSplit > -1) + ? lastSplit - firstSplit + : definition.fullName.Length - firstSplit; + string line2 = definition.fullName.Substring(firstSplit, stringLength).Replace("_", " ") + .UppercaseFirst(); + if (line2.StartsWith("Op ")) + { + line2 = line2.Replace("Op ", ""); + subtitle.text = definition.type.Name; + } + + title = line2; + } + else + { + //TODO: handle class names not found + //Debug.Log($"Couldn't find classname for {nodeDefinition.fullName}"); + } + } + } + } + + private void DrawHelpButton() + { + Button helpButton = new Button(ShowNodeDocs) + { + name = "help-button", + }; + helpButton.Add(new 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(); + if (group != null) + { + group.SaveNewData(); + } + } + + public void RefreshOverloadPopup() + { + // 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); +#if UNITY_2019_3_OR_NEWER + _popup.RegisterValueChangedCallback( +#else + _popup.OnValueChanged( +#endif + (e) => + { + // TODO - store data in the dropdown and use formatListItemCallback? + SetNewFullName(overloadDefinitions.ElementAt(_popup.index).fullName); + }); + inputContainer.Add(_popup); + } + } + + private void SetNewFullName(string newFullName) + { + data.fullName = newFullName; + definition = UdonEditorManager.Instance.GetNodeDefinition(data.fullName); + data.Resize(definition.Inputs.Count); + // Todo: see if we can get rid of this reload. Tried ValidateNodeData,LayoutPorts,RestoreConnections but noodles were left hanging + this.Reload(); + } + + 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}, clearing data"); + data.flowUIDs[i] = ""; + continue; + } + + // 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}"); + data.nodeUIDs[i] = ""; + continue; + } + + // No matching port for this data, skip + if (portsIn == null) continue; + 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 == null || !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() + { + // set data to this graph + data.SetGraph(_graphView.graphData); + + for (int i = 0; i < data.nodeValues.Length; i++) + { + if (definition.Inputs.Count <= i) + { + continue; + } + + Type expectedType = definition.Inputs[i].type; + + // Skip over if the value is null and that's ok + if (data.nodeValues[i] == null) + { + if (expectedType == null || Nullable.GetUnderlyingType(expectedType) != null) + { + continue; + } + else + { + data.nodeValues[i] = SerializableObjectContainer.Serialize(default, expectedType); + continue; + } + } + + object value = data.nodeValues[i].Deserialize(); + if (value == null) + { + if (expectedType == null || Nullable.GetUnderlyingType(expectedType) != null) + { + // type is nullable, leave it alone + continue; + } + else + { + // not a nullable type - set a default + data.nodeValues[i] = SerializableObjectContainer.Serialize(default, expectedType); + } + } + + if (!expectedType.IsInstanceOfType(value)) + { + data.nodeValues[i] = SerializableObjectContainer.Serialize(null, expectedType); + } + } + } + + 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, + None, + Change, + }; + + private VariableNodeType _variableNodeType = VariableNodeType.None; + + + public virtual void LayoutPorts() + { + ClearPorts(); + SetupFlowPorts(); + + // Don't setup in ports for Get_Variable node types, instead add variable popup + if (name.CompareTo("Get_Variable") == 0) + { + _variableNodeType = VariableNodeType.Get; + RefreshVariablePopup(); + } + else if (name.CompareTo("Event_OnVariableChange") == 0) + { + _variableNodeType = VariableNodeType.Change; + } + else + { + // Add Variable popup and in-ports for Set_Variable + if (name.CompareTo("Set_Variable") == 0) + { + _variableNodeType = VariableNodeType.Set; + RefreshVariablePopup(); + } + + SetupInPorts(); + } + + SetupOutPorts(); + + RefreshExpandedState(); + RefreshPorts(); + } + + public void ClearPorts() + { + portsFlowIn?.ForEach(port => port.RemoveFromHierarchy()); + portsFlowOut?.ForEach(port => port.RemoveFromHierarchy()); + portsIn?.Values.ForEach(port => port.RemoveFromHierarchy()); + portsOut?.Values.ForEach(port => port.RemoveFromHierarchy()); + } + + private EditorUI.PopupField<string> _variablePopupField; + // TODO: Test this again after we have the new graph serializing the addition of nodes + public void RefreshVariablePopup() + { + if (_variableNodeType == VariableNodeType.None) + { + Debug.LogError($"Not Creating Variable Pop-Up for Non Variable Node {data.fullName}"); + } + + // 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(); + + var options = _graphView.GetVariableNames.Where(t => !t.StartsWith("__")).ToList(); + + // Get value of selected node in rather roundabout way + int originalIndex = _graphView.GetVariableNodes + .IndexOf(_graphView.GetVariableNodes.FirstOrDefault(v => v.uid == (string)value)); + + // Allow OnVariableChange events to start with just any existing index + if (_variableNodeType == VariableNodeType.Change && originalIndex == -1) originalIndex = 0; + + int currentIndex = _graphView.GetVariableNames.FindIndex(s => s == _graphView.GetVariableNames[originalIndex]); + + if (currentIndex < 0) + { + Debug.LogWarning($"Node {name} didn't have a variable assigned, removing"); + _graphView.RemoveNodeAndData(data); + return; + } + + // Create popup, set current value and set function to update data when it's changed. + if (_variablePopupField == null) + { + // First time creating, just add it + _variablePopupField = new EditorUI.PopupField<string>(options, currentIndex); + inputContainer.Add(_variablePopupField); + } + else + { + // Remaking it - remove the old one, at the new one at its previous location + int index = inputContainer.IndexOf(_variablePopupField); + _variablePopupField.RemoveFromHierarchy(); + _variablePopupField = new EditorUI.PopupField<string>(options, currentIndex); + inputContainer.Insert(index, _variablePopupField); + } +#if UNITY_2019_3_OR_NEWER + _variablePopupField.RegisterValueChangedCallback( +#else + _variablePopupField.OnValueChanged( +#endif + (e) => + { + // Ensure we've selected an existing variable + if (_variablePopupField.index < options.Count) + { + int trueIndex = _graphView.GetVariableNames.FindIndex(s => s == _variablePopupField.text); + + // 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[trueIndex].uid; + // Get Variable nodes only have one entry, so index is 0 below + SetNewValue(newUid, 0); + + this.Reload(); // Didn't want to do this, but can't get flows to restore otherwise Ideally, we call RefreshTitle(), LayoutPorts(), RestoreConnections(), RestoreFlows() + } + }); + + string startingUid = _graphView.GetVariableNodes[originalIndex].uid; + SetNewValue(startingUid, 0); + } + + public void SetNewValue(object newValue, int index, Type inType = null) + { + data.nodeValues[index] = SerializableObjectContainer.Serialize(newValue, inType); + } + + private void SetupOutPorts() + { + portsOut = new Dictionary<int, UdonPort>(); + for (int i = 0; i < definition.Outputs.Count; i++) + { + var item = definition.Outputs[i]; + + // Convert object type to variable type for Get_Variable nodes, or run them through the SlotTypeConverter for all other nodes + Type type = (_variableNodeType == VariableNodeType.Get || _variableNodeType == VariableNodeType.Change) + ? GetTypeForDefinition(definition) + : UdonGraphExtensions.SlotTypeConverter(item.type, definition.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() + { + portsIn = new Dictionary<int, UdonPort>(); + + // Expand node data to hold values for all inputs + data.Resize(definition.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 first input for Set_ReturnValue since that's the special variable + if (name.CompareTo("Set_ReturnValue") == 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 < definition.Inputs.Count; index++) + { + UdonNodeParameter input = definition.Inputs[index]; + string label = ""; + // TODO: Ask Cubed what this does? Or figure it out. + if (definition.Inputs.Count > index && index >= 0) + { + label = definition.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 = (_variableNodeType == VariableNodeType.Set && index == 1) + ? type = GetTypeForDefinition(definition) + : UdonGraphExtensions.SlotTypeConverter(input.type, definition.fullName); + + if (_variableNodeType == VariableNodeType.Set && index == 2) + { + AddToClassList("send-change"); + } + + // 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() + { + if (definition.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 (!definition.fullName.StartsWith("Event_")) + { + label = definition.inputFlowNames.Count > 0 ? definition.inputFlowNames[0] : ""; + AddFlowPort(Direction.Input, label, ++inFlowIndex); + } + + // add output flow + label = definition.outputFlowNames.Count > 0 ? definition.outputFlowNames[0] : ""; + AddFlowPort(Direction.Output, label, ++outFlowIndex); + if (_specialFlows.Contains(definition.fullName)) + { + label = definition.outputFlowNames.Count > 1 ? definition.outputFlowNames[1] : ""; + AddFlowPort(Direction.Output, label, ++outFlowIndex); + } + + // Add the number of output flows we need for a Block + if (definition.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(EditorGV.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") + { + RestoreFlows(); + } + } + } + + 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 as UdonPort, Direction.Input); + } + else if (edge.input != null && edge.input.portType != null) + { + _graphView.OpenPortSearch(edge.input.portType, position, edge.input as UdonPort, Direction.Output); + } + } + + #endregion + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs.meta new file mode 100644 index 00000000..426e8041 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dcd657bc1dcf357448d27bcfa8c5dc36 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes.meta new file mode 100644 index 00000000..99b3dc3d --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 50be104b42303f24fa6166d0c5b542f1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs new file mode 100644 index 00000000..1521f75c --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs @@ -0,0 +1,30 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +using EngineUI = UnityEngine.UIElements; +using EditorUI = UnityEditor.UIElements; +#else +using EditorGV = UnityEditor.Experimental.UIElements.GraphView; +using EngineUI = UnityEngine.Experimental.UIElements; +using EditorUI = UnityEditor.Experimental.UIElements; +#endif +using VRC.Udon.Graph; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes +{ + public class GetOrSetProgramVariableNode : UdonNode + { + private EditorUI.PopupField<string> _programVariablePopup; + + public GetOrSetProgramVariableNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null) : + base(nodeDefinition, view, nodeData) + { + } + + public override void Initialize() + { + base.Initialize(); + _programVariablePopup = + this.GetProgramPopup(UdonNodeExtensions.ProgramPopupType.Variables, _programVariablePopup); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs.meta new file mode 100644 index 00000000..243a80e5 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cbfa6b1c2cf44feca09853837fc740bb +timeCreated: 1623890922
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs new file mode 100644 index 00000000..1d8b75ef --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs @@ -0,0 +1,28 @@ +#if UNITY_2019_3_OR_NEWER +using EditorGV = UnityEditor.Experimental.GraphView; +using EngineUI = UnityEngine.UIElements; +using EditorUI = UnityEditor.UIElements; +#else +using EditorGV = UnityEditor.Experimental.UIElements.GraphView; +using EngineUI = UnityEngine.Experimental.UIElements; +using EditorUI = UnityEditor.Experimental.UIElements; +#endif +using VRC.Udon.Graph; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes +{ + public class SendCustomEventNode : UdonNode + { + private EditorUI.PopupField<string> _eventNamePopup; + + public SendCustomEventNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null) : base(nodeDefinition, view, nodeData) + { + } + + public override void Initialize() + { + base.Initialize(); + _eventNamePopup = this.GetProgramPopup(UdonNodeExtensions.ProgramPopupType.Events, _eventNamePopup); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs.meta new file mode 100644 index 00000000..a71b401c --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8164fc2c5c5b43428503cf064e8b53f0 +timeCreated: 1624411228
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs new file mode 100644 index 00000000..30691476 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs @@ -0,0 +1,42 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +using EditorGV = UnityEditor.Experimental.GraphView; +using EngineUI = UnityEngine.UIElements; +using EditorUI = UnityEditor.UIElements; +using UnityEngine.UIElements; +#else +using EditorGV = UnityEditor.Experimental.UIElements.GraphView; +using EngineUI = UnityEngine.Experimental.UIElements; +using EditorUI = UnityEditor.Experimental.UIElements; +#endif +using System.Linq; +using UnityEngine; +using VRC.Udon.Graph; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes +{ + public class SetReturnValueNode : UdonNode + { + public SetReturnValueNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null) : base(nodeDefinition, view, nodeData) + { + } + + public override void Initialize() + { + base.Initialize(); + + string returnVariable = UdonBehaviour.ReturnVariableName; + string uuid = null; + + if (!_graphView.GetVariableNames.Contains(returnVariable)) + uuid = _graphView.AddNewVariable("Variable_SystemObject", returnVariable, false); + else + uuid = _graphView.GetVariableNodes.FirstOrDefault(n => (string)n.nodeValues[1].Deserialize() == returnVariable)?.uid; + + if (!string.IsNullOrWhiteSpace(uuid)) + SetNewValue(uuid, 0); + else + Debug.LogError("Could not find return value name!"); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs.meta new file mode 100644 index 00000000..6c3ac32b --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6e65118bfe2d43f1ad2412dba47c21ee +timeCreated: 1623892831
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs new file mode 100644 index 00000000..97176a9e --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs @@ -0,0 +1,25 @@ +using UnityEngine; +using UnityEngine.UIElements; +using VRC.Udon.Graph; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes +{ + public class SetVariableNode : UdonNode + { + public SetVariableNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null) : base(nodeDefinition, view, nodeData) + { + } + + public override void Initialize() + { + base.Initialize(); + + UdonPort sendChangePort = portsIn[2]; + var toggle = sendChangePort.Q<Toggle>(); + if (toggle != null) + { + sendChangePort.Insert(0, toggle); + } + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs.meta new file mode 100644 index 00000000..21ef0abe --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fd9209fd8a363ee42a49a2afaeb35805 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs new file mode 100644 index 00000000..c650e567 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs @@ -0,0 +1,164 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +using EditorGV = UnityEditor.Experimental.GraphView; +using EngineUI = UnityEngine.UIElements; +using EditorUI = UnityEditor.UIElements; +using UnityEngine.UIElements; +#else +using UnityEditor.Experimental.UIElements.GraphView; +using EditorGV = UnityEditor.Experimental.UIElements.GraphView; +using EngineUI = UnityEngine.Experimental.UIElements; +using EditorUI = UnityEditor.Experimental.UIElements; +using UnityEngine.Experimental.UIElements; +#endif +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using VRC.Udon.Common; +using VRC.Udon.Compiler.Compilers; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes +{ + public static class UdonNodeExtensions + { + + #region Methods for creating popup selectors from Program variables / event + public static readonly HashSet<string> InternalEventNames = new HashSet<string>() + { + "_start", "_update", "_lateUpdate", "_fixedUpdate", "onAnimatorIk", "_onAnimatorMove", "_onBecameInvisible", "_onBecameVisible", + "_onPlayerCollisionEnter", "_onCollisionEnter", "_onCollisionEnter2D", "_onPlayerCollisionExit", "_onCollisionExit", "_onCollisionExit2D", "_onPlayerCollisionStay", "_onCollisionStay", "_onCollisionStay2D", + "_onPlayerTriggerEnter", "_onTriggerEnter", "_onTriggerEnter2D", "_onPlayerTriggerExit", "_onTriggerExit", "_onTriggerExit2D", "_onPlayerTriggerStay", "_onTriggerStay", "_onTriggerStay2D", + "_onDestroy", "_onDisable", "_onDrawGizmos", "_onDrawGizmosSelected", "_onEnable", "_onJointBreak", "_onJointBreak2D", "_onMouseDown", "_onMouseDrag", "_onMouseEnter", "_onMouseExit", "_onMouseOver", "_onMouseUp", "_onMouseUpAsButton", + "_onPlayerParticleCollision", "_onParticleCollision", "_onParticleTrigger", "_onPostRender", "_onPreCull", "_onPreRender", "_onRenderImage", "_onRenderObject", "_onTransformChildrenChanged", "_onTransformParentChanged", "_onValidate", "_onWillRenderObject", + "_interact", "_onDrop", "_onPickup", "_onPickupUseDown", "_onPickupUseUp", "_onPreSerialization", "_onPostSerialization", "_onDeserialization", "_onVideoEnd", "_onVideoPause", "_onVideoPlay", "_onVideoStart", "_midiNoteOn", "_midiNoteOff", "_midiControlChange", + "_onOwnershipRequest", "_onNetworkReady", "_onOwnershipTransferred", "_onPlayerJoined", "_onPlayerLeft", "_onSpawn", "_onStationEntered", "_onStationExited", + }; + + public enum ProgramPopupType + { + Variables, Events + } + + private static List<string> GetCustomEventsFromAsset(AbstractSerializedUdonProgramAsset asset) + { + // don't return internal event names or VariableChange events + return asset.RetrieveProgram().EntryPoints.GetExportedSymbols().Where(e => + !InternalEventNames.Contains(e) && !e.StartsWithCached(VariableChangedEvent.EVENT_PREFIX)).ToList(); + } + + public static EditorUI.PopupField<string> GetProgramPopup(this UdonNode node, ProgramPopupType popupType, EditorUI.PopupField<string> _eventNamePopup) + { + string PLACEHOLDER = "----"; + string MISSING = "MISSING! Was"; + + List<string> _options = new List<string>(){PLACEHOLDER}; + var data = node.data; + + bool unavailable = true; + if(data.nodeUIDs.Length < 1 || string.IsNullOrEmpty(data.nodeUIDs[0])) + { + switch (popupType) + { + case ProgramPopupType.Events: + _options = GetCustomEventsFromAsset(node.Graph.graphProgramAsset.SerializedProgramAsset); + break; + case ProgramPopupType.Variables: + node.Graph.RefreshVariables(false); + _options = new List<string>(node.Graph.GetVariableNames).Where(x=>!x.StartsWithCached(VariableChangedEvent.OLD_VALUE_PREFIX)).ToList(); + break; + } + unavailable = _options.Count == 0; + _options.Insert(0, PLACEHOLDER); + } + else if(data.InputNodeAtIndex(0)?.fullName == "Get_Variable") + { + // So much work to get the underlying node referenced by a variable. Would be nice to have a method for this. + var parts = data.nodeUIDs[0].Split('|'); + if (parts.Length < 1) return null; + + string targetId = parts[0]; + + var variableGetterNode = node.Graph.graphData.FindNode(targetId); + if (variableGetterNode == null || variableGetterNode.nodeValues.Length < 1) return null; + + string variableId = variableGetterNode.nodeValues[0].Deserialize() as string; + if (string.IsNullOrWhiteSpace(variableId)) return null; + + string variableName = node.Graph.GetVariableName(variableId); + if (string.IsNullOrWhiteSpace(variableName)) return null; + + if (node.Graph.udonBehaviour != null && node.Graph.udonBehaviour.publicVariables.TryGetVariableValue(variableName, out UdonBehaviour ub)) + { + if (ub != null) + { + switch (popupType) + { + case ProgramPopupType.Events: + _options = GetCustomEventsFromAsset(ub.programSource.SerializedProgramAsset); + break; + case ProgramPopupType.Variables: + _options = ub.programSource?.SerializedProgramAsset?.RetrieveProgram()?.SymbolTable + .GetSymbols().Where(s => !s.StartsWithCached(UdonGraphCompiler.INTERNAL_VARIABLE_PREFIX) && !s.StartsWithCached(VariableChangedEvent.OLD_VALUE_PREFIX)).ToList(); + break; + } + _options.Insert(0, PLACEHOLDER); + unavailable = false; + } + } + } + + + int currentIndex = 0; + int targetNodeValueIndex = node.data.fullName.Contains("SendCustomNetworkEvent") ? 2 : 1; + string targetVarName = data.nodeValues[targetNodeValueIndex].Deserialize() as string; + if (targetVarName != null && targetVarName.StartsWithCached(MISSING)) targetVarName = null; + + // If we have a target variable name: + if (!string.IsNullOrWhiteSpace(targetVarName)) + { + if (_options.Contains(targetVarName)) + { + currentIndex = _options.IndexOf(targetVarName); + } + else + { + _options[0] = unavailable ? targetVarName : $"{MISSING} {targetVarName}"; + } + } + + if (_eventNamePopup == null) + { + _eventNamePopup = new EditorUI.PopupField<string>(_options, currentIndex); + _eventNamePopup.name = popupType == ProgramPopupType.Events ? "EventNamePopup" : "VariablePopup"; + var eventNamePort = node.inputContainer.Q(null, popupType == ProgramPopupType.Events ? "eventName" : "symbolName"); + eventNamePort?.Add(_eventNamePopup); + if (unavailable) + { + _eventNamePopup.SetEnabled(false); + } + } + else + { + // Remaking it - remove the old one, at the new one at its previous location + int index = node.inputContainer.IndexOf(_eventNamePopup); + _eventNamePopup.RemoveFromHierarchy(); + _eventNamePopup = new EditorUI.PopupField<string>(_options, currentIndex); + node.inputContainer.Insert(index, _eventNamePopup); + } +#if UNITY_2019_3_OR_NEWER + _eventNamePopup.RegisterValueChangedCallback( +#else + _eventNamePopup.OnValueChanged( +#endif + (e) => + { + node.SetNewValue(e.newValue.CompareTo(PLACEHOLDER) == 0 ? "" : e.newValue.ToString(), targetNodeValueIndex); + // Todo: update text field directly and save instead of calling Reload + node.Reload(); + }); + + return _eventNamePopup; + } + #endregion + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs.meta new file mode 100644 index 00000000..93534521 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 00b770580aae40ffb9f6a1d898b52269 +timeCreated: 1625678192
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs new file mode 100644 index 00000000..8b808489 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs @@ -0,0 +1,450 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +using UnityEngine.UIElements; +using EditorUI = UnityEditor.UIElements; +using EngineUI = UnityEngine.UIElements; +#else +using UnityEditor.Experimental.UIElements.GraphView; +using UnityEngine.Experimental.UIElements; +using EditorUI = UnityEditor.Experimental.UIElements; +using EngineUI = UnityEngine.Experimental.UIElements; +#endif +using System; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.Udon.Graph; +using VRC.Udon.Serialization; + +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; + + 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; + } + + UdonPort 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 bool _isSendChangePort = false; + + private void SetupPort() + { + _isSendChangePort = portName == "sendChange"; + if (_isSendChangePort) + { + m_EdgeConnector = null; + } + else + { + this.AddManipulator(m_EdgeConnector); + } + + tooltip = UdonGraphExtensions.FriendlyTypeName(portType); + + FullName = _udonNodeData.fullName; + + if (portType == null || direction == Direction.Output) + { + return; + } + + if (TryGetValueObject(out object result, portType)) + { + var field = UdonFieldFactory.CreateField( + portType, + result, + newValue => SetNewValue(newValue) + ); + + if (field != null) + { + SetupField(field); + } + } + + if (_udonNodeData.fullName.StartsWithCached("Const")) + { + RemoveConnectorAndLabel(); + } + else if (_udonNodeData.fullName.StartsWithCached("Set_Variable") && _nodeValueIndex == 2) + { + _isSendChangePort = true; + RemoveConnector(); + AddToClassList("send-change"); + } + + AddToClassList(portName); + + 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 +#if UNITY_2019_3_OR_NEWER + field.RegisterValueChangedCallback( +#else + field.OnValueChanged( +#endif + 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 Label(friendlyName); + _inputFieldTypeLabel = label; + + return _inputField; + } + + private void SetupField(VisualElement field) + { + // 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; +#if UNITY_2019_3_OR_NEWER + tfield.RegisterValueChangedCallback( +#else + tfield.OnValueChanged( +#endif + (e) => + { + string newValue = e.newValue.SanitizeVariableName(); + tfield.value = newValue; + SetNewValue(newValue); + }); + RemoveConnectorAndLabel(); + } + + // Add label, shown when input is connected. Not shown by default + var friendlyName = UdonGraphExtensions.FriendlyTypeName(portType).FriendlyNameify(); + var label = new Label(friendlyName); + _inputFieldTypeLabel = label; + field.AddToClassList("portField"); + + _inputField = field; + Add(_inputField); + } + + private void RemoveConnectorAndLabel() + { + RemoveConnector(); + this.Q(null, "connectorText")?.RemoveFromHierarchy(); + } + + private void RemoveConnector() + { + this.Q("connector")?.RemoveFromHierarchy(); + } + +#pragma warning disable 0649 // variable never assigned + private 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) + { + AddToClassList("connected"); + base.Connect(edge); + + Undo.RecordObject(((UdonNode)node).Graph.graphProgramAsset, "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); + this.Compile(); + } + 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()); + } + + if (_isSendChangePort) + { + DisconnectAll(); + this.Reload(); + } + } + + 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(); + } + } + else + { + 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 + { + 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) + { + RemoveFromClassList("connected"); + if (node == null) return; + Undo.RecordObject(((UdonNode)node).Graph.graphProgramAsset, "Connect 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(""); + this.Compile(); + } + else if (direction == Direction.Input && portType != null) + { + // Direction is input + // We are a value port + SetDataFromNewConnection("", GetIndex()); + } + } + + 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, Type type = null) + { + // Initialize out object + result = null; + + // get container from node values + SerializableObjectContainer container = _udonNodeData.nodeValues[_nodeValueIndex]; + + // Null check, failure + if (container == null) + return false; + + // Deserialize into result, return failure on null + result = container.Deserialize(); + + // Strings will deserialize as null, that's ok + if (type == null || type == typeof(string)) + { + return true; + } + // any other type is not ok to be null + else if (result == null) + { + return false; + } + + // Success - return true + return type.IsInstanceOfType(result); + } + + private void SetNewValue(object newValue) + { + _udonNodeData.nodeValues[_nodeValueIndex] = SerializableObjectContainer.Serialize(newValue, portType); + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs.meta new file mode 100644 index 00000000..e09d6a0e --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f83d1d3578dd28498c71a980bca86dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs new file mode 100644 index 00000000..79baae39 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs @@ -0,0 +1,6 @@ +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonStackNode + { + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs.meta new file mode 100644 index 00000000..827649e8 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1d5984c5be753da439b8a33ffbee8d36 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs new file mode 100644 index 00000000..1b59c5d3 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs @@ -0,0 +1,122 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEngine.UIElements; +using UnityEditor.Experimental.GraphView; +#else +using UnityEngine.Experimental.UIElements; +using UnityEditor.Experimental.UIElements.GraphView; +#endif +using System.Collections.Generic; +using UnityEngine; +using VRC.Udon.Graph; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonVariablesBlackboard : Blackboard, IUdonGraphElementDataProvider + { + private CustomData _customData = new CustomData(); + private UdonGraph _graph; + private Dictionary<string, BlackboardRow> _idToRow; + + 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); + + _idToRow = new Dictionary<string, BlackboardRow>(); + } + + public new void Clear() + { + _idToRow.Clear(); + base.Clear(); + } + + public void AddFromData(UdonNodeData nodeData) + { + // don't add internal variables, which start with __ + // Todo: handle all "__" variables instead, need to tell community first and let the word spread + string newVariableName = (string)nodeData.nodeValues[(int)UdonParameterProperty.ValueIndices.name].Deserialize(); + if (newVariableName.StartsWithCached("__returnValue")) + { + return; + } + + UdonNodeDefinition definition = UdonEditorManager.Instance.GetNodeDefinition(nodeData.fullName); + if (definition != null) + { + BlackboardRow row = new BlackboardRow(new UdonParameterField(_graph, nodeData), + new UdonParameterProperty(_graph, definition, nodeData)); + contentContainer.Add(row); + _idToRow.Add(nodeData.uid, row); + } + this.Reload(); + } + + public void RemoveByID(string id) + { + if (_idToRow.TryGetValue(id, out BlackboardRow row)) + { + Remove(row); + _idToRow.Remove(id); + } + } + + public void SetVisible(bool value) + { + visible = value; + _customData.visible = value; + SaveData(); + } + + public override void UpdatePresenterPosition() + { + _customData.layout = GetPosition(); + SaveData(); + } + + private void SaveData() + { + _graph.SaveGraphElementData(this); + } + + public UdonGraphElementData GetData() + { + return new UdonGraphElementData(UdonGraphElementType.VariablesWindow, this.GetUid(), JsonUtility.ToJson(_customData)); + } + + public class CustomData { + public bool visible = true; + public Rect layout = new Rect(10, 130, 200, 150); + } + + internal void LoadData(UdonGraphElementData data) + { + _idToRow = new Dictionary<string, BlackboardRow>(); + JsonUtility.FromJsonOverwrite(data.jsonData, _customData); + SetPosition(_customData.layout); + this.visible = _customData.visible; + } + } + +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs.meta new file mode 100644 index 00000000..ecb9b2d3 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d0a4730c5f61b247b27b54f280300b5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search.meta new file mode 100644 index 00000000..e32446e5 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b713b1dddee74a340a64c7755fa1f9e6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs new file mode 100644 index 00000000..d29e25db --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs @@ -0,0 +1,64 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +#else +using UnityEditor.Experimental.UIElements.GraphView; +#endif +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using VRC.Udon.Graph; +using VRC.Udon.Graph.Interfaces; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + + public class UdonFocusedSearchWindow : UdonSearchWindowBase + { + + public INodeRegistry targetRegistry; + internal List<SearchTreeEntry> _fullRegistry; + + #region ISearchWindowProvider + + public override List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context) + { + + _fullRegistry = new List<SearchTreeEntry>(); + + Texture2D icon = EditorGUIUtility.FindTexture("cs Script Icon"); + + var registryName = GetSimpleNameForRegistry(targetRegistry); + _fullRegistry.Add(new SearchTreeGroupEntry(new GUIContent($"{registryName} Search"), 0)); + + // add Registry Level + AddEntriesForRegistry(_fullRegistry, targetRegistry, 1, true); + + return _fullRegistry; + } + + public override bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) + { + if (entry.userData is UdonNodeDefinition definition && !_graphView.IsDuplicateEventNode(definition.fullName)) + { + _graphView.AddNodeFromSearch(definition, GetGraphPositionFromContext(context)); + return true; + } + else + { + return false; + } + } + + // TODO: move this to Extension + private string GetSimpleNameForRegistry(INodeRegistry registry) + { + string registryName = registry.ToString().Replace("NodeRegistry", "").FriendlyNameify(); + registryName = registryName.Substring(registryName.LastIndexOf(".") + 1); + registryName = registryName.Replace("UnityEngine", ""); + return registryName; + } + + #endregion + + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs.meta new file mode 100644 index 00000000..6bbeda5f --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6581176c97993bb40976acff208bd0b1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs new file mode 100644 index 00000000..0660019b --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs @@ -0,0 +1,113 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +#else +using UnityEditor.Experimental.UIElements.GraphView; +#endif +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using VRC.Udon.Graph; +using VRC.Udon.Graph.Interfaces; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + + public class UdonFullSearchWindow : UdonSearchWindowBase + { + + static private List<SearchTreeEntry> _slowRegistryCache; + #region ISearchWindowProvider + + override public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context) + { + if (!skipCache && (_slowRegistryCache != null && _slowRegistryCache.Count > 0)) return _slowRegistryCache; + + _slowRegistryCache = new List<SearchTreeEntry> + { + new SearchTreeGroupEntry(new GUIContent("Full Search"), 0) + }; + + var topRegistries = UdonEditorManager.Instance.GetTopRegistries(); + + Texture2D icon = null; + var groupEntries = new Dictionary<string, SearchTreeGroupEntry>(); + foreach (var topRegistry in topRegistries) + { + string topName = topRegistry.Key.Replace("NodeRegistry", ""); + + if (topName != "Udon") + { + _slowRegistryCache.Add(new SearchTreeGroupEntry(new GUIContent(topName), 1)); + } + + // get all registries, save into registryName > INodeRegistry Lookup + var subRegistries = new Dictionary<string, INodeRegistry>(); + foreach (KeyValuePair<string, INodeRegistry> registry in topRegistry.Value.OrderBy(s => s.Key)) + { + string baseRegistryName = registry.Key.Replace("NodeRegistry", "").FriendlyNameify().ReplaceFirst(topName, ""); + string registryName = baseRegistryName.UppercaseFirst(); + subRegistries.Add(registryName, registry.Value); + } + + // Go through each registry entry and add the top-level registry and associated array registry + foreach (KeyValuePair<string, INodeRegistry> regEntry in subRegistries) + { + INodeRegistry registry = regEntry.Value; + string registryName = regEntry.Key; + + int level = 2; + // Special cases for Udon sub-levels, added at top + if (topName == "Udon") + { + level = 1; + if (registryName == "Event" || registryName == "Type") + { + registryName = $"{registryName}s"; + } + } + + if (!registryName.EndsWith("[]")) + { + // add Registry Level + var groupEntry = new SearchTreeGroupEntry(new GUIContent(registryName, icon), level) { userData = registry }; + _slowRegistryCache.Add(groupEntry); + } + + // Check for Array Type first + string regArrayType = $"{registryName}[]"; + if (subRegistries.TryGetValue(regArrayType, out INodeRegistry arrayRegistry)) + { + // we have a matching subRegistry, add that next + var arrayLevel = level + 1; + var arrayGroupEntry = new SearchTreeGroupEntry(new GUIContent(regArrayType, icon), arrayLevel) { userData = registry }; + _slowRegistryCache.Add(arrayGroupEntry); + + // Add all array entries + AddEntriesForRegistry(_slowRegistryCache, arrayRegistry, arrayLevel + 1); + } + + AddEntriesForRegistry(_slowRegistryCache, registry, level + 1, true); + + } + } + return _slowRegistryCache; + } + + public override bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) + { + // checking type so we can support selecting registries as well + if (entry.userData is UdonNodeDefinition definition && !_graphView.IsDuplicateEventNode(definition.fullName)) + { + _graphView.AddNodeFromSearch(definition, GetGraphPositionFromContext(context)); + return true; + } + else + { + return false; + } + } + + #endregion + + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs.meta new file mode 100644 index 00000000..135a1da8 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b721120e6c1d320448a55fe87a7de824 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs new file mode 100644 index 00000000..e7279762 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs @@ -0,0 +1,124 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +#else +using UnityEditor.Experimental.UIElements.GraphView; +#endif +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.Udon.Graph; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + + public class UdonPortSearchWindow : UdonSearchWindowBase + { + + internal List<SearchTreeEntry> _fullRegistry; + + #region ISearchWindowProvider + + public Type typeToSearch; + public UdonPort startingPort; + public Direction direction; + + public class VariableInfo + { + public string uid; + public bool isGetter; + + public VariableInfo(string uid, bool isGetter) + { + this.uid = uid; + this.isGetter = isGetter; + } + } + + override public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context) + { + _fullRegistry = new List<SearchTreeEntry> + { + new SearchTreeGroupEntry(new GUIContent($"{direction.ToString()} Search"), 0) + }; + + var defsToAdd = new Dictionary<string, List<UdonNodeDefinition>>(); + var registries = UdonEditorManager.Instance.GetNodeRegistries(); + foreach (var item in registries) + { + var definitions = item.Value.GetNodeDefinitions().ToList(); + + var registryName = item.Key.FriendlyNameify().Replace("NodeRegistry", ""); + defsToAdd.Add(registryName, new List<UdonNodeDefinition>()); + + foreach (var def in definitions) + { + var collection = direction == Direction.Input ? def.Inputs : def.Outputs; + if(collection.Any(p=>p.type == typeToSearch)) + { + defsToAdd[registryName].Add(def); + } + } + } + + var variables = _graphView.GetVariableNodes; + + // Add Getters and Setters for matched variable types + Texture2D icon = EditorGUIUtility.FindTexture("GameManager Icon"); + string typeToSearchSimple = typeToSearch.ToString().Replace(".", ""); + foreach (var item in variables) + { + string variableSimpleName = item.fullName.Replace("Variable_", ""); + string getOrSet = direction == Direction.Output ? "Get" : "Set"; + if(variableSimpleName == typeToSearchSimple) + { + string customVariableName = item.nodeValues[1].Deserialize().ToString(); + _fullRegistry.Add(new SearchTreeEntry(new GUIContent($"{getOrSet} {customVariableName}", icon)) + { + level = 1, + userData = new VariableInfo(item.uid, direction == Direction.Output), + }); + } + } + + foreach (var item in defsToAdd) + { + // Skip empty lists + if (item.Value.Count == 0) continue; + + _fullRegistry.Add(new SearchTreeGroupEntry(new GUIContent(item.Key), 1)); + AddEntries(_fullRegistry, item.Value, 2); + } + + return _fullRegistry; + } + + public override bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) + { + var position = GetGraphPositionFromContext(context) - new Vector2(140, 0); + // checking type so we can support selecting registries as well + if (entry.userData is UdonNodeDefinition definition && !_graphView.IsDuplicateEventNode(definition.fullName)) + { + var node = _graphView.AddNodeFromSearch(definition, position); + _graphView.ConnectNodeTo(node, startingPort, direction, typeToSearch); + return true; + } + else if(entry.userData is VariableInfo data) + { + UdonNode node = _graphView.MakeVariableNode(data.uid, position, data.isGetter ? GraphView.UdonGraph.VariableNodeType.Getter : GraphView.UdonGraph.VariableNodeType.Setter ); + _graphView.AddElement(node); + _graphView.ConnectNodeTo(node, startingPort, direction, typeToSearch); + _graphView.RefreshVariables(true); + return true; + } + else + { + return false; + } + } + + #endregion + + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs.meta new file mode 100644 index 00000000..e5ff6867 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e94c084f399869b42a21244fd07778c4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs new file mode 100644 index 00000000..67a19eb2 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs @@ -0,0 +1,183 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +#else +using UnityEditor.Experimental.UIElements.GraphView; +#endif +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.Udon.Graph; +using VRC.Udon.Graph.Interfaces; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonRegistrySearchWindow : UdonSearchWindowBase + { + private UdonSearchManager _searchManager; + private static List<SearchTreeEntry> _registryCache; + private List<(string, string)> _shortcutRegistries = new List<(string, string)>() + { + ("UnityEngine","Debug"), + ("Udon","Special"), + ("Udon","Type"), + ("VRC", "UdonCommonInterfacesIUdonEventReceiver") + }; + + private HashSet<string> _hiddenRegistries = new HashSet<string>() + { + }; + + public void Initialize(UdonGraphWindow editorWindow, UdonGraph graphView, UdonSearchManager manager) + { + base.Initialize(editorWindow, graphView); + _searchManager = manager; + } + + #region ISearchWindowProvider + + override public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context) + { + if (!skipCache && (_registryCache != null && _registryCache.Count > 0)) return _registryCache; + + _registryCache = new List<SearchTreeEntry>(); + + Texture2D icon = EditorGUIUtility.FindTexture("cs Script Icon"); + _registryCache.Add(new SearchTreeGroupEntry(new GUIContent("Quick Search"), 0)); + + var topRegistriesLookup = new Dictionary<string, List<KeyValuePair<string, INodeRegistry>>>(); + foreach (var entry in UdonEditorManager.Instance.GetTopRegistries()) + { + topRegistriesLookup.Add(entry.Key, new List<KeyValuePair<string, INodeRegistry>>(entry.Value)); + } + + // Add shortcut registries + foreach (var item in _shortcutRegistries) + { + if (topRegistriesLookup.TryGetValue(item.Item1, out var searchRegistry)) + { + string subRegistryName = $"{item.Item1}{item.Item2}NodeRegistry"; + var subRegistry = searchRegistry.FindAll(r => r.Key == subRegistryName); + if (subRegistry.Count == 1) + { + topRegistriesLookup.Add(item.Item2, subRegistry); + } + else + { + Debug.LogWarning($"Could not find sub-registry {subRegistryName}"); + } + } + } + + // Combine Events into special top-level element + var vrcEvents = topRegistriesLookup["VRC"].Find(r=>r.Key == "VRCEventNodeRegistry").Value.GetNodeDefinitions(); + var udonEvents = topRegistriesLookup["Udon"].Find(r => r.Key == "UdonEventNodeRegistry").Value.GetNodeDefinitions(); + var allEvents = vrcEvents.Concat(udonEvents); + var eventRegistry = new EventRegistry(); + foreach (var item in allEvents) + { + eventRegistry.definitions.Add(item); + } + topRegistriesLookup.Add("Events", new List<KeyValuePair<string, INodeRegistry>>() { new KeyValuePair<string, INodeRegistry>("Events", eventRegistry) }); + + // Build lookup from newly organized list + var topRegistries = topRegistriesLookup.OrderBy(s => s.Key); + foreach (var topRegistry in topRegistries) + { + string topName = topRegistry.Key.Replace("NodeRegistry", ""); + + // Handle Shortcut registries with only 1 top-level registry listed + if (topRegistry.Value.Count == 1) + { + string registryName = UdonGraphExtensions.FriendlyNameify(topName); + _registryCache.Add(new SearchTreeGroupEntry(new GUIContent(registryName)) { level = 1, userData = topRegistry.Value }); + AddEntriesForRegistry(_registryCache, topRegistry.Value.First().Value, 2); + continue; + } + + // Skip empty 'Udon' top level + if (topName != "Udon") + { + _registryCache.Add(new SearchTreeGroupEntry(new GUIContent(topName), 1)); + } + + foreach (KeyValuePair<string, INodeRegistry> registry in topRegistry.Value.OrderBy(s => s.Key)) + { + string baseRegistryName = registry.Key.Replace("NodeRegistry", "").FriendlyNameify().ReplaceFirst(topName, ""); + string registryName = baseRegistryName.UppercaseFirst(); + + // Plural-ize Event->Events and Type->Types + if (topName == "Udon" && (registryName == "Event" || registryName == "Type")) + { + registryName = $"{registryName}s"; + } + else + { + // add Registry Level + if (registryName.StartsWithCached("Object") || registryName.StartsWithCached("Type")) + { + registryName = $"{topName}.{registryName}"; + } + + // skip certain registries + if (_hiddenRegistries.Contains(registryName)) + { + continue; + } + + _registryCache.Add(new SearchTreeEntry(new GUIContent(registryName, icon, $"{topName}.{registryName}")) { level = 2, userData = registry.Value }); + } + } + } + return _registryCache; + } + + public override bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) + { + // checking type so we can support selecting registries as well + if (entry.userData is INodeRegistry) + { + _searchManager.QueueOpenFocusedSearch(entry.userData as INodeRegistry, context.screenMousePosition); + return true; + } + else if (entry.userData is UdonNodeDefinition definition && !_graphView.IsDuplicateEventNode(definition.fullName)) + { + _graphView.AddNodeFromSearch(definition, GetGraphPositionFromContext(context)); + return true; + } + else + { + return false; + } + } + + #endregion + + public class EventRegistry : INodeRegistry + { + public List<UdonNodeDefinition> definitions = new List<UdonNodeDefinition>(); + + public UdonNodeDefinition GetNodeDefinition(string identifier) + { + throw new NotImplementedException(); + } + + public IEnumerable<UdonNodeDefinition> GetNodeDefinitions() + { + return definitions; + } + + public IEnumerable<UdonNodeDefinition> GetNodeDefinitions(string baseIdentifier) + { + throw new NotImplementedException(); + } + + public Dictionary<string, INodeRegistry> GetNodeRegistries() + { + throw new NotImplementedException(); + } + } + + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs.meta new file mode 100644 index 00000000..e0a73c89 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a6c453fae11b5349a33399e258d1578 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs new file mode 100644 index 00000000..49df2467 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs @@ -0,0 +1,119 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +#else +using UnityEditor.Experimental.UIElements.GraphView; +#endif +using System; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.Udon.Graph.Interfaces; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonSearchManager + { + private UdonGraph _view; + private UdonGraphWindow _window; + + // Search Windows + private UdonFocusedSearchWindow _focusedSearchWindow; + private UdonRegistrySearchWindow _registrySearchWindow; + private UdonFullSearchWindow _fullSearchWindow; + public UdonVariableTypeWindow _variableSearchWindow; + public UdonPortSearchWindow _portSearchWindow; + + public UdonSearchManager(UdonGraph view, UdonGraphWindow window) + { + _view = view; + _window = window; + + SetupSearchTypes(); + + view.nodeCreationRequest += OnRequestNodeCreation; + } + + private void SetupSearchTypes() + { + if (_registrySearchWindow == null) + _registrySearchWindow = ScriptableObject.CreateInstance<UdonRegistrySearchWindow>(); + _registrySearchWindow.Initialize(_window, _view, this); + + if (_fullSearchWindow == null) + _fullSearchWindow = ScriptableObject.CreateInstance<UdonFullSearchWindow>(); + _fullSearchWindow.Initialize(_window, _view); + + if (_focusedSearchWindow == null) + _focusedSearchWindow = ScriptableObject.CreateInstance<UdonFocusedSearchWindow>(); + _focusedSearchWindow.Initialize(_window, _view); + + if (_variableSearchWindow == null) + _variableSearchWindow = ScriptableObject.CreateInstance<UdonVariableTypeWindow>(); + _variableSearchWindow.Initialize(_window, _view); + + if (_portSearchWindow == null) + _portSearchWindow = ScriptableObject.CreateInstance<UdonPortSearchWindow>(); + _portSearchWindow.Initialize(_window, _view); + } + + private void OnRequestNodeCreation(NodeCreationContext context) + { + // started on empty space + if (context.target == null) + { + // If we have a node selected (but not set as context.target because that's a container for new nodes to go into), search within that node's registry + if (Settings.SearchOnSelectedNodeRegistry && _view.selection.Count > 0 && _view.selection.First() is UdonNode) + { + _focusedSearchWindow.targetRegistry = (_view.selection.First() as UdonNode).Registry; + SearchWindow.Open(new SearchWindowContext(context.screenMousePosition, 360, 360), _focusedSearchWindow); + } + else + { + // Create Search Window that only searches Top-Level Registries + SearchWindow.Open(new SearchWindowContext(context.screenMousePosition, 360, 360), _registrySearchWindow); + } + } + else if (context.target is UdonGraph) + { + // Slightly hacky method to figure out that we want a full-search window + SearchWindow.Open(new SearchWindowContext(context.screenMousePosition, 360, 360), _fullSearchWindow); + } + } + + public void OpenVariableSearch(Vector2 screenMousePosition) + { + // offset search window to appear next to mouse + screenMousePosition.x += 140; + screenMousePosition.y += 0; + SearchWindow.Open(new SearchWindowContext(screenMousePosition, 360, 360), _variableSearchWindow); + } + + public void OpenPortSearch(Type type, Vector2 screenMousePosition, UdonPort port, Direction direction) + { + // offset search window to appear next to mouse + screenMousePosition = _portSearchWindow._editorWindow.position.position + screenMousePosition; + screenMousePosition.x += 140; + screenMousePosition.y += 0; + _portSearchWindow.typeToSearch = type; + _portSearchWindow.startingPort = port; + _portSearchWindow.direction = direction; + SearchWindow.Open(new SearchWindowContext(screenMousePosition, 360, 360), _portSearchWindow); + } + + private Vector2 _searchWindowPosition; + public void QueueOpenFocusedSearch(INodeRegistry registry, Vector2 position) + { + _searchWindowPosition = position; + _focusedSearchWindow.targetRegistry = registry; + EditorApplication.update += TryOpenFocusedSearch; + } + + private void TryOpenFocusedSearch() + { + if (SearchWindow.Open(new SearchWindowContext(_searchWindowPosition, 360, 360), _focusedSearchWindow)) + { + EditorApplication.update -= TryOpenFocusedSearch; + } + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs.meta new file mode 100644 index 00000000..4f5b97c8 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e5a10bb1987c27944bd08a88119b2844 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs new file mode 100644 index 00000000..0108480c --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs @@ -0,0 +1,195 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +using UnityEngine.UIElements; +using UnityEditor.UIElements; +#else +using UnityEditor.Experimental.UIElements.GraphView; +using UnityEngine.Experimental.UIElements; +using UnityEditor.Experimental.UIElements; +#endif +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using VRC.Udon.Graph; +using VRC.Udon.Graph.Interfaces; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonSearchWindowBase : ScriptableObject, ISearchWindowProvider + { + // Reference to actual Graph View + internal UdonGraph _graphView; + private List<SearchTreeEntry> _exampleLookup; + internal UdonGraphWindow _editorWindow; + protected bool skipCache = false; + + private readonly HashSet<string> nodesToSkip = new HashSet<string>() + { + "Get_Variable", + "Set_Variable", + "Comment", + "Event_OnVariableChange", + }; + + public virtual void Initialize(UdonGraphWindow editorWindow, UdonGraph graphView) + { + _editorWindow = editorWindow; + _graphView = graphView; + } + + #region ISearchWindowProvider + + public virtual List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context) + { + if (!skipCache && ( _exampleLookup != null && _exampleLookup.Count > 0)) return _exampleLookup; + + _exampleLookup = new List<SearchTreeEntry>(); + + Texture2D icon = EditorGUIUtility.FindTexture("cs Script Icon"); + _exampleLookup.Add(new SearchTreeGroupEntry(new GUIContent("Create Node"), 0)); + + return _exampleLookup; + } + + public virtual bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) + { + return true; + } + + #endregion + + internal Vector2 GetGraphPositionFromContext(SearchWindowContext context) + { +#if UNITY_2019_3_OR_NEWER + var windowRoot = _editorWindow.rootVisualElement; +#else + var windowRoot = _editorWindow.GetRootVisualContainer(); +#endif + var windowMousePosition = windowRoot.ChangeCoordinatesTo(windowRoot.parent, + context.screenMousePosition - _editorWindow.position.position); + var graphMousePosition = _graphView.contentViewContainer.WorldToLocal(windowMousePosition); + return graphMousePosition; + } + + internal void AddEntries(List<SearchTreeEntry> cache, IEnumerable<UdonNodeDefinition> definitions, int level, + bool stripToLastDot = false) + { + Texture2D icon = AssetPreview.GetMiniTypeThumbnail(typeof(GameObject)); + Texture2D iconGetComponents = EditorGUIUtility.FindTexture("d_ViewToolZoom"); + Texture2D iconOther = new Texture2D(1, 1); + iconOther.SetPixel(0,0, new Color(0,0,0,0)); + iconOther.Apply(); + Dictionary<string, UdonNodeDefinition> baseNodeDefinition = new Dictionary<string, UdonNodeDefinition>(); + + foreach (UdonNodeDefinition nodeDefinition in definitions.OrderBy( + s => UdonGraphExtensions.PrettyFullName(s))) + { + string baseIdentifier = nodeDefinition.fullName; + string[] splitBaseIdentifier = baseIdentifier.Split(new[] { "__" }, StringSplitOptions.None); + if (splitBaseIdentifier.Length >= 2) + { + baseIdentifier = $"{splitBaseIdentifier[0]}__{splitBaseIdentifier[1]}"; + } + + if (baseNodeDefinition.ContainsKey(baseIdentifier)) + { + continue; + } + + baseNodeDefinition.Add(baseIdentifier, nodeDefinition); + } + + var nodesOfGetComponentType = new List<SearchTreeEntry>(); + var nodesOfOtherType = new List<SearchTreeEntry>(); + + // add all subTypes + foreach (KeyValuePair<string, UdonNodeDefinition> nodeDefinitionsEntry in baseNodeDefinition) + { + string nodeName = UdonGraphExtensions.PrettyBaseName(nodeDefinitionsEntry.Key); + nodeName = nodeName.UppercaseFirst(); + nodeName = nodeName.Replace("_", " "); + if (stripToLastDot) + { + int lastDotIndex = nodeName.LastIndexOf('.'); + nodeName = nodeName.Substring(lastDotIndex + 1); + } + + // Skip some nodes that should be added in other ways (variables and comments) + if (nodeName.StartsWithCached("Variable") || nodesToSkip.Contains(nodeDefinitionsEntry.Key)) + { + continue; + } + + if (nodeName.StartsWithCached("Object")) + { + nodeName = $"{nodeDefinitionsEntry.Value.type.Namespace}.{nodeName}"; + } + + if (nodeNamesGetComponentType.Contains(nodeName)) + { + nodesOfGetComponentType.Add(new SearchTreeEntry(new GUIContent(nodeName, iconGetComponents)) { level = level+1, userData = nodeDefinitionsEntry.Value }); + continue; + } + + // Only put 'Equals' in the 'Other' category if this definition is not an Enum + if (nodeNamesOtherType.Contains(nodeName) || nodeName == "Equals" && !nodeDefinitionsEntry.Value.type.IsEnum) + { + nodesOfOtherType.Add(new SearchTreeEntry(new GUIContent(nodeName, iconOther)) { level = level+1, userData = nodeDefinitionsEntry.Value }); + continue; + } + + cache.Add(new SearchTreeEntry(new GUIContent(nodeName, icon)) { level = level, userData = nodeDefinitionsEntry.Value }); + } + + // add getComponents level + if (nodesOfGetComponentType.Count > 0) + { + cache.Add(new SearchTreeGroupEntry(new GUIContent("GetComponents"), level)); + foreach (var entry in nodesOfGetComponentType) + { + cache.Add(entry); + } + } + + // add other level + if (nodesOfOtherType.Count > 0) + { + cache.Add(new SearchTreeGroupEntry(new GUIContent("Other"), level)); + foreach (var entry in nodesOfOtherType) + { + cache.Add(entry); + } + } + } + + private static HashSet<string> nodeNamesGetComponentType = new HashSet<string>() + { + "GetComponent", + "GetComponentInChildren", + "GetComponentInParent", + "GetComponents", + "GetComponentsInChildren", + "GetComponentsInParent", + }; + + private static HashSet<string> nodeNamesOtherType = new HashSet<string>() + { + "Equality", + "GetHashCode", + "GetInstanceID", + "GetType", + "Implicit", + "Inequality", + "Tostring", + }; + + // adds all entries so we can use this for regular and array registries + internal void AddEntriesForRegistry(List<SearchTreeEntry> cache, INodeRegistry registry, int level, + bool stripToLastDot = false) + { + AddEntries(cache, registry.GetNodeDefinitions(), level, stripToLastDot); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs.meta new file mode 100644 index 00000000..3bae0009 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d825ed3ba6aa7f14294e73efefc217d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs new file mode 100644 index 00000000..31f07a5c --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs @@ -0,0 +1,52 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +#else +using UnityEditor.Experimental.UIElements.GraphView; +#endif +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonVariableTypeWindow : UdonSearchWindowBase + { + internal List<SearchTreeEntry> _fullRegistry; + + #region ISearchWindowProvider + + override public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context) + { + if (!skipCache && _fullRegistry != null) return _fullRegistry; + + _fullRegistry = new List<SearchTreeEntry>(); + + _fullRegistry.Add(new SearchTreeGroupEntry(new GUIContent("Variable Type Search"), 0)); + + var definitions = UdonEditorManager.Instance.GetNodeDefinitions("Variable_").ToList().OrderBy(n=>n.name); + foreach (var definition in definitions) + { + _fullRegistry.Add(new SearchTreeEntry(new GUIContent(UdonGraphExtensions.FriendlyTypeName(definition.type).FriendlyNameify())) + { + level = 1, + userData = definition.fullName, + }); + } + + return _fullRegistry; + } + + override public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) + { + if(_graphView == null) + { + return false; + } + _graphView.AddNewVariable((string)entry.userData); + return true; + } + + #endregion + + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs.meta new file mode 100644 index 00000000..70109177 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 16fc7a7a059deeb458fdcdf719b467a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs new file mode 100644 index 00000000..3899a163 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq.Expressions; +using UnityEngine; +using VRC.Udon.Common.Interfaces; + +// nicked from GraphProcessor project +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram +{ + public static class TypeExtension + { + public static bool IsReallyAssignableFrom(this Type type, Type otherType) + { + if (type == null && otherType != null) return false; + if (otherType == null && type != null) return false; + + if (type == otherType) + return true; + if (type.IsAssignableFrom(otherType)) + return true; + if (otherType.IsAssignableFrom(type)) + return true; + if (type == typeof(IUdonEventReceiver) && otherType == typeof(Component)) + return true; + + return false; + } + + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs.meta new file mode 100644 index 00000000..fdb7364b --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e2f2300f99ce0ea4a8d9a20b464384df +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs new file mode 100644 index 00000000..23060dd9 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs @@ -0,0 +1,180 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.UIElements; +using UnityEngine.UIElements; +#else +using UnityEditor.Experimental.UIElements; +using UnityEngine.Experimental.UIElements; +#endif +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using UnityEngine; +using VRC.SDKBase; +using VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView; +using Object = UnityEngine.Object; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public static class UdonFieldFactory + { + static readonly Dictionary<Type, Type> fieldDrawers = new Dictionary<Type, Type>(); + + static readonly MethodInfo createFieldMethod = + typeof(UdonFieldFactory).GetMethod("CreateFieldSpecific", BindingFlags.Static | BindingFlags.Public); + + static UdonFieldFactory() + { + AddDrawer(typeof(bool), typeof(Toggle)); + AddDrawer(typeof(int), typeof(IntegerField)); + AddDrawer(typeof(uint), typeof(UnsignedIntegerField)); + AddDrawer(typeof(long), typeof(LongField)); + AddDrawer(typeof(ulong), typeof(UnsignedLongField)); + AddDrawer(typeof(short), typeof(ShortField)); + AddDrawer(typeof(ushort), typeof(UnsignedShortField)); + AddDrawer(typeof(float), typeof(FloatField)); + AddDrawer(typeof(double), typeof(DoubleField)); + AddDrawer(typeof(string), typeof(TextField)); + AddDrawer(typeof(Bounds), typeof(BoundsField)); + AddDrawer(typeof(Color), typeof(ColorField)); + AddDrawer(typeof(Vector2), typeof(Vector2Field)); + AddDrawer(typeof(Vector2Int), typeof(Vector2IntField)); + AddDrawer(typeof(Vector3), typeof(Vector3Field)); + AddDrawer(typeof(Vector3Int), typeof(Vector3IntField)); + AddDrawer(typeof(Vector4), typeof(Vector4Field)); + AddDrawer(typeof(AnimationCurve), typeof(CurveField)); + AddDrawer(typeof(Enum), typeof(EnumField)); + AddDrawer(typeof(Gradient), typeof(GradientField)); + AddDrawer(typeof(Object), typeof(ObjectField)); + AddDrawer(typeof(Rect), typeof(RectField)); + AddDrawer(typeof(RectInt), typeof(RectIntField)); + AddDrawer(typeof(char), typeof(CharField)); + AddDrawer(typeof(byte), typeof(ByteField)); + AddDrawer(typeof(sbyte), typeof(SByteField)); + AddDrawer(typeof(decimal), typeof(DecimalField)); + AddDrawer(typeof(Quaternion), typeof(QuaternionField)); + AddDrawer(typeof(LayerMask), typeof(LayerMaskField)); + AddDrawer(typeof(VRCUrl), typeof(VRCUrlField)); + } + + static void AddDrawer(Type fieldType, Type drawerType) + { + var iNotifyType = typeof(INotifyValueChanged<>).MakeGenericType(fieldType); + + if (!iNotifyType.IsAssignableFrom(drawerType)) + { + Debug.LogWarning("The custom field drawer " + drawerType + + " does not implements INotifyValueChanged< " + fieldType + " >"); + return; + } + + fieldDrawers[fieldType] = drawerType; + } + + public static INotifyValueChanged<T> CreateField<T>(T value) + { + return CreateField(value != null ? value.GetType() : typeof(T)) as INotifyValueChanged<T>; + } + + public static VisualElement CreateField(Type t) + { + Type drawerType; + + fieldDrawers.TryGetValue(t, out drawerType); + + if (drawerType == null) + drawerType = fieldDrawers.FirstOrDefault(kp => kp.Key.IsReallyAssignableFrom(t)).Value; + + if (drawerType == null) + { + return null; + } + + object field; + + if (drawerType == typeof(EnumField)) + { + field = new EnumField(Activator.CreateInstance(t) as Enum); + } + else + { + try + { + field = Activator.CreateInstance(drawerType, + BindingFlags.CreateInstance | + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.Instance | + BindingFlags.OptionalParamBinding, null, + new object[] {Type.Missing}, CultureInfo.CurrentCulture); + } + catch + { + field = Activator.CreateInstance(drawerType, + BindingFlags.CreateInstance | + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.Instance | + BindingFlags.OptionalParamBinding, null, + new object[] { }, CultureInfo.CurrentCulture); + } + } + + // For mutiline + if (field is TextField textField) + textField.multiline = true; + + return field as VisualElement; + } + + public static INotifyValueChanged<T> CreateFieldSpecific<T>(T value, Action<object> onValueChanged) + { + var fieldDrawer = CreateField<T>(value); + + if (fieldDrawer == null) + return null; + + fieldDrawer.value = value; + if (onValueChanged != null) + { +#if UNITY_2019_3_OR_NEWER + fieldDrawer.RegisterValueChangedCallback( +#else + fieldDrawer.OnValueChanged( +#endif + (e) => onValueChanged(e.newValue)); + } + + return fieldDrawer as INotifyValueChanged<T>; + } + + public static VisualElement CreateField(Type fieldType, object value, Action<object> onValueChanged) + { + if (typeof(Enum).IsAssignableFrom(fieldType)) + fieldType = typeof(Enum); + + if (fieldType.IsArray) + { + return new UdonArrayEditor(fieldType, onValueChanged, value); + } + + VisualElement field = null; + + try + { + var createFieldSpecificMethod = createFieldMethod.MakeGenericMethod(fieldType); + field = createFieldSpecificMethod.Invoke(null, new object[] {value, onValueChanged}) as VisualElement; + + // delay textFields + if (field is TextField) ((TextField) field).isDelayed = true; + } + catch (Exception e) + { + Debug.LogError(e); + } + + return field; + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs.meta new file mode 100644 index 00000000..d4980abf --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dfca99fb2e099d84da6c504cd521aa70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs new file mode 100644 index 00000000..d4ff35d6 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs @@ -0,0 +1,1392 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.Experimental.GraphView; +using UnityGraph = UnityEditor.Experimental.GraphView; +using UnityEngine.UIElements; +using MenuAction = UnityEngine.UIElements.DropdownMenuAction; +#else +using UnityEditor.Experimental.UIElements.GraphView; +using UnityGraph = UnityEditor.Experimental.UIElements.GraphView; +using UnityEngine.Experimental.UIElements; +using MenuAction = UnityEngine.Experimental.UIElements.DropdownMenu.MenuAction; +#endif +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using VRC.Udon.Graph; +using VRC.Udon.Serialization; + + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public static class UdonGraphCommands + { + public const string Reload = "Reload"; + public const string Compile = "Compile"; + } + + public class UdonGraph : UnityGraph.GraphView + { + private GridBackground _background; + private UdonMinimap _map; + private UdonVariablesBlackboard _blackboard; + + // copied over from Legacy.UdonGraph, + public UdonGraphProgramAsset graphProgramAsset; + public UdonBehaviour udonBehaviour; + + public UdonGraphData graphData + { + get => graphProgramAsset.graphData; + set + { + graphProgramAsset.graphData = value; + EditorUtility.SetDirty(graphProgramAsset); + } + } + + // Tracking variables + private List<UdonNodeData> _variableNodes = new List<UdonNodeData>(); + private ImmutableList<string> _variableNames; + + private Vector2 lastMousePosition; + private VisualElement mouseTipContainer; + private TextElement mouseTip; + private Vector2 mouseTipOffset = new Vector2(20, -22); + + private UdonSearchManager _searchManager; + + private bool _reloading = false; + + private bool _dragging = false; + + public bool IsReloading => _reloading; + + // Enable stuff from NodeGraphProcessor + private UdonGraphWindow _window; + + public ImmutableList<string> GetVariableNames + { + get => _variableNames; + private set { } + } + + public List<UdonNodeData> GetVariableNodes + { + get => _variableNodes; + private set { } + } + + public bool IsReservedName(string name) + { + return name.StartsWith("__"); + } + + public UdonGraph(UdonGraphWindow window) + { + _window = window; + + this.StretchToParentSize(); + SetupBackground(); + SetupMap(); + SetupBlackboard(); + SetupZoom(0.2f, 3); + SetupDragAndDrop(); + + this.AddManipulator(new ContentDragger()); + this.AddManipulator(new SelectionDragger()); + this.AddManipulator(new RectangleSelector()); + + mouseTipContainer = new VisualElement() + { + name = "mouse-tip-container", + }; + Add(mouseTipContainer); + mouseTip = new TextElement() + { + name = "mouse-tip", + visible = true, + }; + SetMouseTip(""); + mouseTipContainer.Add(mouseTip); + + // This event is used to send commands from updated port fields + RegisterCallback<ExecuteCommandEvent>(OnExecuteCommand); + + // Save last known mouse position for better pasting. Is there a performance hit for this? + RegisterCallback<MouseMoveEvent>(OnMouseMove); + RegisterCallback<KeyDownEvent>(OnKeyDown); + + _searchManager = new UdonSearchManager(this, window); + + graphViewChanged = OnViewChanged; + serializeGraphElements = OnSerializeGraphElements; + unserializeAndPaste = OnUnserializeAndPaste; + canPasteSerializedData = CheckCanPaste; + viewTransformChanged = OnViewTransformChanged; + } + + private void OnViewTransformChanged(UnityGraph.GraphView graphView) + { + graphProgramAsset.viewTransform.position = this.viewTransform.position; + graphProgramAsset.viewTransform.scale = this.viewTransform.scale.x; + EditorUtility.SetDirty(graphProgramAsset); + } + + private bool CheckCanPaste(string pasteData) + { + UdonNodeData[] copiedNodeDataArray; + try + { + copiedNodeDataArray = JsonUtility + .FromJson<SerializableObjectContainer.ArrayWrapper<UdonNodeData>>( + UdonGraphExtensions.UnZipString(pasteData)) + .value; + } + catch + { + //oof ouch that's not valid data + return false; + } + + return true; + } + + public void Initialize(UdonGraphProgramAsset asset, UdonBehaviour udonBehaviour) + { + if (graphProgramAsset != null) + { + SaveGraphToDisk(); + } + + graphProgramAsset = asset; + if (udonBehaviour != null) + { + this.udonBehaviour = udonBehaviour; + } + + graphData = new UdonGraphData(graphProgramAsset.GetGraphData()); + + DoDelayedReload(); + EditorApplication.update += DelayedRestoreViewFromData; + + // When pressing ctrl-s, we save the graph + EditorSceneManager.sceneSaved += _ => SaveGraphToDisk(); + } + + private void DelayedRestoreViewFromData() + { + EditorApplication.update -= DelayedRestoreViewFromData; + //Todo: restore from saved data instead of FrameAll +#if UNITY_2019_3_OR_NEWER + FrameAll(); +#else + UpdateViewTransform(graphProgramAsset.viewTransform.position, + Vector3.one * graphProgramAsset.viewTransform.scale); + contentViewContainer.MarkDirtyRepaint(); +#endif + } + + public UdonNode AddNodeFromSearch(UdonNodeDefinition definition, Vector2 position) + { + UdonNode node = UdonNode.CreateNode(definition, this); + AddElement(node); + + node.SetPosition(new Rect(position, Vector2.zero)); + node.Select(this, false); + + return node; + } + + public void ConnectNodeTo(UdonNode node, UdonPort startingPort, UnityGraph.Direction direction, Type typeToSearch) + { + // Find port to connect to + var collection = direction == Direction.Input ? node.portsIn : node.portsOut; + UdonPort endPort = collection.FirstOrDefault(p => p.Value.portType == typeToSearch).Value; + // If found, add edge and serialize the connection in the programAsset + if(endPort != null) + { + // Important not to create and add this edge, we'll restore it below instead + startingPort.ConnectTo(endPort); + (startingPort.node as UdonNode).RestoreConnections(); + (endPort.node as UdonNode).RestoreConnections(); + Compile(); + } + } + + public void SaveGraphToDisk() + { + if (graphProgramAsset == null) + return; + + EditorUtility.SetDirty(graphProgramAsset); + } + + private void OnKeyDown(KeyDownEvent evt) + { + if (evt.target == this && evt.keyCode == KeyCode.Tab) + { + var screenPosition = GUIUtility.GUIToScreenPoint(evt.originalMousePosition); + nodeCreationRequest(new NodeCreationContext() { screenMousePosition = screenPosition, target = this }); + evt.StopImmediatePropagation(); + } + else if (evt.keyCode == KeyCode.A && evt.ctrlKey) + { + // Select every graph element + ClearSelection(); + foreach (var element in graphElements.ToList()) + { + AddToSelection(element); + } + } + else if (evt.keyCode == KeyCode.G && evt.shiftKey) + { + Undo.RecordObject(graphProgramAsset, "Changed Name"); + graphProgramAsset.graphData.name = Guid.NewGuid().ToString(); + } + } + + public bool GetBlackboardVisible() + { + return _blackboard.visible; + } + + public bool GetMinimapVisible() + { + return _map.visible; + } + + public void ToggleShowVariables(bool value) + { + _blackboard.SetVisible(value); + } + + public void ToggleShowMiniMap(bool value) + { + _map.SetVisible(value); + } + + public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) + { + if (evt.target is UnityGraph.GraphView || evt.target is UdonNode) + { + // Create a Group, enclosing any selected nodes + evt.menu.AppendAction("Create Group", (m) => + { + UdonGroup group = UdonGroup.Create("Group", GetRectFromMouse(), this); + Undo.RecordObject(graphProgramAsset, "Add Group"); + AddElement(group); + group.UpdateDataId(); + + foreach (ISelectable item in selection) + { + if (item is UdonNode) + { + group.AddElement(item as UdonNode); + } + else if(item is UdonComment) + { + group.AddElement(item as UdonComment); + } + } + group.Initialize(); + SaveGraphElementData(group); + }, MenuAction.AlwaysEnabled); + var selectedItems = selection.Where(i=>i is UdonNode || i is UdonComment).ToList(); + if (selectedItems.Count > 0) + { + evt.menu.AppendAction("Remove From Group", (m) => + { + Undo.RecordObject(graphProgramAsset, "Remove Items from Group"); + int count = selectedItems.Count; + for (int i = count - 1; i >=0; i--) + { + if(selectedItems.ElementAt(i) is UdonNode) + { + UdonNode node = selectedItems.ElementAt(i) as UdonNode; + if (node.group != null) + { + node.group.RemoveElement(node); + } + } + else if (selectedItems.ElementAt(i) is UdonComment) + { + UdonComment comment = selectedItems.ElementAt(i) as UdonComment; + if (comment.group != null) + { + comment.group.RemoveElement(comment); + } + } + } + + }, MenuAction.AlwaysEnabled); + } + + // Create a Comment + evt.menu.AppendAction("Create Comment", (m) => + { + UdonComment comment = UdonComment.Create("Comment", GetRectFromMouse(), this); + Undo.RecordObject(graphProgramAsset, "Add Comment"); + AddElement(comment); + }, MenuAction.AlwaysEnabled); + + evt.menu.AppendSeparator(); + } + + base.BuildContextualMenu(evt); + } + + private Rect GetRectFromMouse() + { + return new Rect(contentViewContainer.WorldToLocal(lastMousePosition), Vector2.zero); + } + + private void OnMouseMove(MouseMoveEvent evt) + { + lastMousePosition = evt.mousePosition; + MoveMouseTip(lastMousePosition); + } + + private void MoveMouseTip(Vector2 position) + { + if (mouseTipContainer.visible) + { + var newLayout = mouseTipContainer.layout; + newLayout.position = position + mouseTipOffset; +#if UNITY_2019_3_OR_NEWER + mouseTipContainer.transform.position = newLayout.position; +#else + mouseTipContainer.layout = newLayout; +#endif + } + } + + public bool IsDuplicateEventNode(string fullName, string uid = "") + { + if (fullName.StartsWith("Event_") && + (fullName != "Event_Custom") + ) + { + if (this.Query(fullName).ToList().Count > 0) + { + Debug.LogWarning( + $"Can't create more than one {fullName} node, try managing your flow with a Block node instead!"); + return true; + } + } + else if(fullName.StartsWith(Common.VariableChangedEvent.EVENT_PREFIX)) + { + bool isDuplicate = graphData.EventNodes.Any(d => + d.nodeValues.Length > 0 && d.nodeValues[0].Deserialize().ToString() == uid); + if (isDuplicate) + { + Debug.LogWarning( + $"Can't create more than one Change Event for {GetVariableName(uid)}, try managing your flow with a Block node instead!"); + } + return isDuplicate; + } + + return false; + } + + private string OnSerializeGraphElements(IEnumerable<GraphElement> selection) + { + Bounds bounds = new Bounds(); + bool startedBounds = false; + List<UdonNodeData> nodeData = new List<UdonNodeData>(); + List<UdonNodeData> variables = new List<UdonNodeData>(); + foreach (var item in selection) + { + // Only serializing UdonNode for now + if (item is UdonNode) + { + UdonNode node = (UdonNode)item; + // Calculate bounding box to enclose all items + if (!startedBounds) + { + bounds = new Bounds(node.data.position, Vector3.zero); + startedBounds = true; + } + else + { + bounds.Encapsulate(node.data.position); + } + + // Handle Get/Set Variables + if (node.data.fullName == "Get_Variable" || node.data.fullName == "Set_Variable" || node.data.fullName == "Set_ReturnValue") + { + // make old-school get-variable node data from existing variable + var targetUid = node.data.nodeValues[0].Deserialize(); + var matchingNode = GetVariableNodes.First(v => v.uid == (string)targetUid); + if (matchingNode != null && !variables.Contains(matchingNode)) + { + variables.Add(matchingNode); + } + } + + nodeData.Add(new UdonNodeData(node.data)); + } + } + + // Add variables at beginning of list so they get created first + nodeData.InsertRange(0, variables); + + // Go through each item and offset its position by the center of the group (normalizes the coordinates around 0,0) + var offset = new Vector2(bounds.center.x, bounds.center.y); + foreach (UdonNodeData data in nodeData) + { + var ogPosition = data.position; + data.position -= offset; + } + + string result = UdonGraphExtensions.ZipString(JsonUtility.ToJson( + new SerializableObjectContainer.ArrayWrapper<UdonNodeData>(nodeData.ToArray()))); + + return result; + } + + private void OnUnserializeAndPaste(string operationName, string pasteData) + { + ClearSelection(); + + UdonNodeData[] copiedNodeDataArray; + // Note: CheckCanPaste already does this check but it doesn't cost much to do it twice + try + { + copiedNodeDataArray = JsonUtility + .FromJson<SerializableObjectContainer.ArrayWrapper<UdonNodeData>>( + UdonGraphExtensions.UnZipString(pasteData)) + .value; + } + catch + { + //oof ouch that's not valid data + return; + } + + var copiedNodeDataList = new List<UdonNodeData>(); + // Add new variables if needed + for (int i = 0; i < copiedNodeDataArray.Length; i++) + { + if (copiedNodeDataArray[i].fullName.StartsWith("Variable_")) + { + if (!graphData.nodes.Exists(n => n.uid == copiedNodeDataArray[i].uid)) + { + // set graph to this one in case it was pasted from somewhere else + copiedNodeDataArray[i].SetGraph(graphData); + + // check for conflicting variable names + int nameIndex = (int)UdonParameterProperty.ValueIndices.name; + string varName = (string)copiedNodeDataArray[i].nodeValues[nameIndex].Deserialize(); + if (GetVariableNames.Contains(varName)) + { + // if we already have a variable with that name, find a new name and serialize it into the data + varName = GetUnusedVariableNameLike(varName); + copiedNodeDataArray[i].nodeValues[nameIndex] = + SerializableObjectContainer.Serialize(varName); + } + + _blackboard.AddFromData(copiedNodeDataArray[i]); + graphData.nodes.Add(copiedNodeDataArray[i]); + } + } + else if(IsDuplicateEventNode(copiedNodeDataArray[i].fullName)) + { + // don't add duplicate event nodes + } + else + { + copiedNodeDataList.Add(copiedNodeDataArray[i]); + } + } + + // Remove duplicate events + RefreshVariables(false); + + // copy modified list back to array + copiedNodeDataArray = copiedNodeDataList.ToArray(); + + _reloading = true; + var graphMousePosition = GetRectFromMouse().position; + List<UdonNode> pastedNodes = new List<UdonNode>(); + Dictionary<string, string> uidMap = new Dictionary<string, string>(); + UdonNodeData[] newNodeDataArray = new UdonNodeData[copiedNodeDataArray.Length]; + + for (int i = 0; i < copiedNodeDataArray.Length; i++) + { + UdonNodeData nodeData = copiedNodeDataArray[i]; + newNodeDataArray[i] = new UdonNodeData(graphData, nodeData.fullName) + { + position = nodeData.position + graphMousePosition, + uid = Guid.NewGuid().ToString(), + nodeUIDs = new string[nodeData.nodeUIDs.Length], + nodeValues = nodeData.nodeValues, + flowUIDs = new string[nodeData.flowUIDs.Length] + }; + + uidMap.Add(nodeData.uid, newNodeDataArray[i].uid); + } + + for (int i = 0; i < copiedNodeDataArray.Length; i++) + { + UdonNodeData nodeData = copiedNodeDataArray[i]; + UdonNodeData newNodeData = newNodeDataArray[i]; + + // Set the new node to point at this graph if it came from a different one + newNodeData.SetGraph(graphData); + + for (int j = 0; j < newNodeData.nodeUIDs.Length; j++) + { + if (uidMap.ContainsKey(nodeData.nodeUIDs[j].Split('|')[0])) + { + newNodeData.nodeUIDs[j] = uidMap[nodeData.nodeUIDs[j].Split('|')[0]]; + } + } + + for (int j = 0; j < newNodeData.flowUIDs.Length; j++) + { + if (uidMap.ContainsKey(nodeData.flowUIDs[j].Split('|')[0])) + { + newNodeData.flowUIDs[j] = uidMap[nodeData.flowUIDs[j].Split('|')[0]]; + } + } + + UdonNode udonNode = UdonNode.CreateNode(newNodeData, this); + if (udonNode != null) + { + graphData.nodes.Add(newNodeData); + AddElement(udonNode); + pastedNodes.Add(udonNode); + } + } + + // Select all newly-pasted nodes after reload + foreach (var item in pastedNodes) + { + item.RestoreConnections(); + item.BringToFront(); + AddToSelection(item as GraphElement); + } + + _reloading = false; + Compile(); + } + + // This is needed to properly clear the selection in some cases (like deleting a stack node) even though it doesn't appear to do anything + public override void ClearSelection() + { + base.ClearSelection(); + } + + public void MarkSceneDirty() + { + if (!EditorApplication.isPlaying) + { + EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); + } + } + + private GraphViewChange OnViewChanged(GraphViewChange changes) + { + bool dirty = false; + bool needsVariableRefresh = false; + // Remove node from Data when removed from Graph + if (!_reloading && changes.elementsToRemove != null && changes.elementsToRemove.Count > 0) + { + + foreach (var element in changes.elementsToRemove) + { + if (element is UdonNode) + { + var nodeData = ((UdonNode)element).data; + RemoveNodeAndData(nodeData); + continue; + } + + if (element is Edge) + { + Undo.RecordObject(graphProgramAsset, $"delete-{element.name}"); + continue; + } + + if (element is UdonParameterField) + { + needsVariableRefresh = true; + + var pField = element as UdonParameterField; + if (graphData.nodes.Contains(pField.Data)) + { + RemoveNodeAndData(pField.Data); + } + } + + if (element is IUdonGraphElementDataProvider) + { + Undo.RecordObject(graphProgramAsset, $"delete-{element.name}"); + var provider = (IUdonGraphElementDataProvider) element; + DeleteGraphElementData(provider, false); + RemoveElement(element); + } + } + + ClearSelection(); + dirty = true; + } + + if (dirty) + { + MarkSceneDirty(); + SaveGraphToDisk(); + } + + if (needsVariableRefresh) + { + RefreshVariables(true); + } + + return changes; + } + + public void DoDelayedCompile() + { + EditorApplication.update += DelayedCompile; + } + + private void DelayedCompile() + { + EditorApplication.update -= DelayedCompile; + graphProgramAsset.RefreshProgram(); + } + + private bool _waitingToReload; + public void DoDelayedReload() + { + if (!_waitingToReload && !_reloading) + { + _waitingToReload = true; + EditorApplication.update += DelayedReload; + } + } + + void DelayedReload() + { + _waitingToReload = false; + EditorApplication.update -= DelayedReload; + Reload(); + } + + private void SetupBackground() + { + _background = new GridBackground + { + name = "bg" + }; + Insert(0, _background); + _background.StretchToParentSize(); + } + + private void SetupBlackboard() + { + _blackboard = new UdonVariablesBlackboard(this); + + _blackboard.addItemRequested = BlackboardAddVariable; + _blackboard.editTextRequested = BlackboardEditVariableName; + _blackboard.SetPosition(new Rect(10, 130, 200, 150)); + Add(_blackboard); + } + + private void BlackboardEditVariableName(Blackboard b, VisualElement v, string newValue) + { + UdonParameterField field = (UdonParameterField) v; + Undo.RecordObject(graphProgramAsset, "Rename Variable"); + + // Sanitize value for variable name + string newVariableName = newValue.SanitizeVariableName(); + newVariableName = GetUnusedVariableNameLike(newVariableName); + field.Data.nodeValues[(int)UdonParameterProperty.ValueIndices.name] = SerializableObjectContainer.Serialize(newVariableName); + field.text = newVariableName; + + // Find all nodes that are getters/setters for this variable + // Change their title text by hand + nodes.ForEach((node => + { + UdonNode udonNode = (UdonNode) node; + if (udonNode != null && udonNode.IsVariableNode) + { + udonNode.RefreshTitle(); + } + })); + + RefreshVariables(true); + } + + private void BlackboardAddVariable(Blackboard obj) + { + var screenPosition = GUIUtility.GUIToScreenPoint(lastMousePosition); + _searchManager.OpenVariableSearch(screenPosition); + } + + public void OpenPortSearch(Type type, Vector2 position, UdonPort output, Direction direction) + { + _searchManager.OpenPortSearch(type, position, output, direction); + } + + private void SetupMap() + { + _map = new UdonMinimap(this); + Add(_map); + } + + private void OnExecuteCommand(ExecuteCommandEvent evt) + { + switch (evt.commandName) + { + case UdonGraphCommands.Reload: + DoDelayedReload(); + break; + case UdonGraphCommands.Compile: + Compile(); + break; + default: + break; + } + } + + public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) + { + var result = ports.ToList().Where( + port => port.direction != startPort.direction + && port.node != startPort.node + && port.portType.IsReallyAssignableFrom(startPort.portType) + && (port.capacity == Port.Capacity.Multi || port.connections.Count() == 0) + ).ToList(); + return result; + } + +#if UNITY_2019_3_OR_NEWER + private StyleSheet neonStyle = (StyleSheet) Resources.Load("UdonGraphNeonStyle"); +#endif + + public void Reload() + { + if (_reloading) return; + + _reloading = true; + +#if UNITY_2019_3_OR_NEWER + + if (Settings.UseNeonStyle && !styleSheets.Contains(neonStyle)) + { + styleSheets.Add(neonStyle); + } + else if (!Settings.UseNeonStyle && !styleSheets.Contains(neonStyle)) + { + styleSheets.Remove(neonStyle); + } +#else + string customStyle = "UdonGraphNeonStyle"; + if (Settings.UseNeonStyle && !HasStyleSheetPath(customStyle)) + { + AddStyleSheetPath(customStyle); + } + else if (!Settings.UseNeonStyle && HasStyleSheetPath(customStyle)) + { + RemoveStyleSheetPath(customStyle); + } +#endif + Undo.undoRedoPerformed -= + OnUndoRedo; //Remove old handler if present to prevent duplicates, doesn't cause errors if not present + Undo.undoRedoPerformed += OnUndoRedo; + + // Clear out Blackboard here + _blackboard.Clear(); + + // clear existing elements, probably need to update to only clear nodes and edges + DeleteElements(graphElements.ToList()); + + RefreshVariables(false); + + List<UdonNodeData> nodesToDelete = new List<UdonNodeData>(); + // add all nodes to graph + for (int i = 0; i < graphData.nodes.Count; i++) + { + UdonNodeData nodeData = graphData.nodes[i]; + + // Check for Node type - create nodes, separate out Variables + if (nodeData.fullName.StartsWithCached("Variable_")) + { + _blackboard.AddFromData(nodeData); + } + else if (nodeData.fullName.StartsWithCached("Comment")) + { + // one way conversion from Comment Node > Comment Group + var commentString = nodeData.nodeValues[0].Deserialize(); + if (commentString != null) + { + var comment = UdonComment.Create((string) commentString, + new Rect(nodeData.position, Vector2.zero), this); + AddElement(comment); + SaveGraphElementData(comment); + } + + // Remove from data, no longer a node + nodesToDelete.Add(nodeData); + } + else + { + try + { + UdonNode udonNode = UdonNode.CreateNode(nodeData, this); + AddElement(udonNode); + } + catch (Exception e) + { + Debug.LogError($"Error Loading Node {nodeData.fullName} : {e.Message}"); + nodesToDelete.Add(nodeData); + continue; + } + } + } + + // Delete old comments and data that could not be turned into an UdonNode + foreach (UdonNodeData nodeData in nodesToDelete) + { + if (graphData.nodes.Remove(nodeData)) + { + Debug.Log($"removed node {nodeData.fullName}"); + } + } + + // reconnect nodes + nodes.ForEach((genericNode) => + { + UdonNode udonNode = (UdonNode)genericNode; + udonNode.RestoreConnections(); + }); + + // Add all Graph Elements + if (graphProgramAsset.graphElementData != null) + { + var orderedElements = graphProgramAsset.graphElementData.ToList().OrderByDescending(e => e.type); + foreach (var elementData in orderedElements) + { + GraphElement element = RestoreElementFromData(elementData); + if (element != null) + { + AddElement(element); + if (element is UdonGroup group) + { + group.Initialize(); + } + } + } + } + + _reloading = false; + Compile(); + } + + // TODO: create generic to restore any supported element from UdonGraphElementData? + private GraphElement RestoreElementFromData(UdonGraphElementData data) + { + switch (data.type) + { + case UdonGraphElementType.GraphElement: + { + return null; + } + + case UdonGraphElementType.UdonGroup: + { + return UdonGroup.Create(data, this); + } + + case UdonGraphElementType.UdonComment: + { + return UdonComment.Create(data, this); + } + case UdonGraphElementType.Minimap: + { + _map.LoadData(data); + return null; + } + case UdonGraphElementType.VariablesWindow: + { + _blackboard.LoadData(data); + return null; + } + default: + return null; + } + } + + private void OnUndoRedo() + { + Reload(); + } + + public void RefreshVariables(bool recompile = true) + { + // we want internal variables at the end of the list so they can be trivially filtered out + _variableNodes = graphData.nodes + .Where(n => n.fullName.StartsWithCached("Variable_")) + .Where(n => n.nodeValues.Length > 1 && n.nodeValues[1] != null) + .OrderBy(n => ((string)n.nodeValues[1].Deserialize()).StartsWith("__")) + .ToList(); + _variableNames = ImmutableList.Create( + _variableNodes.Select(s => (string) s.nodeValues[1].Deserialize()).ToArray() + ); + + // Refresh variable options in popup + nodes.ForEach(node => + { + if (node is UdonNode udonNode && udonNode.IsVariableNode) + { + udonNode.RefreshVariablePopup(); + } + }); + + // We usually want to compile after a Refresh + if(recompile) + Compile(); + DoDelayedReload(); + } + + // Returns UID of newly created variable + public string AddNewVariable(string typeName = "Variable_SystemString", string variableName = "", + bool isPublic = false) + { + // Figure out unique variable name to use + string newVariableName = string.IsNullOrEmpty(variableName) ? "newVariable" : variableName; + newVariableName = GetUnusedVariableNameLike(newVariableName); + + string newVarUid = Guid.NewGuid().ToString(); + UdonNodeData newNodeData = new UdonNodeData(graphData, typeName) + { + uid = newVarUid, + nodeUIDs = new string[5], + nodeValues = new[] + { + SerializableObjectContainer.Serialize(default), + SerializableObjectContainer.Serialize(newVariableName, typeof(string)), + SerializableObjectContainer.Serialize(isPublic, typeof(bool)), + SerializableObjectContainer.Serialize(false, typeof(bool)), + SerializableObjectContainer.Serialize("none", typeof(string)) + }, + position = Vector2.zero + }; + + graphData.nodes.Add(newNodeData); + _blackboard.AddFromData(newNodeData); + RefreshVariables(true); + return newVarUid; + } + + public void RemoveNodeAndData(UdonNodeData nodeData) + { + Undo.RecordObject(graphProgramAsset, $"Removing {nodeData.fullName}"); + + if (nodeData.fullName.StartsWithCached("Variable_")) + { + var allVariableNodes = new HashSet<Node>(); + // Find all get/set variable nodes that reference this node + nodes.ForEach((graphNode => + { + UdonNode udonNode = graphNode as UdonNode; + if (udonNode != null && udonNode.IsVariableNode) + { + // Get variable uid and recursively remove all nodes that refer to it + var values = udonNode.data.nodeValues[0].stringValue.Split('|'); + if (values.Length > 1) + { + string targetVariable = values[1]; + if (targetVariable.CompareTo(nodeData.uid) == 0) + { + // We have a match! Delete this node + allVariableNodes.Add(graphNode); + RemoveNodeAndData(udonNode.data); + } + } + } + })); + + // remove each edge connected to a Get/Set Variable node which will be deleted + edges.ForEach(edge => + { + if (allVariableNodes.Contains(edge.input.node) || allVariableNodes.Contains(edge.output.node)) + { + (edge.output as UdonPort)?.Disconnect(edge); + (edge.input as UdonPort)?.Disconnect(edge); + RemoveElement(edge); + } + }); + + // remove from existing blackboard + _blackboard.RemoveByID(nodeData.uid); + RefreshVariables(true); + } + + UdonNode node = (UdonNode)GetNodeByGuid(nodeData.uid); + if (node != null) + { + node.RemoveFromHierarchy(); + } + + if (graphData.nodes.Contains(nodeData)) + { + graphData.nodes.Remove(nodeData); + } + } + + public void Compile() + { + UdonEditorManager.Instance.QueueAndRefreshProgram(graphProgramAsset); + } + + private bool ShouldUpdateAsset => !IsReloading && graphProgramAsset != null; + + private readonly HashSet<UdonGraphElementType> singleElementTypes = new HashSet<UdonGraphElementType>() + { + UdonGraphElementType.Minimap, UdonGraphElementType.VariablesWindow + }; + + public void SaveGraphElementData(IUdonGraphElementDataProvider provider) + { + if (ShouldUpdateAsset) + { + UdonGraphElementData newData = provider.GetData(); + if (graphProgramAsset.graphElementData == null) + { + graphProgramAsset.graphElementData = new UdonGraphElementData[0]; + } + + int index = -1; + // Some elements like minimap and variables window should only ever have one entry, so find by type + if (singleElementTypes.Contains(newData.type)) + { + index = Array.FindIndex(graphProgramAsset.graphElementData, e => e.type == newData.type); + } + // other elements can have multiples, so find by uid + else + { + index = Array.FindIndex(graphProgramAsset.graphElementData, e => e.uid == newData.uid); + } + if (index > -1) + { + // Update + graphProgramAsset.graphElementData[index] = newData; + } + else + { + // Add + int arrayLength = graphProgramAsset.graphElementData.Length; + Array.Resize(ref graphProgramAsset.graphElementData, arrayLength+1); + graphProgramAsset.graphElementData[arrayLength] = newData; + } + SaveGraphToDisk(); + } + } + + public void DeleteGraphElementData(IUdonGraphElementDataProvider provider, bool save = true) + { + int index = Array.FindIndex(graphProgramAsset.graphElementData, e => e.uid == provider.GetData().uid); + // remove if found + if (index > -1) + { + graphProgramAsset.graphElementData = graphProgramAsset.graphElementData.Where((source, i) => i != index).ToArray(); + } + + if (save) + { + SaveGraphToDisk(); + } + } + + #region Drag and Drop Support + + private void SetupDragAndDrop() + { + RegisterCallback<DragEnterEvent>(OnDragEnter); + RegisterCallback<DragPerformEvent>(OnDragPerform, TrickleDown.TrickleDown); + RegisterCallback<DragUpdatedEvent>(OnDragUpdated); + RegisterCallback<DragExitedEvent>(OnDragExited); + RegisterCallback<DragLeaveEvent>((e)=>OnDragExited(null)); + } + + private void OnDragEnter(DragEnterEvent e) + { + OnDragEnter(e.mousePosition, e.ctrlKey, e.altKey); + } + + private void OnDragEnter(Vector2 mousePosition, bool ctrlKey, bool altKey) + { + MoveMouseTip(mousePosition); + + var dragData = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>; + _dragging = false; + + if (dragData != null) + { + // Handle drag from exposed parameter view + if (dragData.OfType<UdonParameterField>().Any()) + { + _dragging = true; + string tip = "Get Variable\n+Ctrl: Set Variable\n+Alt: On Var Change"; + if (ctrlKey) + { + tip = "Set Variable"; + } else if (altKey) + { + tip = "On Variable Changed"; + } + SetMouseTip(tip); + } + } + + if (DragAndDrop.objectReferences.Length == 1 && DragAndDrop.objectReferences[0] != null) + { + var target = DragAndDrop.objectReferences[0]; + switch (target) + { + case GameObject g: + case Component c: + { + string type = GetDefinitionNameForType(target.GetType()); + if (UdonEditorManager.Instance.GetNodeDefinition(type) != null) + { + _dragging = true; + } + break; + } + } + } + + if (_dragging) + { + DragAndDrop.visualMode = ctrlKey ? DragAndDropVisualMode.Link : DragAndDropVisualMode.Copy; + } + } + + private void OnDragUpdated(DragUpdatedEvent e) + { + if (_dragging) + { + MoveMouseTip(e.mousePosition); + DragAndDrop.visualMode = e.ctrlKey ? DragAndDropVisualMode.Link : DragAndDropVisualMode.Copy; + } + else + { + OnDragEnter(e.mousePosition, e.ctrlKey, e.altKey); + } + } + + private void OnDragPerform(DragPerformEvent e) + { + if (!_dragging) return; + var graphMousePosition = this.contentViewContainer.WorldToLocal(e.mousePosition); + var draggedVariables = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>; + + if (draggedVariables != null) + { + // Handle Drop of Variables + var parameters = draggedVariables.OfType<UdonParameterField>(); + if (parameters.Any()) + { + RefreshVariables(false); + VariableNodeType nodeType = VariableNodeType.Getter; + if (e.ctrlKey) nodeType = VariableNodeType.Setter; + else if (e.altKey) nodeType = VariableNodeType.Change; + foreach (var parameter in parameters) + { + UdonNode udonNode = MakeVariableNode(parameter.Data.uid, graphMousePosition, nodeType); + if (udonNode != null) + { + AddElement(udonNode); + } + } + RefreshVariables(true); + } + } + + // Handle Drop of single GameObjects and Assets + if (DragAndDrop.objectReferences.Length == 1 && DragAndDrop.objectReferences[0] != null) + { + var target = DragAndDrop.objectReferences[0]; + switch (target) + { + case Component c: + SetupDraggedObject(c, graphMousePosition); + break; + + case GameObject g: + SetupDraggedObject(g, graphMousePosition); + break; + } + } + + _dragging = false; + } + + private void OnDragExited(DragExitedEvent e) + { + SetMouseTip(""); + _dragging = false; + } + + #endregion + public enum VariableNodeType + { + Getter, + Setter, + Return, + Change, + } + + public string GetVariableName(string uid) + { + var targetNode = GetVariableNodes.Where(n => n.uid == uid).First(); + try + { + return targetNode.nodeValues[1].Deserialize() as string; + } + catch (Exception e) + { + Debug.LogError($"Couldn't find variable name for {uid}: {e.Message}"); + return ""; + } + } + + public UdonNode MakeVariableNode(string selectedUid, Vector2 graphMousePosition, VariableNodeType nodeType) + { + string definitionName = ""; + switch (nodeType) + { + case VariableNodeType.Getter: + definitionName = "Get_Variable"; + break; + case VariableNodeType.Setter: + definitionName = "Set_Variable"; + break; + case VariableNodeType.Return: + definitionName = "Set_ReturnValue"; + break; + case VariableNodeType.Change: + definitionName = "Event_OnVariableChange"; + break; + } + + if (nodeType == VariableNodeType.Change) + { + string variableName = GetVariableName(selectedUid); + if (!string.IsNullOrWhiteSpace(variableName)) + { + string eventName = UdonGraphExtensions.GetVariableChangeEventName(variableName); + if (IsDuplicateEventNode(eventName, selectedUid)) + { + return null; + } + } + } + + var definition = UdonEditorManager.Instance.GetNodeDefinition(definitionName); + var nodeData = this.graphData.AddNode(definition.fullName); + nodeData.nodeValues = new SerializableObjectContainer[2]; + nodeData.nodeUIDs = new string[1]; + nodeData.nodeValues[0] = SerializableObjectContainer.Serialize(selectedUid); + nodeData.position = graphMousePosition; + + Undo.RecordObject(graphProgramAsset, "Add Variable"); + var udonNode = UdonNode.CreateNode(nodeData, this); + return udonNode; + } + + public string GetUnusedVariableNameLike(string newVariableName) + { + RefreshVariables(false); + + while (GetVariableNames.Contains(newVariableName)) + { + char lastChar = newVariableName[newVariableName.Length - 1]; + if(char.IsDigit(lastChar)) + { + string newLastChar = (int.Parse(lastChar.ToString()) + 1).ToString(); + newVariableName = newVariableName.Substring(0, newVariableName.Length - 1) + newLastChar; + } + else + { + newVariableName = $"{newVariableName}_1"; + } + } + + return newVariableName; + } + + private void SetMouseTip(string message) + { + if (mouseTipContainer.visible) + { + mouseTip.text = message; + } + } + + private void LinkAfterCompile(string variableName, object target) + { + UdonAssemblyProgramAsset.AssembleDelegate listener = null; + listener = (success, assembly) => + { + if (!success) return; + + //TODO: get actual variable name in case it was auto-changed on add + var result = udonBehaviour.publicVariables.TrySetVariableValue(variableName, target); + if (result) + { + graphProgramAsset.OnAssemble -= listener; + } + }; + + graphProgramAsset.OnAssemble += listener; + EditorUtility.SetDirty(graphProgramAsset); + AssetDatabase.SaveAssets(); + graphProgramAsset.RefreshProgram(); + } + + private string GetDefinitionNameForType(Type t) + { + string variableType = $"Variable_{t}".SanitizeVariableName(); + variableType = variableType.Replace("UdonBehaviour", "CommonInterfacesIUdonEventReceiver"); + return variableType; + } + + private void SetupDraggedObject(UnityEngine.Object o, Vector2 graphMousePosition) + { + // Ensure variable type is allowed + + // create new Component variable and add to graph + string variableType = GetDefinitionNameForType(o.GetType()); + string variableName = GetUnusedVariableNameLike(o.name.SanitizeVariableName()); + + SetMouseTip($"Made {variableName}"); + + string uid = AddNewVariable(variableType, variableName, true); + RefreshVariables(false); + + object target = o; + // Cast component to expected type + if (o is Component) target = Convert.ChangeType(o, o.GetType()); + var variableNode = MakeVariableNode(uid, graphMousePosition, UdonGraph.VariableNodeType.Getter); + AddElement(variableNode); + + LinkAfterCompile(variableName, target); + } + + [Serializable] + public class ViewTransformData + { + public Vector2 position = Vector2.zero; + public float scale = 1f; + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs.meta new file mode 100644 index 00000000..62ff2be8 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9214873dab0ea8a4b91861cd5a04dae3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs new file mode 100644 index 00000000..2a226b20 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs @@ -0,0 +1,29 @@ +using System; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram +{ + [Serializable] + public class UdonGraphElementData + { + public UdonGraphElementType type; + public string uid; + public string jsonData; + + public UdonGraphElementData(UdonGraphElementType type, string uid, string jsonData) + { + this.type = type; + this.jsonData = jsonData; + this.uid = uid; + } + } + + public enum UdonGraphElementType + { + GraphElement, + UdonStackNode, + UdonGroup, + UdonComment, + Minimap, + VariablesWindow, + }; +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs.meta new file mode 100644 index 00000000..23bdc696 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f166d8f1c152ef34899019ab9a4fd0f2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs new file mode 100644 index 00000000..6c0a1847 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs @@ -0,0 +1,144 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEngine.UIElements; +using UnityEditor.UIElements; +#else +using UnityEngine.Experimental.UIElements; +using UnityEditor.Experimental.UIElements; +#endif +using UnityEditor; +using UnityEngine; + + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonGraphStatus : ToolbarToggle + { + private VisualElement _detailsContainer; + private UdonProgramSourceView _programSourceView; + private UdonGraphProgramAsset _graphAsset; + private TextElement _label; + + public UdonGraphStatus(VisualElement detailsContainer) + { + _detailsContainer = detailsContainer; + + _label = new TextElement() {text = "-", name = "Content"}; + _label.StretchToParentSize(); + Add(_label); +#if UNITY_2019_3_OR_NEWER + this.RegisterValueChangedCallback(ShowGraphAssetDetails); +#else + OnValueChanged(ShowGraphAssetDetails); +#endif + + _programSourceView = new UdonProgramSourceView(); + } + + private void ShowGraphAssetDetails(ChangeEvent<bool> changeEvent) + { + // Just remove it + if (!changeEvent.newValue) + { + RemoveIfContaining(_programSourceView); + return; + } + + // else we need to make a new one and show it + if (_graphAsset == null) + { + Debug.LogError("Can't show Asset Details for a null asset."); + return; + } + + RemoveIfContaining(_programSourceView); + _detailsContainer.Add(_programSourceView); + _programSourceView.BringToFront(); + } + + private void RemoveIfContaining(VisualElement element) + { + if (_detailsContainer.Contains(element)) + { + _detailsContainer.Remove(element); + } + } + + public void OnAssemble(bool success, string assembly) + { + if (!enabledInHierarchy || _label == null) return; + + string newText; + Color flashColor; + Color targetColor; + + // change visuals based on success + if (success) + { + newText = "OK"; + flashColor = new Color(0, 1, 0); + targetColor = new Color(0.1f, 0.25f, 0.1f, alpha); + } + else + { + newText = "!"; + flashColor = new Color(1, 0, 0); + targetColor = new Color(0.25f, 0.1f, 0.1f, alpha); + } + + // Update visuals + _label.text = newText; + _label.style.backgroundColor = flashColor; + LerpColor(targetColor); + _programSourceView.SetText(assembly); + } + + private Color targetColor; + private Color startColor; + private float lerpAmount = 0f; + private float alpha = 0.75f; + + private void LerpColor(Color newColor) + { + startColor = _label.style.backgroundColor.value; + targetColor = newColor; + lerpAmount = 0f; + EditorApplication.update += UpdateColor; + } + + private void UpdateColor() + { + lerpAmount += 0.01f; + + if (lerpAmount >= 1) + { + EditorApplication.update -= UpdateColor; + } + + if (_label != null) + { + _label.style.backgroundColor = Color.Lerp(startColor, targetColor, lerpAmount); + } + } + + public override void Blur() + { + EditorApplication.update -= UpdateColor; + } + + ~UdonGraphStatus() + { + EditorApplication.update -= UpdateColor; + if (_graphAsset != null) + { + _graphAsset.OnAssemble -= OnAssemble; + } + } + + internal void LoadAsset(UdonGraphProgramAsset asset) + { + _graphAsset = asset; + _graphAsset.OnAssemble += OnAssemble; + _programSourceView.LoadAsset(asset); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs.meta new file mode 100644 index 00000000..4d7c23c0 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54dd824c6c614b94183d92710efe4f5f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs new file mode 100644 index 00000000..3bce7881 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs @@ -0,0 +1,58 @@ +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public static class Settings + { + private static string UseNeonStyleString = "UdonGraphViewSettings.UseNeonStyle"; + private static string LastGraphGuidString = "UdonGraphViewSettings.LastGraphGuid"; + private static string LastUdonBehaviourPathString = "UdonGraphViewSettings.LastUdonBehaviourPath"; + private static string LastUdonBehaviourScenePathString = "UdonGraphViewSettings.LastUdonBehaviourScenePath"; + private static string SearchOnSelectedNodeRegistryString = "UdonGraphViewSettings.SearchOnSelectedNodeRegistry"; + private static string GridSnapSizeString = "UdonGraphViewSettings.GridSnapSize"; + private static string SearchOnNoodleDropString = "UdonGraphViewSettings.SearchOnNoodleDrop"; + + public static bool UseNeonStyle + { + get { return PlayerPrefs.GetInt(UseNeonStyleString, 0) == 1; } + set { PlayerPrefs.SetInt(UseNeonStyleString, value ? 1 : 0); } + } + + public static string LastGraphGuid + { + get { return PlayerPrefs.GetString(LastGraphGuidString, ""); } + set { PlayerPrefs.SetString(LastGraphGuidString, value); } + } + + public static string LastUdonBehaviourPath + { + get { return PlayerPrefs.GetString(LastUdonBehaviourPathString, ""); } + set { PlayerPrefs.SetString(LastUdonBehaviourPathString, value); } + } + + public static string LastUdonBehaviourScenePath + { + get { return PlayerPrefs.GetString(LastUdonBehaviourScenePathString, ""); } + set { PlayerPrefs.SetString(LastUdonBehaviourScenePathString, value); } + } + + public static bool SearchOnSelectedNodeRegistry + { + get { return PlayerPrefs.GetInt(SearchOnSelectedNodeRegistryString, 1) == 1; } + set { PlayerPrefs.SetInt(SearchOnSelectedNodeRegistryString, value ? 1 : 0); } + } + + public static int GridSnapSize + { + get { return PlayerPrefs.GetInt(GridSnapSizeString, 0); } + set { PlayerPrefs.SetInt(GridSnapSizeString, value); } + } + + public static bool SearchOnNoodleDrop + { + get { return PlayerPrefs.GetInt(SearchOnNoodleDropString, 1) == 1; } + set { PlayerPrefs.SetInt(SearchOnNoodleDropString, value ? 1 : 0); } + } + + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs.meta new file mode 100644 index 00000000..1b726b27 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87e2044d3bcb715499ac68cc7380a9ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs new file mode 100644 index 00000000..70b655c3 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs @@ -0,0 +1,324 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEngine.UIElements; +using UnityEditor.UIElements; +#else +using UnityEngine.Experimental.UIElements; +using UnityEditor.Experimental.UIElements; +using UnityEngine.Experimental.UIElements.StyleEnums; +#endif +using System.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using VRC.Udon.Graph; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonGraphWindow : EditorWindow + { + private VisualElement _rootView; + + // Reference to actual Graph View + [SerializeField] private UdonGraph _graphView; + + private UdonGraphProgramAsset _graphAsset; + private UdonWelcomeView _welcomeView; + private VisualElement _curtain; + + // Toolbar and Buttons + private Toolbar _toolbar; + private Label _graphAssetName; + private ToolbarMenu _toolbarOptions; + private UdonGraphStatus _graphStatus; + private ToolbarButton _graphReload; + private ToolbarButton _graphCompile; + private VisualElement _updateOrderField; + private IntegerField _updateOrderIntField; + + [MenuItem("VRChat SDK/Udon Graph")] + private static void ShowWindow() + { + // Get or focus the window + var window = GetWindow<UdonGraphWindow>("Udon Graph", true, typeof(SceneView)); + window.titleContent = new GUIContent("Udon Graph"); + } + + private void LogPlayModeState(PlayModeStateChange state) + { + switch (state) + { + case PlayModeStateChange.EnteredEditMode: + if (_rootView.Contains(_curtain)) + { + _curtain.RemoveFromHierarchy(); + } + + break; + case PlayModeStateChange.ExitingEditMode: + break; + case PlayModeStateChange.EnteredPlayMode: + _rootView.Add(_curtain); + break; + case PlayModeStateChange.ExitingPlayMode: + break; + default: + break; + } + } + + private void OnEnable() + { + EditorApplication.playModeStateChanged += LogPlayModeState; + + InitializeRootView(); + + _curtain = new VisualElement() + { + name = "curtain", + }; + _curtain.Add(new Label("Graph Locked in Play Mode")); + + _welcomeView = new UdonWelcomeView(); + _welcomeView.StretchToParentSize(); + _rootView.Add(_welcomeView); + + SetupToolbar(); + + Undo.undoRedoPerformed -= + OnUndoRedo; //Remove old handler if present to prevent duplicates, doesn't cause errors if not present + Undo.undoRedoPerformed += OnUndoRedo; + + if (_graphAsset != null) + { + UdonBehaviour udonBehaviour = null; + string gPath = Settings.LastUdonBehaviourPath; + string sPath = Settings.LastUdonBehaviourScenePath; + if (!string.IsNullOrEmpty(gPath) && !string.IsNullOrEmpty(sPath)) + { + var targetScene = EditorSceneManager.GetSceneByPath(sPath); + if (targetScene != null && targetScene.isLoaded && targetScene.IsValid()) + { + var targetObject = GameObject.Find(gPath); + if (targetObject != null) + { + udonBehaviour = targetObject.GetComponent<UdonBehaviour>(); + } + } + } + + InitializeGraph(_graphAsset, udonBehaviour); + } + } + + private void InitializeRootView() + { +#if UNITY_2019_3_OR_NEWER + _rootView = rootVisualElement; + _rootView.styleSheets.Add((StyleSheet) Resources.Load("UdonGraphStyle")); +#else + _rootView = this.GetRootVisualContainer(); + _rootView.AddStyleSheetPath("UdonGraphStyle2018"); +#endif + } + + public void InitializeGraph(UdonGraphProgramAsset graph, UdonBehaviour udonBehaviour = null) + { + this._graphAsset = graph; + + InitializeWindow(); + + _graphView = _rootView.Children().FirstOrDefault(e => e is UdonGraph) as UdonGraph; + if (_graphView == null) + { + Debug.LogError("GraphView has not been added to the BaseGraph root view!"); + return; + } + + _graphView.Initialize(graph, udonBehaviour); + + _graphStatus.LoadAsset(graph); + // Store GUID for this asset to settings for easy reload later + if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(graph, out string guid, out long localId)) + { + Settings.LastGraphGuid = guid; + } + + if (udonBehaviour != null) + { + Settings.LastUdonBehaviourPath = udonBehaviour.transform.GetHierarchyPath(); + Settings.LastUdonBehaviourScenePath = udonBehaviour.gameObject.scene.path; + } + + _graphAssetName.text = graph.name; + ShowGraphTools(true); + } + + private void InitializeWindow() + { + if (_graphView == null) + { + _graphView = new UdonGraph(this); + // we could add the toolbar in here + } + + RemoveIfContaining(_welcomeView); + RemoveIfContaining(_graphView); + _rootView.Insert(0, _graphView); + } + + private void ReloadWelcome() + { + RemoveIfContaining(_welcomeView); + _rootView.Add(_welcomeView); + ShowGraphTools(false); + } + + // TODO: maybe move this to GraphView since it's so connected? + private void SetupToolbar() + { + _toolbar = new Toolbar() + { + name = "UdonToolbar", + }; + _rootView.Add(_toolbar); + + _toolbar.Add(new ToolbarButton(() => { ReloadWelcome(); }) + {text = "Welcome"}); + + _graphAssetName = new Label() + { + name = "assetName", + }; + _toolbar.Add(_graphAssetName); + +#if UNITY_2019_3_OR_NEWER + _toolbar.Add(new ToolbarSpacer() + { + flex = true, + }); +#else + _toolbar.Add(new ToolbarFlexSpacer()); +#endif + _updateOrderField = new VisualElement() + { + visible = false, + }; + _updateOrderField.Add(new Label("UpdateOrder")); + _updateOrderIntField = new IntegerField() + { + name = "UpdateOrderIntegerField", + value = (_graphAsset == null) ? 0 : _graphAsset.graphData.updateOrder, + }; +#if UNITY_2019_3_OR_NEWER + _updateOrderIntField.RegisterValueChangedCallback((e) => +#else + _updateOrderIntField.OnValueChanged(e => +#endif + { + _graphView.graphProgramAsset.graphData.updateOrder = e.newValue; + _updateOrderField.visible = false; + }); + _updateOrderIntField.isDelayed = true; + _updateOrderField.Add(_updateOrderIntField); + _toolbar.Add(_updateOrderField); + + _toolbarOptions = new ToolbarMenu + { + text = "Settings" + }; + // Show Variables Window + _toolbarOptions.menu.AppendAction("Show Variables", + (m) => { _graphView.ToggleShowVariables(!_graphView.GetBlackboardVisible()); }, + (s) => { return BoolToStatus(_graphView.GetBlackboardVisible()); }); + // Show Minimap + _toolbarOptions.menu.AppendAction("Show MiniMap", + (m) => { _graphView.ToggleShowMiniMap(!_graphView.GetMinimapVisible()); }, + (s) => { return BoolToStatus(_graphView.GetMinimapVisible()); }); + _toolbarOptions.menu.AppendSeparator(); + // Show Update Order + _toolbarOptions.menu.AppendAction("Show UpdateOrder", (m) => + { +#if UNITY_2019_3_OR_NEWER + _updateOrderField.visible = !(m.status == DropdownMenuAction.Status.Checked); +#else + _updateOrderField.visible = !(m.status == DropdownMenu.MenuAction.StatusFlags.Checked); +#endif + if (_updateOrderField.visible) + { + _updateOrderIntField.value = _graphAsset.graphData.updateOrder; + } + + _updateOrderIntField.Focus(); + _updateOrderIntField.SelectAll(); + }, (s) => { return BoolToStatus(_updateOrderField.visible); }); + // Search On Noodle Drop + _toolbarOptions.menu.AppendAction("Search on Noodle Drop", + (m) => { Settings.SearchOnNoodleDrop = !Settings.SearchOnNoodleDrop; }, + (s) => { return BoolToStatus(Settings.SearchOnNoodleDrop); }); + // Search On Selected Node + _toolbarOptions.menu.AppendAction("Search on Selected Node", + (m) => { Settings.SearchOnSelectedNodeRegistry = !Settings.SearchOnSelectedNodeRegistry; }, + (s) => { return BoolToStatus(Settings.SearchOnSelectedNodeRegistry); }); + _toolbar.Add(_toolbarOptions); + + _graphCompile = new ToolbarButton(() => + { + if (_graphAsset != null && _graphAsset is AbstractUdonProgramSource udonProgramSource) + { + UdonEditorManager.Instance.QueueAndRefreshProgram(udonProgramSource); + } + }) + {text = "Compile"}; + _toolbar.Add(_graphCompile); + + _graphReload = new ToolbarButton(() => { _graphView.Reload(); }) + {text = "Reload"}; + _toolbar.Add(_graphReload); + + _graphStatus = new UdonGraphStatus(_rootView); + _toolbar.Add(_graphStatus); + + ShowGraphTools(false); + } + +#if UNITY_2019_3_OR_NEWER + private DropdownMenuAction.Status BoolToStatus(bool value) + { + return value ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal; + } +#else + private DropdownMenu.MenuAction.StatusFlags BoolToStatus(bool value) + { + return value ? DropdownMenu.MenuAction.StatusFlags.Checked : DropdownMenu.MenuAction.StatusFlags.Normal; + } +#endif + + public void ShowGraphTools(bool value) + { + _graphAssetName.style.visibility = value ? Visibility.Visible : Visibility.Hidden; + _toolbarOptions.style.visibility = value ? Visibility.Visible : Visibility.Hidden; + _graphCompile.style.visibility = value ? Visibility.Visible : Visibility.Hidden; + _graphReload.style.visibility = value ? Visibility.Visible : Visibility.Hidden; + _graphStatus.style.visibility = value ? Visibility.Visible : Visibility.Hidden; + } + + private void RemoveIfContaining(VisualElement element) + { + if (_rootView.Contains(element)) + { + _rootView.Remove(element); + } + } + + private void OnUndoRedo() + { + Repaint(); + } + + public UdonGraphData GetGraphDataFromAsset(UdonGraphProgramAsset asset) + { + InitializeGraph(asset); + return _graphView.graphData; + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs.meta new file mode 100644 index 00000000..d20bdf69 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c6f017dc2674fec4da54a57b2655a948 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs new file mode 100644 index 00000000..5fed928b --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs @@ -0,0 +1,58 @@ +#if UNITY_2019_3_OR_NEWER +using EditorUI = UnityEditor.UIElements; +using UnityEngine.UIElements; +using UnityEditor.Experimental.GraphView; +using MenuAction = UnityEngine.UIElements.DropdownMenuAction; +#else +using EditorUI = UnityEditor.Experimental.UIElements; +using UnityEngine.Experimental.UIElements; +using UnityEditor.Experimental.UIElements.GraphView; +using MenuAction = UnityEngine.Experimental.UIElements.DropdownMenu.MenuAction; +#endif +using UnityEditor; +using UnityEngine; +using VRC.Udon.Graph; +using VRC.Udon.Serialization; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonParameterField : BlackboardField + { + private UdonGraph udonGraph; + private UdonNodeData nodeData; + public UdonNodeData Data => nodeData; + + public UdonParameterField(UdonGraph udonGraph, UdonNodeData nodeData) + { + this.udonGraph = udonGraph; + this.nodeData = nodeData; + + // Get Definition or exit early + UdonNodeDefinition definition = UdonEditorManager.Instance.GetNodeDefinition(nodeData.fullName); + if (definition == null) + { + Debug.LogWarning($"Couldn't create Parameter Field for {nodeData.fullName}"); + return; + } + + this.text = (string) nodeData.nodeValues[(int) UdonParameterProperty.ValueIndices.name].Deserialize(); + this.typeText = UdonGraphExtensions.PrettyString(definition.name).FriendlyNameify(); + + this.AddManipulator(new ContextualMenuManipulator(BuildContextualMenu)); + + this.Q("icon").AddToClassList("parameter-" + definition.type); + this.Q("icon").visible = true; + + var textField = (TextField) this.Q("textField"); + textField.isDelayed = true; + } + + void BuildContextualMenu(ContextualMenuPopulateEvent evt) + { + evt.menu.AppendAction("Rename", (a) => OpenTextEditor(), MenuAction.AlwaysEnabled); + evt.menu.AppendAction("Delete", (a) => udonGraph.RemoveNodeAndData(nodeData), MenuAction.AlwaysEnabled); + + evt.StopPropagation(); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs.meta new file mode 100644 index 00000000..56cdf576 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5dcd92112af21784ba5bf6383abab768 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs new file mode 100644 index 00000000..aa2b82b0 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs @@ -0,0 +1,224 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEngine.UIElements; +using EditorUI = UnityEditor.UIElements; +using EngineUI = UnityEngine.UIElements; +#else +using UnityEngine.Experimental.UIElements; +using EditorUI = UnityEditor.Experimental.UIElements; +using EngineUI = UnityEngine.Experimental.UIElements; +#endif +using System.Collections.Generic; +using UnityEngine; +using VRC.Udon.Graph; +using VRC.Udon.Serialization; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonParameterProperty : VisualElement + { + protected UdonGraph graph; + protected UdonNodeData nodeData; + protected UdonNodeDefinition definition; + + //public ExposedParameter parameter { get; private set; } + + public Toggle isPublic { get; private set; } + public Toggle isSynced { get; private set; } + public VisualElement defaultValueContainer { get; private set; } + public EditorUI.PopupField<string> syncField { get; private set; } + private VisualElement _inputField; + + public enum ValueIndices + { + value = 0, + name = 1, + isPublic = 2, + isSynced = 3, + syncType = 4 + } + + private static SerializableObjectContainer[] GetDefaultNodeValues() + { + return new[] + { + SerializableObjectContainer.Serialize("", typeof(string)), + SerializableObjectContainer.Serialize("newVariableName", typeof(string)), + SerializableObjectContainer.Serialize(false, typeof(bool)), + SerializableObjectContainer.Serialize(false, typeof(bool)), + SerializableObjectContainer.Serialize("none", typeof(string)) + }; + } + + // 0 = Value, 1 = name, 2 = public, 3 = synced, 4 = syncType + public UdonParameterProperty(UdonGraph graphView, UdonNodeDefinition definition, UdonNodeData nodeData) + { + this.graph = graphView; + this.definition = definition; + this.nodeData = nodeData; + + string friendlyName = UdonGraphExtensions.FriendlyTypeName(definition.type).FriendlyNameify(); + + // Public Toggle + isPublic = new Toggle + { + text = "public", + value = (bool) GetValue(ValueIndices.isPublic) + }; +#if UNITY_2019_3_OR_NEWER + isPublic.RegisterValueChangedCallback( +#else + isPublic.OnValueChanged( +#endif + e => { SetNewValue(e.newValue, ValueIndices.isPublic); }); + Add(isPublic); + + if(UdonNetworkTypes.CanSync(definition.type)) + { + // Is Synced Field + isSynced = new Toggle + { + text = "synced", + value = (bool)GetValue(ValueIndices.isSynced), + name = "syncToggle", + }; + +#if UNITY_2019_3_OR_NEWER + isSynced.RegisterValueChangedCallback( +#else + isSynced.OnValueChanged( +#endif + e => + { + SetNewValue(e.newValue, ValueIndices.isSynced); + syncField.visible = e.newValue; + }); + + // Sync Field, add to isSynced + List<string> choices = new List<string>() + { + "none" + }; + + if(UdonNetworkTypes.CanSyncLinear(definition.type)) + { + choices.Add("linear"); + } + + if(UdonNetworkTypes.CanSyncSmooth(definition.type)) + { + choices.Add("smooth"); + } + + syncField = new EditorUI.PopupField<string>(choices, 0) + { + visible = isSynced.value, + }; + syncField.Insert(0, new Label("smooth:")); + +#if UNITY_2019_3_OR_NEWER + syncField.RegisterValueChangedCallback( +#else + syncField.OnValueChanged( +#endif + e => + { + SetNewValue(e.newValue, ValueIndices.syncType); + }); + + // Only show sync smoothing dropdown if there are choices to be made + if (choices.Count > 1) + { + isSynced.Add(syncField); + } + + Add(isSynced); + } + else + { + // Cannot Sync + SetNewValue(false, ValueIndices.isSynced); + Add(new Label($"{friendlyName} cannot be synced.")); + } + + // Container to show/edit Default Value + defaultValueContainer = new VisualElement(); + defaultValueContainer.Add( + new Label("default value") {name = "default-value-label"}); + + // Generate Default Value Field + var value = TryGetValueObject(out object result); + _inputField = UdonFieldFactory.CreateField( + definition.type, + result, + newValue => SetNewValue(newValue, ValueIndices.value) + ); + if (_inputField != null) + { + defaultValueContainer.Add(_inputField); + Add(defaultValueContainer); + } + } + + private object GetValue(ValueIndices index) + { + if ((int) index >= nodeData.nodeValues.Length) + { + Debug.LogWarning($"Can't get {index} from {definition.name} variable"); + return null; + } + + return nodeData.nodeValues[(int) index].Deserialize(); + } + + private bool TryGetValueObject(out object result) + { + result = null; + + var container = nodeData.nodeValues[0]; + if (container == null) + { + return false; + } + + result = container.Deserialize(); + if (result == null) + { + return false; + } + + return true; + } + + private void SetNewValue(object newValue, ValueIndices index) + { + nodeData.nodeValues[(int) index] = SerializableObjectContainer.Serialize(newValue); + } + + // Convenience wrapper for field types that don't need special initialization + private VisualElement SetupField<TField, TType>() + where TField : VisualElement, INotifyValueChanged<TType>, new() + { + var field = new TField(); + return SetupField<TField, TType>(field); + } + + // Works for any TextValueField types, needs to know fieldType and object type + private VisualElement SetupField<TField, TType>(TField field) + where TField : VisualElement, INotifyValueChanged<TType> + { + field.AddToClassList("portField"); + if (TryGetValueObject(out object result)) + { + field.value = (TType) result; + } +#if UNITY_2019_3_OR_NEWER + field.RegisterValueChangedCallback( +#else + field.OnValueChanged( +#endif + (e) => SetNewValue(e.newValue, ValueIndices.value)); + _inputField = field; + return _inputField; + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs.meta new file mode 100644 index 00000000..0a095238 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 70616b8b964e3664780fc03f65f27f4f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs new file mode 100644 index 00000000..a0e755ae --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs @@ -0,0 +1,60 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEngine.UIElements; +#else +using UnityEngine.Experimental.UIElements; +#endif + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonProgramSourceView : VisualElement + { + private UdonProgramAsset _asset; + + private VisualElement _assemblyContainer; + private ScrollView _scrollView; + private Label _assemblyHeader; + private TextElement _assemblyField; + + public UdonProgramSourceView () + { + // Create and add container and children to display latest Assembly + _assemblyContainer = new VisualElement() { name = "Container", }; + _assemblyHeader = new Label("Udon Assembly") + { + name = "Header", + }; + + _scrollView = new ScrollView(); + + _assemblyField = new TextElement() + { + name = "AssemblyField", + }; + + _assemblyContainer.Add(_assemblyHeader); + _assemblyContainer.Add(_scrollView); + _assemblyContainer.Add(_assemblyField); + _scrollView.contentContainer.Add(_assemblyField); + + Add(_assemblyContainer); + } + + public void LoadAsset(UdonGraphProgramAsset asset) + { + _asset = asset; + } + + public void SetText(string newValue) + { + _assemblyField.text = newValue; + } + + public void Unload() + { + if(_asset != null) + { + _asset = null; + } + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs.meta new file mode 100644 index 00000000..e8027811 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fddc146e8502d7b49a294b6264d66dfd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs new file mode 100644 index 00000000..f29ad0fc --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs @@ -0,0 +1,212 @@ +#if UNITY_2019_3_OR_NEWER +using UnityEditor.UIElements; +using UnityEngine.UIElements; +#else +using UnityEditor.Experimental.UIElements; +using UnityEngine.Experimental.UIElements; +#endif +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class UdonWelcomeView : VisualElement + { + private Button _openLastGraphButton; + + public UdonWelcomeView() + { + name = "udon-welcome"; + this.RegisterCallback<AttachToPanelEvent>(Initialize); + } + + + private void Initialize(AttachToPanelEvent evt) + { + // switch event to do some UI updates instead of initialization from here on out + UnregisterCallback<AttachToPanelEvent>(Initialize); + // this.RegisterCallback<AttachToPanelEvent>(OnAttach); + + // Add Header + Add(new TextElement() + { + name = "intro", + text = "Udon Graph", + }); + + Add(new TextElement() + { + name = "header-message", + text = + "The Udon Graph is your gateway to creating amazing things in VRChat.\nCheck out the Readme and UdonExampleScene in the VRChat Examples folder to get started." + }); + + var mainContainer = new VisualElement() + { + name = "main", + }; + + Add(mainContainer); + + var template = EditorGUIUtility.Load("Assets/Udon/Editor/Resources/UdonChangelog.uxml") as VisualTreeAsset; + #if UNITY_2019_3_OR_NEWER + var changelog = template.CloneTree((string) null); + #else + var changelog = template.CloneTree(null); + #endif + changelog.name = "changelog"; + mainContainer.Add(changelog); + + var column2 = new VisualElement() {name = "column-2"}; + mainContainer.Add(column2); + + // Add Button for Last Graph + if (!string.IsNullOrEmpty(Settings.LastGraphGuid)) + { + _openLastGraphButton = new Button(() => + { + var assetPath = AssetDatabase.GUIDToAssetPath(Settings.LastGraphGuid); + var graphName = assetPath.Substring(assetPath.LastIndexOf("/") + 1).Replace(".asset", ""); + + // Find actual asset from guid + var asset = AssetDatabase.LoadAssetAtPath<UdonGraphProgramAsset>(assetPath); + if (asset != null) + { + var w = EditorWindow.GetWindow<UdonGraphWindow>("Udon Graph", true, typeof(SceneView)); + // get reference to saved UdonBehaviour if possible + UdonBehaviour udonBehaviour = null; + string gPath = Settings.LastUdonBehaviourPath; + string sPath = Settings.LastUdonBehaviourScenePath; + if (!string.IsNullOrEmpty(gPath) && !string.IsNullOrEmpty(sPath)) + { + var targetScene = EditorSceneManager.GetSceneByPath(sPath); + if (targetScene != null && targetScene.isLoaded && targetScene.IsValid()) + { + var targetObject = GameObject.Find(gPath); + if (targetObject != null) + { + udonBehaviour = targetObject.GetComponent<UdonBehaviour>(); + } + } + } + + // Initialize graph with restored udonBehaviour or null if not found / not saved + w.InitializeGraph(asset, udonBehaviour); + } + }); + + UpdateLastGraphButtonLabel(); + column2.Add(_openLastGraphButton); + } + + var settingsTemplate = + EditorGUIUtility.Load("Assets/Udon/Editor/Resources/UdonSettings.uxml") as VisualTreeAsset; + #if UNITY_2019_3_OR_NEWER + var settings = settingsTemplate.CloneTree((string)null); + #else + var settings = settingsTemplate.CloneTree(null); + #endif + settings.name = "settings"; + column2.Add(settings); + + // get reference to first settings section + var section = settings.Q("section"); + + // Add Grid Snap setting + var gridSnapContainer = new VisualElement(); + gridSnapContainer.AddToClassList("settings-item-container"); + var gridSnapField = new IntegerField(3) + { + value = Settings.GridSnapSize + }; +#if UNITY_2019_3_OR_NEWER + gridSnapField.RegisterValueChangedCallback( +#else + gridSnapField.OnValueChanged( +#endif + e => { Settings.GridSnapSize = e.newValue; }); + gridSnapContainer.Add(new Label("Grid Snap Size")); + gridSnapContainer.Add(gridSnapField); + section.Add(gridSnapContainer); + var gridSnapLabel = new Label("Snap elements to a grid as you move them. 0 for No Snapping."); + gridSnapLabel.AddToClassList("settings-label"); + section.Add(gridSnapLabel); + + // Add Search On Selected Node settings + var searchOnSelectedNode = (new Toggle() + { + text = "Focus Search On Selected Node", + value = Settings.SearchOnSelectedNodeRegistry, + }); +#if UNITY_2019_3_OR_NEWER + searchOnSelectedNode.RegisterValueChangedCallback( +#else + searchOnSelectedNode.OnValueChanged( +#endif + (toggleEvent) => { Settings.SearchOnSelectedNodeRegistry = toggleEvent.newValue; }); + section.Add(searchOnSelectedNode); + var searchOnLabel = + new Label( + "Highlight a node and press Spacebar to open a Search Window focused on nodes for that type. "); + searchOnLabel.AddToClassList("settings-label"); + section.Add(searchOnLabel); + + // Add Search On Noodle Drop settings + var searchOnNoodleDrop = (new Toggle() + { + text = "Search On Noodle Drop", + value = Settings.SearchOnNoodleDrop, + }); +#if UNITY_2019_3_OR_NEWER + searchOnNoodleDrop.RegisterValueChangedCallback( +#else + searchOnNoodleDrop.OnValueChanged( +#endif +(toggleEvent) => { Settings.SearchOnNoodleDrop = toggleEvent.newValue; }); + section.Add(searchOnNoodleDrop); + var searchOnDropLabel = + new Label("Drop a noodle into empty space to search for anything that can be connected."); + searchOnDropLabel.AddToClassList("settings-label"); + section.Add(searchOnDropLabel); + + // Add UseNeonStyle setting + var useNeonStyle = (new Toggle() + { + text = "Use Neon Style", + value = Settings.UseNeonStyle, + }); +#if UNITY_2019_3_OR_NEWER + useNeonStyle.RegisterValueChangedCallback( +#else + useNeonStyle.OnValueChanged( +#endif + (toggleEvent) => { Settings.UseNeonStyle = toggleEvent.newValue; }); + section.Add(useNeonStyle); + var useNeonStyleLabel = + new Label("Try out an experimental Neon Style. We will support User Styles in an upcoming version."); + useNeonStyleLabel.AddToClassList("settings-label"); + section.Add(useNeonStyleLabel); + } + + private void UpdateLastGraphButtonLabel() + { + if (_openLastGraphButton == null) return; + + string currentButtonAssetGuid = (string) _openLastGraphButton.userData; + if (string.Compare(currentButtonAssetGuid, Settings.LastGraphGuid) != 0) + { + var assetPath = AssetDatabase.GUIDToAssetPath(Settings.LastGraphGuid); + var graphName = assetPath.Substring(assetPath.LastIndexOf("/") + 1).Replace(".asset", ""); + + _openLastGraphButton.userData = Settings.LastGraphGuid; + _openLastGraphButton.text = $"Open {graphName}"; + } + } + + private void OnAttach(AttachToPanelEvent evt) + { + UpdateLastGraphButtonLabel(); + } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs.meta new file mode 100644 index 00000000..ab3bc509 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e5786fc577943ae45953c6f54c97116b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs new file mode 100644 index 00000000..9e04df93 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs @@ -0,0 +1,200 @@ + +using System.Collections.Generic; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.Video; +#if UNITY_2019_3_OR_NEWER +using UnityEngine.UIElements; +using UnityEditor.UIElements; +#else +using UnityEngine.Experimental.UIElements; +using UnityEditor.Experimental.UIElements; +#endif + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView +{ + public class VideoPlayerElement : VisualElement + { + private VideoPlayer _player; + private Scene _tempScene; + private Toolbar _toolbar; + private TextElement _header; + private Image _videoProxy; + + public VideoPlayerElement() + { + // Constructing Items + _header = new TextElement() + { + text = "Using Focused Search", + name = "header", + }; + + _videoProxy = new Image() + { + name = "video-proxy", + image = Resources.Load<Texture2D>("videoStill"), + scaleMode = ScaleMode.ScaleToFit, + }; + + _toolbar = new Toolbar(); + _toolbar.Add(new ToolbarButton(Play) {text = "Play", name = "button-play"}); + _toolbar.Add(new ToolbarButton(Pause) {text = "Pause", name = "button-pause"}); + + // Adding Items + Add(_header); + Add(_videoProxy); + Add(_toolbar); + } + + private void OnEnable() + { + ShowFrame(); + RegisterCallback<MouseDownEvent>(OnMouseDown); + } + + private void OnDisable() + { + UnregisterCallback<MouseDownEvent>(OnMouseDown); + if (_tempScene != null && _tempScene.IsValid()) + { + EditorSceneManager.UnloadSceneAsync(_tempScene); + } + } + + private void OnMouseDown(MouseDownEvent evt) + { + PlayPauseToggle(); + } + + public void PlayPauseToggle() + { + if (GetCurrentPlayer().isPlaying) + { + Pause(); + } + else if (GetCurrentPlayer().isPaused) + { + Play(); + } + } + + public void Play() + { + GetCurrentPlayer().Play(); + } + + public void Pause() + { + GetCurrentPlayer().Pause(); + } + + public void LoadVideo(string url) + { + var player = GetCurrentPlayer(); + player.url = url; + player.sendFrameReadyEvents = true; + player.frameReady += OnFrameReady; + player.isLooping = true; + ShowFrame(); + } + + public void ShowFrame() + { + var player = GetCurrentPlayer(); + if (player == null || string.IsNullOrEmpty(player.url)) + { + return; + } + + player.frameReady += PauseOnNextFrame; + player.Play(); + } + + public void UnloadVideo() + { + var player = GetCurrentPlayer(); + player.url = null; + player.sendFrameReadyEvents = false; + player.frameReady -= OnFrameReady; + } + + private void OnFrameReady(VideoPlayer source, long frameIdx) + { + _videoProxy.image = source.texture; + MarkDirtyRepaint(); + } + + private void PauseOnNextFrame(VideoPlayer source, long frameIdx) + { + var player = GetCurrentPlayer(); + player.frameReady -= PauseOnNextFrame; + player.Pause(); + } + + private Scene GetTempScene() + { + if (_tempScene == null) + { + _tempScene = EditorSceneManager.NewPreviewScene(); + var root = new GameObject("VideoPlayer"); + EditorSceneManager.MoveGameObjectToScene(root, _tempScene); + } + + return _tempScene; + } + + private VideoPlayer GetCurrentPlayer() + { + // Try to get player from scene if it's not cached + if (_player == null) + { + _player = GetTempScene().GetRootGameObjects()[0].GetComponent<VideoPlayer>(); + + // Make new player if it doesn't exist in the scene + if (_player == null) + { + _player = GetTempScene().GetRootGameObjects()[0].AddComponent<VideoPlayer>(); + _player.renderMode = VideoRenderMode.APIOnly; + _player.source = VideoSource.Url; + _player.audioOutputMode = VideoAudioOutputMode.None; + _player.playOnAwake = false; + _player.Prepare(); + } + } + + return _player; + } + + + public new class UxmlFactory : UxmlFactory<VideoPlayerElement, UxmlTraits> + { + } + +#if UNITY_2019_3_OR_NEWER + public new class UxmlTraits : UnityEngine.UIElements.UxmlTraits +#else + public new class UxmlTraits : UnityEngine.Experimental.UIElements.UxmlTraits +#endif + { + UxmlStringAttributeDescription m_Url = new UxmlStringAttributeDescription + {name = "url-attr", defaultValue = ""}; + + public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription { get; } + + public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) + { + base.Init(ve, bag, cc); + + var ate = ve as VideoPlayerElement; + + ate.Clear(); + + ate.urlAttr = m_Url.GetValueFromBag(bag, cc); + } + } + + public string urlAttr { get; set; } + } +}
\ No newline at end of file diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs.meta new file mode 100644 index 00000000..5a3d82fb --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aabdd863f82551d40bd3a1b0835d2fc3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs new file mode 100644 index 00000000..98183bf4 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs @@ -0,0 +1,637 @@ +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.CSharp; +using UnityEngine; +using VRC.Udon.Compiler.Compilers; +using VRC.Udon.Graph; +using VRC.Udon.Graph.Interfaces; +using CompressionLevel = System.IO.Compression.CompressionLevel; +using Object = UnityEngine.Object; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI +{ + public static class UdonGraphExtensions + { + private static readonly Dictionary<string, string> FriendlyNameCache; + + static UdonGraphExtensions() + { + FriendlyNameCache = new Dictionary<string, string>(); + StartsWithCache = new Dictionary<(string s, string prefix), bool>(); + } + + #region Serialization Utilities + private const byte ZIP_VERSION = 0; + public static string ZipString(string str) + { + using (MemoryStream output = new MemoryStream()) + { + using (DeflateStream gzip = + new DeflateStream(output, CompressionLevel.Optimal)) //, CompressionMode.Compress + { + using (StreamWriter writer = + new StreamWriter(gzip, Encoding.UTF8)) + { + writer.Write(str); + } + } + List<byte> outputList = output.ToArray().ToList(); + outputList.Insert(0, ZIP_VERSION); //Version Number + return Convert.ToBase64String(outputList.ToArray()); + } + } + + public static string UnZipString(string input) + { + List<byte> inputList = new List<byte>(Convert.FromBase64String(input)); + if (inputList[0] != ZIP_VERSION) //Version Number + { + return ""; + } + inputList.RemoveAt(0); + using (MemoryStream inputStream = new MemoryStream(inputList.ToArray())) + { + using (DeflateStream gzip = + new DeflateStream(inputStream, CompressionMode.Decompress)) + { + using (StreamReader reader = + new StreamReader(gzip, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } + } + #endregion + + #region Color Utilities + + public static class NodeColors + { + public static readonly Color Base = new Color(77f / 255f, 157f / 255f, 1); + public static readonly Color Const = new Color(0.4f, 0.14f, 1f); + public static readonly Color Variable = new Color(1f, 0.86f, 0.3f); + public static readonly Color Function = new Color(1f, 0.42f, 0.14f); + public static readonly Color Event = new Color(0.53f, 1f, 0.3f); + public static readonly Color Return = new Color(0.89f, 0.2f, 0.2f); + } + + private static readonly MD5 _md5Hasher = MD5.Create(); + private static readonly Dictionary<Type, Color> _typeColors = new Dictionary<Type, Color>(); + + public static Color MapTypeToColor(Type type) + { + if (type == null) + { + return Color.white; + } + + if (type.IsPrimitive) + { + return new Color(0.12f, 0.53f, 0.9f); + } + + if (typeof(Object).IsAssignableFrom(type)) + { + return new Color(0.9f, 0.23f, 0.39f); + } + + if (type.IsValueType) + { + return NodeColors.Variable; + } + + if (_typeColors.ContainsKey(type)) + { + return _typeColors[type]; + } + + byte[] hashed = _md5Hasher.ComputeHash(type.ToString() == "T" + ? Encoding.UTF8.GetBytes("T") + : Encoding.UTF8.GetBytes(type.Name)); + int iValue = BitConverter.ToInt32(hashed, 0); + + //TODO: Make this provide more varied colors + Color color = Color.HSVToRGB((iValue & 0xff) / 255f, .69f, 1f); + + + _typeColors.Add(type, color); + return color; + } + #endregion + + #region Documentation Utilities + + public static bool ShouldShowDocumentationLink(UdonNodeDefinition definition) + { + List<string> specialNames = new List<string> + { + "Block", + "Branch", + "For", + "While", + "Foreach", + "Get_Variable", + "Set_Variable", + "Set_ReturnValue", + "Event_Custom", + "Event_OnDataStorageAdded", + "Event_OnDataStorageChanged", + "Event_OnDataStorageRemoved", + "Event_OnDrop", + "Event_Interact", + "Event_OnNetworkReady", + "Event_OnOwnershipTransferred", + "Event_OnPickup", + "Event_OnPickupUseDown", + "Event_OnPickupUseUp", + "Event_OnPlayerJoined", + "Event_OnPlayerLeft", + "Event_OnSpawn", + "Event_OnStationEntered", + "Event_OnStationExited", + "Event_OnVideoEnd", + "Event_OnVideoPause", + "Event_OnVideoPlay", + "Event_OnVideoStart", + "Event_MidiNoteOn", + "Event_MidiNoteOff", + "Event_MidiControlChange", + "VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEvent__SystemString__SystemVoid", + "VRCUdonCommonInterfacesIUdonEventReceiver.__SetHeapVariable__SystemString_SystemObject__SystemVoid", + "VRCUdonCommonInterfacesIUdonEventReceiver.__GetHeapVariable__SystemString__SystemObject", + "Const_VRCUdonCommonInterfacesIUdonEventReceiver", + }; + + // Don't show for any of these + return !(definition.type == null || + definition.type.Namespace == null || + specialNames.Contains(definition.fullName) || + (!definition.type.Namespace.Contains("UnityEngine") && + !definition.type.Namespace.Contains("System"))); + } + + public static string GetDocumentationLink(UdonNodeDefinition definition) + { + if (definition.fullName.StartsWithCached("Event_")) + { + string url = "https://docs.unity3d.com/2018.4/Documentation/ScriptReference/MonoBehaviour."; + url += definition.name; + url += ".html"; + return url; + } + + if (definition.fullName.Contains("Array.__ctor")) + { + //I couldn't find the array constructor documentation + return "https://docs.microsoft.com/en-us/dotnet/api/system.array?view=netframework-4.8"; + } + + if (definition.fullName.Contains("Array.__Get")) + { + return "https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#indexer-operator-"; + } + + if (definition.fullName.Contains(".__Equals__SystemObject")) + { + return "https://docs.microsoft.com/en-us/dotnet/api/system.object.equals?view=netframework-4.8"; + } + + if (definition.name.Contains("[]")) + { + string url = "https://docs.microsoft.com/en-us/dotnet/api/system.array."; + url += definition.name.Split(' ')[1]; + url += "?view=netframework-4.8"; + return url; + } + + if (definition.type.Namespace.Contains("UnityEngine")) + { + string url = "https://docs.unity3d.com/2018.4/Documentation/ScriptReference/"; + if (definition.type.Namespace != "UnityEngine") + { + url += definition.type.Namespace.Replace("UnityEngine.", ""); + url += "."; + } + url += definition.type.Name; + + if (definition.fullName.Contains("__get_") || definition.fullName.Contains("__set_")) + { + if (definition.fullName.Contains("__get_")) + { + url += "-" + definition.name.Split(new[] { "get_" }, StringSplitOptions.None)[1]; + } + else + { + url += "-" + definition.name.Split(new[] { "set_" }, StringSplitOptions.None)[1]; + } + + url += ".html"; + return url; + } + + if (definition.fullName.Contains("Const_") || definition.fullName.Contains("Type_") || definition.fullName.Contains("Variable_")) + { + url += ".html"; + return url; + } + + { + // Methods + url += "." + definition.name.Split(' ')[1]; + url += ".html"; + return url; + } + } + + if (definition.type.Namespace.Contains("System")) + { + string url = "https://docs.microsoft.com/en-us/dotnet/api/system."; + url += definition.type.Name; + if (definition.fullName.Contains("__get_") || definition.fullName.Contains("__set_")) + { + url += "." + definition.name.Split(' ')[1].Replace("get_", "").Replace("set_", ""); + url += "?view=netframework-4.8"; + return url; + } + + if (definition.name == "ctor") + { + url += ".-ctor"; + url += "?view=netframework-4.8#System_"; + url += definition.type.Name + "__ctor_"; + foreach (var pType in definition.Inputs) + { + url += "System_" + pType.type.Name.Replace('[', '_').Replace(']', '_') + "_"; + } + + return url; + } + + if (definition.fullName.Contains("Const_") || definition.fullName.Contains("Type_")) + { + url += "?view=netframework-4.8"; + return url; + } + + { + // Methods + // not entirely sure what case this catches, but we were always doing the split before, and it was breaking if we didn't have . in the name. + if (definition.name.Contains('.')) + { + url += "." + definition.name.Split(' ')[1]; + } + url += "?view=netframework-4.8"; + return url; + } + } + + return ""; + } + + #endregion + + public static string GetVariableChangeEventName(string variableName) + { + return UdonGraphCompiler.GetVariableChangeEventName(variableName); + } + + public static string FriendlyNameify(this string typeString) + { + if (typeString == null) + { + return null; + } + + if (FriendlyNameCache.ContainsKey(typeString)) + { + return FriendlyNameCache[typeString]; + } + string originalString = typeString; + typeString = typeString.Replace("Single", "float"); + typeString = typeString.Replace("Int32", "int"); + typeString = typeString.Replace("String", "string"); + typeString = typeString.Replace("VRCUdonCommonInterfacesIUdonEventReceiver", "UdonBehaviour"); + typeString = typeString.Replace("UdonCommonInterfacesIUdonEventReceiver", "UdonBehaviour"); + typeString = typeString.Replace("IUdonEventReceiver", "UdonBehaviour"); + typeString = typeString.Replace("Const_VRCUdonCommonInterfacesIUdonEventReceiver", "UdonBehaviour"); + if(typeString != "SystemArray") + { + typeString = typeString.Replace("Array", "[]"); + } + + typeString = typeString.Replace("SDK3VideoComponentsBaseBase", ""); + typeString = typeString.Replace("SDKBase", ""); + typeString = typeString.Replace("SDK3Components", ""); + typeString = typeString.Replace("VRCVRC", "VRC"); + typeString = typeString.Replace("TMPro", ""); + typeString = typeString.Replace("VideoVideo", "Video"); + typeString = typeString.Replace("VRCUdonCommon", ""); + typeString = typeString.Replace("Shuffle[]", "ShuffleArray"); + // ReSharper disable once StringLiteralTypo + if (typeString.Replace("ector", "").Contains("ctor")) //Handle "Vector/vector" + { + typeString = typeString.ReplaceLast("ctor", "constructor"); + } + + if (typeString == "IUdonEventReceiver") + { + typeString = "UdonBehaviour"; + } + FriendlyNameCache.Add(originalString, typeString); + return typeString; + } + + private static readonly Dictionary<(string s, string prefix), bool> StartsWithCache; + public static bool StartsWithCached(this string s, string prefix) + { + if (StartsWithCache.ContainsKey((s, prefix))) + { + return StartsWithCache[(s, prefix)]; + } + bool doesStartWith = s.StartsWith(prefix); + StartsWithCache.Add((s, prefix), doesStartWith); + return doesStartWith; + } + + public static string UppercaseFirst(this string s) + { + if (string.IsNullOrEmpty(s)) + { + return string.Empty; + } + char[] a = s.ToCharArray(); + a[0] = char.ToUpper(a[0]); + return new string(a); + } + + public static string ReplaceFirst(this string text, string search, string replace) + { + int pos = text.IndexOf(search, StringComparison.Ordinal); + if (pos < 0) + { + return text; + } + return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + } + + public static Type SlotTypeConverter(Type type, string fullName) + { + if (type == null) + { + return typeof(object); + } + + if (fullName.Contains("IUdonEventReceiver") && type == typeof(Object)) + { + return typeof(UdonBehaviour); + } + + return type; + } + + public static string FriendlyTypeName(Type t) + { + if (t == null) + { + return "Flow"; + } + + if (!t.IsPrimitive) + { + if (t == typeof(Object)) + { + return "Unity Object"; + } + return t.Name; + } + using (CSharpCodeProvider provider = new CSharpCodeProvider()) + { + CodeTypeReference typeRef = new CodeTypeReference(t); + return provider.GetTypeOutput(typeRef); + } + } + + public static string ReplaceLast(this string source, string find, string replace) + { + int place = source.LastIndexOf(find, StringComparison.Ordinal); + + if (place == -1) + return source; + + string result = source.Remove(place, find.Length).Insert(place, replace); + return result; + } + + public static string PrettyBaseName(string baseIdentifier) + { + string result = baseIdentifier.Replace("UnityEngine", "").Replace("System", ""); + string[] resultSplit = result.Split(new[] { "__" }, StringSplitOptions.None); + if (resultSplit.Length >= 2) + { + result = $"{resultSplit[0]}{resultSplit[1]}"; + } + result = result.FriendlyNameify(); + result = result.Replace("op_", ""); + result = result.Replace("_", " "); + return result; + } + + public static string PrettyString(string s) + { + switch (s) + { + case "op_Equality": + s = "=="; + break; + + case "op_Inequality": + s = "!="; + break; + + case "op_Addition": + s = "+"; + break; + case "VRCUdonCommonInterfacesIUdonEventReceiver": + s = "UdonBehaviour"; + break; + // ReSharper disable once RedundantEmptySwitchSection + default: + break; + } + return s; + } + + public static string ParseByCase(string strInput) + { + string strOutput = ""; + int intCurrentCharPos = 0; + int intLastCharPos = strInput.Length - 1; + for (intCurrentCharPos = 0; intCurrentCharPos <= intLastCharPos; intCurrentCharPos++) + { + char chrCurrentInputChar = strInput[intCurrentCharPos]; + char chrPreviousInputChar = chrCurrentInputChar; + if (intCurrentCharPos > 0) + { + chrPreviousInputChar = strInput[intCurrentCharPos - 1]; + } + + if (char.IsUpper(chrCurrentInputChar) && char.IsLower(chrPreviousInputChar)) + { + strOutput += " "; + } + + strOutput += chrCurrentInputChar; + } + + return strOutput; + } + + public static string PrettyFullName(UdonNodeDefinition nodeDefinition, bool keepLong = false) + { + string fullName = nodeDefinition.fullName; + string result; + if (keepLong) + { + result = fullName.Replace("UnityEngine", "UnityEngine.").Replace("System", "System."); + } + else + { + result = fullName.Replace("UnityEngine", "").Replace("System", ""); + } + + string[] resultSplit = result.Split(new[] { "__" }, StringSplitOptions.None); + if (resultSplit.Length >= 3) + { + string outName = ""; + if (nodeDefinition.type != typeof(void)) + { + if (nodeDefinition.Outputs.Count > 0) + { + outName = string.Join(", ", nodeDefinition.Outputs.Select(o => o.name)); + } + } + + result = nodeDefinition.Inputs.Count > 0 + ? $"{resultSplit[0]}{resultSplit[1]}({string.Join(", ", nodeDefinition.Inputs.Select(s => s.name))}{outName})" + : $"{resultSplit[0]}{resultSplit[1]}({resultSplit[2].Replace("_", ", ")}{outName})"; + } + else if (resultSplit.Length >= 2) + { + result = $"{resultSplit[0]}{resultSplit[1]}()"; + } + + if (!keepLong) + { + result = result.FriendlyNameify(); + result = result.Replace("op_", ""); + result = result.Replace("_", " "); + } + + return result; + } + + public static string GetSimpleNameForRegistry(INodeRegistry registry) + { + string registryName = registry.ToString().Replace("NodeRegistry", "").FriendlyNameify(); + registryName = registryName.Substring(registryName.LastIndexOf(".") + 1); + registryName = registryName.Replace("UnityEngine", ""); + return registryName; + } + + private static Dictionary<UdonNodeDefinition, INodeRegistry> _definitionToRegistryLookup; + public static INodeRegistry GetRegistryForDefinition(UdonNodeDefinition definition) + { + // Create lookup if needed + if(_definitionToRegistryLookup == null) + { + _definitionToRegistryLookup = new Dictionary<UdonNodeDefinition, INodeRegistry>(); + + foreach (var registry in UdonEditorManager.Instance.GetNodeRegistries()) + { + foreach (var nodeDefinition in registry.Value.GetNodeDefinitions()) + { + _definitionToRegistryLookup.Add(nodeDefinition, registry.Value); + } + } + } + + // Return found Registry or Null + if (_definitionToRegistryLookup.ContainsKey(definition)) + { + return _definitionToRegistryLookup[definition]; + } + else + { + return null; + } + } + + public static string ToLowerFirstChar(this string input) + { + string newString = input; + if (!String.IsNullOrEmpty(newString) && Char.IsUpper(newString[0])) + newString = Char.ToLower(newString[0]) + newString.Substring(1); + return newString; + } + + public static string SanitizeVariableName(this string result) + { + result = result.Replace(" ", ""); + if (char.IsNumber(result[0])) + { + result = $"A{result}"; + } + Regex rgx = new Regex("[^a-zA-Z0-9 _]"); + result = rgx.Replace(result, ""); + return result; + } + + public static char EscapeLikeALiteral(string src) + { + switch (src) + { + //case "\\'": return '\''; + //case "\\"": return '\"'; + case "\\0": return '\0'; + case "\\a": return '\a'; + case "\\b": return '\b'; + case "\\f": return '\f'; + case "\\n": return '\n'; + case "\\r": return '\r'; + case "\\t": return '\t'; + case "\\v": return '\v'; + case "\\": return '\\'; + default: + throw new InvalidOperationException($"src was {src}"); + } + } + + public static string UnescapeLikeALiteral(char src) + { + switch (src) + { + //case "\\'": return '\''; + //case "\\"": return '\"'; + case '\0': return "\\0"; + case '\a': return "\\a"; + case '\b': return "\\b"; + case '\f': return "\\f"; + case '\n': return "\\n"; + case '\r': return "\\r"; + case '\t': return "\\t"; + case '\v': return "\\v"; + //case '\\': return "\\"; + default: + return src.ToString(); + } + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs.meta new file mode 100644 index 00000000..7b34c5a2 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57422d3fdb0cc124189c68f87b7157cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs new file mode 100644 index 00000000..af8d5efd --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEditor; +using UnityEngine; +using VRC.Udon.Common.Interfaces; +using VRC.Udon.Editor.ProgramSources.Attributes; +using VRC.Udon.Editor.ProgramSources.UdonGraphProgram; +using VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView; +using VRC.Udon.Graph; +using VRC.Udon.Graph.Interfaces; +using VRC.Udon.Serialization.OdinSerializer; +using Object = UnityEngine.Object; + +[assembly: UdonProgramSourceNewMenu(typeof(UdonGraphProgramAsset), "Udon Graph Program Asset")] + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram +{ + [CreateAssetMenu(menuName = "VRChat/Udon/Udon Graph Program Asset", fileName = "New Udon Graph Program Asset")] + public class UdonGraphProgramAsset : UdonAssemblyProgramAsset, IUdonGraphDataProvider + { + [SerializeField] + public UdonGraphData graphData = new UdonGraphData(); + + [SerializeField] + public UdonGraphElementData[] graphElementData = new UdonGraphElementData[0]; + + [SerializeField] + public UdonGraph.ViewTransformData viewTransform = new UdonGraph.ViewTransformData(); + + [SerializeField] + public string version = "1.0.0"; + + [SerializeField] + private bool showAssembly = false; + + [NonSerialized, OdinSerialize] + private Dictionary<string, (object value, Type type)> heapDefaultValues = new Dictionary<string, (object value, Type type)>(); + + protected override void DrawProgramSourceGUI(UdonBehaviour udonBehaviour, ref bool dirty) + { + if (GUILayout.Button("Open Udon Graph", "LargeButton")) + { + var w = EditorWindow.GetWindow<UdonGraphWindow>("Udon Graph", true, typeof(SceneView)); + w.InitializeGraph(this, udonBehaviour); + } + + DrawInteractionArea(udonBehaviour); + DrawPublicVariables(udonBehaviour, ref dirty); + DrawAssemblyErrorTextArea(); + DrawAssemblyTextArea(false, ref dirty); + } + + protected override void RefreshProgramImpl() + { + if(graphData == null) + { + return; + } + + CompileGraph(); + base.RefreshProgramImpl(); + ApplyDefaultValuesToHeap(); + } + + protected override void DrawAssemblyTextArea(bool allowEditing, ref bool dirty) + { + EditorGUI.BeginChangeCheck(); + bool newShowAssembly = EditorGUILayout.Foldout(showAssembly, "Compiled Graph Assembly"); + if(EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "Toggle Assembly Foldout"); + showAssembly = newShowAssembly; + } + + if(!showAssembly) + { + return; + } + + EditorGUI.indentLevel++; + base.DrawAssemblyTextArea(allowEditing, ref dirty); + EditorGUI.indentLevel--; + } + + [PublicAPI] + protected void CompileGraph() + { + udonAssembly = UdonEditorManager.Instance.CompileGraph(graphData, null, out Dictionary<string, (string uid, string fullName, int index)> _, out heapDefaultValues); + } + + [PublicAPI] + protected void ApplyDefaultValuesToHeap() + { + IUdonSymbolTable symbolTable = program?.SymbolTable; + IUdonHeap heap = program?.Heap; + if(symbolTable == null || heap == null) + { + return; + } + + foreach(KeyValuePair<string, (object value, Type type)> defaultValue in heapDefaultValues) + { + if(!symbolTable.HasAddressForSymbol(defaultValue.Key)) + { + continue; + } + + uint symbolAddress = symbolTable.GetAddressFromSymbol(defaultValue.Key); + (object value, Type declaredType) = defaultValue.Value; + if(typeof(Object).IsAssignableFrom(declaredType)) + { + if(value != null && !declaredType.IsInstanceOfType(value)) + { + heap.SetHeapVariable(symbolAddress, null, declaredType); + continue; + } + + if((Object)value == null) + { + heap.SetHeapVariable(symbolAddress, null, declaredType); + continue; + } + } + + if(value != null) + { + if(!declaredType.IsInstanceOfType(value)) + { + value = declaredType.IsValueType ? Activator.CreateInstance(declaredType) : null; + } + } + + if(declaredType == null) + { + declaredType = typeof(object); + } + heap.SetHeapVariable(symbolAddress, value, declaredType); + } + } + + protected override object GetPublicVariableDefaultValue(string symbol, Type type) + { + IUdonSymbolTable symbolTable = program?.SymbolTable; + IUdonHeap heap = program?.Heap; + if(symbolTable == null || heap == null) + { + return null; + } + + if(!heapDefaultValues.ContainsKey(symbol)) + { + return null; + } + + (object value, Type declaredType) = heapDefaultValues[symbol]; + if(!typeof(Object).IsAssignableFrom(declaredType)) + { + return value; + } + + return (Object)value == null ? null : value; + } + + protected override object DrawPublicVariableField(string symbol, object variableValue, Type variableType, ref bool dirty, + bool enabled) + { + EditorGUILayout.BeginHorizontal(); + variableValue = base.DrawPublicVariableField(symbol, variableValue, variableType, ref dirty, enabled); + object defaultValue = null; + if(heapDefaultValues.ContainsKey(symbol)) + { + defaultValue = heapDefaultValues[symbol].value; + } + + if(variableValue == null || !variableValue.Equals(defaultValue)) + { + if(defaultValue != null) + { + if(!dirty && GUILayout.Button("Reset to Default Value")) + { + variableValue = defaultValue; + dirty = true; + } + } + } + + EditorGUILayout.EndHorizontal(); + + return variableValue; + } + + #region Serialization Methods + + protected override void OnAfterDeserialize() + { + foreach(UdonNodeData node in graphData.nodes) + { + node.SetGraph(graphData); + } + } + + #endregion + + public UdonGraphData GetGraphData() + { + return graphData; + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs.meta new file mode 100644 index 00000000..99057af8 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f11136daadff0b44ac2278a314682ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs new file mode 100644 index 00000000..219bfa57 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs @@ -0,0 +1,21 @@ +using UnityEditor; + +namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram +{ + [CustomEditor(typeof(UdonGraphProgramAsset))] + public class UdonGraphProgramAssetEditor : UdonAssemblyProgramAssetEditor + { + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + var asset = (UdonGraphProgramAsset) target; + EditorGUI.BeginChangeCheck(); + asset.graphData.updateOrder = EditorGUILayout.IntField("UpdateOrder", asset.graphData.updateOrder); + if (EditorGUI.EndChangeCheck()) + { + EditorUtility.SetDirty(target); + AssetDatabase.SaveAssets(); + } + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs.meta new file mode 100644 index 00000000..cdae2436 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 31d6811854f59254aa1a263a8d566eb2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram.meta new file mode 100644 index 00000000..a35c9141 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 47cfad4ac2eccd148aa5f5f9c5403d81 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs new file mode 100644 index 00000000..dfd288bd --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs @@ -0,0 +1,2080 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using JetBrains.Annotations; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEditorInternal; +using UnityEngine; +using UnityEngine.Assertions; +using VRC.Udon.Common; +using VRC.Udon.Common.Interfaces; +using VRC.Udon.ProgramSources; + +namespace VRC.Udon.Editor.ProgramSources +{ + public class UdonProgramAsset : AbstractUdonProgramSource, ISerializationCallbackReceiver + { + protected IUdonProgram program; + + [SerializeField] + protected AbstractSerializedUdonProgramAsset serializedUdonProgramAsset; + + public override AbstractSerializedUdonProgramAsset SerializedProgramAsset + { + get + { + AssetDatabase.TryGetGUIDAndLocalFileIdentifier(this, out string guid, out long _); + if(serializedUdonProgramAsset != null) + { + if(serializedUdonProgramAsset.name == guid) + { + return serializedUdonProgramAsset; + } + + string oldSerializedUdonProgramAssetPath = Path.Combine("Assets", "SerializedUdonPrograms", $"{serializedUdonProgramAsset.name}.asset"); + AssetDatabase.DeleteAsset(oldSerializedUdonProgramAssetPath); + } + + string serializedUdonProgramAssetPath = Path.Combine("Assets", "SerializedUdonPrograms", $"{guid}.asset"); + + serializedUdonProgramAsset = (SerializedUdonProgramAsset)AssetDatabase.LoadAssetAtPath( + Path.Combine("Assets", "SerializedUdonPrograms", $"{guid}.asset"), + typeof(SerializedUdonProgramAsset) + ); + + if(serializedUdonProgramAsset != null) + { + return serializedUdonProgramAsset; + } + + serializedUdonProgramAsset = CreateInstance<SerializedUdonProgramAsset>(); + if(!AssetDatabase.IsValidFolder(Path.Combine("Assets", "SerializedUdonPrograms"))) + { + AssetDatabase.CreateFolder("Assets", "SerializedUdonPrograms"); + } + + AssetDatabase.CreateAsset(serializedUdonProgramAsset, serializedUdonProgramAssetPath); + AssetDatabase.SaveAssets(); + + RefreshProgram(); + AssetDatabase.SaveAssets(); + + AssetDatabase.Refresh(); + + return serializedUdonProgramAsset; + } + } + + public sealed override void RunEditorUpdate(UdonBehaviour udonBehaviour, ref bool dirty) + { + if(program == null && serializedUdonProgramAsset != null) + { + program = serializedUdonProgramAsset.RetrieveProgram(); + } + + if(program == null) + { + RefreshProgram(); + } + + DrawProgramSourceGUI(udonBehaviour, ref dirty); + + if(dirty) + { + EditorUtility.SetDirty(this); + } + } + + protected virtual void DrawProgramSourceGUI(UdonBehaviour udonBehaviour, ref bool dirty) + { + DrawPublicVariables(udonBehaviour, ref dirty); + DrawProgramDisassembly(); + } + + public sealed override void RefreshProgram() + { + if(Application.isPlaying) + { + return; + } + + RefreshProgramImpl(); + + SerializedProgramAsset.StoreProgram(program); + if (this != null) + { + EditorUtility.SetDirty(this); + } + } + + protected virtual void RefreshProgramImpl() + { + } + + [PublicAPI] + protected void DrawInteractionArea(UdonBehaviour udonBehaviour) + { + ImmutableArray<string> exportedSymbols = program.EntryPoints.GetExportedSymbols(); + if (exportedSymbols.Contains("_interact")) + { + EditorGUILayout.LabelField("Interaction", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + + if(udonBehaviour != null) + { + udonBehaviour.interactText = EditorGUILayout.TextField("Interaction Text", udonBehaviour.interactText); + udonBehaviour.proximity = EditorGUILayout.Slider("Proximity", udonBehaviour.proximity, 0f, 100f); + udonBehaviour.interactTextPlacement = (Transform)EditorGUILayout.ObjectField("Text Placement", udonBehaviour.interactTextPlacement, typeof(Transform), true); + } + else + { + using(new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.TextField("Interaction Text", "Use"); + EditorGUILayout.Slider("Proximity", 2.0f, 0f, 100f); + EditorGUILayout.ObjectField("Text Placement", null, typeof(Transform), true); + } + } + + + + EditorGUI.indentLevel--; + } + } + + [PublicAPI] + protected void DrawPublicVariables(UdonBehaviour udonBehaviour, ref bool dirty) + { + IUdonVariableTable publicVariables = null; + if(udonBehaviour != null) + { + publicVariables = udonBehaviour.publicVariables; + } + + EditorGUILayout.LabelField("Public Variables", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + if(program?.SymbolTable == null) + { + EditorGUILayout.LabelField("No public variables."); + EditorGUI.indentLevel--; + return; + } + + IUdonSymbolTable symbolTable = program.SymbolTable; + // Remove non-exported public variables + if(publicVariables != null) + { + foreach(string publicVariableSymbol in publicVariables.VariableSymbols.ToArray()) + { + if(!symbolTable.HasExportedSymbol(publicVariableSymbol)) + { + publicVariables.RemoveVariable(publicVariableSymbol); + } + } + } + + ImmutableArray<string> exportedSymbolNames = symbolTable.GetExportedSymbols(); + if(exportedSymbolNames.Length <= 0) + { + EditorGUILayout.LabelField("No public variables."); + EditorGUI.indentLevel--; + return; + } + + foreach(string exportedSymbol in exportedSymbolNames) + { + Type symbolType = symbolTable.GetSymbolType(exportedSymbol); + if(publicVariables == null) + { + DrawPublicVariableField(exportedSymbol, GetPublicVariableDefaultValue(exportedSymbol, symbolType), symbolType, ref dirty, false); + continue; + } + + if(!publicVariables.TryGetVariableType(exportedSymbol, out Type declaredType) || declaredType != symbolType) + { + publicVariables.RemoveVariable(exportedSymbol); + if(!publicVariables.TryAddVariable(CreateUdonVariable(exportedSymbol, GetPublicVariableDefaultValue(exportedSymbol, declaredType), symbolType))) + { + EditorGUILayout.LabelField($"Error drawing field for symbol '{exportedSymbol}'."); + continue; + } + } + + if(!publicVariables.TryGetVariableValue(exportedSymbol, out object variableValue)) + { + variableValue = GetPublicVariableDefaultValue(exportedSymbol, declaredType); + } + + variableValue = DrawPublicVariableField(exportedSymbol, variableValue, symbolType, ref dirty, true); + if(!dirty) + { + continue; + } + + Undo.RecordObject(udonBehaviour, "Modify Public Variable"); + + if(!publicVariables.TrySetVariableValue(exportedSymbol, variableValue)) + { + if(!publicVariables.TryAddVariable(CreateUdonVariable(exportedSymbol, variableValue, symbolType))) + { + Debug.LogError($"Failed to set public variable '{exportedSymbol}' value."); + } + } + + EditorSceneManager.MarkSceneDirty(udonBehaviour.gameObject.scene); + + if(PrefabUtility.IsPartOfPrefabInstance(udonBehaviour)) + { + PrefabUtility.RecordPrefabInstancePropertyModifications(udonBehaviour); + } + } + + EditorGUI.indentLevel--; + } + + private static IUdonVariable CreateUdonVariable(string symbolName, object value, Type declaredType) + { + Type udonVariableType = typeof(UdonVariable<>).MakeGenericType(declaredType); + return (IUdonVariable)Activator.CreateInstance(udonVariableType, symbolName, value); + } + + [PublicAPI] + protected virtual object GetPublicVariableDefaultValue(string symbol, Type type) + { + return null; + } + + [PublicAPI] + protected void DrawProgramDisassembly() + { + try + { + EditorGUILayout.LabelField("Disassembled Program", EditorStyles.boldLabel); + using(new EditorGUI.DisabledScope(true)) + { + string[] disassembledProgram = UdonEditorManager.Instance.DisassembleProgram(program); + EditorGUILayout.TextArea(string.Join("\n", disassembledProgram)); + } + } + catch(Exception e) + { + Debug.LogException(e); + } + } + + [NonSerialized] + private readonly Dictionary<string, bool> _arrayStates = new Dictionary<string, bool>(); + + protected virtual object DrawPublicVariableField(string symbol, object variableValue, Type variableType, ref bool dirty, bool enabled) + { + using(new EditorGUI.DisabledScope(!enabled)) + { + // ReSharper disable RedundantNameQualifier + if(!variableType.IsInstanceOfType(variableValue)) + { + if(variableType.IsValueType) + { + variableValue = Activator.CreateInstance(variableType); + } + else + { + variableValue = null; + } + } + + EditorGUILayout.BeginHorizontal(); + if(typeof(UnityEngine.Object).IsAssignableFrom(variableType)) + { + UnityEngine.Object unityEngineObjectValue = (UnityEngine.Object)variableValue; + EditorGUI.BeginChangeCheck(); + Rect fieldRect = EditorGUILayout.GetControlRect(); + variableValue = EditorGUI.ObjectField(fieldRect, symbol, unityEngineObjectValue, variableType, true); + + if(variableValue == null && (variableType == typeof(GameObject) || variableType == typeof(Transform) || + variableType == typeof(UdonBehaviour))) + { + EditorGUI.LabelField( + fieldRect, + new GUIContent(symbol), + new GUIContent("Self (" + variableType.Name + ")", AssetPreview.GetMiniTypeThumbnail(variableType)), + EditorStyles.objectField); + } + + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(string)) + { + string stringValue = (string)variableValue; + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.TextField(symbol, stringValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(string[])) + { + string[] valueArray = (string[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.TextField( + $"{i}:", + valueArray.Length > i ? valueArray[i] : ""); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(float)) + { + float floatValue = (float?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.FloatField(symbol, floatValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(float[])) + { + float[] valueArray = (float[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.FloatField( + $"{i}:", + valueArray.Length > i ? valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(int)) + { + int intValue = (int?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.IntField(symbol, intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(int[])) + { + int[] valueArray = (int[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.IntField( + $"{i}:", + valueArray.Length > i ? valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(short)) + { + short intValue = (short?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (short)EditorGUILayout.IntField(symbol, intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(short[])) + { + short[] valueArray = (short[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = (short)EditorGUILayout.IntField( + $"{i}:", + valueArray.Length > i ? valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(long)) + { + long intValue = (long?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (long)EditorGUILayout.IntField(symbol, (int)intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(long[])) + { + long[] valueArray = (long[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.IntField( + $"{i}:", + valueArray.Length > i ? (int)valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(uint)) + { + uint intValue = (uint?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (uint)EditorGUILayout.IntField(symbol, (int)intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(uint[])) + { + uint[] valueArray = (uint[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = (uint)EditorGUILayout.IntField( + $"{i}:", + valueArray.Length > i ? (int)valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(ushort)) + { + ushort intValue = (ushort?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (ushort)EditorGUILayout.IntField(symbol, (int)intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(ushort[])) + { + ushort[] valueArray = (ushort[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = (ushort)EditorGUILayout.IntField( + $"{i}:", + valueArray.Length > i ? (int)valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(ulong)) + { + ulong intValue = (ulong?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (ulong)EditorGUILayout.IntField(symbol, (int)intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(ulong[])) + { + ulong[] valueArray = (ulong[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = (ulong)EditorGUILayout.IntField( + $"{i}:", + valueArray.Length > i ? (int)valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(byte)) + { + byte intValue = (byte?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (byte)EditorGUILayout.IntField(symbol, (int)intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(byte[])) + { + byte[] valueArray = (byte[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = (byte)EditorGUILayout.IntField( + $"{i}:", + valueArray.Length > i ? (int)valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(sbyte)) + { + sbyte intValue = (sbyte?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (sbyte)EditorGUILayout.IntField(symbol, (int)intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(sbyte[])) + { + sbyte[] valueArray = (sbyte[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = (sbyte)EditorGUILayout.IntField( + $"{i}:", + valueArray.Length > i ? (int)valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(double)) + { + double intValue = (double?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (double)EditorGUILayout.DoubleField(symbol, intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(double[])) + { + double[] valueArray = (double[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.DoubleField( + $"{i}:", + valueArray.Length > i ? valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(decimal)) + { + decimal intValue = (decimal?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (decimal)EditorGUILayout.DoubleField(symbol, (double)intValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(decimal[])) + { + decimal[] valueArray = (decimal[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = (decimal)EditorGUILayout.DoubleField( + $"{i}:", + valueArray.Length > i ? (double)valueArray[i] : 0); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(bool)) + { + bool boolValue = (bool?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.Toggle(symbol, boolValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(bool[])) + { + bool[] valueArray = (bool[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.Toggle( + $"{i}:", + valueArray.Length > i ? valueArray[i] : false); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(UnityEngine.Vector2)) + { + Vector2 vector2Value = (Vector2?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.Vector2Field(symbol, vector2Value); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(Vector2[])) + { + Vector2[] valueArray = (Vector2[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.Vector2Field( + $"{i}:", + valueArray.Length > i ? valueArray[i] : Vector2.zero); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(UnityEngine.Vector3)) + { + Vector3 vector3Value = (Vector3?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.Vector3Field(symbol, vector3Value); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(Vector3[])) + { + Vector3[] valueArray = (Vector3[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.Vector3Field( + $"{i}:", + valueArray.Length > i ? valueArray[i] : Vector3.zero); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(UnityEngine.Vector2Int)) + { + Vector2Int vector2IntValue = (Vector2Int?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + Vector2 vector2Value = EditorGUILayout.Vector2Field(symbol, vector2IntValue); + variableValue = new Vector2Int((int)vector2Value.x, (int)vector2Value.y); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(Vector2Int[])) + { + Vector2Int[] valueArray = (Vector2Int[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + Vector2 vector2Value = EditorGUILayout.Vector2Field( + $"{i}:", + valueArray.Length > i ? valueArray[i] : Vector2.zero); + valueArray[i] = new Vector2Int((int)vector2Value.x, (int)vector2Value.y); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(UnityEngine.Vector3Int)) + { + Vector3Int vector3IntValue = (Vector3Int?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + Vector3 vector3Value = EditorGUILayout.Vector3Field(symbol, vector3IntValue); + variableValue = new Vector3Int((int)vector3Value.x, (int)vector3Value.y, (int)vector3Value.z); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(Vector3Int[])) + { + Vector3Int[] valueArray = (Vector3Int[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + Vector3 vector3Value = EditorGUILayout.Vector3Field( + $"{i}:", + valueArray.Length > i ? valueArray[i] : Vector3.zero); + valueArray[i] = new Vector3Int((int)vector3Value.x, (int)vector3Value.y, + (int)vector3Value.z); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(UnityEngine.Vector4)) + { + Vector4 vector4Value = (Vector4?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.Vector4Field(symbol, vector4Value); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(Vector4[])) + { + Vector4[] valueArray = (Vector4[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.Vector4Field( + $"{i}:", + valueArray.Length > i ? valueArray[i] : Vector4.zero); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(UnityEngine.Quaternion)) + { + Quaternion quaternionValue = (Quaternion?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + Vector4 quaternionVector4 = EditorGUILayout.Vector4Field(symbol, new Vector4(quaternionValue.x, quaternionValue.y, quaternionValue.z, quaternionValue.w)); + variableValue = new Quaternion(quaternionVector4.x, quaternionVector4.y, quaternionVector4.z, quaternionVector4.w); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(Quaternion[])) + { + Quaternion[] valueArray = (Quaternion[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + Vector4 vector4 = EditorGUILayout.Vector4Field( + $"{i}:", + valueArray.Length > i ? new Vector4(valueArray[i].x, valueArray[i].y, valueArray[i].z, valueArray[i].w) : Vector4.zero); + + valueArray[i] = new Quaternion(vector4.x, vector4.y, vector4.z, vector4.w); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(Gradient)) + { + Gradient color2Value = variableValue as Gradient; + if (color2Value == null) color2Value = new Gradient(); + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.GradientField(symbol, color2Value); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(Gradient[])) + { + Gradient[] valueArray = (Gradient[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + Gradient g = valueArray.Length > i ? (valueArray[i]) : new Gradient(); + if (g == null) g = new Gradient(); + valueArray[i] = EditorGUILayout.GradientField($"{i}:", g); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(AnimationCurve)) + { + AnimationCurve curve2Value = variableValue as AnimationCurve; + if (curve2Value == null) curve2Value = new AnimationCurve(); + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.CurveField(symbol, curve2Value); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(AnimationCurve[])) + { + AnimationCurve[] valueArray = (AnimationCurve[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + AnimationCurve curve = valueArray.Length > i ? (valueArray[i]) : new AnimationCurve(); + if (curve == null) curve = new AnimationCurve(); + valueArray[i] = EditorGUILayout.CurveField($"{i}:", curve); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(UnityEngine.Color)) + { + Color color2Value = (Color?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.ColorField(symbol, color2Value); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(Color[])) + { + Color[] valueArray = (Color[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.ColorField( + $"{i}:", + valueArray.Length > i ? valueArray[i] : Color.white); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(UnityEngine.Color32)) + { + Color32 colorValue = (Color32?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + variableValue = (Color32)EditorGUILayout.ColorField(symbol, colorValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(Color32[])) + { + Color32[] valueArray = (Color32[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + EditorGUILayout.BeginVertical(); + + EditorGUI.BeginChangeCheck(); + // Show Foldout Header + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + // Save foldout state + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.ColorField( + $"{i}:", + valueArray.Length > i ? valueArray[i] : (Color32)Color.white); + } + } + + EditorGUI.indentLevel--; + } + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(ParticleSystem.MinMaxCurve)) + { + ParticleSystem.MinMaxCurve minMaxCurve = (ParticleSystem.MinMaxCurve?)variableValue ?? default; + EditorGUI.BeginChangeCheck(); + float multiplier = minMaxCurve.curveMultiplier; + AnimationCurve minCurve = minMaxCurve.curveMin; + AnimationCurve maxCurve = minMaxCurve.curveMax; + EditorGUILayout.BeginVertical(); + EditorGUILayout.LabelField(symbol); + EditorGUI.indentLevel++; + multiplier = EditorGUILayout.FloatField("Multiplier", multiplier); + minCurve = EditorGUILayout.CurveField("Min Curve", minCurve); + maxCurve = EditorGUILayout.CurveField("Max Curve", maxCurve); + EditorGUI.indentLevel--; + EditorGUILayout.EndVertical(); + variableValue = new ParticleSystem.MinMaxCurve(multiplier, minCurve, maxCurve); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if(variableType == typeof(ParticleSystem.MinMaxCurve[])) + { + EditorGUI.BeginChangeCheck(); + EditorGUILayout.BeginVertical(); + ParticleSystem.MinMaxCurve[] valueArray = (ParticleSystem.MinMaxCurve[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + + showArray = EditorGUILayout.Foldout(showArray, symbol, true); + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + ParticleSystem.MinMaxCurve minMaxCurve = (ParticleSystem.MinMaxCurve)valueArray[i]; + float multiplier = minMaxCurve.curveMultiplier; + AnimationCurve minCurve = minMaxCurve.curveMin; + AnimationCurve maxCurve = minMaxCurve.curveMax; + EditorGUILayout.BeginVertical(); + EditorGUI.indentLevel++; + multiplier = EditorGUILayout.FloatField("Multiplier", multiplier); + minCurve = EditorGUILayout.CurveField("Min Curve", minCurve); + maxCurve = EditorGUILayout.CurveField("Max Curve", maxCurve); + EditorGUI.indentLevel--; + EditorGUILayout.EndVertical(); + valueArray[i] = new ParticleSystem.MinMaxCurve(multiplier, minCurve, maxCurve); + EditorGUILayout.Space(); + } + } + + EditorGUI.indentLevel--; + } + + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType.IsEnum) + { + Enum enumValue = (Enum)variableValue; + GUI.SetNextControlName("NodeField"); + EditorGUI.BeginChangeCheck(); + variableValue = EditorGUILayout.EnumPopup(symbol, enumValue); + if(EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + // ReSharper disable once PossibleNullReferenceException + else if(variableType.IsArray && variableType.GetElementType().IsEnum) + { + EditorGUI.BeginChangeCheck(); + EditorGUILayout.LabelField(symbol); + EditorGUILayout.BeginVertical(); + Enum[] valueArray = (Enum[])variableValue; + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if(_arrayStates.ContainsKey(symbol)) + { + showArray = _arrayStates[symbol]; + } + else + { + _arrayStates.Add(symbol, false); + } + + showArray = EditorGUILayout.Foldout(showArray, symbol); + _arrayStates[symbol] = showArray; + + if(showArray) + { + EditorGUI.indentLevel++; + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1 + ); + EditorGUILayout.Space(); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if(valueArray != null && valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.EnumPopup( + $"{i}:", + valueArray[i]); + } + } + + EditorGUI.indentLevel--; + } + + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if(variableType == typeof(Type)) + { + Type typeValue = (Type)variableValue; + EditorGUILayout.LabelField(symbol, typeValue == null ? $"Type = null" : $"Type = {typeValue.Name}"); + } + + else if(variableType.IsArray && typeof(UnityEngine.Object).IsAssignableFrom(variableType.GetElementType())) + { + Type elementType = variableType.GetElementType(); + Assert.IsNotNull(elementType); + + EditorGUI.BeginChangeCheck(); + EditorGUILayout.BeginVertical(); + GUI.SetNextControlName("NodeField"); + + bool showArray = false; + if (_arrayStates.ContainsKey(symbol)) + showArray = _arrayStates[symbol]; + else + _arrayStates.Add(symbol, false); + showArray = EditorGUILayout.Foldout( showArray, symbol, true ); + _arrayStates[symbol] = showArray; + + if(variableValue == null) + { + variableValue = Array.CreateInstance(elementType, 0); + } + + UnityEngine.Object[] valueArray = (UnityEngine.Object[])variableValue; + + if (showArray) + { + EditorGUI.indentLevel++; + + int newSize = EditorGUILayout.IntField( + "size:", + valueArray.Length > 0 ? valueArray.Length : 1); + + Array.Resize(ref valueArray, newSize); + Assert.IsNotNull(valueArray); + + if(valueArray.Length > 0) + { + for(int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + valueArray[i] = EditorGUILayout.ObjectField($"{i}:", valueArray.Length > i ? valueArray[i] : null, variableType.GetElementType(), true); + } + } + + EditorGUI.indentLevel--; + } + + EditorGUILayout.EndVertical(); + if(EditorGUI.EndChangeCheck()) + { + Array destinationArray = Array.CreateInstance(elementType, valueArray.Length); + Array.Copy(valueArray, destinationArray, valueArray.Length); + + variableValue = destinationArray; + + dirty = true; + } + } + else if (variableType == typeof(VRC.SDKBase.VRCUrl)) + { + if(variableValue == null) + variableValue = new VRC.SDKBase.VRCUrl(""); + + VRC.SDKBase.VRCUrl url = (VRC.SDKBase.VRCUrl)variableValue; + EditorGUI.BeginChangeCheck(); + variableValue = new VRC.SDKBase.VRCUrl(EditorGUILayout.TextField(symbol, url.Get())); + + if (EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else if (variableType == typeof(VRC.SDKBase.VRCUrl[])) + { + EditorGUI.BeginChangeCheck(); + EditorGUILayout.BeginVertical(); + + GUI.SetNextControlName("NodeField"); + bool showArray = false; + if (_arrayStates.ContainsKey(symbol)) + showArray = _arrayStates[symbol]; + else + _arrayStates.Add(symbol, false); + showArray = EditorGUILayout.Foldout( showArray, symbol, true ); + _arrayStates[symbol] = showArray; + + VRC.SDKBase.VRCUrl[] valueArray = (VRC.SDKBase.VRCUrl[])variableValue; + + if (showArray) + { + EditorGUI.indentLevel++; + EditorGUILayout.Space(); + int newSize = EditorGUILayout.IntField( + "size:", + valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1); + newSize = newSize >= 0 ? newSize : 0; + Array.Resize(ref valueArray, newSize); + + if (valueArray != null && valueArray.Length > 0) + { + for (int i = 0; i < valueArray.Length; i++) + { + GUI.SetNextControlName("NodeField"); + if (valueArray[i] == null) + valueArray[i] = new VRC.SDKBase.VRCUrl(""); + + valueArray[i] = new VRC.SDKBase.VRCUrl( + EditorGUILayout.TextField( + $"{i}:", + valueArray.Length > i ? valueArray[i].Get() : "")); + } + } + EditorGUI.indentLevel--; + } + + EditorGUILayout.EndVertical(); + if (EditorGUI.EndChangeCheck()) + { + variableValue = valueArray; + dirty = true; + } + } + else if (variableType == typeof(LayerMask)) + { + LayerMask maskValue = (LayerMask)variableValue; + GUI.SetNextControlName("NodeField"); + EditorGUI.BeginChangeCheck(); + EditorGUILayout.LabelField(symbol); + // Using workaround from http://answers.unity.com/answers/1387522/view.html + LayerMask tempMask = EditorGUILayout.MaskField(InternalEditorUtility.LayerMaskToConcatenatedLayersMask(maskValue), InternalEditorUtility.layers); + variableValue = InternalEditorUtility.ConcatenatedLayersMaskToLayerMask(tempMask); + if (EditorGUI.EndChangeCheck()) + { + dirty = true; + } + } + else + { + EditorGUILayout.LabelField(symbol + " no defined editor for type of " + variableType); + } + // ReSharper restore RedundantNameQualifier + + IUdonSyncMetadata sync = program.SyncMetadataTable.GetSyncMetadataFromSymbol(symbol); + if(sync != null) + { + GUILayout.Label($"sync{sync.Properties[0].InterpolationAlgorithm.ToString()}", GUILayout.Width(80)); + } + } + + EditorGUILayout.EndHorizontal(); + + return variableValue; + } + + #region Serialization Methods + + void ISerializationCallbackReceiver.OnAfterDeserialize() + { + OnAfterDeserialize(); + } + + void ISerializationCallbackReceiver.OnBeforeSerialize() + { + OnBeforeSerialize(); + } + + [PublicAPI] + protected virtual void OnAfterDeserialize() + { + } + + [PublicAPI] + protected virtual void OnBeforeSerialize() + { + } + + #endregion + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs.meta new file mode 100644 index 00000000..2495f546 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 264ec3c8a1d423f42a144da0df6c5ebe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs new file mode 100644 index 00000000..56a21a85 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs @@ -0,0 +1,19 @@ +using UnityEditor; + +namespace VRC.Udon.Editor.ProgramSources +{ + [CustomEditor(typeof(UdonProgramAsset))] + public class UdonProgramAssetEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + bool dirty = false; + UdonProgramAsset programAsset = (UdonProgramAsset)target; + programAsset.RunEditorUpdate(null, ref dirty); + if(dirty) + { + EditorUtility.SetDirty(target); + } + } + } +} diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs.meta new file mode 100644 index 00000000..3f085920 --- /dev/null +++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41d70977fa7936441afe41442f1862b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: |