using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Experimental.UIElements.GraphView; using UnityEngine; using UnityGraph = UnityEditor.Experimental.UIElements.GraphView; using VRC.Udon.Graph; using VRC.Udon.Serialization; using UnityEngine.Experimental.UIElements; using UnityEditor.SceneManagement; namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView { public static class UdonGraphCommands { public const string Reserialize = "Reserialize"; public const string Reload = "Reload"; public const string SaveNewData = "SaveNewData"; } 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; // Tracking variables private List _variablePopupOptions = new List(); private List _variableNodes = new List(); private Vector2 lastMousePosition; private VisualElement mouseTipContainer; private TextElement mouseTip; private Vector2 mouseTipOffset = new Vector2(20, -22); private UdonSearchManager _searchManager; private bool _reloading = false; public bool IsReloading => _reloading; // Enable stuff from NodeGraphProcessor private UdonGraphWindow _window; public List GetVariableNames { get => _variablePopupOptions; private set { } } public List GetVariableNodes { get => _variableNodes; private set { } } public UdonGraph(UdonGraphWindow window) { _window = window; this.StretchToParentSize(); SetupBackground(); SetupMap(); SetupBlackboard(); SetupZoom(0.2f, 3); SetupDragAndDrop(); this.AddManipulator(new UnityGraph.ContentDragger()); this.AddManipulator(new UnityGraph.SelectionDragger()); this.AddManipulator(new UnityGraph.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 a "Reserialize" command from updated port fields RegisterCallback(OnExecuteCommand); // Save last known mouse position for better pasting. Is there a performance hit for this? RegisterCallback(OnMouseMove); RegisterCallback(OnKeyDown); _searchManager = new UdonSearchManager(this, window); elementsInsertedToStackNode = OnStackNodeInsert; elementsRemovedFromStackNode = OnStackNodeRemove; 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>(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) { _udonBehaviour = udonBehaviour; } graphData = new UdonGraphData(graphProgramAsset.GetGraphData()); EditorApplication.update += DelayedReload; EditorApplication.update += DelayedRestoreViewFromData; // When pressing ctrl-s, we save the graph UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += _ => SaveGraphToDisk(); } private void DelayedRestoreViewFromData() { EditorApplication.update -= DelayedRestoreViewFromData; UpdateViewTransform(graphProgramAsset.viewTransform.position, Vector3.one * graphProgramAsset.viewTransform.scale); } 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); } } } 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); } } ReSerializeData(true); Reload(); }, DropdownMenu.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); } } ReSerializeData(); Reload(); }, DropdownMenu.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); SaveNewData(); }, DropdownMenu.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 layout = mouseTipContainer.layout; layout.position = position + mouseTipOffset; mouseTipContainer.layout = layout; } } public bool IsDuplicateEventNode(string fullName) { 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; } } return false; } private string OnSerializeGraphElements(IEnumerable selection) { Bounds bounds = new Bounds(); bool startedBounds = false; List nodeData = new List(); List variables = new List(); 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") { // 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(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>(UdonGraphExtensions.UnZipString(pasteData)) .value; } catch { //oof ouch that's not valid data return; } var copiedNodeDataList = new List(); // 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)) { 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(); // copy modified list back to array copiedNodeDataArray = copiedNodeDataList.ToArray(); _reloading = true; var graphMousePosition = GetRectFromMouse().position; List pastedNodes = new List(); Dictionary uidMap = new Dictionary(); 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]; 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(newNodeDataArray[i], this); if (udonNode != null) { graphData.nodes.Add(newNodeDataArray[i]); pastedNodes.Add(udonNode); } } _reloading = false; Reload(); // Select all newly-pasted nodes after reload // TODO: figure out why this doesn't work! foreach (var item in pastedNodes) { AddToSelection(item as GraphElement); } } // 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; // Remove node from Data when removed from Graph if (!_reloading && changes.elementsToRemove != null && changes.elementsToRemove.Count > 0) { var newElementsData = graphProgramAsset.graphElementData.ToList(); foreach (var element in changes.elementsToRemove) { if (element is UdonNode) { var nodeData = ((UdonNode)element).data; graphData.RemoveNode(nodeData); continue; } if (element is Edge) { Undo.RecordObject(graphProgramAsset, $"delete-{element.name}"); continue; } if (element is UdonParameterField) { var pField = element as UdonParameterField; if (graphData.nodes.Contains(pField.Data)) { graphData.nodes.Remove(pField.Data); EditorApplication.update += DelayedReload; } } // not an UdonNode or Edge, it's an element serialized in graphElementData var graphElement = newElementsData.Find(e => e.uid.CompareTo(element.persistenceKey) == 0); if (graphElement != null) { Undo.RecordObject(graphProgramAsset, $"delete-{element.name}"); newElementsData.Remove(graphElement); } } if (newElementsData.Count < graphProgramAsset.graphElementData.Length) { graphProgramAsset.graphElementData = newElementsData.ToArray(); } ClearSelection(); dirty = true; } if (dirty) { MarkSceneDirty(); EditorApplication.update += DelayedReserialize; } return changes; } #region StackNode change listeners private void OnStackNodeInsert(StackNode node, int index, IEnumerable elements) { (node as UdonStackNode).OnElementsAdded(elements); } private void OnStackNodeRemove(StackNode node, IEnumerable elements) { (node as UdonStackNode).OnElementsRemoved(elements); } #endregion void DelayedReload() { 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 field, string newValue) { ReSerializeData(); EditorApplication.update += DelayedReload; } private void BlackboardAddVariable(Blackboard obj) { var screenPosition = GUIUtility.GUIToScreenPoint(lastMousePosition); _searchManager.OpenVariableSearch(screenPosition); } public void OpenPortSearch(Type type, Vector2 position, Port output, Direction direction) { _searchManager.OpenPortSearch(type, position, output, direction); } private void SetupMap() { _map = new UdonMinimap(this); Add(_map); } private bool _reserializeNextUpdate = false; private void OnExecuteCommand(ExecuteCommandEvent evt) { switch (evt.commandName) { case UdonGraphCommands.Reload: Reload(); break; case UdonGraphCommands.Reserialize: if (!_reloading && !_reserializeNextUpdate) { _reserializeNextUpdate = true; EditorApplication.update += DelayedReserialize; } break; case UdonGraphCommands.SaveNewData: if (!_reloading) { SaveNewData(); } break; default: break; } } // collects all serialization requests from a single frame and batches them for next frame private void DelayedReserialize() { _reserializeNextUpdate = false; EditorApplication.update -= DelayedReserialize; ReSerializeData(true); } public override List 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; } public void Reload() { _reloading = true; // Is there a better place to do this? string customStyle = "UdonGraphNeonStyle"; if (Settings.UseNeonStyle && !HasStyleSheetPath(customStyle)) { AddStyleSheetPath(customStyle); } else if (!Settings.UseNeonStyle && HasStyleSheetPath(customStyle)) { RemoveStyleSheetPath(customStyle); } 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(); // add all nodes to graph for (int i = graphData.nodes.Count - 1; i >= 0; i--) { UdonNodeData nodeData = graphData.nodes[i]; // Check for Node type - create nodes, separate out Variables if (nodeData.fullName.StartsWithCached("Variable_")) { UdonNodeDefinition definition = UdonEditorManager.Instance.GetNodeDefinition(nodeData.fullName); if (definition != null) { _blackboard.Insert(0, new BlackboardRow(new UdonParameterField(this, nodeData), new UdonParameterProperty(this, definition, 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); } // Remove from data, no longer a node graphData.nodes.RemoveAt(i); } else { UdonNode udonNode = UdonNode.CreateNode(nodeData, this); AddElement(udonNode); if (udonNode != null) continue; Debug.Log($"Removing null node '{nodeData.fullName}'"); graphData.nodes.RemoveAt(i); } } //return; // reconnect nodes nodes.ForEach((genericNode) => { UdonNode udonNode = (UdonNode)genericNode; udonNode.RestoreConnections(); }); // Add all Graph Elements if (graphProgramAsset.graphElementData != null) { foreach (var elementData in graphProgramAsset.graphElementData) { GraphElement element = RestoreElementFromData(elementData); if (element != null) { AddElement(element); } } } _reloading = false; ReSerializeData(false); } // 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.UdonStackNode: { return UdonStackNode.Create(data, this); } 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() { graphData = new UdonGraphData(graphProgramAsset.GetGraphData()); Reload(); } private void RefreshVariables() { _variableNodes = graphData.nodes .Where(n => n.fullName.StartsWithCached("Variable_")).Where(n => n.nodeValues.Length > 1 && n.nodeValues[1] != null).ToList(); _variablePopupOptions = _variableNodes.Select(s => (string)s.nodeValues[1].Deserialize()).ToList(); } // 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); ReSerializeData(false); Reload(); return newVarUid; } public void RemoveNodeData(UdonNodeData nodeData) { if (graphData.nodes.Contains(nodeData)) { graphData.nodes.Remove(nodeData); } Reload(); } public void ReSerializeData(bool saveNewData = true) { // While reloading, we delete and re-add all the data, so we don't want to serialize during that time. if (_reloading) { return; } if (graphProgramAsset == null) { Debug.LogError("Can't Reserialize without an asset"); return; } SerializedObject serializedGraphProgramAsset = new SerializedObject((UdonGraphProgramAsset)graphProgramAsset); SerializedProperty graphDataProperty = serializedGraphProgramAsset.FindProperty("graphData"); SerializedProperty nodesProperty = graphDataProperty.FindPropertyRelative("nodes"); if (nodesProperty.arraySize > graphData.nodes.Count) { nodesProperty.ClearArray(); } for (int i = 0; i < graphData.nodes.ToList().Count; i++) { if (nodesProperty.arraySize < graphData.nodes.Count) { nodesProperty.InsertArrayElementAtIndex(i); } SerializedProperty nodeProperty = nodesProperty.GetArrayElementAtIndex(i); SerializedProperty fullNameProperty = nodeProperty.FindPropertyRelative("fullName"); fullNameProperty.stringValue = graphData.nodes[i].fullName; SerializedProperty uidProperty = nodeProperty.FindPropertyRelative("uid"); uidProperty.stringValue = graphData.nodes[i].uid; SerializedProperty positionProperty = nodeProperty.FindPropertyRelative("position"); positionProperty.vector2Value = graphData.nodes[i].position; SerializedProperty nodeUIDsProperty = nodeProperty.FindPropertyRelative("nodeUIDs"); while (nodeUIDsProperty.arraySize > graphData.nodes[i].nodeUIDs.Length) { nodeUIDsProperty.DeleteArrayElementAtIndex(nodeUIDsProperty.arraySize - 1); } for (int j = 0; j < graphData.nodes[i].nodeUIDs.Length; j++) { if (nodeUIDsProperty.arraySize < graphData.nodes[i].nodeUIDs.Length) { nodeUIDsProperty.InsertArrayElementAtIndex(j); nodeUIDsProperty.GetArrayElementAtIndex(j).stringValue = ""; } SerializedProperty nodeUIDProperty = nodeUIDsProperty.GetArrayElementAtIndex(j); nodeUIDProperty.stringValue = graphData.nodes[i].nodeUIDs[j]; } SerializedProperty flowUIDsProperty = nodeProperty.FindPropertyRelative("flowUIDs"); while (flowUIDsProperty.arraySize > graphData.nodes[i].flowUIDs.Length) { flowUIDsProperty.DeleteArrayElementAtIndex(flowUIDsProperty.arraySize - 1); } for (int j = 0; j < graphData.nodes[i].flowUIDs.Length; j++) { if (flowUIDsProperty.arraySize < graphData.nodes[i].flowUIDs.Length) { flowUIDsProperty.InsertArrayElementAtIndex(j); flowUIDsProperty.GetArrayElementAtIndex(j).stringValue = ""; } SerializedProperty flowUIDProperty = flowUIDsProperty.GetArrayElementAtIndex(j); flowUIDProperty.stringValue = graphData.nodes[i].flowUIDs[j]; } SerializedProperty nodeValuesProperty = nodeProperty.FindPropertyRelative("nodeValues"); while (nodeValuesProperty.arraySize > graphData.nodes[i].nodeValues.Length) { nodeValuesProperty.DeleteArrayElementAtIndex(nodeValuesProperty.arraySize - 1); } for (int j = 0; j < graphData.nodes[i].nodeValues.Length; j++) { if (nodeValuesProperty.arraySize < graphData.nodes[i].nodeValues.Length) { nodeValuesProperty.InsertArrayElementAtIndex(j); nodeValuesProperty.GetArrayElementAtIndex(j).FindPropertyRelative("unityObjectValue").objectReferenceValue = null; nodeValuesProperty.GetArrayElementAtIndex(j).FindPropertyRelative("stringValue").stringValue = ""; } SerializedProperty nodeValueProperty = nodeValuesProperty.GetArrayElementAtIndex(j); if (graphData.nodes[i].nodeValues[j] == null) { continue; } object nodeValue = graphData.nodes[i].nodeValues[j].Deserialize(); if (nodeValue != null) { if (nodeValue is UnityEngine.Object value) { if (value != null) { nodeValueProperty.FindPropertyRelative("unityObjectValue").objectReferenceValue = graphData.nodes[i].nodeValues[j].unityObjectValue; } } } nodeValueProperty.FindPropertyRelative("stringValue").stringValue = graphData.nodes[i].nodeValues[j].stringValue; } var baseNode = GetNodeByGuid(graphData.nodes[i].uid); if (baseNode != null) { UdonNode node = baseNode as UdonNode; node.data = graphData.nodes[i]; } } serializedGraphProgramAsset.ApplyModifiedProperties(); if (saveNewData) { SaveNewData(); } if (graphProgramAsset is AbstractUdonProgramSource udonProgramSource) { UdonEditorManager.Instance.QueueAndRefreshProgram(udonProgramSource); } } // Copied from source code, this is what happens when you press 'a' on the keyboard public void Recenter() { Rect rectToFit; Vector3 frameTranslation = Vector3.zero; Vector3 frameScaling = Vector3.one; rectToFit = CalculateRectToFitAll(contentViewContainer); CalculateFrameTransform(rectToFit, layout, 30, out frameTranslation, out frameScaling); Matrix4x4.TRS(frameTranslation, Quaternion.identity, frameScaling); UpdateViewTransform(frameTranslation, frameScaling); contentViewContainer.MarkDirtyRepaint(); } public void SaveNewData() { List elementData = new List(); graphElements.ForEach((element) => { // save data from each element that can provide UdonGraphElementData if (element is IUdonGraphElementDataProvider) { var data = ((IUdonGraphElementDataProvider)element).GetData(); elementData.Add(data); } }); // add blackboard data elementData.Add(_blackboard.GetData()); elementData.Add(_map.GetData()); // Save new data to asset if (graphProgramAsset != null) { graphProgramAsset.graphElementData = elementData.ToArray(); EditorUtility.SetDirty(graphProgramAsset); AssetDatabase.SaveAssets(); } } #region Drag and Drop Support private void SetupDragAndDrop() { RegisterCallback(OnDragPerform, TrickleDown.TrickleDown); RegisterCallback(OnDragUpdated); RegisterCallback(OnDragExited); RegisterCallback((e)=>OnDragExited(null)); } private void OnDragUpdated(DragUpdatedEvent e) { MoveMouseTip(e.mousePosition); var dragData = DragAndDrop.GetGenericData("DragSelection") as List; bool dragging = false; if (dragData != null) { // Handle drag from exposed parameter view if (dragData.OfType().Any()) { dragging = true; SetMouseTip(e.ctrlKey ? "Set Variable" : "Get Variable\n+Ctrl: Set Variable" ); } } if (DragAndDrop.objectReferences.Length == 1 && DragAndDrop.objectReferences[0] != null) { var target = DragAndDrop.objectReferences[0]; switch (target) { case GameObject g: case Component c: dragging = true; break; } } if (dragging) { DragAndDrop.visualMode = e.ctrlKey ? DragAndDropVisualMode.Link : DragAndDropVisualMode.Copy; } } private void OnDragPerform(DragPerformEvent e) { var graphMousePosition = this.contentViewContainer.WorldToLocal(e.mousePosition); var draggedVariables = DragAndDrop.GetGenericData("DragSelection") as List; if (draggedVariables != null) { // Handle Drop of Variables var parameters = draggedVariables.OfType(); if (parameters.Any()) { RefreshVariables(); var nodes = new List(); foreach (var parameter in parameters) { // Make Setter if ctrl is held, otherwise make Getter UdonNode udonNode = MakeVariableNode(parameter.Data.uid, graphMousePosition, !e.ctrlKey); AddElement(udonNode); } ReSerializeData(); } } // 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; } } } private void OnDragExited(DragExitedEvent e) { SetMouseTip(""); } #endregion public UdonNode MakeVariableNode(string selectedUid, Vector2 graphMousePosition, bool isGetter) { string defName = isGetter ? "Get_Variable" : "Set_Variable"; var definition = UdonEditorManager.Instance.GetNodeDefinition(defName); var nodeData = this.graphData.AddNode(definition.fullName); nodeData.nodeValues = new SerializableObjectContainer[1]; nodeData.nodeUIDs = new string[1]; nodeData.nodeValues[0] = SerializableObjectContainer.Serialize(selectedUid); nodeData.position = graphMousePosition; var udonNode = UdonNode.CreateNode(nodeData, this); return udonNode; } private string GetUnusedVariableNameLike(string newVariableName) { int defaultCount = _variableNodes.Count(n => ((string)n.nodeValues[1].Deserialize()).Contains(newVariableName)); if (defaultCount > 0) { newVariableName = string.Concat(newVariableName, defaultCount + 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 void SetupDraggedObject(UnityEngine.Object o, Vector2 graphMousePosition) { // create new Component variable and add to graph string variableType = $"Variable_{o.GetType()}".SanitizeVariableName(); // handle special case for UdonBehaviours variableType = variableType.Replace("UdonBehaviour", "CommonInterfacesIUdonEventReceiver"); string variableName = GetUnusedVariableNameLike(o.name.SanitizeVariableName()); SetMouseTip($"Made {variableName}"); string uid = AddNewVariable(variableType, variableName, true); RefreshVariables(); object target = o; // Cast component to expected type if (o is Component) target = Convert.ChangeType(o, o.GetType()); var variableNode = MakeVariableNode(uid, graphMousePosition, true); AddElement(variableNode); variableNode.Reserialize(); LinkAfterCompile(variableName, target); } [Serializable] public class ViewTransformData { public Vector2 position = Vector2.zero; public float scale = 1f; } } }