1189 lines
44 KiB
C#
1189 lines
44 KiB
C#
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<string> _variablePopupOptions = new List<string>();
|
|
private List<UdonNodeData> _variableNodes = new List<UdonNodeData>();
|
|
|
|
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<string> GetVariableNames { get => _variablePopupOptions; private set { } }
|
|
public List<UdonNodeData> 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<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);
|
|
|
|
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<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)
|
|
{
|
|
_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<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")
|
|
{
|
|
// 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))
|
|
{
|
|
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<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];
|
|
|
|
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<GraphElement> elements)
|
|
{
|
|
(node as UdonStackNode).OnElementsAdded(elements);
|
|
}
|
|
|
|
private void OnStackNodeRemove(StackNode node, IEnumerable<GraphElement> 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<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;
|
|
}
|
|
|
|
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<UdonGraphElementData> elementData = new List<UdonGraphElementData>();
|
|
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<DragPerformEvent>(OnDragPerform, TrickleDown.TrickleDown);
|
|
RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
|
|
RegisterCallback<DragExitedEvent>(OnDragExited);
|
|
RegisterCallback<DragLeaveEvent>((e)=>OnDragExited(null));
|
|
}
|
|
|
|
private void OnDragUpdated(DragUpdatedEvent e)
|
|
{
|
|
|
|
MoveMouseTip(e.mousePosition);
|
|
|
|
var dragData = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>;
|
|
bool dragging = false;
|
|
|
|
if (dragData != null)
|
|
{
|
|
// Handle drag from exposed parameter view
|
|
if (dragData.OfType<UdonParameterField>().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<ISelectable>;
|
|
|
|
if (draggedVariables != null)
|
|
{
|
|
// Handle Drop of Variables
|
|
var parameters = draggedVariables.OfType<UdonParameterField>();
|
|
if (parameters.Any())
|
|
{
|
|
RefreshVariables();
|
|
var nodes = new List<UdonNode>();
|
|
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;
|
|
}
|
|
}
|
|
}
|