graph_edit.gd 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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 node_logic = preload("res://scenes/Nodes/node_logic.gd") #load the script logic
  12. # Called when the node enters the scene tree for the first time.
  13. func _ready() -> void:
  14. snapping_enabled = false
  15. show_grid = false
  16. zoom = 0.9
  17. #parse json
  18. var file = FileAccess.open("res://scenes/main/process_help.json", FileAccess.READ)
  19. if file:
  20. var result = JSON.parse_string(file.get_as_text())
  21. if typeof(result) == TYPE_DICTIONARY:
  22. node_data = result
  23. else:
  24. push_error("Invalid JSON")
  25. func init(main_node: Node, graphedit: GraphEdit, openhelp: Callable, multipleconnections: Window) -> void:
  26. control_script = main_node
  27. graph_edit = graphedit
  28. open_help = openhelp
  29. multiple_connections = multipleconnections
  30. func _make_node(command: String, skip_undo_redo := false) -> GraphNode:
  31. if node_data.has(command):
  32. var node_info = node_data[command]
  33. if node_info.get("category", "") == "utility":
  34. #Find utility node with matching name and create a version of it in the graph edit
  35. #and position it close to the origin right click to open the menu
  36. var effect: GraphNode = Nodes.get_node(NodePath(command)).duplicate()
  37. effect.name = command
  38. add_child(effect, true)
  39. if command == "outputfile":
  40. effect.init() #initialise ui from user prefs
  41. effect.connect("open_help", open_help)
  42. effect.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom) #set node to current mouse position in graph edit
  43. _register_inputs_in_node(effect) #link sliders for changes tracking
  44. _register_node_movement() #link nodes for tracking position changes for changes tracking
  45. control_script.changesmade = true
  46. if not skip_undo_redo:
  47. # Remove node with UndoRedo
  48. control_script.undo_redo.create_action("Add Node")
  49. control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(effect))
  50. control_script.undo_redo.add_undo_method(Callable(effect, "queue_free"))
  51. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  52. control_script.undo_redo.commit_action()
  53. return effect
  54. else: #auto generate node from json
  55. #get the title to display at the top of the node
  56. var title
  57. if node_info.get("category", "") == "pvoc":
  58. title = "%s: %s" % [node_info.get("category", "").to_upper(), node_info.get("title", "")]
  59. else:
  60. title = "%s: %s" % [node_info.get("subcategory", "").to_pascal_case(), node_info.get("title", "")]
  61. var shortdescription = node_info.get("short_description", "") #for tooltip
  62. #get node properties
  63. var stereo = node_info.get("stereo", false)
  64. var inputs = JSON.parse_string(node_info.get("inputtype", ""))
  65. var outputs = JSON.parse_string(node_info.get("outputtype", ""))
  66. var portcount = max(inputs.size(), outputs.size())
  67. var parameters = node_info.get("parameters", {})
  68. var graphnode = GraphNode.new()
  69. for i in range(portcount):
  70. #add a number of control nodes equal to whatever is higher input or output ports
  71. var control = Control.new()
  72. graphnode.add_child(control)
  73. #check if input or output is enabled
  74. var enable_input = i < inputs.size()
  75. var enable_output = i < outputs.size()
  76. #get the colour of the port for time or pvoc ins/outs
  77. var input_colour = Color("#ffffff90")
  78. var output_colour = Color("#ffffff90")
  79. if enable_input:
  80. if inputs[i] == 1:
  81. input_colour = Color("#000000b0")
  82. if enable_output:
  83. if outputs[i] == 1:
  84. output_colour = Color("#000000b0")
  85. #enable and set ports
  86. if enable_input == true and enable_output == false:
  87. graphnode.set_slot(i, true, inputs[i], input_colour, false, 0, output_colour)
  88. elif enable_input == false and enable_output == true:
  89. graphnode.set_slot(i, false, 0, input_colour, true, outputs[i], output_colour)
  90. elif enable_input == true and enable_output == true:
  91. graphnode.set_slot(i, true, inputs[i], input_colour, true, outputs[i], output_colour)
  92. else:
  93. pass
  94. #set meta data for the process
  95. graphnode.set_meta("command", command)
  96. graphnode.set_meta("stereo_input", stereo)
  97. #adjust size, position and title of the node
  98. graphnode.title = title
  99. graphnode.tooltip_text = shortdescription
  100. graphnode.size.x = 306
  101. graphnode.custom_minimum_size.y = 80
  102. graphnode.set_position_offset((control_script.effect_position + graph_edit.scroll_offset) / graph_edit.zoom)
  103. graphnode.name = command
  104. if parameters.is_empty():
  105. var noparams = Label.new()
  106. noparams.text = "No adjustable parameters"
  107. graphnode.add_child(noparams)
  108. else:
  109. for param_key in parameters.keys():
  110. var param_data = parameters[param_key]
  111. if param_data.get("uitype", "") == "hslider":
  112. #instance the slider scene
  113. var slider = valueslider.instantiate()
  114. #get slider text
  115. var slider_label = param_data.get("paramname", "")
  116. var slider_tooltip = param_data.get("paramdescription", "")
  117. #get slider properties
  118. var brk = param_data.get("automatable", false)
  119. var time = param_data.get("time", false)
  120. var min = param_data.get("min", false)
  121. var max = param_data.get("max", false)
  122. var flag = param_data.get("flag", "")
  123. var minrange = param_data.get("minrange", 0)
  124. var maxrange = param_data.get("maxrange", 10)
  125. var step = param_data.get("step", 0.01)
  126. var value = param_data.get("value", 1)
  127. var exponential = param_data.get("exponential", false)
  128. #set labels and tooltips
  129. slider.get_node("SliderLabel").text = slider_label
  130. if brk == true:
  131. slider.get_node("SliderLabel").text += "~"
  132. slider.tooltip_text = slider_tooltip
  133. slider.get_node("SliderLabel").tooltip_text = slider_tooltip
  134. #set meta data
  135. var hslider = slider.get_node("HSplitContainer/HSlider")
  136. hslider.set_meta("brk", brk)
  137. hslider.set_meta("time", time)
  138. hslider.set_meta("min", min)
  139. hslider.set_meta("max", max)
  140. hslider.set_meta("flag", flag)
  141. #set slider params
  142. hslider.min_value = minrange
  143. hslider.max_value = maxrange
  144. hslider.step = step
  145. hslider.value = value
  146. hslider.exp_edit = exponential
  147. graphnode.add_child(slider)
  148. graphnode.set_script(node_logic)
  149. add_child(graphnode, true)
  150. graphnode.connect("open_help", open_help)
  151. _register_inputs_in_node(graphnode) #link sliders for changes tracking
  152. _register_node_movement() #link nodes for tracking position changes for changes tracking
  153. if not skip_undo_redo:
  154. # Remove node with UndoRedo
  155. control_script.undo_redo.create_action("Add Node")
  156. control_script.undo_redo.add_undo_method(Callable(graph_edit, "remove_child").bind(graphnode))
  157. control_script.undo_redo.add_undo_method(Callable(graphnode, "queue_free"))
  158. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  159. control_script.undo_redo.commit_action()
  160. return graphnode
  161. return null
  162. func _on_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  163. var to_graph_node = get_node(NodePath(to_node))
  164. # Get the type of the input port using GraphNode's built-in method
  165. var port_type = to_graph_node.get_input_port_type(to_port)
  166. # If port type is 1 and already has a connection, reject the request
  167. if port_type == 1:
  168. var connections = get_connection_list()
  169. var existing_connections = 0
  170. for conn in connections:
  171. if conn.to_node == to_node and conn.to_port == to_port:
  172. existing_connections += 1
  173. if existing_connections >= 1:
  174. var interface_settings = ConfigHandler.load_interface_settings()
  175. if interface_settings.disable_pvoc_warning == false:
  176. multiple_connections.popup_centered()
  177. return
  178. # If no conflict, allow the connection
  179. connect_node(from_node, from_port, to_node, to_port)
  180. control_script.changesmade = true
  181. func _on_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void:
  182. disconnect_node(from_node, from_port, to_node, to_port)
  183. control_script.changesmade = true
  184. func _on_graph_edit_node_selected(node: Node) -> void:
  185. selected_nodes[node] = true
  186. func _on_graph_edit_node_deselected(node: Node) -> void:
  187. selected_nodes[node] = false
  188. func _unhandled_key_input(event: InputEvent) -> void:
  189. if event is InputEventKey and event.pressed and not event.echo:
  190. if event.keycode == KEY_BACKSPACE:
  191. _on_graph_edit_delete_nodes_request(PackedStringArray(selected_nodes.keys().filter(func(k): return selected_nodes[k])))
  192. pass
  193. func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
  194. control_script.undo_redo.create_action("Delete Nodes (Undo only)")
  195. #get the number of inputs in the patch
  196. var number_of_inputs = 0
  197. for allnodes in get_children():
  198. if allnodes.get_meta("command") == "inputfile":
  199. number_of_inputs += 1
  200. for node in selected_nodes.keys():
  201. if selected_nodes[node]:
  202. #check if node is the output or the last input node and do nothing
  203. if (number_of_inputs <= 1 and node.get_meta("command") == "inputfile") or node.get_meta("command") == "outputfile":
  204. pass
  205. else:
  206. number_of_inputs -= 1
  207. # Store duplicate and state for undo
  208. var node_data = node.duplicate()
  209. var position = node.position_offset
  210. # Store all connections for undo
  211. var conns = []
  212. for con in get_connection_list():
  213. if con["to_node"] == node.name or con["from_node"] == node.name:
  214. conns.append(con)
  215. # Delete
  216. remove_connections_to_node(node)
  217. node.queue_free()
  218. control_script.changesmade = true
  219. # Register undo restore
  220. control_script.undo_redo.add_undo_method(Callable(self, "add_child").bind(node_data, true))
  221. control_script.undo_redo.add_undo_method(Callable(node_data, "set_position_offset").bind(position))
  222. for con in conns:
  223. control_script.undo_redo.add_undo_method(Callable(self, "connect_node").bind(
  224. con["from_node"], con["from_port"],
  225. con["to_node"], con["to_port"]
  226. ))
  227. control_script.undo_redo.add_undo_method(Callable(self, "set_node_selected").bind(node_data, true))
  228. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  229. control_script.undo_redo.add_undo_method(Callable(self, "_register_inputs_in_node").bind(node_data)) #link sliders for changes tracking
  230. control_script.undo_redo.add_undo_method(Callable(self, "_register_node_movement")) # link nodes for changes tracking
  231. # Clear selection
  232. selected_nodes = {}
  233. control_script.undo_redo.commit_action()
  234. func set_node_selected(node: Node, selected: bool) -> void:
  235. selected_nodes[node] = selected
  236. #
  237. func remove_connections_to_node(node):
  238. for con in get_connection_list():
  239. if con["to_node"] == node.name or con["from_node"] == node.name:
  240. disconnect_node(con["from_node"], con["from_port"], con["to_node"], con["to_port"])
  241. control_script.changesmade = true
  242. #copy and paste nodes with vertical offset on paste
  243. func copy_selected_nodes():
  244. copied_nodes_data.clear()
  245. copied_connections.clear()
  246. # Store selected nodes and their slider values
  247. for node in get_children():
  248. # Check if the node is selected and not an 'inputfile' or 'outputfile'
  249. if node is GraphNode and selected_nodes.get(node, false):
  250. if node.get_meta("command") == "outputfile":
  251. continue # Skip these nodes
  252. var node_data = {
  253. "name": node.name,
  254. "type": node.get_class(),
  255. "offset": node.position_offset,
  256. "slider_values": {}
  257. }
  258. for child in node.get_children():
  259. if child is HSlider or child is VSlider:
  260. node_data["slider_values"][child.name] = child.value
  261. copied_nodes_data.append(node_data)
  262. # Store connections between selected nodes
  263. for conn in get_connection_list():
  264. var from_ref = get_node_or_null(NodePath(conn["from_node"]))
  265. var to_ref = get_node_or_null(NodePath(conn["to_node"]))
  266. var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
  267. var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
  268. # Skip if any of the connected nodes are 'inputfile' or 'outputfile'
  269. 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")):
  270. continue
  271. if is_from_selected and is_to_selected:
  272. # Store connection as dictionary
  273. var conn_data = {
  274. "from_node": conn["from_node"],
  275. "from_port": conn["from_port"],
  276. "to_node": conn["to_node"],
  277. "to_port": conn["to_port"]
  278. }
  279. copied_connections.append(conn_data)
  280. func paste_copied_nodes():
  281. if copied_nodes_data.is_empty():
  282. return
  283. var name_map = {}
  284. var pasted_nodes = []
  285. # Step 1: Find topmost and bottommost Y of copied nodes
  286. var min_y = INF
  287. var max_y = -INF
  288. for node_data in copied_nodes_data:
  289. var y = node_data["offset"].y
  290. min_y = min(min_y, y)
  291. max_y = max(max_y, y)
  292. # Step 2: Decide where to paste the group
  293. var base_y_offset = max_y + 350 # Pasting below the lowest node
  294. # Step 3: Paste nodes, preserving vertical layout
  295. for node_data in copied_nodes_data:
  296. var original_node = get_node_or_null(NodePath(node_data["name"]))
  297. if not original_node:
  298. continue
  299. var new_node = original_node.duplicate()
  300. new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
  301. var relative_y = node_data["offset"].y - min_y
  302. new_node.position_offset = Vector2(
  303. node_data["offset"].x,
  304. base_y_offset + relative_y
  305. )
  306. # Restore sliders
  307. for child in new_node.get_children():
  308. if child.name in node_data["slider_values"]:
  309. child.value = node_data["slider_values"][child.name]
  310. add_child(new_node, true)
  311. new_node.connect("open_help", open_help)
  312. _register_inputs_in_node(new_node) #link sliders for changes tracking
  313. _register_node_movement() # link nodes for changes tracking
  314. name_map[node_data["name"]] = new_node.name
  315. pasted_nodes.append(new_node)
  316. # Step 4: Reconnect new nodes
  317. for conn_data in copied_connections:
  318. var new_from = name_map.get(conn_data["from_node"], null)
  319. var new_to = name_map.get(conn_data["to_node"], null)
  320. if new_from and new_to:
  321. connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
  322. # Step 5: Select pasted nodes
  323. for pasted_node in pasted_nodes:
  324. set_selected(pasted_node)
  325. selected_nodes[pasted_node] = true
  326. control_script.changesmade = true
  327. # Remove node with UndoRedo
  328. control_script.undo_redo.create_action("Paste Nodes")
  329. for pasted_node in pasted_nodes:
  330. control_script.undo_redo.add_undo_method(Callable(self, "remove_child").bind(pasted_node))
  331. control_script.undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
  332. control_script.undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
  333. control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
  334. control_script.undo_redo.commit_action()
  335. #functions for tracking changes for save state detection
  336. func _register_inputs_in_node(node: Node):
  337. #tracks input to nodes sliders and codeedit to track if patch is saved
  338. # Track Sliders
  339. for slider in node.find_children("*", "HSlider", true, false):
  340. # Create a Callable to the correct method
  341. var callable = Callable(self, "_on_any_slider_changed")
  342. # Check if it's already connected, and connect if not
  343. if not slider.is_connected("value_changed", callable):
  344. slider.connect("value_changed", callable)
  345. for slider in node.find_children("*", "VBoxContainer", true, false):
  346. # Also connect to meta_changed if the slider has that signal
  347. if slider.has_signal("meta_changed"):
  348. var meta_callable = Callable(self, "_on_any_slider_meta_changed")
  349. if not slider.is_connected("meta_changed", meta_callable):
  350. slider.connect("meta_changed", meta_callable)
  351. # Track CodeEdits
  352. for editor in node.find_children("*", "CodeEdit", true, false):
  353. var callable = Callable(self, "_on_any_input_changed")
  354. if not editor.is_connected("text_changed", callable):
  355. editor.connect("text_changed", callable)
  356. func _on_any_slider_meta_changed():
  357. control_script.changesmade = true
  358. print("Meta changed in slider")
  359. func _register_node_movement():
  360. for graphnode in get_children():
  361. if graphnode is GraphNode:
  362. var callable = Callable(self, "_on_graphnode_moved")
  363. if not graphnode.is_connected("position_offset_changed", callable):
  364. graphnode.connect("position_offset_changed", callable)
  365. func _on_graphnode_moved():
  366. control_script.changesmade = true
  367. func _on_any_slider_changed(value: float) -> void:
  368. control_script.changesmade = true
  369. func _on_any_input_changed():
  370. control_script.changesmade = true
  371. func _track_changes():
  372. control_script.changesmade = true