diff --git a/material_maker/doc/user_interface_main_menu.rst b/material_maker/doc/user_interface_main_menu.rst index ee5ddc1d9..46f3b9e39 100644 --- a/material_maker/doc/user_interface_main_menu.rst +++ b/material_maker/doc/user_interface_main_menu.rst @@ -70,6 +70,10 @@ Edit menu * *Frame selected nodes* creates a comment node that frames selected nodes +* *Detach Connections* detaches selection from existing connections + +* *Remove with Reconnect* is similar to *Detach Connections* but removes the selection + * *Select all* selects all nodes in the current graph view * *Select none* clears the selection in the current graph view diff --git a/material_maker/globals.gd b/material_maker/globals.gd index 79d377a66..1df878159 100644 --- a/material_maker/globals.gd +++ b/material_maker/globals.gd @@ -64,7 +64,8 @@ const DEFAULT_CONFIG : Dictionary = { node_minimize_button = true, node_close_button = true, ui_field_sensitivity = 1.0, - color_picker_floating = false + color_picker_floating = false, + auto_node_insertion = true, } diff --git a/material_maker/main_window.gd b/material_maker/main_window.gd index 4d7f9b658..4b5dff06c 100644 --- a/material_maker/main_window.gd +++ b/material_maker/main_window.gd @@ -81,6 +81,8 @@ const MENU : Array[Dictionary] = [ { menu="Edit/Swap node inputs", command="edit_swap_node_inputs", shortcut="Alt+S"}, { menu="Edit/-" }, { menu="Edit/Frame selected nodes", command="frame_nodes", shortcut="Control+Shift+F" }, + { menu="Edit/Detach Connections", command="edit_detach_node_connections", shortcut="Control+Shift+C" }, + { menu="Edit/Remove with Reconnect", command="edit_remove_with_reconnections", shortcut="Control+Shift+X" }, { menu="Edit/-" }, { menu="Edit/Select All", command="edit_select_all", shortcut="Control+A" }, { menu="Edit/Select None", command="edit_select_none", shortcut="Control+Shift+A" }, @@ -975,6 +977,16 @@ func edit_swap_node_inputs() -> void: if graph_edit != null: graph_edit.swap_node_inputs() +func edit_remove_with_reconnections() -> void: + var graph_edit : MMGraphEdit = get_current_graph_edit() + if graph_edit != null: + graph_edit.remove_with_reconnections() + +func edit_detach_node_connections() -> void: + var graph_edit : MMGraphEdit = get_current_graph_edit() + if graph_edit != null: + graph_edit.remove_with_reconnections(true) + func edit_select_all() -> void: var graph_edit : MMGraphEdit = get_current_graph_edit() if graph_edit != null: diff --git a/material_maker/nodes/reroute/reroute.gd b/material_maker/nodes/reroute/reroute.gd index 8bbcbb9fb..35c1f4b72 100644 --- a/material_maker/nodes/reroute/reroute.gd +++ b/material_maker/nodes/reroute/reroute.gd @@ -1,5 +1,6 @@ extends MMGraphNodeMinimal +class_name MMGraphReroute const PREVIEW_SIZES : Array[int] = [ 0, 64, 128, 192] diff --git a/material_maker/panels/graph_edit/graph_edit.gd b/material_maker/panels/graph_edit/graph_edit.gd index d9bb53344..ac16dc6b3 100644 --- a/material_maker/panels/graph_edit/graph_edit.gd +++ b/material_maker/panels/graph_edit/graph_edit.gd @@ -21,10 +21,14 @@ var need_save : bool = false var save_crash_recovery_path = "" var need_save_crash_recovery : bool = false + var top_generator = null var generator = null var grab_accumulation : Dictionary[String, Vector2] +var target_drop_connection : Dictionary +var target_drop_node : MMGraphNodeMinimal + @onready var grab_icon := preload("res://material_maker/icons/grab.svg") var has_grab : bool = false: set(v): @@ -36,6 +40,9 @@ var has_grab : bool = false: else: Input.set_custom_mouse_cursor(null) grab_accumulation.clear() + if not target_drop_connection.is_empty(): + drop_node_on_connection(target_drop_node, target_drop_connection) + target_drop_connection.clear() const PREVIEW_COUNT = 2 var current_preview : Array = [ null, null ] @@ -144,7 +151,6 @@ func process_port_click(pressed : bool): port_click_port_index = -1 return - func _input(event : InputEvent) -> void: # Handle node grab if has_grab: @@ -282,17 +288,30 @@ func _gui_input(event) -> void: if c.has_method("on_clicked_output"): c.on_clicked_output(slot.index, Input.is_key_pressed(KEY_SHIFT)) return - # Avoid conflicting with: - # - drag-cut (Ctrl + RMB) - # - reroute insertion on connection lines (Shift + RMB) - # - lasso selection (Alt + LMB) - if not (event.ctrl_pressed or event.shift_pressed or event.alt_pressed): + var closest_connection : Dictionary = get_closest_connection_at_point( + get_local_mouse_position(), connection_lines_thickness + 2.0) + if not closest_connection.is_empty(): + node_popup.target_connection = closest_connection + request_popup(closest_connection.from_node, closest_connection.from_port, + get_local_mouse_position(), false) + elif not (event.ctrl_pressed or event.shift_pressed or event.alt_pressed): + # Avoid conflicting with: + # - drag-cut (Ctrl + RMB) + # - reroute insertion on connection lines (Shift + RMB) + # - lasso selection (Alt + LMB) node_popup.position = Vector2i(get_screen_transform()*get_local_mouse_position()) node_popup.show_popup() else: if event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: has_grab = false + else: + if not target_drop_connection.is_empty() and not Input.is_key_pressed(KEY_D): + drop_node_on_connection(target_drop_node, target_drop_connection) + target_drop_connection.clear() + remove_theme_color_override("activity") + remove_theme_color_override("connection_hover_tint_color") + if event.double_click: if get_nodes_under_mouse().is_empty(): on_ButtonUp_pressed() @@ -330,6 +349,20 @@ func _gui_input(event) -> void: KEY_G: if not get_selected_nodes().is_empty(): has_grab = true + KEY_D: + if (Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) + and has_graph_node_selection()): + hint_node_drop_allowed(false) + highlight_connection(target_drop_connection) + KEY_G: + if not get_selected_nodes().is_empty(): + has_grab = true + elif not event.pressed: + var scancode_with_modifiers = event.get_keycode_with_modifiers() + match scancode_with_modifiers: + KEY_D: + if has_graph_node_selection() and mm_globals.get_config("auto_node_insertion"): + hint_node_drop_allowed(true) KEY_ESCAPE: has_grab = false _ when event.unicode >= KEY_0 and event.unicode <= KEY_9: @@ -392,6 +425,105 @@ func _gui_input(event) -> void: lasso_points.clear() queue_redraw() + handle_node_detach(event) + handle_node_drop(event) + +## Detaches node(s) from connections if D is held down while dragging a node. +## [br][br]Unsets [param target_drop_connection] +func handle_node_detach(event: InputEventMouseMotion) -> void: + if (Input.is_key_pressed(KEY_D) and event.button_mask & MOUSE_BUTTON_MASK_LEFT != 0 + and get_selected_nodes().size() and event.relative.length() > 0.0): + if not get_nodes_under_mouse().is_empty(): + remove_with_reconnections(true) + target_drop_connection.clear() + +## Checks whether a node can be dropped onto a connection +## and sets [param target_drop_connection] and [param target_drop_node] +## [br][br]Node connections are performed by [method drop_node_on_connection] +func handle_node_drop(event : InputEventMouseMotion) -> void: + if not mm_globals.get_config("auto_node_insertion"): + return + var single_node_selected := get_selected_nodes().size() == 1 + if (single_node_selected and event.button_mask & MOUSE_BUTTON_MASK_LEFT != 0 + and event.relative.length() > 0.0) or (has_grab and single_node_selected): + hint_node_drop_allowed(not Input.is_key_pressed(KEY_D)) + var node : GraphElement = get_selected_nodes()[0] + if node is not GraphNode or node is MMGraphPortal: + return + target_drop_node = node + for c in get_connection_list(): + if node.name in c.values(): + return + var active_conn : Dictionary + var node_rect := node.get_rect() + var conns := get_connections_intersecting_with_rect(node_rect) + if not conns.is_empty(): + if conns.size() > 1: + conns.sort_custom(compare_connection_by_port_height) + active_conn = conns.front() + highlight_connection(active_conn) + if not Input.is_key_pressed(KEY_D): + target_drop_connection = active_conn + else: + target_drop_connection.clear() + for c in get_connection_list(): + if c != active_conn: + highlight_connection(c, 0.0) + else: + remove_theme_color_override("activity") + remove_theme_color_override("connection_hover_tint_color") + +func compare_connection_by_port_height(a : Dictionary, b : Dictionary) -> bool: + var target := target_drop_node + if not target: + return false + var from_node : MMGraphNodeMinimal = get_node(NodePath(a.from_node)) + var to_node : MMGraphNodeMinimal = get_node(NodePath(a.to_node)) + var from_node_dist_to_target := target.position_offset.distance_squared_to(from_node.position_offset) + var to_node_dist_to_target := target.position_offset.distance_squared_to(to_node.position_offset) + if from_node_dist_to_target < to_node_dist_to_target: + var upper : MMGraphNodeMinimal = get_node(NodePath(a.from_node)) + var lower : MMGraphNodeMinimal = get_node(NodePath(b.from_node)) + var upper_slot_pos := upper.get_output_port_position(a.from_port) + upper.position_offset + var lower_slot_pos := lower.get_output_port_position(b.from_port) + lower.position_offset + return upper_slot_pos.y < lower_slot_pos.y + else: + var upper : MMGraphNodeMinimal = get_node(NodePath(a.to_node)) + var lower : MMGraphNodeMinimal = get_node(NodePath(b.to_node)) + var upper_slot_pos := upper.get_input_port_position(a.to_port) + upper.position_offset + var lower_slot_pos := lower.get_input_port_position(b.to_port) + lower.position_offset + return upper_slot_pos.y < lower_slot_pos.y + +func drop_node_on_connection(node : GraphNode, connection : Dictionary) -> void: + undoredo.start_group() + if node != null: + on_disconnect_node(connection.from_node, connection.from_port, + connection.to_node, connection.to_port) + for new_slot in node.get_input_port_count(): + var slot_type : int = node.get_input_port_type(new_slot) + var from_node : MMGraphNodeMinimal = get_node(NodePath(connection.from_node)) + var from_slot : int = from_node.get_output_port_type(connection.from_port) + if (from_slot == slot_type or slot_type == 42 or from_slot == 42): + on_connect_node(connection.from_node, connection.from_port, node.name, new_slot) + break + for new_slot in node.get_output_port_count(): + var slot_type : int = node.get_output_port_type(new_slot) + var to_node : MMGraphNodeMinimal = get_node(NodePath(connection.to_node)) + var to_slot : int = to_node.get_input_port_type(connection.to_port) + if (to_slot == slot_type or slot_type == 42 or to_slot == 42): + on_connect_node(node.name, new_slot, connection.to_node, connection.to_port) + break + undoredo.end_group() + +func hint_node_drop_allowed(should_allow : bool) -> void: + add_theme_color_override("activity", Color(1.5,1.5,1.5,1.0) if should_allow else Color.BLACK) + add_theme_color_override("connection_hover_tint_color", Color.TRANSPARENT) + get_node("_connection_layer").queue_redraw() + +func highlight_connection(connection: Dictionary, amount: float = 0.5) -> void: + if not connection.is_empty(): + set_connection_activity(connection.from_node, connection.from_port, + connection.to_node, connection.to_port, amount) func get_padded_node_rect(graph_node:GraphNode) -> Rect2: var rect : Rect2 = graph_node.get_global_rect() @@ -409,6 +541,10 @@ func _draw() -> void: MMGraphPortal.draw_links(self) # Misc. useful functions + +func has_graph_node_selection() -> bool: + return get_selected_nodes().filter(func(n): return n is GraphNode).size() + func get_source(node, port) -> Dictionary: for c in get_connection_list(): if c.to_node == node and c.to_port == port: @@ -1063,6 +1199,45 @@ func duplicate_selected() -> void: func duplicate_selected_with_inputs() -> void: do_paste(serialize_selection([], true)) +## Detaches a node from an existing connection. +## Detached node will not be removed if [param keep_nodes] is set +func remove_with_reconnections(keep_nodes: bool = false) -> void: + var selection := get_selected_nodes() + var from_node : MMGraphNodeMinimal + var to_node : MMGraphNodeMinimal + var from_slot : int = -1 + var to_slot : int = -1 + undoredo.start_group() + for node in selection: + if node is not GraphNode: + node.selected = false + var connection_list : Array[Dictionary] = get_connection_list() + connection_list.sort_custom(func(a, b): return a.to_port < b.to_port) + for c : Dictionary in connection_list: + var from : MMGraphNodeMinimal = get_node(NodePath(c.from_node)) + var to : MMGraphNodeMinimal = get_node(NodePath(c.to_node)) + if from == node or to == node: + on_disconnect_node(c.from_node, c.from_port, c.to_node, c.to_port) + if from == node: + to_node = to + to_slot = c.to_port + if to == node: + from_node = from + from_slot = c.from_port + if from_node and to_node and from_slot != -1 and to_slot != -1: + var from_slot_type := from_node.get_output_port_type(from_slot) + var to_slot_type := to_node.get_input_port_type(to_slot) + if (from_slot_type == to_slot_type + or from_slot_type == 42 or to_slot_type == 42): + on_connect_node(from_node.name, from_slot, to_node.name, to_slot) + from_node = null + to_node = null + from_slot = -1 + to_slot = -1 + if not keep_nodes: + remove_selection() + undoredo.end_group() + func select_all() -> void: for c in get_children(): if c is GraphElement: @@ -1172,7 +1347,9 @@ func highlight_connections() -> void: while Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): await get_tree().process_frame for c in get_connection_list(): - set_connection_activity(c.from_node, c.from_port, c.to_node, c.to_port, 1.0 if get_node(NodePath(c.from_node)).selected or get_node(NodePath(c.to_node)).selected else 0.0) + set_connection_activity(c.from_node, c.from_port, c.to_node, c.to_port, + 1.0 if get_node(NodePath(c.from_node)).selected + or get_node(NodePath(c.to_node)).selected else 0.0) highlighting_connections = false func _on_GraphEdit_node_selected(node : GraphElement) -> void: diff --git a/material_maker/windows/add_node_popup/add_node_popup.gd b/material_maker/windows/add_node_popup/add_node_popup.gd index 97555aa14..c72b630d6 100644 --- a/material_maker/windows/add_node_popup/add_node_popup.gd +++ b/material_maker/windows/add_node_popup/add_node_popup.gd @@ -13,12 +13,13 @@ var qc_slot : int var qc_slot_type : int var qc_is_output : bool +var target_connection : Dictionary + @onready var library_manager = get_node("/root/MainWindow/NodeLibraryManager") func get_current_graph(): return get_parent().get_current_graph_edit() - func _ready() -> void: filter.connect("text_changed", Callable(self, "update_list")) filter.connect("text_submitted", Callable(self, "filter_entered")) @@ -29,32 +30,61 @@ func filter_entered(_filter) -> void: _on_list_item_activated(0) mm_steam.unlock_achievement("ACH_TREASURE_HUNTER") +func add_node_to_connection(node_data) -> void: + var current_graph : GraphEdit = get_current_graph() + current_graph.undoredo.start_group() + var nodes : Array = current_graph.create_nodes(node_data, insert_position) + var node : GraphNode = nodes[0] + if node != null: + current_graph.on_disconnect_node( + target_connection.from_node, target_connection.from_port, + target_connection.to_node, target_connection.to_port) + for new_slot in node.get_input_port_count(): + var slot_type : int = node.get_input_port_type(new_slot) + var from_node = current_graph.get_node(NodePath(target_connection.from_node)) + var from_slot = from_node.get_output_port_type(target_connection.from_port) + if (from_slot == slot_type or slot_type == 42 or from_slot == 42): + current_graph.on_connect_node(target_connection.from_node, + target_connection.from_port, node.name, new_slot) + break + for new_slot in node.get_output_port_count(): + var slot_type : int = node.get_output_port_type(new_slot) + var to_node = current_graph.get_node(NodePath(target_connection.to_node)) + var to_slot = to_node.get_input_port_type(target_connection.to_port) + if (to_slot == slot_type or slot_type == 42 or to_slot == 42): + current_graph.on_connect_node(node.name, new_slot, + target_connection.to_node, target_connection.to_port) + break + current_graph.undoredo.end_group() func add_node(node_data) -> void: var current_graph : GraphEdit = get_current_graph() - current_graph.undoredo.start_group() - var nodes : Array = await current_graph.create_nodes(node_data, insert_position) - if not nodes.is_empty(): - var node : GraphNode = nodes[0] as GraphNode - if node != null: - if qc_node != "": # dragged from port - var port_position : Vector2 - if qc_is_output: - for new_slot in node.get_output_port_count(): - var slot_type : int = node.get_output_port_type(new_slot) - if qc_slot_type == slot_type or slot_type == 42 or qc_slot_type == 42: - current_graph.on_connect_node(node.name, new_slot, qc_node, qc_slot) - port_position = node.get_output_port_position(new_slot) - break - else: - for new_slot in node.get_input_port_count(): - var slot_type : int = node.get_input_port_type(new_slot) - if qc_slot_type == slot_type or slot_type == 42 or qc_slot_type == 42: - current_graph.on_connect_node(qc_node, qc_slot, node.name, new_slot) - port_position = node.get_input_port_position(new_slot) - break - node.position_offset -= port_position/current_graph.zoom - current_graph.undoredo.end_group() + if not target_connection.is_empty(): + add_node_to_connection(node_data) + else: + current_graph.undoredo.start_group() + var nodes : Array = current_graph.create_nodes(node_data, insert_position) + if not nodes.is_empty(): + var node : GraphNode = nodes[0] as GraphNode + if node != null: + if qc_node != "": # dragged from port + var port_position : Vector2 + if qc_is_output: + for new_slot in node.get_output_port_count(): + var slot_type : int = node.get_output_port_type(new_slot) + if qc_slot_type == slot_type or slot_type == 42 or qc_slot_type == 42: + current_graph.on_connect_node(node.name, new_slot, qc_node, qc_slot) + port_position = node.get_output_port_position(new_slot) + break + else: + for new_slot in node.get_input_port_count(): + var slot_type : int = node.get_input_port_type(new_slot) + if qc_slot_type == slot_type or slot_type == 42 or qc_slot_type == 42: + current_graph.on_connect_node(qc_node, qc_slot, node.name, new_slot) + port_position = node.get_input_port_position(new_slot) + break + node.position_offset -= port_position/current_graph.zoom + current_graph.undoredo.end_group() get_node("/root/MainWindow/NodeLibraryManager").item_created(node_data.tree_item) todo_renamed_hide() @@ -241,3 +271,6 @@ func _on_sort_menu_pressed() -> void: panel.show() else: panel.popup(Rect2(panel.get_mouse_position() * content_scale_factor, panel.size)) + +func _on_popup_hide() -> void: + target_connection.clear() diff --git a/material_maker/windows/add_node_popup/add_node_popup.tscn b/material_maker/windows/add_node_popup/add_node_popup.tscn index d5a724b44..55196e227 100644 --- a/material_maker/windows/add_node_popup/add_node_popup.tscn +++ b/material_maker/windows/add_node_popup/add_node_popup.tscn @@ -228,6 +228,7 @@ theme_type_variation = &"MM_AddNodePanelList" same_column_width = true fixed_icon_size = Vector2i(18, 18) +[connection signal="popup_hide" from="." to="." method="_on_popup_hide"] [connection signal="object_selected" from="PanelContainer/VBoxContainer/Buttons/Button1" to="." method="add_node"] [connection signal="object_selected" from="PanelContainer/VBoxContainer/Buttons/Button2" to="." method="add_node"] [connection signal="object_selected" from="PanelContainer/VBoxContainer/Buttons/Button3" to="." method="add_node"] diff --git a/material_maker/windows/preferences/preferences.tscn b/material_maker/windows/preferences/preferences.tscn index 34a245592..f847cd8d2 100644 --- a/material_maker/windows/preferences/preferences.tscn +++ b/material_maker/windows/preferences/preferences.tscn @@ -479,6 +479,27 @@ text = "On" flat = false config_variable = "auto_size_comment" +[node name="Spacer5" type="Control" parent="HSplitContainer/PreferencesPanel/VBoxContainer/TabContainer/Graph/VBoxContainer" unique_id=343504782] +custom_minimum_size = Vector2(0, 10) +layout_mode = 2 + +[node name="AutoInsertNode" type="HBoxContainer" parent="HSplitContainer/PreferencesPanel/VBoxContainer/TabContainer/Graph/VBoxContainer" unique_id=1679099489] +layout_mode = 2 + +[node name="Label" type="Label" parent="HSplitContainer/PreferencesPanel/VBoxContainer/TabContainer/Graph/VBoxContainer/AutoInsertNode" unique_id=1911272184] +layout_mode = 2 +text = "Automatic node insertion" + +[node name="AutoInsertNode" parent="HSplitContainer/PreferencesPanel/VBoxContainer/TabContainer/Graph/VBoxContainer/AutoInsertNode" unique_id=1933611024 instance=ExtResource("1")] +custom_minimum_size = Vector2(200, 0) +layout_mode = 2 +size_flags_horizontal = 10 +tooltip_text = "When enabled, a node positioned on top of a connection line will be automatically inserted into the connection if compatible. +(The 'D' key can be used to block the action while dragging a node)." +text = "On" +flat = false +config_variable = "auto_node_insertion" + [node name="Spacer1" type="Control" parent="HSplitContainer/PreferencesPanel/VBoxContainer/TabContainer/Graph/VBoxContainer" unique_id=826818285] custom_minimum_size = Vector2(0, 10) layout_mode = 2