浏览代码

added undo and redo to copy paste

Jonathan Higgins 2 月之前
父节点
当前提交
b47a0dda53
共有 1 个文件被更改,包括 62 次插入99 次删除
  1. 62 99
      scenes/main/scripts/graph_edit.gd

+ 62 - 99
scenes/main/scripts/graph_edit.gd

@@ -7,6 +7,7 @@ var multiple_connections
 var selected_nodes = {} #used to track which nodes in the GraphEdit are selected
 var selected_nodes = {} #used to track which nodes in the GraphEdit are selected
 var copied_nodes_data = [] #stores node data on ctrl+c
 var copied_nodes_data = [] #stores node data on ctrl+c
 var copied_connections = [] #stores all connections on ctrl+c
 var copied_connections = [] #stores all connections on ctrl+c
+var last_pasted_nodes = [] #stores last pasted nodes for undo/redo
 var node_data = {} #stores json with all nodes in it
 var node_data = {} #stores json with all nodes in it
 var valueslider = preload("res://scenes/Nodes/valueslider.tscn") #slider scene for use in nodes
 var valueslider = preload("res://scenes/Nodes/valueslider.tscn") #slider scene for use in nodes
 var addremoveinlets = preload("res://scenes/Nodes/addremoveinlets.tscn") #add remove inlets scene for use in nodes
 var addremoveinlets = preload("res://scenes/Nodes/addremoveinlets.tscn") #add remove inlets scene for use in nodes
@@ -400,40 +401,44 @@ func _on_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void:
 			control_script.undo_redo.add_do_method(delete_node.bind(node))
 			control_script.undo_redo.add_do_method(delete_node.bind(node))
 			#register undo
 			#register undo
 			control_script.undo_redo.add_undo_method(restore_node.bind(node))
 			control_script.undo_redo.add_undo_method(restore_node.bind(node))
+			#store a reference to the removed node
 			control_script.undo_redo.add_undo_reference(node)
 			control_script.undo_redo.add_undo_reference(node)
 			
 			
 			
 			
+	#remove deleted nodes from the selected nodes dictionary
 	selected_nodes = {}
 	selected_nodes = {}
 
 
-	var connections_to_restore = []
-	for con in get_connection_list():
-		if (con["from_node"] in nodes or con["to_node"] in nodes) and !connections_to_restore.has(con):
-			connections_to_restore.append(con)
+	#get all connections going to the deleted nodes and store them in an array
+	var connections_to_restore = get_connections_to_nodes(nodes)
 			
 			
+	#register undo method for restoring those connections
 	control_script.undo_redo.add_undo_method(restore_connections.bind(connections_to_restore))
 	control_script.undo_redo.add_undo_method(restore_connections.bind(connections_to_restore))
 	control_script.undo_redo.commit_action()
 	control_script.undo_redo.commit_action()
 	
 	
-	for node_name in nodes:
-		var node: GraphNode = get_node_or_null(NodePath(node_name))
-		if node and is_instance_valid(node):
-			# Skip output nodes
-			if node.get_meta("command") == "outputfile":
-				continue
-			delete_node(node)
-			
 
 
 func delete_node(node_to_delete: GraphNode) -> void:
 func delete_node(node_to_delete: GraphNode) -> void:
 	remove_connections_to_node(node_to_delete)
 	remove_connections_to_node(node_to_delete)
+	#remove child instead of queue free keeps a reference to the node in memory (until undo limit is hit) meaning nodes can be restored easily
 	remove_child(node_to_delete)
 	remove_child(node_to_delete)
 	control_script.changesmade = true
 	control_script.changesmade = true
 
 
 func restore_node(node_to_restore: GraphNode) -> void:
 func restore_node(node_to_restore: GraphNode) -> void:
 	add_child(node_to_restore)
 	add_child(node_to_restore)
+	node_to_restore.connect("open_help", open_help)
+	node_to_restore.node_moved.connect(_auto_link_nodes)
 	set_node_selected(node_to_restore, true)
 	set_node_selected(node_to_restore, true)
 	_track_changes()
 	_track_changes()
 	_register_inputs_in_node(node_to_restore)
 	_register_inputs_in_node(node_to_restore)
 	_register_node_movement()
 	_register_node_movement()
