591 lines
24 KiB
C#
591 lines
24 KiB
C#
// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved.
|
|
// This code can only be used under the standard Unity Asset Store End User License Agreement
|
|
// A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Doozy.Runtime.Common.Extensions;
|
|
using Doozy.Runtime.Nody;
|
|
using Doozy.Runtime.Nody.Nodes.Internal;
|
|
using Doozy.Runtime.Nody.Nodes.System;
|
|
using Doozy.Runtime.UIElements.Extensions;
|
|
using UnityEditor;
|
|
using UnityEditor.Experimental.GraphView;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using FlowGraph = Doozy.Runtime.Nody.FlowGraph;
|
|
using Object = UnityEngine.Object;
|
|
// ReSharper disable MemberCanBePrivate.Global
|
|
// ReSharper disable UnusedAutoPropertyAccessor.Local
|
|
// ReSharper disable ForCanBeConvertedToForeach
|
|
// ReSharper disable LoopCanBeConvertedToQuery
|
|
|
|
namespace Doozy.Editor.Nody
|
|
{
|
|
public class FlowGraphView : GraphView
|
|
{
|
|
public Action<FlowNodeView> OnNodeSelected;
|
|
|
|
public FlowGraph flowGraph { get; internal set; }
|
|
public NodyNodeSearchWindow searchWindow { get; private set; }
|
|
|
|
private GridBackground gridBackground { get; set; }
|
|
|
|
public Vector2 cachedMousePosition { get; private set; }
|
|
public Vector2 graphMousePosition => contentViewContainer.WorldToLocal(cachedMousePosition);
|
|
|
|
public ContentDragger contentDragger { get; }
|
|
public SelectionDragger selectionDragger { get; }
|
|
public RectangleSelector rectangleSelector { get; }
|
|
|
|
private IEnumerable<FlowPortView> portViews => ports.ToList().Cast<FlowPortView>();
|
|
|
|
public FlowGraphView()
|
|
{
|
|
InjectGridBackground();
|
|
CreateSearchWindow();
|
|
|
|
this.SetStyleFlexGrow(1);
|
|
this.AddManipulator(contentDragger = new ContentDragger());
|
|
this.AddManipulator(selectionDragger = new SelectionDragger());
|
|
this.AddManipulator(rectangleSelector = new RectangleSelector());
|
|
SetupZoom(0.1f, 5f);
|
|
|
|
RegisterCallback<MouseMoveEvent>(OnMouseMoveEvent);
|
|
Undo.undoRedoPerformed -= UndoRedoPerformed;
|
|
Undo.undoRedoPerformed += UndoRedoPerformed;
|
|
}
|
|
|
|
public void ClearGraphView(bool clearInspector)
|
|
{
|
|
graphViewChanged -= OnGraphViewChanged;
|
|
DeleteElements(graphElements.ToList());
|
|
graphViewChanged += OnGraphViewChanged;
|
|
if (clearInspector) ClearInspector();
|
|
}
|
|
|
|
public void RefreshNodeViews()
|
|
{
|
|
if (flowGraph == null) return;
|
|
graphViewChanged -= OnGraphViewChanged;
|
|
DeleteElements(nodes);
|
|
graphViewChanged += OnGraphViewChanged;
|
|
for (int i = 0; i < flowGraph.nodes.Count; i++)
|
|
{
|
|
FlowNode node = flowGraph.nodes[i];
|
|
FlowNodeView nodeView = CreateNodeView(node);
|
|
nodeView.UpdatePresenterPosition();
|
|
}
|
|
}
|
|
|
|
public void RefreshEdges()
|
|
{
|
|
if (flowGraph == null) return;
|
|
graphViewChanged -= OnGraphViewChanged;
|
|
DeleteElements(edges);
|
|
graphViewChanged += OnGraphViewChanged;
|
|
for (int i = 0; i < flowGraph.outputPorts.Count; i++)
|
|
{
|
|
FlowPort outputPort = flowGraph.outputPorts[i];
|
|
if (!outputPort.isConnected) continue;
|
|
FlowPortView outputPortView = GetPortView(outputPort);
|
|
if (outputPortView == null) continue;
|
|
for (int j = 0; j < outputPort.connections.Count; j++)
|
|
{
|
|
string inputPortId = outputPort.connections[j];
|
|
FlowPortView inputPortView = GetPortView(inputPortId);
|
|
if (inputPortView == null) continue;
|
|
FlowEdgeView edgeView = outputPortView.ConnectTo(inputPortView);
|
|
AddElement(edgeView);
|
|
}
|
|
}
|
|
}
|
|
|
|
public FlowNode CreateNode(Type type, bool createView = false, bool recordUndo = true)
|
|
{
|
|
if (flowGraph == null) return null;
|
|
var node = ScriptableObject.CreateInstance(type) as FlowNode;
|
|
Debug.Assert(node != null, nameof(node) + " != null");
|
|
node.name = ObjectNames.NicifyVariableName(type.Name.Replace("Node", ""));
|
|
if (recordUndo) Undo.RecordObject(flowGraph, $"{ObjectNames.NicifyVariableName(nameof(CreateNode))}");
|
|
flowGraph.nodes.Add(node);
|
|
node.SetFlowGraph(flowGraph);
|
|
node.SetPosition(graphMousePosition);
|
|
EditorUtility.SetDirty(flowGraph);
|
|
AssetDatabase.AddObjectToAsset(node, flowGraph);
|
|
if (recordUndo) Undo.RegisterCreatedObjectUndo(node, $"{ObjectNames.NicifyVariableName(nameof(CreateNode))}");
|
|
// AssetDatabase.SaveAssets();
|
|
AssetDatabase.SaveAssetIfDirty(node);
|
|
AssetDatabase.SaveAssetIfDirty(flowGraph);
|
|
if (createView) CreateNodeView(node);
|
|
return node;
|
|
}
|
|
|
|
public FlowNodeView GetNodeView(FlowNode flowNode)
|
|
{
|
|
return GetNodeByGuid(flowNode.nodeId) as FlowNodeView;
|
|
}
|
|
|
|
public void CenterGraphOnNode(FlowNode node, bool selectNode = false)
|
|
{
|
|
schedule.Execute(() =>
|
|
{
|
|
if (flowGraph == null) return;
|
|
if (node == null) return;
|
|
FlowNodeView nodeView = GetNodeView(node);
|
|
ClearSelection();
|
|
AddToSelection(nodeView);
|
|
FrameSelection();
|
|
if (selectNode) return;
|
|
ClearSelection();
|
|
});
|
|
}
|
|
|
|
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
|
|
{
|
|
// Vector2 mousePosition = evt.mousePosition;
|
|
|
|
if (evt.target is FlowPortView flowPortView)
|
|
{
|
|
evt.menu.AppendAction
|
|
(
|
|
"Delete",
|
|
dropdownMenuAction => DeletePort(flowPortView),
|
|
dropdownMenuAction => flowPortView.canDeletePort ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled
|
|
);
|
|
return;
|
|
}
|
|
|
|
base.BuildContextualMenu(evt);
|
|
if (evt.target is GraphView || evt.target is Node || evt.target is Group)
|
|
{
|
|
if (evt.target is FlowNodeView)
|
|
{
|
|
evt.menu.RemoveItemAt(1);
|
|
evt.menu.RemoveItemAt(1);
|
|
evt.menu.RemoveItemAt(1);
|
|
evt.menu.RemoveItemAt(1);
|
|
evt.menu.RemoveItemAt(2);
|
|
evt.menu.RemoveItemAt(2);
|
|
}
|
|
else
|
|
{
|
|
|
|
evt.menu.RemoveItemAt(1);
|
|
evt.menu.RemoveItemAt(1);
|
|
evt.menu.RemoveItemAt(1);
|
|
evt.menu.RemoveItemAt(1);
|
|
evt.menu.RemoveItemAt(1);
|
|
evt.menu.RemoveItemAt(3);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void AddToSelection(ISelectable selectable)
|
|
{
|
|
base.AddToSelection(selectable);
|
|
if (selection.Count > 1)
|
|
{
|
|
OnNodeSelected?.Invoke(null);
|
|
return;
|
|
}
|
|
|
|
if (selectable is FlowNodeView nodeView)
|
|
OnNodeSelected?.Invoke(nodeView);
|
|
}
|
|
|
|
public override void RemoveFromSelection(ISelectable selectable)
|
|
{
|
|
base.RemoveFromSelection(selectable);
|
|
|
|
if (selection.Count > 1)
|
|
OnNodeSelected?.Invoke(null);
|
|
|
|
switch (selection.LastOrDefault())
|
|
{
|
|
case null:
|
|
return;
|
|
case FlowNodeView nodeView:
|
|
OnNodeSelected?.Invoke(nodeView);
|
|
return;
|
|
default:
|
|
OnNodeSelected?.Invoke(null);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public override void ClearSelection()
|
|
{
|
|
base.ClearSelection();
|
|
OnNodeSelected?.Invoke(null);
|
|
}
|
|
|
|
public override EventPropagation DeleteSelection()
|
|
{
|
|
EventPropagation result = base.DeleteSelection();
|
|
OnNodeSelected?.Invoke(null);
|
|
return result;
|
|
}
|
|
|
|
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
|
|
{
|
|
return
|
|
ports
|
|
.ToList()
|
|
.Where(endPort => endPort.direction != startPort.direction && endPort.node != startPort.node)
|
|
.ToList();
|
|
}
|
|
|
|
internal void PopulateView(FlowGraph graphReference)
|
|
{
|
|
ClearGraphView(true); // clear the view (from the previous graph)
|
|
flowGraph = graphReference; // set graph reference
|
|
if (flowGraph == null) return; // graph null -> stop
|
|
CleanGraph(); // clean graph (remove nulls)
|
|
AddRootNode(); // add root node (if missing)
|
|
flowGraph.UpdateNodes(); // update nodes
|
|
RefreshNodeViews(); // create node views
|
|
RefreshEdges(); // create edges
|
|
}
|
|
|
|
private void InjectGridBackground()
|
|
{
|
|
Insert(0, gridBackground = new GridBackground()); //ToDo: check if a simple image would not be better as the background
|
|
|
|
// gridBackground.RegisterCallback<CustomStyleResolvedEvent>(evt =>
|
|
// {
|
|
// typeof(GridBackground).GetField("m_GridBackgroundColor", BindingFlags.NonPublic | BindingFlags.Instance)?
|
|
// .SetValue(gridBackground, EditorColors.Nody.GridBackground);
|
|
//
|
|
// typeof(GridBackground).GetField("m_LineColor", BindingFlags.NonPublic | BindingFlags.Instance)?
|
|
// .SetValue(gridBackground, EditorColors.Nody.LineColor);
|
|
//
|
|
// typeof(GridBackground).GetField("m_ThickLineColor", BindingFlags.NonPublic | BindingFlags.Instance)?
|
|
// .SetValue(gridBackground, EditorColors.Nody.ThickLineColor);
|
|
//
|
|
// typeof(GridBackground).GetField("m_Spacing", BindingFlags.NonPublic | BindingFlags.Instance)?
|
|
// .SetValue(gridBackground, 12);
|
|
// });
|
|
}
|
|
|
|
private void CreateSearchWindow()
|
|
{
|
|
searchWindow =
|
|
ScriptableObject
|
|
.CreateInstance<NodyNodeSearchWindow>()
|
|
.SetGraphView(this);
|
|
|
|
nodeCreationRequest =
|
|
context =>
|
|
SearchWindow.Open
|
|
(
|
|
new SearchWindowContext(context.screenMousePosition),
|
|
searchWindow
|
|
);
|
|
}
|
|
|
|
private void OnMouseMoveEvent(IMouseEvent evt)
|
|
{
|
|
cachedMousePosition = evt.mousePosition;
|
|
}
|
|
|
|
private void UndoRedoPerformed()
|
|
{
|
|
// Debug.Log($"UndoRedoPerformed");
|
|
// ClearGraphView(false); //clear graph view
|
|
|
|
if (flowGraph == null) return; //stop if no graph is loaded
|
|
var tempSelection = selection.ToList(); //save current selected
|
|
flowGraph.UpdateNodes(); //update nodes
|
|
RefreshNodeViews(); //create node views
|
|
RefreshEdges(); //create edges
|
|
EditorUtility.SetDirty(flowGraph); //mark graph as dirty
|
|
tempSelection.RemoveNulls(); //remove nulls from temp selection (in case a selected thing got deleted)
|
|
tempSelection.ForEach(AddToSelection); //add temp back to selection
|
|
// AssetDatabase.SaveAssetIfDirty(flowGraph);
|
|
// AssetDatabase.SaveAssets();
|
|
// NodyInspectorWindow.instance.UpdateSelection((FlowNodeView)selection.FirstOrDefault());
|
|
}
|
|
|
|
/// <summary> Deselect current loaded flowGraph and any of its node (if selected) </summary>
|
|
private void ClearInspector()
|
|
{
|
|
if (flowGraph == null) return;
|
|
if (Selection.activeObject == flowGraph) //check if the graph asset is selected in the Inspector -> if it is -> deselect the graph
|
|
{
|
|
Selection.activeObject = null;
|
|
}
|
|
else //the graph was not selected in the Inspector -> check each node
|
|
{
|
|
for (int i = 0; i < flowGraph.nodes.Count; i++)
|
|
{
|
|
FlowNode node = flowGraph.nodes[i];
|
|
if (Selection.activeObject != node)
|
|
continue;
|
|
Selection.activeObject = null; //one of the graph's nodes was selected in the Inspector -> deselect the node
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private FlowPortView GetPortView(FlowPort flowPort)
|
|
{
|
|
foreach (FlowPortView portView in portViews)
|
|
if (portView.flowPort == flowPort)
|
|
return portView;
|
|
|
|
return null;
|
|
}
|
|
|
|
private FlowPortView GetPortView(string portId)
|
|
{
|
|
foreach (FlowPortView portView in portViews)
|
|
if (portView.flowPort.portId == portId)
|
|
return portView;
|
|
|
|
return null;
|
|
}
|
|
|
|
private GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
|
|
{
|
|
graphViewChange.elementsToRemove?.ToList().ForEach(element =>
|
|
{
|
|
switch (element)
|
|
{
|
|
case null:
|
|
return;
|
|
|
|
//Edge Removal
|
|
case FlowEdgeView edgeView:
|
|
DisconnectPorts(edgeView);
|
|
break;
|
|
|
|
//Node Removal
|
|
case FlowNodeView nodeView:
|
|
DeleteNode(nodeView);
|
|
break;
|
|
}
|
|
|
|
});
|
|
|
|
//Edge Creation
|
|
graphViewChange.edgesToCreate?.ToList().ForEach(edge =>
|
|
{
|
|
if (!(edge is FlowEdgeView edgeView))
|
|
return;
|
|
|
|
var outputPortView = (FlowPortView)edgeView.output;
|
|
if (outputPortView == null) return;
|
|
edgeView.outputPortView = outputPortView;
|
|
|
|
var inputPortView = (FlowPortView)edgeView.input;
|
|
if (inputPortView == null) return;
|
|
edgeView.inputPortView = inputPortView;
|
|
|
|
ConnectPorts(edgeView.outputPortView.flowPort, inputPortView.flowPort);
|
|
});
|
|
|
|
// AssetDatabase.SaveAssets();
|
|
return graphViewChange;
|
|
}
|
|
|
|
private void ConnectPorts(FlowPort outputPort, FlowPort inputPort)
|
|
{
|
|
Undo.RecordObjects(new Object[] { outputPort.node, inputPort.node }, "Connect");
|
|
|
|
if (outputPort.acceptsOnlyOneConnection)
|
|
if (outputPort.isConnected)
|
|
NodyUtils.DisconnectPort(outputPort, flowGraph);
|
|
|
|
if (inputPort.acceptsOnlyOneConnection)
|
|
if (inputPort.isConnected)
|
|
NodyUtils.DisconnectPort(inputPort, flowGraph);
|
|
|
|
outputPort.connections.Add(inputPort.portId);
|
|
outputPort.onConnected?.Invoke(inputPort);
|
|
inputPort.connections.Add(outputPort.portId);
|
|
inputPort.onConnected?.Invoke(outputPort);
|
|
|
|
EditorUtility.SetDirty(outputPort.node);
|
|
EditorUtility.SetDirty(inputPort.node);
|
|
}
|
|
|
|
private void DeleteNode(FlowNodeView nodeView)
|
|
{
|
|
if (flowGraph == null) return; //null graph -> stop
|
|
if (nodeView == null) return; //null node view -> stop
|
|
if (nodeView.flowNode == null) return; //null node reference -> stop
|
|
if (nodeView.flowNode.canBeDeleted == false) return; //node marked not to be deleted -> stop
|
|
if (nodeView.flowNode.nodeType == NodeType.System) return; //node is system node -> stop
|
|
Undo.RecordObject(flowGraph, "Delete Node"); //save undo for graph
|
|
NodyUtils.DisconnectNode(nodeView.flowNode, flowGraph); //disconnect node
|
|
flowGraph.nodes.Remove(nodeView.flowNode); //remove node from graph
|
|
EditorUtility.SetDirty(flowGraph); //mark graph as dirty
|
|
Undo.DestroyObjectImmediate(nodeView.flowNode); //save undo for node and destroy the node (asset)
|
|
// AssetDatabase.SaveAssets(); //save assets
|
|
AssetDatabase.SaveAssetIfDirty(flowGraph);
|
|
}
|
|
|
|
private void DeletePort(FlowPortView portView)
|
|
{
|
|
if (flowGraph == null) return; //null graph -> stop
|
|
if (portView == null) return; //null port view -> stop
|
|
if (portView.flowPort == null) return; //null port reference -> stop
|
|
if (portView.flowNode == null) return; //null node reference -> stop
|
|
FlowPort port = portView.flowPort; //target port
|
|
FlowNode node = portView.flowNode; //target node
|
|
if (!node.CanDeletePort(port.portId)) return; //cannot delete port -> stop
|
|
|
|
//port is valid and can be deleted
|
|
|
|
if (port.isConnected) //port is connected -> need to disconnect first and mark everything for undo
|
|
{
|
|
var nodeViews = new List<FlowNodeView>();
|
|
var flowNodes = new List<FlowNode>();
|
|
var otherPorts = new List<FlowPort>();
|
|
|
|
for (int i = 0; i < port.connections.Count; i++)
|
|
{
|
|
string otherPortId = port.connections[i];
|
|
FlowPortView otherPortView = GetPortView(otherPortId); //get other port's view
|
|
FlowPort otherPort = otherPortView?.flowPort; //get a reference to the other port
|
|
if (otherPort == null) continue; //null port -> skip this id
|
|
otherPorts.Add(otherPort); //add other ports (to disconnect)
|
|
flowNodes.Add(otherPort.node); //add other nodes (to save for undo and mark as dirty)
|
|
nodeViews.Add(otherPortView.nodeView);
|
|
}
|
|
flowNodes.Add(node); //add the node to the undo objects list
|
|
nodeViews.Add(portView.nodeView);
|
|
// ReSharper disable once CoVariantArrayConversion
|
|
Undo.RecordObjects(flowNodes.ToArray(), "Delete Port"); //save undo for all the nodes
|
|
for (int i = 0; i < otherPorts.Count; i++) //disconnect all ports
|
|
{
|
|
FlowPort otherPort = otherPorts[i];
|
|
NodyUtils.DisconnectPortFromPort(port, otherPort);
|
|
}
|
|
port.connections.Clear(); //sanity check - remove all connection port ids from target port
|
|
node.DeletePort(port.portId); //delete the port
|
|
for (int i = 0; i < flowNodes.Count; i++)
|
|
{
|
|
FlowNode n = flowNodes[i];
|
|
EditorUtility.SetDirty(n); //mark all nodes as dirty
|
|
AssetDatabase.SaveAssetIfDirty(n); //save this s$%t
|
|
}
|
|
// AssetDatabase.SaveAssets();
|
|
for (int i = 0; i < nodeViews.Count; i++)
|
|
{
|
|
FlowNodeView nodeView = nodeViews[i];
|
|
nodeView.RefreshNodeView(); //refresh node views
|
|
nodeView.RefreshPortsViews(); //refresh node port views
|
|
}
|
|
|
|
// if (NodyInspectorWindow.isOpen)
|
|
// inspector.Refresh();
|
|
return; //stop here
|
|
}
|
|
|
|
//port wasn't connected -> mark for undo and delete it
|
|
|
|
Undo.RecordObject(node, "Delete Port"); //save undo for the node
|
|
node.DeletePort(port.portId); //delete the port
|
|
EditorUtility.SetDirty(node); //mark node as dirty
|
|
AssetDatabase.SaveAssetIfDirty(node); //save this shit
|
|
portView.nodeView.RefreshNodeView(); //refresh node views
|
|
portView.nodeView.RefreshPortsViews(); //refresh node port views
|
|
// if (NodyInspectorWindow.isOpen)
|
|
// inspector.Refresh();
|
|
}
|
|
|
|
private static void DisconnectPorts(FlowEdgeView edgeView)
|
|
{
|
|
if (edgeView?.outputPortView?.flowPort == null) return;
|
|
if (edgeView.inputPortView?.flowPort == null) return;
|
|
DisconnectPorts(edgeView.outputPortView.flowPort, edgeView.inputPortView.flowPort);
|
|
edgeView.outputPortView.nodeView.RefreshNodeView();
|
|
edgeView.inputPortView.nodeView.RefreshNodeView();
|
|
}
|
|
|
|
private static void DisconnectPorts(FlowPort p1, FlowPort p2)
|
|
{
|
|
Undo.RecordObjects(new Object[] { p1.node, p2.node }, "Disconnect");
|
|
NodyUtils.DisconnectPortFromPort(p1, p2);
|
|
EditorUtility.SetDirty(p1.node);
|
|
EditorUtility.SetDirty(p2.node);
|
|
}
|
|
|
|
private void AddRootNode() //ToDo: add subgraph settings (EnterNode instead of StartNode)
|
|
{
|
|
if (flowGraph == null) return;
|
|
if (flowGraph.rootNode != null) return;
|
|
for (int i = 0; i < flowGraph.nodes.Count; i++)
|
|
{
|
|
FlowNode node = flowGraph.nodes[i];
|
|
if (!(node is StartNode rootNode))
|
|
continue;
|
|
flowGraph.rootNode = rootNode;
|
|
EditorUtility.SetDirty(flowGraph);
|
|
AssetDatabase.SaveAssetIfDirty(flowGraph);
|
|
break;
|
|
}
|
|
if (flowGraph.rootNode != null) return;
|
|
flowGraph.rootNode = CreateNode(typeof(StartNode), false, false).SetPosition(Vector2.zero);
|
|
flowGraph.nodes.Remove(flowGraph.rootNode);
|
|
flowGraph.nodes.Insert(0, flowGraph.rootNode);
|
|
EditorUtility.SetDirty(flowGraph);
|
|
AssetDatabase.SaveAssetIfDirty(flowGraph);
|
|
FrameAll();
|
|
}
|
|
|
|
private FlowNodeView CreateNodeView(FlowNode node)
|
|
{
|
|
var nodeView = FlowNodeView.GetView(this, node);
|
|
nodeView.OnNodeSelected = OnNodeSelected;
|
|
AddElement(nodeView);
|
|
|
|
switch (flowGraph.graphState)
|
|
{
|
|
case GraphState.Running:
|
|
case GraphState.Paused:
|
|
if (node is GlobalNode globalNode)
|
|
{
|
|
nodeView.UpdateNodeState(NodeState.Running);
|
|
}
|
|
|
|
if (nodeView.flowNode.flowGraph.activeNode == node)
|
|
{
|
|
nodeView.UpdateNodeState(NodeState.Active);
|
|
}
|
|
break;
|
|
case GraphState.Idle:
|
|
//ignored
|
|
break;
|
|
}
|
|
|
|
return nodeView;
|
|
}
|
|
|
|
private void CleanGraph()
|
|
{
|
|
if (flowGraph == null) return;
|
|
|
|
bool flowIsDirty = false;
|
|
|
|
//remove null nodes
|
|
for (int i = flowGraph.nodes.Count - 1; i >= 0; i--)
|
|
{
|
|
if (flowGraph.nodes[i] != null)
|
|
continue;
|
|
flowGraph.nodes.RemoveAt(i);
|
|
flowIsDirty = true;
|
|
}
|
|
|
|
if (flowIsDirty)
|
|
{
|
|
EditorUtility.SetDirty(flowGraph);
|
|
// RefreshGraphView();
|
|
}
|
|
}
|
|
}
|
|
}
|