graph_edit.gd 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. extends GraphEdit
  2. var control_script
  3. var graph_edit
  4. var open_help
  5. var multiple_connections
  6. var selected_nodes = {} #used to track which nodes in the GraphEdit are selected
  7. var copied_nodes_data = [] #stores node data on ctrl+c
  8. var copied_connections = [] #stores all connections on ctrl+c
  9. var node_data = {} #stores json with all nodes in it
  10. var valueslider = preload("res://scenes/Nodes/valueslider.tscn") #slider scene for use in nodes
  11. var addremoveinlets = preload("res://scenes/Nodes/addremoveinlets.tscn") #add remove inlets scene for use in nodes
  12. var node_logic = preload("res://scenes/Nodes/node_logic.gd") #load the script logic
  13. # Called when the node enters the scene tree for the first time.
  14. func _ready() -> void:
  15. snapping_enabled = false
  16. show_grid = false
  17. zoom = 0.9
  18. #parse json
  19. var file = FileAccess.open("res://scenes/main/process_help.json", FileAccess.READ)
  20. if file:
  21. var result = JSON.parse_string(file.get_as_text())
  22. if typeof(result) == TYPE_DICTIONARY:
  23. node_data = result
  24. else:
  25. push_error("Invalid JSON")
  26. func init(main_node: Node, graphedit: GraphEdit, openhelp: Callable, multipleconnections: Window) -> void:
  27. control_script = main_node
  28. graph_edit = graphedit
  29. open_help = openhelp
  30. multiple_connections = multipleconnections
  31. func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
  32. if node_data.has(command):
  33. var node_info = node_data[command]
  34. if node_info.get("category", "") == "utility":
  35. #Find utility node with matching name and create a version of it in the graph edit
  36. #and position it close to the origin right click to open the menu
  37. var effect: GraphNode = Nodes.get_node(NodePath(command)).duplicate()
  38. effect.name = command
  39. add_child(effect, true)
  40. if command == "outputfile":
  41. effect.init() #initialise ui from user prefs
  42. effect.connect("open_help", open_help)
  43. effect.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
  44. _register_inputs_in_node(effect) #link sliders for changes tracking
  45. _register_node_movement() #link nodes for tracking position changes for changes tracking
  46. control_script.changesmade = true
  47. if not skip_undo_redo:
  48. # Remove node with UndoRedo
  49. control_script.undo_redo.create_action("Add Node")
  50. control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
  51. control_script.undo_redo.add_undo_method(Callable(effect, "queue_free"))
  52. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  53. control_script.undo_redo.commit_action()
  54. return effect
  55. else: #auto generate node from json
  56. #get the title to display at the top of the node
  57. var title
  58. if node_info.get("category", "") == "pvoc":
  59. title = "%s: %s" % [node_info.get("category", "").to_upper(), node_info.get("title", "")]
  60. else:
  61. title = "%s: %s" % [node_info.get("subcategory", "").to_pascal_case(), node_info.get("title", "")]
  62. var shortdescription = node_info.get("short_description", "") #for tooltip
  63. #get node properties
  64. var stereo = node_info.get("stereo", false)
  65. var outputisstereo = node_info.get("outputisstereo", false) #used to identify the few processes that always output in stereo making the thread need to be stereo
  66. var inputs = JSON.parse_string(node_info.get("inputtype", ""))
  67. var outputs = JSON.parse_string(node_info.get("outputtype", ""))
  68. var portcount = max(inputs.size(), outputs.size())
  69. var parameters = node_info.get("parameters", {})
  70. var graphnode = GraphNode.new()
  71. #set meta data for the process
  72. graphnode.set_meta("command", command)
  73. graphnode.set_meta("stereo_input", stereo)
  74. graphnode.set_meta("output_is_stereo", outputisstereo)
  75. if inputs.size() == 0 and outputs.size() > 0:
  76. graphnode.set_meta("input", true)
  77. else:
  78. graphnode.set_meta("input", false)
  79. #adjust size, position and title of the node
  80. graphnode.title = title
  81. graphnode.tooltip_text = shortdescription
  82. graphnode.size.x = 306
  83. graphnode.custom_minimum_size.y = 80
  84. graphnode.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom)
  85. graphnode.name = command
  86. #add one small control node to the top of the node to aline first inlet to top
  87. var first_inlet = Control.new()
  88. graphnode.add_child(first_inlet)
  89. if parameters.is_empty():
  90. var noparams = Label.new()
  91. noparams.text = "No adjustable parameters"
  92. noparams.custom_minimum_size.x = 270
  93. noparams.custom_minimum_size.y = 57
  94. noparams.vertical_alignment = 1
  95. graphnode.add_child(noparams)
  96. else:
  97. for param_key in parameters.keys():
  98. var param_data = parameters[param_key]
  99. if param_data.get("uitype", "") == "hslider":
  100. #instance the slider scene
  101. var slider = valueslider.instantiate()
  102. #get slider text
  103. var slider_label = param_data.get("paramname", "")
  104. var slider_tooltip = param_data.get("paramdescription", "")
  105. #name slider
  106. slider.name = slider_label.replace(" ", "")
  107. #get slider properties
  108. var brk = param_data.get("automatable", false)
  109. var time = param_data.get("time", false)
  110. var outputduration = param_data.get("outputduration", false)
  111. var min = param_data.get("min", false)
  112. var max = param_data.get("max", false)
  113. var flag = param_data.get("flag", "")
  114. var minrange = param_data.get("minrange", 0)
  115. var maxrange = param_data.get("maxrange", 10)
  116. var step = param_data.get("step", 0.01)
  117. var value = param_data.get("value", 1)
  118. var exponential = param_data.get("exponential", false)
  119. #set labels and tooltips
  120. slider.get_node("SliderLabel").text = slider_label
  121. if brk == true:
  122. slider.get_node("SliderLabel").text += "~"
  123. slider.tooltip_text = slider_tooltip
  124. slider.get_node("SliderLabel").tooltip_text = slider_tooltip
  125. #set meta data
  126. var hslider = slider.get_node("HSplitContainer/HSlider")
  127. hslider.set_meta("brk", brk)
  128. hslider.set_meta("time", time)
  129. hslider.set_meta("min", min)
  130. hslider.set_meta("max", max)
  131. hslider.set_meta("flag", flag)
  132. #set slider params
  133. hslider.min_value = minrange
  134. hslider.max_value = maxrange
  135. hslider.step = step
  136. hslider.value = value
  137. hslider.exp_edit = exponential
  138. #add output duration meta to main if true
  139. if outputduration:
  140. graphnode.set_meta("outputduration", value)
  141. #scale automation window
  142. var automationwindow = slider.get_node("BreakFileMaker")
  143. if automationwindow.content_scale_factor < control_script.uiscale:
  144. automationwindow.size = automationwindow.size * control_script.uiscale
  145. automationwindow.content_scale_factor = control_script.uiscale
  146. graphnode.add_child(slider)
  147. elif param_data.get("uitype", "") == "checkbutton":
  148. #make a checkbutton
  149. var checkbutton = CheckButton.new()
  150. #get button text
  151. var checkbutton_label = param_data.get("paramname", "")
  152. var checkbutton_tooltip = param_data.get("paramdescription", "")
  153. #name checkbutton
  154. checkbutton.name = checkbutton_label.replace(" ", "")
  155. #get checkbutton properties
  156. var flag = param_data.get("flag", "")
  157. checkbutton.text = checkbutton_label
  158. checkbutton.tooltip_text = checkbutton_tooltip
  159. var checkbutton_pressed = param_data.get("value", "false")
  160. #get button state
  161. if str(checkbutton_pressed).to_lower() == "true":
  162. checkbutton.button_pressed = true
  163. #set checkbutton meta
  164. checkbutton.set_meta("flag", flag)
  165. graphnode.add_child(checkbutton)
  166. elif param_data.get("uitype", "") == "optionbutton":
  167. #make optionbutton and label
  168. var label = Label.new()
  169. var optionbutton = OptionButton.new()
  170. var margin = MarginContainer.new()
  171. #get button text
  172. var optionbutton_label = param_data.get("paramname", "")
  173. var optionbutton_tooltip = param_data.get("paramdescription", "")
  174. #name optionbutton
  175. optionbutton.name = optionbutton_label.replace(" ", "").to_lower()
  176. #add meta flag if this is a sample rate selector for running thread sample rate checks
  177. if optionbutton.name == "samplerate":
  178. graphnode.set_meta("node_sets_sample_rate", true)
  179. #get optionbutton properties
  180. var options = JSON.parse_string(param_data.get("step", ""))
  181. var value = param_data.get("value", 1)
  182. var flag = param_data.get("flag", "")
  183. label.text = optionbutton_label
  184. optionbutton.tooltip_text = optionbutton_tooltip
  185. #fill option button
  186. for option in options:
  187. optionbutton.add_item(str(option))
  188. #select the given id
  189. optionbutton.select(int(value))
  190. #set flag meta
  191. optionbutton.set_meta("flag", flag)
  192. #add margin size for vertical spacing
  193. margin.add_theme_constant_override("margin_bottom", 4)
  194. graphnode.add_child(label)
  195. graphnode.add_child(optionbutton)
  196. graphnode.add_child(margin)
  197. elif param_data.get("uitype", "") == "addremoveinlets":
  198. var addremove = addremoveinlets.instantiate()
  199. addremove.name = "addremoveinlets"
  200. #get parameters
  201. var min_inlets = param_data.get("minrange", 0)
  202. var max_inlets = param_data.get("maxrange", 10)
  203. var default_inlets = param_data.get("value", 1)
  204. #set meta
  205. addremove.set_meta("min", min_inlets)
  206. addremove.set_meta("max", max_inlets)
  207. addremove.set_meta("default", default_inlets)
  208. graphnode.add_child(addremove)
  209. control_script.changesmade = true
  210. #add control nodes if number of child nodes is lower than the number of inlets or outlets
  211. for i in range(portcount - graphnode.get_child_count()):
  212. #add a number of control nodes equal to whatever is higher input or output ports
  213. var control = Control.new()
  214. control.custom_minimum_size.y = 57
  215. graphnode.add_child(control)
  216. if graphnode.has_node("addremoveinlets"):
  217. graphnode.move_child(graphnode.get_node("addremoveinlets"), graphnode.get_child_count() - 1)
  218. #add ports
  219. for i in range(portcount):
  220. #check if input or output is enabled
  221. var enable_input = i < inputs.size()
  222. var enable_output = i < outputs.size()
  223. #get the colour of the port for time or pvoc ins/outs
  224. var input_colour = Color("#ffffff90")
  225. var output_colour = Color("#ffffff90")
  226. if enable_input:
  227. if inputs[i] == 1:
  228. input_colour = Color("#000000b0")
  229. if enable_output:
  230. if outputs[i] == 1:
  231. output_colour = Color("#000000b0")
  232. #enable and set ports
  233. if enable_input == true and enable_output == false:
  234. graphnode.set_slot(i, true, inputs[i], input_colour, false, 0, output_colour)
  235. elif enable_input == false and enable_output == true:
  236. graphnode.set_slot(i, false, 0, input_colour, true, outputs[i], output_colour)
  237. elif enable_input == true and enable_output == true:
  238. graphnode.set_slot(i, true, inputs[i], input_colour, true, outputs[i], output_colour)
  239. else:
  240. pass
  241. graphnode.set_script(node_logic)
  242. add_child(graphnode, true)
  243. graphnode.connect("open_help", open_help)
  244. graphnode.connect("inlet_removed", Callable(self, "on_inlet_removed"))
  245. _register_inputs_in_node(graphnode) #link sliders for changes tracking
  246. _register_node_movement() #link nodes for tracking position changes for changes tracking
  247. if not skip_undo_redo:
  248. # Remove node with UndoRedo
  249. control_script.undo_redo.create_action("Add Node")
  250. control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(graphnode))
  251. control_script.undo_redo.add_undo_method(Callable(graphnode, "queue_free"))
  252. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  253. control_script.undo_redo.commit_action()
  254. return graphnode
  255. return null
  256. func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  257. var to_graph_node = get_node(NodePath(to_node))
  258. var from_graph_node = get_node(NodePath(from_node))
  259. # Get the type of the input port using GraphNode's built-in method
  260. var port_type = to_graph_node.get_input_port_type(to_port)
  261. # If port type is 1 and already has a connection, reject the request
  262. if port_type == 1:
  263. var connections = get_connection_list()
  264. var existing_connections = 0
  265. for conn in connections:
  266. if conn.to_node == to_node and conn.to_port == to_port:
  267. existing_connections += 1
  268. if existing_connections >= 1:
  269. var interface_settings = ConfigHandler.load_interface_settings()
  270. if interface_settings.disable_pvoc_warning == false:
  271. multiple_connections.popup_centered()
  272. return
  273. if from_graph_node.get_meta("command") == "inputfile" and to_graph_node.get_meta("command") == "outputfile":
  274. return
  275. # If no conflict, allow the connection
  276. connect_node(from_node, from_port, to_node, to_port)
  277. control_script.changesmade = true
  278. func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  279. disconnect_node(from_node, from_port, to_node, to_port)
  280. control_script.changesmade = true
  281. func _on_graph_edit_node_selected(node: Node) -> void:
  282. selected_nodes[node] = true
  283. func _on_graph_edit_node_deselected(node: Node) -> void:
  284. selected_nodes[node] = false
  285. func _unhandled_key_input(event: InputEvent) -> void:
  286. if event is InputEventKey and event.pressed and not event.echo:
  287. if event.keycode == KEY_BACKSPACE:
  288. _on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
  289. pass
  290. func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
  291. control_script.undo_redo.create_action("Delete Nodes (Undo only)")
  292. for node in selected_nodes.keys():
  293. if is_instance_valid(node) and selected_nodes[node]:
  294. if selected_nodes[node]:
  295. #check if node is the output or the last input node and do nothing
  296. if node.get_meta("command") == "outputfile":
  297. pass
  298. else:
  299. # Store duplicate and state for undo
  300. var node_data = node.duplicate()
  301. var position = node.position_offset
  302. # Store all connections for undo
  303. var conns = []
  304. for con in get_connection_list():
  305. if con["to_node"] == node.name or con["from_node"] == node.name:
  306. conns.append(con)
  307. # Delete
  308. remove_connections_to_node(node)
  309. node.queue_free()
  310. control_script.changesmade = true
  311. # Register undo restore
  312. control_script.undo_redo.add_undo_method(Callable(self, "add_child").bind(node_data, true))
  313. control_script.undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
  314. for con in conns:
  315. control_script.undo_redo.add_undo_method(Callable(self, "connect_node").bind(
  316. con["from_node"], con["from_port"],
  317. con["to_node"], con["to_port"]
  318. ))
  319. control_script.undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
  320. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  321. control_script.undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
  322. control_script.undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
  323. # Clear selection
  324. selected_nodes = {}
  325. control_script.undo_redo.commit_action()
  326. func set_node_selected(node: Node, selected: bool) -> void:
  327. selected_nodes[node] = selected
  328. #
  329. func remove_connections_to_node(node):
  330. for con in get_connection_list():
  331. if con["to_node"] == node.name or con["from_node"] == node.name:
  332. disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  333. control_script.changesmade = true
  334. #copy and paste nodes with vertical offset on paste
  335. func copy_selected_nodes():
  336. copied_nodes_data.clear()
  337. copied_connections.clear()
  338. # Store selected nodes and their slider values
  339. for node in get_children():
  340. # Check if the node is selected and not an 'inputfile' or 'outputfile'
  341. if node is GraphNode and selected_nodes.get(node, false):
  342. if node.get_meta("command") == "outputfile":
  343. continue # Skip these nodes
  344. var node_data = {
  345. "name": node.name,
  346. "type": node.get_class(),
  347. "offset": node.position_offset,
  348. "slider_values": {}
  349. }
  350. for child in node.get_children():
  351. if child is HSlider or child is VSlider:
  352. node_data["slider_values"][child.name] = child.value
  353. copied_nodes_data.append(node_data)
  354. # Store connections between selected nodes
  355. for conn in get_connection_list():
  356. var from_ref = get_node_or_null(NodePath(conn["from_node"]))
  357. var to_ref = get_node_or_null(NodePath(conn["to_node"]))
  358. var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
  359. var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
  360. # Skip if any of the connected nodes are 'inputfile' or 'outputfile'
  361. if (from_ref != null and (from_ref.get_meta("command") == "inputfile" or from_ref.get_meta("command") == "outputfile")) or (to_ref != null and (to_ref.get_meta("command") == "inputfile" or to_ref.get_meta("command") == "outputfile")):
  362. continue
  363. if is_from_selected and is_to_selected:
  364. # Store connection as dictionary
  365. var conn_data = {
  366. "from_node": conn["from_node"],
  367. "from_port": conn["from_port"],
  368. "to_node": conn["to_node"],
  369. "to_port": conn["to_port"]
  370. }
  371. copied_connections.append(conn_data)
  372. func paste_copied_nodes():
  373. if copied_nodes_data.is_empty():
  374. return
  375. var name_map = {}
  376. var pasted_nodes = []
  377. # Step 1: Find topmost and bottommost Y of copied nodes
  378. var min_y = INF
  379. var max_y = -INF
  380. for node_data in copied_nodes_data:
  381. var y = node_data["offset"].y
  382. min_y = min(min_y, y)
  383. max_y = max(max_y, y)
  384. # Step 2: Decide where to paste the group
  385. var base_y_offset = max_y + 350 # Pasting below the lowest node
  386. # Step 3: Paste nodes, preserving vertical layout
  387. for node_data in copied_nodes_data:
  388. var original_node = get_node_or_null(NodePath(node_data["name"]))
  389. if not original_node:
  390. continue
  391. var new_node = original_node.duplicate()
  392. new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
  393. var relative_y = node_data["offset"].y - min_y
  394. new_node.position_offset = Vector2(
  395. node_data["offset"].x,
  396. base_y_offset + relative_y
  397. )
  398. # Restore sliders
  399. for child in new_node.get_children():
  400. if child.name in node_data["slider_values"]:
  401. child.value = node_data["slider_values"][child.name]
  402. add_child(new_node, true)
  403. new_node.connect("open_help", open_help)
  404. _register_inputs_in_node(new_node) #link sliders for changes tracking
  405. _register_node_movement() # link nodes for changes tracking
  406. name_map[node_data["name"]] = new_node.name
  407. pasted_nodes.append(new_node)
  408. # Step 4: Reconnect new nodes
  409. for conn_data in copied_connections:
  410. var new_from = name_map.get(conn_data["from_node"], null)
  411. var new_to = name_map.get(conn_data["to_node"], null)
  412. if new_from and new_to:
  413. connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
  414. # Step 5: Select pasted nodes
  415. for pasted_node in pasted_nodes:
  416. set_selected(pasted_node)
  417. selected_nodes[pasted_node] = true
  418. control_script.changesmade = true
  419. # Remove node with UndoRedo
  420. control_script.undo_redo.create_action("Paste Nodes")
  421. for pasted_node in pasted_nodes:
  422. control_script.undo_redo.add_undo_method(Callable(self, "remove_child").bind(pasted_node))
  423. control_script.undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
  424. control_script.undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
  425. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  426. control_script.undo_redo.commit_action()
  427. #functions for tracking changes for save state detection
  428. func _register_inputs_in_node(node: Node):
  429. #tracks input to nodes sliders and codeedit to track if patch is saved
  430. # Track Sliders
  431. for slider in node.find_children("*", "HSlider", true, false):
  432. # Create a Callable to the correct method
  433. var callable = Callable(self, "_on_any_slider_changed")
  434. # Check if it's already connected, and connect if not
  435. if not slider.is_connected("value_changed", callable):
  436. slider.connect("value_changed", callable)
  437. for slider in node.find_children("*", "VBoxContainer", true, false):
  438. # Also connect to meta_changed if the slider has that signal
  439. if slider.has_signal("meta_changed"):
  440. var meta_callable = Callable(self, "_on_any_slider_meta_changed")
  441. if not slider.is_connected("meta_changed", meta_callable):
  442. slider.connect("meta_changed", meta_callable)
  443. # Track CodeEdits
  444. for editor in node.find_children("*", "CodeEdit", true, false):
  445. var callable = Callable(self, "_on_any_input_changed")
  446. if not editor.is_connected("text_changed", callable):
  447. editor.connect("text_changed", callable)
  448. func _on_any_slider_meta_changed():
  449. control_script.changesmade = true
  450. print("Meta changed in slider")
  451. func _register_node_movement():
  452. for graphnode in get_children():
  453. if graphnode is GraphNode:
  454. var callable = Callable(self, "_on_graphnode_moved")
  455. if not graphnode.is_connected("position_offset_changed", callable):
  456. graphnode.connect("position_offset_changed", callable)
  457. func _on_graphnode_moved():
  458. control_script.changesmade = true
  459. func _on_any_slider_changed(value: float) -> void:
  460. control_script.changesmade = true
  461. func _on_any_input_changed():
  462. control_script.changesmade = true
  463. func _track_changes():
  464. control_script.changesmade = true
  465. func _on_copy_nodes_request() -> void:
  466. graph_edit.copy_selected_nodes()
  467. #get_viewport().set_input_as_handled()
  468. func _on_paste_nodes_request() -> void:
  469. control_script.simulate_mouse_click() #hacky fix to stop tooltips getting stuck
  470. await get_tree().process_frame
  471. graph_edit.paste_copied_nodes()
  472. func on_inlet_removed(node_name: StringName, port_index: int):
  473. var connections = get_connection_list()
  474. for conn in connections:
  475. if conn.to_node == node_name and conn.to_port == port_index:
  476. disconnect_node(conn.from_node, conn.from_port, conn.to_node, conn.to_port)