+	control_script.changesmade = true
 	
 	
+func get_connections_to_nodes(nodes: Array) -> Array:
+	var connections_to_nodes = []
+	for con in get_connection_list():
+		if (con["from_node"] in nodes or con["to_node"] in nodes) and !connections_to_nodes.has(con):
+			connections_to_nodes.append(con)
+	return connections_to_nodes
+
 func restore_connections(connections_to_restore: Array) -> void:
 func restore_connections(connections_to_restore: Array) -> void:
 	for con in connections_to_restore:
 	for con in connections_to_restore:
 		var from_node = con["from_node"]
 		var from_node = con["from_node"]
@@ -454,124 +459,82 @@ func remove_connections_to_node(node):
 			
 			
 #copy and paste nodes with vertical offset on paste
 #copy and paste nodes with vertical offset on paste
 func copy_selected_nodes():
 func copy_selected_nodes():
+	if selected_nodes.size() == 0:
+		return
 	copied_nodes_data.clear()
 	copied_nodes_data.clear()
 	copied_connections.clear()
 	copied_connections.clear()
+	
+	var copied_node_names = []
 
 
-	# Store selected nodes and their slider values
+	# Store selected nodes
 	for node in get_children():
 	for node in get_children():
-		# Check if the node is selected and not an 'inputfile' or 'outputfile'
+		# Check if the node is selected and not an 'outputfile'
 		if node is GraphNode and selected_nodes.get(node, false):
 		if node is GraphNode and selected_nodes.get(node, false):
 			if node.get_meta("command") == "outputfile":
 			if node.get_meta("command") == "outputfile":
 				continue  # Skip these nodes
 				continue  # Skip these nodes
+			
+			#this is throwing errors, i think this is due to it trying to deep copy engine level things it doesn't need such as file dialog elements, seems to work anyway
+			var copied_node = node.duplicate()
+			
+			copied_nodes_data.append(copied_node)
+			copied_node_names.append(node.name)
+
+	copied_connections = get_connections_to_nodes(copied_node_names)
 
 
-			var node_data = {
-				"name": node.name,
-				"type": node.get_class(),
-				"offset": node.position_offset,
-				"slider_values": {}
-			}
-
-			for child in node.get_children():
-				if child is HSlider or child is VSlider:
-					node_data["slider_values"][child.name] = child.value
-
-			copied_nodes_data.append(node_data)
-
-	# Store connections between selected nodes
-	for conn in get_connection_list():
-		var from_ref = get_node_or_null(NodePath(conn["from_node"]))
-		var to_ref = get_node_or_null(NodePath(conn["to_node"]))
-
-		var is_from_selected = from_ref != null and selected_nodes.get(from_ref, false)
-		var is_to_selected = to_ref != null and selected_nodes.get(to_ref, false)
-
-		# Skip if any of the connected nodes are 'outputfile'
-		if (from_ref != null and from_ref.get_meta("command") == "outputfile") or (to_ref != null and to_ref.get_meta("command") == "outputfile"):
-			continue
-
-		if is_from_selected and is_to_selected:
-			# Store connection as dictionary
-			var conn_data = {
-				"from_node": conn["from_node"],
-				"from_port": conn["from_port"],
-				"to_node": conn["to_node"],
-				"to_port": conn["to_port"]
-			}
-			copied_connections.append(conn_data)
 
 
 func paste_copied_nodes():
 func paste_copied_nodes():
 	if copied_nodes_data.is_empty():
 	if copied_nodes_data.is_empty():
 		return
 		return
 
 
-	var name_map = {}
+	var pasted_connections = copied_connections.duplicate(true)
+	
+	control_script.undo_redo.create_action("Paste Nodes")
+	
 	var pasted_nodes = []
 	var pasted_nodes = []
-
-	# Step 1: Find topmost and bottommost Y of copied nodes
+	#Find topmost and bottommost Y of copied nodes and decide where to paste
 	var min_y = INF
 	var min_y = INF
 	var max_y = -INF
 	var max_y = -INF
-	for node_data in copied_nodes_data:
-		var y = node_data["offset"].y
+	for node in copied_nodes_data:
+		var y = node.position_offset.y
 		min_y = min(min_y, y)
 		min_y = min(min_y, y)
 		max_y = max(max_y, y)
 		max_y = max(max_y, y)
 
 
