graph_edit.gd 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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. # Called when the node enters the scene tree for the first time.
  10. func _ready() -> void:
  11. snapping_enabled = false
  12. show_grid = false
  13. zoom = 0.9
  14. func init(main_node: Node, graphedit: GraphEdit, openhelp: Callable, multipleconnections: Window) -> void:
  15. control_script = main_node
  16. graph_edit = graphedit
  17. open_help = openhelp
  18. multiple_connections = multipleconnections
  19. func _make_node(command: String):
  20. #Find node with matching name to button and create a version of it in the graph edit
  21. #and position it close to the origin right click to open the menu
  22. var effect: GraphNode = Nodes.get_node(NodePath(command)).duplicate()
  23. effect.name = command
  24. add_child(effect, true)
  25. effect.connect("open_help", open_help)
  26. effect.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
  27. _register_inputs_in_node(effect) #link sliders for changes tracking
  28. _register_node_movement() #link nodes for tracking position changes for changes tracking
  29. control_script.changesmade = true
  30. # Remove node with UndoRedo
  31. control_script.undo_redo.create_action("Add Node")
  32. control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
  33. control_script.undo_redo.add_undo_method(Callable(effect, "queue_free"))
  34. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  35. control_script.undo_redo.commit_action()
  36. func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  37. var to_graph_node = get_node(NodePath(to_node))
  38. # Get the type of the input port using GraphNode's built-in method
  39. var port_type = to_graph_node.get_input_port_type(to_port)
  40. # If port type is 1 and already has a connection, reject the request
  41. if port_type == 1:
  42. var connections = get_connection_list()
  43. var existing_connections = 0
  44. for conn in connections:
  45. if conn.to_node == to_node and conn.to_port == to_port:
  46. existing_connections += 1
  47. if existing_connections >= 1:
  48. var interface_settings = ConfigHandler.load_interface_settings()
  49. if interface_settings.disable_pvoc_warning == false:
  50. multiple_connections.popup_centered()
  51. return
  52. # If no conflict, allow the connection
  53. connect_node(from_node, from_port, to_node, to_port)
  54. control_script.changesmade = true
  55. func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  56. disconnect_node(from_node, from_port, to_node, to_port)
  57. control_script.changesmade = true
  58. func _on_graph_edit_node_selected(node: Node) -> void:
  59. selected_nodes[node] = true
  60. func _on_graph_edit_node_deselected(node: Node) -> void:
  61. selected_nodes[node] = false
  62. func _unhandled_key_input(event: InputEvent) -> void:
  63. if event is InputEventKey and event.pressed and not event.echo:
  64. if event.keycode == KEY_BACKSPACE:
  65. _on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
  66. pass
  67. func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
  68. control_script.undo_redo.create_action("Delete Nodes (Undo only)")
  69. #get the number of inputs in the patch
  70. var number_of_inputs = 0
  71. for allnodes in get_children():
  72. if allnodes.get_meta("command") == "inputfile":
  73. number_of_inputs += 1
  74. for node in selected_nodes.keys():
  75. if selected_nodes[node]:
  76. #check if node is the output or the last input node and do nothing
  77. if (number_of_inputs <= 1 and node.get_meta("command") == "inputfile") or node.get_meta("command") == "outputfile":
  78. pass
  79. else:
  80. number_of_inputs -= 1
  81. # Store duplicate and state for undo
  82. var node_data = node.duplicate()
  83. var position = node.position_offset
  84. # Store all connections for undo
  85. var conns = []
  86. for con in get_connection_list():
  87. if con["to_node"] == node.name or con["from_node"] == node.name:
  88. conns.append(con)
  89. # Delete
  90. remove_connections_to_node(node)
  91. node.queue_free()
  92. control_script.changesmade = true
  93. # Register undo restore
  94. control_script.undo_redo.add_undo_method(Callable(self, "add_child").bind(node_data, true))
  95. control_script.undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
  96. for con in conns:
  97. control_script.undo_redo.add_undo_method(Callable(self, "connect_node").bind(
  98. con["from_node"], con["from_port"],
  99. con["to_node"], con["to_port"]
  100. ))
  101. control_script.undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
  102. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  103. control_script.undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
  104. control_script.undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
  105. # Clear selection
  106. selected_nodes = {}
  107. control_script.undo_redo.commit_action()
  108. func set_node_selected(node: Node, selected: bool) -> void:
  109. selected_nodes[node] = selected
  110. #
  111. func remove_connections_to_node(node):
  112. for con in get_connection_list():
  113. if con["to_node"] == node.name or con["from_node"] == node.name:
  114. disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  115. control_script.changesmade = true
  116. #copy and paste nodes with vertical offset on paste
  117. func copy_selected_nodes():
  118. copied_nodes_data.clear()
  119. copied_connections.clear()
  120. # Store selected nodes and their slider values
  121. for node in get_children():
  122. # Check if the node is selected and not an 'inputfile' or 'outputfile'
  123. if node is GraphNode and selected_nodes.get(node, false):
  124. if node.get_meta("command") == "outputfile":
  125. continue # Skip these nodes
  126. var node_data = {
  127. "name": node.name,
  128. "type": node.get_class(),
  129. "offset": node.position_offset,
  130. "slider_values": {}
  131. }
  132. for child in node.get_children():
  133. if child is HSlider or child is VSlider:
  134. node_data["slider_values"][child.name] = child.value
  135. copied_nodes_data.append(node_data)
  136. # Store connections between selected nodes
  137. for conn in get_connection_list():
  138. var from_ref = get_node_or_null(NodePath(conn["from_node"]))
  139. var to_ref = get_node_or_null(NodePath(conn["to_node"]))
  140. var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
  141. var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
  142. # Skip if any of the connected nodes are 'inputfile' or 'outputfile'
  143. 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")):
  144. continue
  145. if is_from_selected and is_to_selected:
  146. # Store connection as dictionary
  147. var conn_data = {
  148. "from_node": conn["from_node"],
  149. "from_port": conn["from_port"],
  150. "to_node": conn["to_node"],
  151. "to_port": conn["to_port"]
  152. }
  153. copied_connections.append(conn_data)
  154. func paste_copied_nodes():
  155. if copied_nodes_data.is_empty():
  156. return
  157. var name_map = {}
  158. var pasted_nodes = []
  159. # Step 1: Find topmost and bottommost Y of copied nodes
  160. var min_y = INF
  161. var max_y = -INF
  162. for node_data in copied_nodes_data:
  163. var y = node_data["offset"].y
  164. min_y = min(min_y, y)
  165. max_y = max(max_y, y)
  166. # Step 2: Decide where to paste the group
  167. var base_y_offset = max_y + 350 # Pasting below the lowest node
  168. # Step 3: Paste nodes, preserving vertical layout
  169. for node_data in copied_nodes_data:
  170. var original_node = get_node_or_null(NodePath(node_data["name"]))
  171. if not original_node:
  172. continue
  173. var new_node = original_node.duplicate()
  174. new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
  175. var relative_y = node_data["offset"].y - min_y
  176. new_node.position_offset = Vector2(
  177. node_data["offset"].x,
  178. base_y_offset + relative_y
  179. )
  180. # Restore sliders
  181. for child in new_node.get_children():
  182. if child.name in node_data["slider_values"]:
  183. child.value = node_data["slider_values"][child.name]
  184. add_child(new_node, true)
  185. new_node.connect("open_help", open_help)
  186. _register_inputs_in_node(new_node) #link sliders for changes tracking
  187. _register_node_movement() # link nodes for changes tracking
  188. name_map[node_data["name"]] = new_node.name
  189. pasted_nodes.append(new_node)
  190. # Step 4: Reconnect new nodes
  191. for conn_data in copied_connections:
  192. var new_from = name_map.get(conn_data["from_node"], null)
  193. var new_to = name_map.get(conn_data["to_node"], null)
  194. if new_from and new_to:
  195. connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
  196. # Step 5: Select pasted nodes
  197. for pasted_node in pasted_nodes:
  198. set_selected(pasted_node)
  199. selected_nodes[pasted_node] = true
  200. control_script.changesmade = true
  201. # Remove node with UndoRedo
  202. control_script.undo_redo.create_action("Paste Nodes")
  203. for pasted_node in pasted_nodes:
  204. control_script.undo_redo.add_undo_method(Callable(self, "remove_child").bind(pasted_node))
  205. control_script.undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
  206. control_script.undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
  207. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  208. control_script.undo_redo.commit_action()
  209. #functions for tracking changes for save state detection
  210. func _register_inputs_in_node(node: Node):
  211. #tracks input to nodes sliders and codeedit to track if patch is saved
  212. # Track Sliders
  213. for slider in node.find_children("*", "HSlider", true, false):
  214. # Create a Callable to the correct method
  215. var callable = Callable(self, "_on_any_slider_changed")
  216. # Check if it's already connected, and connect if not
  217. if not slider.is_connected("value_changed", callable):
  218. slider.connect("value_changed", callable)
  219. for slider in node.find_children("*", "VBoxContainer", true, false):
  220. # Also connect to meta_changed if the slider has that signal
  221. if slider.has_signal("meta_changed"):
  222. var meta_callable = Callable(self, "_on_any_slider_meta_changed")
  223. if not slider.is_connected("meta_changed", meta_callable):
  224. slider.connect("meta_changed", meta_callable)
  225. # Track CodeEdits
  226. for editor in node.find_children("*", "CodeEdit", true, false):
  227. var callable = Callable(self, "_on_any_input_changed")
  228. if not editor.is_connected("text_changed", callable):
  229. editor.connect("text_changed", callable)
  230. func _on_any_slider_meta_changed():
  231. control_script.changesmade = true
  232. print("Meta changed in slider")
  233. func _register_node_movement():
  234. for graphnode in get_children():
  235. if graphnode is GraphNode:
  236. var callable = Callable(self, "_on_graphnode_moved")
  237. if not graphnode.is_connected("position_offset_changed", callable):
  238. graphnode.connect("position_offset_changed", callable)
  239. func _on_graphnode_moved():
  240. control_script.changesmade = true
  241. func _on_any_slider_changed(value: float) -> void:
  242. control_script.changesmade = true
  243. func _on_any_input_changed():
  244. control_script.changesmade = true
  245. func _track_changes():
  246. control_script.changesmade = true