diff --git a/editor/animation/animation_state_machine_editor.cpp b/editor/animation/animation_state_machine_editor.cpp index c111d0298ab..eae2fba454b 100644 --- a/editor/animation/animation_state_machine_editor.cpp +++ b/editor/animation/animation_state_machine_editor.cpp @@ -125,6 +125,86 @@ String AnimationNodeStateMachineEditor::_get_root_playback_path(String &r_node_d return base_path; } +void AnimationNodeStateMachineEditor::_reconnect_transition() { + if (reconnecting_transition_index < 0 || reconnecting_transition_target.is_empty()) { + return; + } + + StringName old_from = state_machine->get_transition_from(reconnecting_transition_index); + StringName old_to = state_machine->get_transition_to(reconnecting_transition_index); + StringName new_from; + StringName new_to; + + if (reconnecting_transition_start) { + new_from = reconnecting_transition_target; + new_to = old_to; + } else { + new_from = old_from; + new_to = reconnecting_transition_target; + } + + // Preserve transition properties. + Ref transition = state_machine->get_transition(reconnecting_transition_index); + + updating = true; + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Reconnect Transition")); + + // Remove old transition. + undo_redo->add_do_method(state_machine.ptr(), "remove_transition", old_from, old_to); + undo_redo->add_undo_method(state_machine.ptr(), "add_transition", old_from, old_to, transition); + + // Add new transition. + undo_redo->add_do_method(state_machine.ptr(), "add_transition", new_from, new_to, transition); + undo_redo->add_undo_method(state_machine.ptr(), "remove_transition", new_from, new_to); + + undo_redo->add_do_method(this, "_select_transition", new_from, new_to); + undo_redo->add_undo_method(this, "_select_transition", old_from, old_to); + + undo_redo->add_do_method(this, "_update_graph"); + undo_redo->add_undo_method(this, "_update_graph"); + undo_redo->commit_action(); + updating = false; + + selected_transition_from = new_from; + selected_transition_to = new_to; + _update_mode(); +} + +void AnimationNodeStateMachineEditor::_select_transition(const StringName &p_from, const StringName &p_to) { + selected_transition_from = p_from; + selected_transition_to = p_to; + selected_transition_index = -1; + + // Find transition index. + for (int i = 0; i < state_machine->get_transition_count(); i++) { + if (state_machine->get_transition_from(i) == p_from && state_machine->get_transition_to(i) == p_to) { + selected_transition_index = i; + break; + } + } + + selected_node = StringName(); + selected_nodes.clear(); + + connected_nodes.clear(); + connected_nodes.insert(selected_transition_from); + connected_nodes.insert(selected_transition_to); + + // Push transition to the inspector. + if (selected_transition_index >= 0) { + Ref tr = state_machine->get_transition(selected_transition_index); + if (!state_machine->is_transition_across_group(selected_transition_index)) { + EditorNode::get_singleton()->push_item(tr.ptr(), "", true); + } else { + EditorNode::get_singleton()->push_item(tr.ptr(), "", true); + EditorNode::get_singleton()->push_item(nullptr, "", true); + } + } + + _update_mode(); +} + void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref &p_event) { AnimationTree *tree = AnimationTreeEditor::get_singleton()->get_animation_tree(); if (!tree) { @@ -266,22 +346,7 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref= 0) { - selected_transition_from = transition_lines[closest].from_node; - selected_transition_to = transition_lines[closest].to_node; - selected_transition_index = closest; - - // Update connected_nodes for the selected transition. - connected_nodes.clear(); - connected_nodes.insert(selected_transition_from); - connected_nodes.insert(selected_transition_to); - - Ref tr = state_machine->get_transition(closest); - if (!state_machine->is_transition_across_group(closest)) { - EditorNode::get_singleton()->push_item(tr.ptr(), "", true); - } else { - EditorNode::get_singleton()->push_item(tr.ptr(), "", true); - EditorNode::get_singleton()->push_item(nullptr, "", true); - } + _select_transition(transition_lines[closest].from_node, transition_lines[closest].to_node); } state_machine_draw->queue_redraw(); @@ -354,6 +419,72 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Refqueue_redraw(); } + // Start transition reconnection. + if (mb.is_valid() && mb->is_pressed() && tool_select->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + // Check if we're clicking on a hovered transition endpoint to start dragging. + if (hovered_transition_index >= 0 && !read_only) { + reconnecting = true; + reconnecting_transition_index = hovered_transition_index; + reconnecting_transition_start = hovered_transition_start; + reconnecting_transition_pos = mb->get_position(); + reconnecting_transition_target = StringName(); + + StringName connected_node = reconnecting_transition_start ? transition_lines[reconnecting_transition_index].to_node : transition_lines[reconnecting_transition_index].from_node; + + reconnecting_from_node_rect_index = -1; + for (int i = 0; i < node_rects.size(); i++) { + if (node_rects[i].node_name == connected_node) { + reconnecting_from_node_rect_index = i; + break; + } + } + + // Clear other selections when starting transition drag. + selected_transition_from = StringName(); + selected_transition_to = StringName(); + selected_transition_index = -1; + selected_node = StringName(); + selected_nodes.clear(); + connected_nodes.clear(); + + state_machine_draw->queue_redraw(); + return; + } + } + + // End transition reconnection. + if (mb.is_valid() && reconnecting && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed()) { + if (reconnecting_transition_target != StringName()) { + // Check if reconnection is valid. + StringName old_from = state_machine->get_transition_from(reconnecting_transition_index); + StringName old_to = state_machine->get_transition_to(reconnecting_transition_index); + StringName new_from = reconnecting_transition_start ? reconnecting_transition_target : old_from; + StringName new_to = reconnecting_transition_start ? old_to : reconnecting_transition_target; + + if (new_from == old_from && new_to == old_to) { + // No change. + } else if (new_from == new_to) { + EditorNode::get_singleton()->show_warning(TTR("Cannot transition to self!")); + } else if (new_to == SceneStringName(Start)) { + EditorNode::get_singleton()->show_warning(TTR("Cannot transition to \"Start\"!")); + } else if (new_from == SceneStringName(End)) { + EditorNode::get_singleton()->show_warning(TTR("Cannot transition from \"End\"!")); + } else if (state_machine->has_transition(new_from, new_to)) { + EditorNode::get_singleton()->show_warning(vformat(TTR("Transition from \"%s\" to \"%s\" already exists!"), new_from, new_to)); + } else { + _reconnect_transition(); + } + } + + // Reset dragging state. + reconnecting = false; + reconnecting_transition_index = -1; + reconnecting_transition_start = false; + reconnecting_transition_target = StringName(); + state_machine_draw->queue_redraw(); + return; + } + // Start box selecting if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && tool_select->is_pressed()) { box_selecting = true; @@ -439,6 +570,24 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Refget_position(); + reconnecting_transition_target = StringName(); + reconnecting_to_node_rect_index = -1; + + for (int i = node_rects.size() - 1; i >= 0; i--) { + if (node_rects[i].node.has_point(reconnecting_transition_pos)) { + reconnecting_transition_target = node_rects[i].node_name; + reconnecting_to_node_rect_index = i; + break; + } + } + + state_machine_draw->queue_redraw(); + return; + } + // Move mouse while moving a node if (mm.is_valid() && dragging_selected_attempt && !read_only) { dragging_selected = true; @@ -535,10 +684,27 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Refqueue_redraw(); } - // set tooltip for transition + // Skip if over node. + for (int i = node_rects.size() - 1; i >= 0; i--) { + if (node_rects[i].node.has_point(mm->get_position())) { + if (hovered_transition_index != -1) { + hovered_transition_index = -1; + hovered_transition_start = false; + state_machine_draw->queue_redraw(); + } + state_machine_draw->set_tooltip_text(""); + return; + } + } + + // Set transition tooltip or reconnection endpoint. if (tool_select->is_pressed()) { - int closest = -1; - float closest_d = 1e20; + int closest_for_highlight = -1; + int closest_for_tooltip = -1; + float closest_d_highlight = 1e20; + float closest_d_tooltip = 1e20; + bool closest_is_start = false; + for (int i = 0; i < transition_lines.size(); i++) { Vector2 cpoint = Geometry2D::get_closest_point_to_segment(mm->get_position(), transition_lines[i].from, transition_lines[i].to); float d = cpoint.distance_to(mm->get_position()); @@ -546,15 +712,48 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Refget_position().distance_to(transition_lines[i].from); + float dist_to_end = mm->get_position().distance_to(transition_lines[i].to); + + bool near_start = (dist_to_start <= hover_distance); + bool near_end = (dist_to_end <= hover_distance); + + // Determine which end is closer if both are within range. + bool is_start_closer = dist_to_start < dist_to_end; + + if ((near_start || near_end) && d < closest_d_highlight) { + StringName from_node = transition_lines[i].from_node; + StringName to_node = transition_lines[i].to_node; + + bool is_start_endpoint = near_start && (is_start_closer || !near_end); + closest_d_highlight = d; + closest_for_highlight = i; + closest_is_start = is_start_endpoint; } } - if (closest >= 0) { - String from = String(transition_lines[closest].from_node); - String to = String(transition_lines[closest].to_node); + // Update hovered endpoint for reconnection. + if (hovered_transition_index != closest_for_highlight || hovered_transition_start != closest_is_start) { + hovered_transition_index = closest_for_highlight; + hovered_transition_start = closest_is_start; + state_machine_draw->queue_redraw(); + } + + // Set tooltip for any part of the transition line. + if (closest_for_tooltip >= 0) { + String from = transition_lines[closest_for_tooltip].from_node; + String to = transition_lines[closest_for_tooltip].to_node; String tooltip = from + " -> " + to; state_machine_draw->set_tooltip_text(tooltip); } else { @@ -834,9 +1033,7 @@ void AnimationNodeStateMachineEditor::_add_transition(const bool p_nested_action updating = false; } - selected_transition_from = connecting_from; - selected_transition_to = connecting_to_node; - selected_transition_index = transition_lines.size(); + _select_transition(connecting_from, connecting_to_node); if (!state_machine->is_transition_across_group(selected_transition_index)) { EditorNode::get_singleton()->push_item(tr.ptr(), "", true); @@ -850,7 +1047,7 @@ void AnimationNodeStateMachineEditor::_add_transition(const bool p_nested_action connecting = false; } -void AnimationNodeStateMachineEditor::_connection_draw(const Vector2 &p_from, const Vector2 &p_to, AnimationNodeStateMachineTransition::SwitchMode p_mode, bool p_enabled, bool p_selected, bool p_travel, float p_fade_ratio, bool p_auto_advance, bool p_is_across_group, float p_opacity) { +void AnimationNodeStateMachineEditor::_connection_draw(const Vector2 &p_from, const Vector2 &p_to, AnimationNodeStateMachineTransition::SwitchMode p_mode, bool p_enabled, bool p_selected, bool p_travel, float p_fade_ratio, bool p_auto_advance, bool p_is_across_group, float p_opacity, bool p_endpoint_hovered, bool p_endpoint_hovered_start) { Color line_color = p_enabled ? theme_cache.transition_color : theme_cache.transition_disabled_color; Color icon_color = p_enabled ? theme_cache.transition_icon_color : theme_cache.transition_icon_disabled_color; Color highlight_color = p_enabled ? theme_cache.highlight_color : theme_cache.highlight_disabled_color; @@ -863,6 +1060,51 @@ void AnimationNodeStateMachineEditor::_connection_draw(const Vector2 &p_from, co line_color = highlight_color; } + // Add gradient on hovered endpoint. + if (p_endpoint_hovered) { + // Calculate gradient length based on transition length. + float transition_length = p_from.distance_to(p_to); + float gradient_distance = MIN(20.0f, transition_length * 0.2f); + + Vector2 gradient_start; + Vector2 gradient_end; + if (p_endpoint_hovered_start) { + gradient_start = p_from; + gradient_end = p_from + (p_to - p_from).normalized() * gradient_distance; + } else { + gradient_end = p_to; + gradient_start = p_to - (p_to - p_from).normalized() * gradient_distance; + } + + PackedVector2Array points; + PackedColorArray colors; + + points.push_back(gradient_start); + points.push_back(gradient_end); + + Color start_color = highlight_color; + Color end_color = highlight_color; + + if (p_endpoint_hovered_start) { + end_color.a = 0.0f; + } else { + start_color.a = 0.0f; + } + + if (p_selected) { + start_color = start_color.lightened(0.2f); + end_color = end_color.lightened(0.2f); + start_color.a *= 1.5f; + end_color.a *= 1.5f; + } + + colors.push_back(start_color); + colors.push_back(end_color); + + float line_width = p_selected ? 10.0f : 8.0f; + state_machine_draw->draw_polyline_colors(points, colors, line_width, true); + } + if (p_selected) { state_machine_draw->draw_line(p_from, p_to, highlight_color, 6, true); } @@ -1124,7 +1366,44 @@ void AnimationNodeStateMachineEditor::_state_machine_draw() { for (int i = 0; i < transition_lines.size(); i++) { TransitionLine tl = transition_lines[i]; - if (!tl.hidden) { + + if (reconnecting && i == reconnecting_transition_index) { + Vector2 transition_drag_from; + Vector2 transition_drag_to; + + if (reconnecting_transition_start) { + transition_drag_from = reconnecting_transition_pos; + transition_drag_to = (state_machine->get_node_position(tl.to_node) * EDSCALE) - state_machine->get_graph_offset() * EDSCALE; + } else { + transition_drag_from = (state_machine->get_node_position(tl.from_node) * EDSCALE) - state_machine->get_graph_offset() * EDSCALE; + transition_drag_to = reconnecting_transition_pos; + } + + // Check if we're attempting a self-connection. + StringName connected_node = reconnecting_transition_start ? tl.to_node : tl.from_node; + + // Don't draw the line if attempting self-connection. + if (reconnecting_transition_target != connected_node) { + if (reconnecting_from_node_rect_index >= 0) { + if (reconnecting_transition_start) { + _clip_dst_line_to_rect(transition_drag_from, transition_drag_to, node_rects[reconnecting_from_node_rect_index].node); + } else { + _clip_src_line_to_rect(transition_drag_from, transition_drag_to, node_rects[reconnecting_from_node_rect_index].node); + } + } + + if (reconnecting_to_node_rect_index >= 0) { + if (reconnecting_transition_start) { + _clip_src_line_to_rect(transition_drag_from, transition_drag_to, node_rects[reconnecting_to_node_rect_index].node); + } else { + _clip_dst_line_to_rect(transition_drag_from, transition_drag_to, node_rects[reconnecting_to_node_rect_index].node); + } + } + + _connection_draw(transition_drag_from, transition_drag_to, tl.mode, !tl.disabled, false, false, 0.0f, tl.auto_advance, tl.is_across_group, 0.8f, false, false); + } + + } else if (!tl.hidden) { float opacity = 0.2; // Default to reduced opacity. if (selected_transition_from != StringName() && selected_transition_to != StringName()) { @@ -1148,7 +1427,8 @@ void AnimationNodeStateMachineEditor::_state_machine_draw() { opacity = 1.0; } - _connection_draw(tl.from, tl.to, tl.mode, !tl.disabled, tl.selected, tl.travel, tl.fade_ratio, tl.auto_advance, tl.is_across_group, opacity); + bool is_hovered = (hovered_transition_index == i); + _connection_draw(tl.from, tl.to, tl.mode, !tl.disabled, tl.selected, tl.travel, tl.fade_ratio, tl.auto_advance, tl.is_across_group, opacity, is_hovered, hovered_transition_start); } } @@ -1699,6 +1979,7 @@ void AnimationNodeStateMachineEditor::_update_mode() { void AnimationNodeStateMachineEditor::_bind_methods() { ClassDB::bind_method("_update_graph", &AnimationNodeStateMachineEditor::_update_graph); + ClassDB::bind_method("_select_transition", &AnimationNodeStateMachineEditor::_select_transition); BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_STYLEBOX, AnimationNodeStateMachineEditor, panel_style, "panel", "GraphStateMachine"); BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_STYLEBOX, AnimationNodeStateMachineEditor, error_panel_style, "error_panel", "GraphStateMachine"); diff --git a/editor/animation/animation_state_machine_editor.h b/editor/animation/animation_state_machine_editor.h index b5ee0fe9cb3..d2008b40d82 100644 --- a/editor/animation/animation_state_machine_editor.h +++ b/editor/animation/animation_state_machine_editor.h @@ -130,7 +130,7 @@ class AnimationNodeStateMachineEditor : public AnimationTreeNodeEditorPlugin { static AnimationNodeStateMachineEditor *singleton; void _state_machine_gui_input(const Ref &p_event); - void _connection_draw(const Vector2 &p_from, const Vector2 &p_to, AnimationNodeStateMachineTransition::SwitchMode p_mode, bool p_enabled, bool p_selected, bool p_travel, float p_fade_ratio, bool p_auto_advance, bool p_is_across_group, float p_opacity = 1.0); + void _connection_draw(const Vector2 &p_from, const Vector2 &p_to, AnimationNodeStateMachineTransition::SwitchMode p_mode, bool p_enabled, bool p_selected, bool p_travel, float p_fade_ratio, bool p_auto_advance, bool p_is_across_group, float p_opacity = 1.0, bool p_endpoint_hovered = false, bool p_endpoint_hovered_start = false); void _state_machine_draw(); @@ -168,9 +168,21 @@ class AnimationNodeStateMachineEditor : public AnimationTreeNodeEditorPlugin { Vector2 connecting_to; StringName connecting_to_node; + bool reconnecting = false; + int hovered_transition_index = -1; + bool hovered_transition_start = false; + int reconnecting_transition_index = -1; + bool reconnecting_transition_start = false; + int reconnecting_from_node_rect_index = -1; + int reconnecting_to_node_rect_index = -1; + Vector2 reconnecting_transition_pos; + StringName reconnecting_transition_target; + void _add_menu_type(int p_index); void _add_animation_type(int p_index); void _connect_to(int p_index); + void _reconnect_transition(); + void _select_transition(const StringName &p_from, const StringName &p_to); struct NodeRect { StringName node_name;