1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-11 13:10:58 +00:00

Allow reconnecting AnimationNodeStateMachine transitions

This commit is contained in:
vaner-org
2025-08-11 21:24:47 +05:30
parent 80a219a58a
commit 51a54636c1
2 changed files with 325 additions and 32 deletions

View File

@@ -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<AnimationNodeStateMachineTransition> 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<AnimationNodeStateMachineTransition> 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<InputEvent> &p_event) {
AnimationTree *tree = AnimationTreeEditor::get_singleton()->get_animation_tree();
if (!tree) {
@@ -266,22 +346,7 @@ void AnimationNodeStateMachineEditor::_state_machine_gui_input(const Ref<InputEv
}
if (closest >= 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<AnimationNodeStateMachineTransition> 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 Ref<InputEv
state_machine_draw->queue_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 Ref<InputEv
}
}
// Move mouse while reconnecting.
if (mm.is_valid() && reconnecting && !read_only) {
reconnecting_transition_pos = mm->get_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 Ref<InputEv
state_machine_draw->queue_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 Ref<InputEv
continue;
}
if (d < closest_d) {
closest = i;
closest_d = d;
// Check for tooltip (anywhere along the line).
if (d < closest_d_tooltip) {
closest_d_tooltip = d;
closest_for_tooltip = i;
}
// Calculate dynamic hover distance based on transition length.
float transition_length = transition_lines[i].from.distance_to(transition_lines[i].to);
float hover_distance = MIN(20.0f, transition_length * 0.2f); // 20px or 20% of length, whichever is smaller.
// Check distance to both endpoints for highlighting.
float dist_to_start = mm->get_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");

View File

@@ -130,7 +130,7 @@ class AnimationNodeStateMachineEditor : public AnimationTreeNodeEditorPlugin {
static AnimationNodeStateMachineEditor *singleton;
void _state_machine_gui_input(const Ref<InputEvent> &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;