-	# Step 2: Decide where to paste the group
 	var base_y_offset = max_y + 350  # Pasting below the lowest node
 	var base_y_offset = max_y + 350  # Pasting below the lowest node
 
 
 	# Step 3: Paste nodes, preserving vertical layout
 	# Step 3: Paste nodes, preserving vertical layout
-	for node_data in copied_nodes_data:
-		var original_node = get_node_or_null(NodePath(node_data["name"]))
-		if not original_node:
-			continue
-
-		var new_node = original_node.duplicate()
-		new_node.name = node_data["name"] + "_copy_" + str(randi() % 10000)
-
-		var relative_y = node_data["offset"].y - min_y
-		new_node.position_offset = Vector2(
-			node_data["offset"].x,
-			base_y_offset + relative_y
-		)
+	for node in copied_nodes_data:
+		#duplicate the copied node again so it can be pasted more than once and add using restore_node function
+		var pasted_node = node.duplicate()
+		#give the node a random name
+		pasted_node.name = node.name + "_copy_" + str(randi() % 10000) 
+		control_script.undo_redo.add_do_method(restore_node.bind(pasted_node))
+		control_script.undo_redo.add_do_reference(pasted_node) 
+		#adjust the offset of the pasted node to be below the copied node
+		var relative_y = pasted_node.position_offset.y - min_y
+		pasted_node.position_offset.y = base_y_offset + relative_y
 		
 		
+		for con in pasted_connections:
+			if con["from_node"] == node.name:
+				con["from_node"] = pasted_node.name
+				print(pasted_node.name)
+			if con["to_node"] == node.name:
+				con["to_node"] = pasted_node.name
+		
+		control_script.undo_redo.add_undo_method(delete_node.bind(pasted_node))
+		pasted_nodes.append(pasted_node)
+	
+	control_script.undo_redo.add_do_method(restore_connections.bind(pasted_connections))
 
 
-		# Restore sliders
-		for child in new_node.get_children():
-			if child.name in node_data["slider_values"]:
-				child.value = node_data["slider_values"][child.name]
-
-		add_child(new_node, true)
-		new_node.connect("open_help", open_help)
-		new_node.node_moved.connect(_auto_link_nodes)
-		_register_inputs_in_node(new_node) #link sliders for changes tracking
-		_register_node_movement() # link nodes for changes tracking
-		name_map[node_data["name"]] = new_node.name
-		pasted_nodes.append(new_node)
-
-
-	# Step 4: Reconnect new nodes
-	for conn_data in copied_connections:
-		var new_from = name_map.get(conn_data["from_node"], null)
-		var new_to = name_map.get(conn_data["to_node"], null)
-
-		if new_from and new_to:
-			connect_node(new_from, conn_data["from_port"], new_to, conn_data["to_port"])
-
-	# Step 5: Select pasted nodes
+	control_script.undo_redo.commit_action()
+	
+	#Select pasted nodes
 	for pasted_node in pasted_nodes:
 	for pasted_node in pasted_nodes:
 		set_selected(pasted_node)
 		set_selected(pasted_node)
 		selected_nodes[pasted_node] = true
 		selected_nodes[pasted_node] = true
 	
 	
 	control_script.changesmade = true
 	control_script.changesmade = true
 	
 	
-	# Remove node with UndoRedo
-	control_script.undo_redo.create_action("Paste Nodes")
-	for pasted_node in pasted_nodes:
-		control_script.undo_redo.add_undo_method(Callable(self, "remove_child").bind(pasted_node))
-		control_script.undo_redo.add_undo_method(Callable(pasted_node, "queue_free"))
-		control_script.undo_redo.add_undo_method(Callable(self, "remove_connections_to_node").bind(pasted_node))
-		control_script.undo_redo.add_undo_method(Callable(self, "_track_changes"))
-	control_script.undo_redo.commit_action()
 	
 	
-
 #functions for tracking changes for save state detection
 #functions for tracking changes for save state detection
 func _register_inputs_in_node(node: Node):
 func _register_inputs_in_node(node: Node):
 	#tracks input to nodes sliders and codeedit to track if patch is saved
 	#tracks input to nodes sliders and codeedit to track if patch is saved