From 9d7c2978f4799e84bcaa4c5692c58391ea7448eb Mon Sep 17 00:00:00 2001 From: Hendrik Brucker Date: Thu, 18 Jan 2024 16:16:17 +0100 Subject: [PATCH] Rework GraphEdit connections (drawing, API, optimizations) - GraphEdit now uses Line2D nodes to draw connection lines and uses a dedicated canvas item shader for them --- core/math/geometry_2d.h | 26 + doc/classes/GraphEdit.xml | 39 +- editor/themes/editor_theme_manager.cpp | 4 + .../4.2-stable.expected | 7 + scene/gui/graph_edit.compat.inc | 5 + scene/gui/graph_edit.cpp | 716 +++++++++++++----- scene/gui/graph_edit.h | 90 ++- scene/gui/graph_edit_arranger.cpp | 64 +- scene/register_scene_types.cpp | 5 +- scene/theme/default_theme.cpp | 3 + 10 files changed, 700 insertions(+), 259 deletions(-) diff --git a/core/math/geometry_2d.h b/core/math/geometry_2d.h index b37fce9e9c2..9907d579a53 100644 --- a/core/math/geometry_2d.h +++ b/core/math/geometry_2d.h @@ -119,6 +119,10 @@ public: } } + static real_t get_distance_to_segment(const Vector2 &p_point, const Vector2 *p_segment) { + return p_point.distance_to(get_closest_point_to_segment(p_point, p_segment)); + } + static bool is_point_in_triangle(const Vector2 &s, const Vector2 &a, const Vector2 &b, const Vector2 &c) { Vector2 an = a - s; Vector2 bn = b - s; @@ -249,6 +253,28 @@ public: return -1; } + static bool segment_intersects_rect(const Vector2 &p_from, const Vector2 &p_to, const Rect2 &p_rect) { + if (p_rect.has_point(p_from) || p_rect.has_point(p_to)) { + return true; + } + + const Vector2 rect_points[4] = { + p_rect.position, + p_rect.position + Vector2(p_rect.size.x, 0), + p_rect.position + p_rect.size, + p_rect.position + Vector2(0, p_rect.size.y) + }; + + // Check if any of the rect's edges intersect the segment. + for (int i = 0; i < 4; i++) { + if (segment_intersects_segment(p_from, p_to, rect_points[i], rect_points[(i + 1) % 4], nullptr)) { + return true; + } + } + + return false; + } + enum PolyBooleanOperation { OPERATION_UNION, OPERATION_DIFFERENCE, diff --git a/doc/classes/GraphEdit.xml b/doc/classes/GraphEdit.xml index 95e760be9f9..e5952d9f716 100644 --- a/doc/classes/GraphEdit.xml +++ b/doc/classes/GraphEdit.xml @@ -143,7 +143,22 @@ [b]Note:[/b] This method suppresses any other connection request signals apart from [signal connection_drag_ended]. - + + + + + + Returns the closest connection to the given point in screen space. If no connection is found within [param max_distance] pixels, an empty [Dictionary] is returned. + A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code]. + For example, getting a connection at a given mouse position can be achieved like this: + [codeblocks] + [gdscript] + var connection = get_closest_connection_at_point(mouse_event.get_position()) + [/gdscript] + [/codeblocks] + + + @@ -154,7 +169,14 @@ - Returns an Array containing the list of connections. A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code]. + Returns an [Array] containing the list of connections. A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code]. + + + + + + + Returns an [Array] containing the list of connections that intersect with the given [Rect2]. A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code]. @@ -233,7 +255,7 @@ The curvature of the lines between the nodes. 0 results in straight lines. - + The thickness of the lines between the nodes. @@ -417,7 +439,16 @@ - Color of the connection's activity (see [method set_connection_activity]). + Color the connection line is interpolated to based on the activity value of a connection (see [method set_connection_activity]). + + + Color which is blended with the connection line when the mouse is hovering over it. + + + Color of the rim around each connection line used for making intersecting lines more distinguishable. + + + Color which is blended with the connection line when the currently dragged connection is hovering over a valid target port. Color of major grid lines/dots. diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index 4ce323c763f..d1fc6021ebe 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1367,6 +1367,10 @@ void EditorThemeManager::_populate_standard_styles(const Ref &p_theme, Th p_theme->set_color("selection_stroke", "GraphEdit", p_theme->get_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor))); p_theme->set_color("activity", "GraphEdit", p_config.accent_color); + p_theme->set_color("connection_hover_tint_color", "GraphEdit", p_config.dark_theme ? Color(0, 0, 0, 0.3) : Color(1, 1, 1, 0.3)); + p_theme->set_color("connection_valid_target_tint_color", "GraphEdit", p_config.dark_theme ? Color(1, 1, 1, 0.4) : Color(0, 0, 0, 0.4)); + p_theme->set_color("connection_rim_color", "GraphEdit", p_config.tree_panel_style->get_bg_color()); + p_theme->set_icon("zoom_out", "GraphEdit", p_theme->get_icon(SNAME("ZoomLess"), EditorStringName(EditorIcons))); p_theme->set_icon("zoom_in", "GraphEdit", p_theme->get_icon(SNAME("ZoomMore"), EditorStringName(EditorIcons))); p_theme->set_icon("zoom_reset", "GraphEdit", p_theme->get_icon(SNAME("ZoomReset"), EditorStringName(EditorIcons))); diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected index 25094dda77e..2c18b439481 100644 --- a/misc/extension_api_validation/4.2-stable.expected +++ b/misc/extension_api_validation/4.2-stable.expected @@ -59,3 +59,10 @@ Validate extension JSON: Error: Field 'classes/TileMap/methods/get_collision_vis Validate extension JSON: Error: Field 'classes/TileMap/methods/get_navigation_visibility_mode': is_const changed value in new API, from false to true. Two TileMap getters were made const. No adjustments should be necessary. + + +GH-86158 +-------- +Validate extension JSON: Error: Field 'classes/GraphEdit/methods/get_connection_line': is_const changed value in new API, from false to true. + +get_connection_line was made const. diff --git a/scene/gui/graph_edit.compat.inc b/scene/gui/graph_edit.compat.inc index 9059637a2a4..7c2af200663 100644 --- a/scene/gui/graph_edit.compat.inc +++ b/scene/gui/graph_edit.compat.inc @@ -38,9 +38,14 @@ void GraphEdit::_set_arrange_nodes_button_hidden_bind_compat_81582(bool p_enable set_show_arrange_button(!p_enable); } +PackedVector2Array GraphEdit::_get_connection_line_bind_compat_86158(const Vector2 &p_from, const Vector2 &p_to) { + return get_connection_line(p_from, p_to); +} + void GraphEdit::_bind_compatibility_methods() { ClassDB::bind_compatibility_method(D_METHOD("is_arrange_nodes_button_hidden"), &GraphEdit::_is_arrange_nodes_button_hidden_bind_compat_81582); ClassDB::bind_compatibility_method(D_METHOD("set_arrange_nodes_button_hidden", "enable"), &GraphEdit::_set_arrange_nodes_button_hidden_bind_compat_81582); + ClassDB::bind_compatibility_method(D_METHOD("get_connection_line", "from_node", "to_node"), &GraphEdit::_get_connection_line_bind_compat_86158); } #endif diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index f5cf7eb59da..c23d21775fd 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -32,8 +32,10 @@ #include "graph_edit.compat.inc" #include "core/input/input.h" +#include "core/math/geometry_2d.h" #include "core/math/math_funcs.h" #include "core/os/keyboard.h" +#include "scene/2d/line_2d.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/graph_edit_arranger.h" @@ -52,7 +54,6 @@ constexpr int MAX_CONNECTION_LINE_CURVE_TESSELATION_STAGES = 5; constexpr int GRID_MINOR_STEPS_PER_MAJOR_LINE = 10; constexpr int GRID_MIN_SNAPPING_DISTANCE = 2; constexpr int GRID_MAX_SNAPPING_DISTANCE = 100; -constexpr float CONNECTING_TARGET_LINE_COLOR_BRIGHTENING = 0.4; bool GraphEditFilter::has_point(const Point2 &p_point) const { return ge->_filter_input(p_point); @@ -212,6 +213,36 @@ GraphEditMinimap::GraphEditMinimap(GraphEdit *p_edit) { minimap_offset = minimap_padding + _convert_from_graph_position(graph_padding); } +Ref GraphEdit::default_connections_shader; + +void GraphEdit::init_shaders() { + default_connections_shader.instantiate(); + default_connections_shader->set_code(R"( +// Connection lines shader. +shader_type canvas_item; +render_mode blend_mix; + +uniform vec4 rim_color : source_color; +uniform int from_type; +uniform int to_type; +uniform float line_width; + +void fragment(){ + float fake_aa_width = 1.5/line_width; + float rim_width = 1.5/line_width; + + float dist = abs(UV.y - 0.5); + float alpha = smoothstep(0.5, 0.5-fake_aa_width, dist); + vec4 final_color = mix(rim_color, COLOR, smoothstep(0.5-rim_width, 0.5-fake_aa_width-rim_width, dist)); + COLOR = vec4(final_color.rgb, final_color.a*alpha); +} +)"); +} + +void GraphEdit::finish_shaders() { + default_connections_shader.unref(); +} + Control::CursorShape GraphEdit::get_cursor_shape(const Point2 &p_pos) const { if (moving_selection) { return CURSOR_MOVE; @@ -232,24 +263,48 @@ Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const S if (is_node_connected(p_from, p_from_port, p_to, p_to_port)) { return OK; } - Connection c; - c.from_node = p_from; - c.from_port = p_from_port; - c.to_node = p_to; - c.to_port = p_to_port; - c.activity = 0; + Ref c; + c.instantiate(); + c->from_node = p_from; + c->from_port = p_from_port; + c->to_node = p_to; + c->to_port = p_to_port; + c->activity = 0; connections.push_back(c); - top_layer->queue_redraw(); + connection_map[p_from].push_back(c); + connection_map[p_to].push_back(c); + + Line2D *line = memnew(Line2D); + line->set_texture_mode(Line2D::LineTextureMode::LINE_TEXTURE_STRETCH); + + Ref line_material; + line_material.instantiate(); + line_material->set_shader(connections_shader); + + float line_width = _get_shader_line_width(); + line_material->set_shader_parameter("line_width", line_width); + line_material->set_shader_parameter("from_type", c->from_port); + line_material->set_shader_parameter("to_type", c->to_port); + + Ref bg_panel = theme_cache.panel; + Color connection_line_rim_color = bg_panel.is_valid() ? bg_panel->get_bg_color() : Color(0.0, 0.0, 0.0, 0.0); + line_material->set_shader_parameter("rim_color", connection_line_rim_color); + line->set_material(line_material); + + connections_layer->add_child(line); + c->_cache.line = line; + minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); return OK; } bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { - for (const Connection &E : connections) { - if (E.from_node == p_from && E.from_port == p_from_port && E.to_node == p_to && E.to_port == p_to_port) { + for (const Ref &conn : connection_map[p_from]) { + if (conn->from_node == p_from && conn->from_port == p_from_port && conn->to_node == p_to && conn->to_port == p_to_port) { return true; } } @@ -258,20 +313,24 @@ bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, con } void GraphEdit::disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { - for (const List::Element *E = connections.front(); E; E = E->next()) { - if (E->get().from_node == p_from && E->get().from_port == p_from_port && E->get().to_node == p_to && E->get().to_port == p_to_port) { + for (const List>::Element *E = connections.front(); E; E = E->next()) { + if (E->get()->from_node == p_from && E->get()->from_port == p_from_port && E->get()->to_node == p_to && E->get()->to_port == p_to_port) { + connection_map[p_from].erase(E->get()); + connection_map[p_to].erase(E->get()); + E->get()->_cache.line->queue_free(); connections.erase(E); - top_layer->queue_redraw(); + minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); return; } } } -void GraphEdit::get_connection_list(List *r_connections) const { - *r_connections = connections; +const List> &GraphEdit::get_connection_list() const { + return connections; } void GraphEdit::set_scroll_offset(const Vector2 &p_offset) { @@ -291,9 +350,9 @@ void GraphEdit::_scroll_moved(double) { callable_mp(this, &GraphEdit::_update_scroll_offset).call_deferred(); awaiting_scroll_offset_update = true; } - top_layer->queue_redraw(); minimap->queue_redraw(); queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); } void GraphEdit::_update_scroll_offset() { @@ -415,20 +474,34 @@ void GraphEdit::_graph_element_moved(Node *p_node) { GraphElement *graph_element = Object::cast_to(p_node); ERR_FAIL_NULL(graph_element); - top_layer->queue_redraw(); minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); } void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_node) { GraphNode *graph_node = Object::cast_to(p_node); ERR_FAIL_NULL(graph_node); - top_layer->queue_redraw(); minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); +} + +void GraphEdit::_graph_node_rect_changed(GraphNode *p_node) { + // Only invalidate the cache when zooming or the node is moved/resized in graph space. + if (panner->is_panning()) { + return; + } + + for (Ref &c : connection_map[p_node->get_name()]) { + c->_cache.dirty = true; + } + + connections_layer->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); } void GraphEdit::add_child_notify(Node *p_child) { @@ -445,12 +518,12 @@ void GraphEdit::add_child_notify(Node *p_child) { GraphNode *graph_node = Object::cast_to(graph_element); if (graph_node) { - graph_element->connect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated).bind(graph_element)); + graph_node->connect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated).bind(graph_element)); + graph_node->connect("item_rect_changed", callable_mp(this, &GraphEdit::_graph_node_rect_changed).bind(graph_node)); } graph_element->connect("raise_request", callable_mp(this, &GraphEdit::_graph_element_moved_to_front).bind(graph_element)); graph_element->connect("resize_request", callable_mp(this, &GraphEdit::_graph_element_resized).bind(graph_element)); - graph_element->connect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw)); graph_element->connect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::queue_redraw)); graph_element->set_scale(Vector2(zoom, zoom)); @@ -482,16 +555,20 @@ void GraphEdit::remove_child_notify(Node *p_child) { GraphNode *graph_node = Object::cast_to(graph_element); if (graph_node) { - graph_element->disconnect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated)); + graph_node->disconnect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated)); + graph_node->disconnect("item_rect_changed", callable_mp(this, &GraphEdit::_graph_node_rect_changed)); + + // Invalidate all adjacent connections, so that they are removed before the next redraw. + for (const Ref &conn : connection_map[graph_node->get_name()]) { + conn->_cache.dirty = true; + } + connections_layer->queue_redraw(); } graph_element->disconnect("raise_request", callable_mp(this, &GraphEdit::_graph_element_moved_to_front)); graph_element->disconnect("resize_request", callable_mp(this, &GraphEdit::_graph_element_resized)); // In case of the whole GraphEdit being destroyed these references can already be freed. - if (connections_layer != nullptr && connections_layer->is_inside_tree()) { - graph_element->disconnect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw)); - } if (minimap != nullptr && minimap->is_inside_tree()) { graph_element->disconnect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::queue_redraw)); } @@ -520,7 +597,6 @@ void GraphEdit::_notification(int p_what) { menu_panel->add_theme_style_override("panel", theme_cache.menu_panel); } break; - case NOTIFICATION_READY: { Size2 hmin = h_scrollbar->get_combined_minimum_size(); Size2 vmin = v_scrollbar->get_combined_minimum_size(); @@ -535,7 +611,6 @@ void GraphEdit::_notification(int p_what) { v_scrollbar->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0); v_scrollbar->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); } break; - case NOTIFICATION_DRAW: { // Draw background fill. draw_style_box(theme_cache.panel, Rect2(Point2(), get_size())); @@ -547,8 +622,8 @@ void GraphEdit::_notification(int p_what) { } break; case NOTIFICATION_RESIZED: { _update_scroll(); - top_layer->queue_redraw(); minimap->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); } break; } } @@ -593,7 +668,7 @@ bool GraphEdit::_filter_input(const Point2 &p_point) { return false; } -void GraphEdit::_top_layer_input(const Ref &p_ev) { +void GraphEdit::_top_connection_layer_input(const Ref &p_ev) { Ref mb = p_ev; if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) { connecting_valid = false; @@ -618,26 +693,26 @@ void GraphEdit::_top_layer_input(const Ref &p_ev) { if (is_in_output_hotzone(graph_node, j, click_pos, port_size)) { if (valid_left_disconnect_types.has(graph_node->get_output_port_type(j))) { // Check disconnect. - for (const Connection &E : connections) { - if (E.from_node == graph_node->get_name() && E.from_port == j) { - Node *to = get_node(NodePath(E.to_node)); + for (const Ref &conn : connection_map[graph_node->get_name()]) { + if (conn->from_node == graph_node->get_name() && conn->from_port == j) { + Node *to = get_node(NodePath(conn->to_node)); if (Object::cast_to(to)) { - connecting_from = E.to_node; - connecting_index = E.to_port; - connecting_out = false; - connecting_type = Object::cast_to(to)->get_input_port_type(E.to_port); - connecting_color = Object::cast_to(to)->get_input_port_color(E.to_port); - connecting_target = false; - connecting_to = pos; + connecting_from_node = conn->to_node; + connecting_from_port_index = conn->to_port; + connecting_from_output = false; + connecting_type = Object::cast_to(to)->get_input_port_type(conn->to_port); + connecting_color = Object::cast_to(to)->get_input_port_color(conn->to_port); + connecting_target_valid = false; + connecting_to_point = pos; if (connecting_type >= 0) { just_disconnected = true; - emit_signal(SNAME("disconnection_request"), E.from_node, E.from_port, E.to_node, E.to_port); - to = get_node(NodePath(connecting_from)); // Maybe it was erased. + emit_signal(SNAME("disconnection_request"), conn->from_node, conn->from_port, conn->to_node, conn->to_port); + to = get_node(NodePath(connecting_from_node)); // Maybe it was erased. if (Object::cast_to(to)) { connecting = true; - emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false); + emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, false); } } return; @@ -646,17 +721,17 @@ void GraphEdit::_top_layer_input(const Ref &p_ev) { } } - connecting_from = graph_node->get_name(); - connecting_index = j; - connecting_out = true; + connecting_from_node = graph_node->get_name(); + connecting_from_port_index = j; + connecting_from_output = true; connecting_type = graph_node->get_output_port_type(j); connecting_color = graph_node->get_output_port_color(j); - connecting_target = false; - connecting_to = pos; + connecting_target_valid = false; + connecting_to_point = pos; if (connecting_type >= 0) { connecting = true; just_disconnected = false; - emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true); + emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, true); } return; } @@ -675,25 +750,25 @@ void GraphEdit::_top_layer_input(const Ref &p_ev) { if (is_in_input_hotzone(graph_node, j, click_pos, port_size)) { if (right_disconnects || valid_right_disconnect_types.has(graph_node->get_input_port_type(j))) { // Check disconnect. - for (const Connection &E : connections) { - if (E.to_node == graph_node->get_name() && E.to_port == j) { - Node *fr = get_node(NodePath(E.from_node)); + for (const Ref &conn : connection_map[graph_node->get_name()]) { + if (conn->to_node == graph_node->get_name() && conn->to_port == j) { + Node *fr = get_node(NodePath(conn->from_node)); if (Object::cast_to(fr)) { - connecting_from = E.from_node; - connecting_index = E.from_port; - connecting_out = true; - connecting_type = Object::cast_to(fr)->get_output_port_type(E.from_port); - connecting_color = Object::cast_to(fr)->get_output_port_color(E.from_port); - connecting_target = false; - connecting_to = pos; + connecting_from_node = conn->from_node; + connecting_from_port_index = conn->from_port; + connecting_from_output = true; + connecting_type = Object::cast_to(fr)->get_output_port_type(conn->from_port); + connecting_color = Object::cast_to(fr)->get_output_port_color(conn->from_port); + connecting_target_valid = false; + connecting_to_point = pos; just_disconnected = true; if (connecting_type >= 0) { - emit_signal(SNAME("disconnection_request"), E.from_node, E.from_port, E.to_node, E.to_port); - fr = get_node(NodePath(connecting_from)); + emit_signal(SNAME("disconnection_request"), conn->from_node, conn->from_port, conn->to_node, conn->to_port); + fr = get_node(NodePath(connecting_from_node)); if (Object::cast_to(fr)) { connecting = true; - emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true); + emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, true); } } return; @@ -702,17 +777,17 @@ void GraphEdit::_top_layer_input(const Ref &p_ev) { } } - connecting_from = graph_node->get_name(); - connecting_index = j; - connecting_out = false; + connecting_from_node = graph_node->get_name(); + connecting_from_port_index = j; + connecting_from_output = false; connecting_type = graph_node->get_input_port_type(j); connecting_color = graph_node->get_input_port_color(j); - connecting_target = false; - connecting_to = pos; + connecting_target_valid = false; + connecting_to_point = pos; if (connecting_type >= 0) { connecting = true; just_disconnected = false; - emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false); + emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, false); } return; } @@ -722,12 +797,11 @@ void GraphEdit::_top_layer_input(const Ref &p_ev) { Ref mm = p_ev; if (mm.is_valid() && connecting) { - connecting_to = mm->get_position(); - connecting_target = false; - top_layer->queue_redraw(); + connecting_to_point = mm->get_position(); minimap->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); - connecting_valid = just_disconnected || click_pos.distance_to(connecting_to / zoom) > MIN_DRAG_DISTANCE_FOR_VALID_CONNECTION; + connecting_valid = just_disconnected || click_pos.distance_to(connecting_to_point / zoom) > MIN_DRAG_DISTANCE_FOR_VALID_CONNECTION; if (connecting_valid) { Vector2 mpos = mm->get_position() / zoom; @@ -739,7 +813,7 @@ void GraphEdit::_top_layer_input(const Ref &p_ev) { Ref port_icon = graph_node->theme_cache.port; - if (!connecting_out) { + if (!connecting_from_output) { for (int j = 0; j < graph_node->get_output_port_count(); j++) { Vector2 pos = graph_node->get_output_port_position(j) * zoom + graph_node->get_position(); Vector2i port_size = Vector2i(port_icon->get_width(), port_icon->get_height()); @@ -753,16 +827,17 @@ void GraphEdit::_top_layer_input(const Ref &p_ev) { if ((type == connecting_type || valid_connection_types.has(ConnectionType(type, connecting_type))) && is_in_output_hotzone(graph_node, j, mpos, port_size)) { - if (!is_node_hover_valid(graph_node->get_name(), j, connecting_from, connecting_index)) { + if (!is_node_hover_valid(graph_node->get_name(), j, connecting_from_node, connecting_from_port_index)) { continue; } - connecting_target = true; - connecting_to = pos; - connecting_target_to = graph_node->get_name(); - connecting_target_index = j; + connecting_target_valid = true; + connecting_to_point = pos; + connecting_target_node = graph_node->get_name(); + connecting_target_port_index = j; return; } } + connecting_target_valid = false; } else { for (int j = 0; j < graph_node->get_input_port_count(); j++) { Vector2 pos = graph_node->get_input_port_position(j) * zoom + graph_node->get_position(); @@ -776,16 +851,17 @@ void GraphEdit::_top_layer_input(const Ref &p_ev) { int type = graph_node->get_input_port_type(j); if ((type == connecting_type || valid_connection_types.has(ConnectionType(connecting_type, type))) && is_in_input_hotzone(graph_node, j, mpos, port_size)) { - if (!is_node_hover_valid(connecting_from, connecting_index, graph_node->get_name(), j)) { + if (!is_node_hover_valid(connecting_from_node, connecting_from_port_index, graph_node->get_name(), j)) { continue; } - connecting_target = true; - connecting_to = pos; - connecting_target_to = graph_node->get_name(); - connecting_target_index = j; + connecting_target_valid = true; + connecting_to_point = pos; + connecting_target_node = graph_node->get_name(); + connecting_target_port_index = j; return; } } + connecting_target_valid = false; } } } @@ -793,17 +869,17 @@ void GraphEdit::_top_layer_input(const Ref &p_ev) { if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed()) { if (connecting_valid) { - if (connecting && connecting_target) { - if (connecting_out) { - emit_signal(SNAME("connection_request"), connecting_from, connecting_index, connecting_target_to, connecting_target_index); + if (connecting && connecting_target_valid) { + if (connecting_from_output) { + emit_signal(SNAME("connection_request"), connecting_from_node, connecting_from_port_index, connecting_target_node, connecting_target_port_index); } else { - emit_signal(SNAME("connection_request"), connecting_target_to, connecting_target_index, connecting_from, connecting_index); + emit_signal(SNAME("connection_request"), connecting_target_node, connecting_target_port_index, connecting_from_node, connecting_from_port_index); } } else if (!just_disconnected) { - if (connecting_out) { - emit_signal(SNAME("connection_to_empty"), connecting_from, connecting_index, mb->get_position()); + if (connecting_from_output) { + emit_signal(SNAME("connection_to_empty"), connecting_from_node, connecting_from_port_index, mb->get_position()); } else { - emit_signal(SNAME("connection_from_empty"), connecting_from, connecting_index, mb->get_position()); + emit_signal(SNAME("connection_from_empty"), connecting_from_node, connecting_from_port_index, mb->get_position()); } } } @@ -905,7 +981,7 @@ bool GraphEdit::is_in_port_hotzone(const Vector2 &p_pos, const Vector2 &p_mouse_ return true; } -PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const Vector2 &p_to) { +PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const Vector2 &p_to) const { Vector ret; if (GDVIRTUAL_CALL(_get_connection_line, p_from, p_to, ret)) { return ret; @@ -930,96 +1006,249 @@ PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const V } } -void GraphEdit::_draw_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_zoom) { - Vector points = get_connection_line(p_from / p_zoom, p_to / p_zoom); - Vector scaled_points; - Vector colors; - float length = (p_from / p_zoom).distance_to(p_to / p_zoom); - for (int i = 0; i < points.size(); i++) { - float d = (p_from / p_zoom).distance_to(points[i]) / length; - colors.push_back(p_color.lerp(p_to_color, d)); - scaled_points.push_back(points[i] * p_zoom); +Ref GraphEdit::get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance) const { + Vector2 transformed_point = p_point + get_scroll_offset(); + + Ref closest_connection; + float closest_distance = p_max_distance; + for (const Ref &c : connections) { + if (c->_cache.aabb.distance_to(transformed_point) > p_max_distance) { + continue; + } + + Vector points = get_connection_line(c->_cache.from_pos * zoom, c->_cache.to_pos * zoom); + for (int i = 0; i < points.size() - 1; i++) { + float distance = Geometry2D::get_distance_to_segment(transformed_point, &points[i]); + if (distance <= lines_thickness * 0.5 + p_max_distance && distance < closest_distance) { + closest_connection = c; + closest_distance = distance; + } + } } - // Thickness below 0.5 doesn't look good on the graph or its minimap. - p_where->draw_polyline_colors(scaled_points, colors, MAX(0.5, Math::floor(p_width * theme_cache.base_scale)), lines_antialiased); + return closest_connection; } -void GraphEdit::_connections_layer_draw() { - // Draw connections. - List::Element *> to_erase; - for (List::Element *E = connections.front(); E; E = E->next()) { - const Connection &c = E->get(); +List> GraphEdit::get_connections_intersecting_with_rect(const Rect2 &p_rect) const { + Rect2 transformed_rect = p_rect; + transformed_rect.position += get_scroll_offset(); - Node *from = get_node(NodePath(c.from_node)); - GraphNode *gnode_from = Object::cast_to(from); - - if (!gnode_from) { - to_erase.push_back(E); + List> intersecting_connections; + for (const Ref &c : connections) { + if (!c->_cache.aabb.intersects(transformed_rect)) { continue; } - Node *to = get_node(NodePath(c.to_node)); - GraphNode *gnode_to = Object::cast_to(to); - - if (!gnode_to) { - to_erase.push_back(E); - continue; + Vector points = get_connection_line(c->_cache.from_pos * zoom, c->_cache.to_pos * zoom); + for (int i = 0; i < points.size() - 1; i++) { + if (Geometry2D::segment_intersects_rect(points[i], points[i + 1], transformed_rect)) { + intersecting_connections.push_back(c); + break; + } } + } + return intersecting_connections; +} - Vector2 frompos = gnode_from->get_output_port_position(c.from_port) * zoom + gnode_from->get_position_offset() * zoom; - Color color = gnode_from->get_output_port_color(c.from_port); - Vector2 topos = gnode_to->get_input_port_position(c.to_port) * zoom + gnode_to->get_position_offset() * zoom; - Color tocolor = gnode_to->get_input_port_color(c.to_port); +void GraphEdit::_draw_minimap_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_from_color, const Color &p_to_color) { + const Vector &points = get_connection_line(p_from, p_to); + LocalVector colors; + colors.reserve(points.size()); - if (c.activity > 0) { - color = color.lerp(theme_cache.activity_color, c.activity); - tocolor = tocolor.lerp(theme_cache.activity_color, c.activity); - } - _draw_connection_line(connections_layer, frompos, topos, color, tocolor, lines_thickness, zoom); + float length_inv = 1.0 / (p_from).distance_to(p_to); + for (const Vector2 &point : points) { + float normalized_curve_position = (p_from).distance_to(point) * length_inv; + colors.push_back(p_from_color.lerp(p_to_color, normalized_curve_position)); } - for (List::Element *&E : to_erase) { - connections.erase(E); + p_where->draw_polyline_colors(points, colors, 0.5, lines_antialiased); +} + +void GraphEdit::_update_connections() { + // Collect all dead connections and remove them. + List>::Element *> dead_connections; + + for (List>::Element *E = connections.front(); E; E = E->next()) { + Ref &c = E->get(); + + if (c->_cache.dirty) { + Node *from = get_node_or_null(NodePath(c->from_node)); + GraphNode *gnode_from = Object::cast_to(from); + if (!gnode_from) { + dead_connections.push_back(E); + continue; + } + Node *to = get_node_or_null(NodePath(c->to_node)); + GraphNode *gnode_to = Object::cast_to(to); + + if (!gnode_to) { + dead_connections.push_back(E); + continue; + } + + const Vector2 from_pos = gnode_from->get_output_port_position(c->from_port) + gnode_from->get_position_offset(); + const Vector2 to_pos = gnode_to->get_input_port_position(c->to_port) + gnode_to->get_position_offset(); + + const Color from_color = gnode_from->get_output_port_color(c->from_port); + const Color to_color = gnode_to->get_input_port_color(c->to_port); + + const int from_type = gnode_from->get_output_port_type(c->from_port); + const int to_type = gnode_to->get_input_port_type(c->to_port); + + c->_cache.from_pos = from_pos; + c->_cache.to_pos = to_pos; + c->_cache.from_color = from_color; + c->_cache.to_color = to_color; + + PackedVector2Array line_points = get_connection_line(from_pos * zoom, to_pos * zoom); + c->_cache.line->set_points(line_points); + + Ref line_material = c->_cache.line->get_material(); + if (line_material.is_null()) { + line_material.instantiate(); + c->_cache.line->set_material(line_material); + } + + float line_width = _get_shader_line_width(); + line_material->set_shader_parameter("line_width", line_width); + line_material->set_shader_parameter("from_type", from_type); + line_material->set_shader_parameter("to_type", to_type); + line_material->set_shader_parameter("rim_color", theme_cache.connection_rim_color); + + // Compute bounding box of the line, including the line width. + c->_cache.aabb = Rect2(line_points[0], Vector2()); + for (int i = 0; i < line_points.size(); i++) { + c->_cache.aabb.expand_to(line_points[i]); + } + c->_cache.aabb.grow_by(lines_thickness * 0.5); + + c->_cache.dirty = false; + } + + // Skip updating/drawing connections that are not visible. + Rect2 viewport_rect = get_viewport_rect(); + viewport_rect.position += get_scroll_offset(); + if (!c->_cache.aabb.intersects(viewport_rect)) { + continue; + } + + Color from_color = c->_cache.from_color; + Color to_color = c->_cache.to_color; + + if (c->activity > 0) { + from_color = from_color.lerp(theme_cache.activity_color, c->activity); + to_color = to_color.lerp(theme_cache.activity_color, c->activity); + } + + if (c == hovered_connection) { + from_color = from_color.blend(theme_cache.connection_hover_tint_color); + to_color = to_color.blend(theme_cache.connection_hover_tint_color); + } + + // Update Line2D node. + Ref line_gradient = memnew(Gradient); + + float line_width = _get_shader_line_width(); + c->_cache.line->set_width(line_width); + line_gradient->set_color(0, from_color); + line_gradient->set_color(1, to_color); + + c->_cache.line->set_gradient(line_gradient); + } + + for (const List>::Element *E : dead_connections) { + List> &connections_from = connection_map[E->get()->from_node]; + List> &connections_to = connection_map[E->get()->to_node]; + connections_from.erase(E->get()); + connections_to.erase(E->get()); + E->get()->_cache.line->queue_free(); + + connections.erase(E->get()); } } void GraphEdit::_top_layer_draw() { + if (!box_selecting) { + return; + } + + top_layer->draw_rect(box_selecting_rect, theme_cache.selection_fill); + top_layer->draw_rect(box_selecting_rect, theme_cache.selection_stroke, false); +} + +void GraphEdit::_update_top_connection_layer() { _update_scroll(); - if (connecting) { - Node *node_from = get_node_or_null(NodePath(connecting_from)); - ERR_FAIL_NULL(node_from); - GraphNode *graph_node_from = Object::cast_to(node_from); - ERR_FAIL_NULL(graph_node_from); - Vector2 pos; - if (connecting_out) { - pos = graph_node_from->get_output_port_position(connecting_index) * zoom; + if (!connecting) { + dragged_connection_line->clear_points(); + + return; + } + + GraphNode *graph_node_from = Object::cast_to(get_node_or_null(NodePath(connecting_from_node))); + ERR_FAIL_NULL(graph_node_from); + + Vector2 from_pos = graph_node_from->get_position() / zoom; + Vector2 to_pos = connecting_to_point / zoom; + int from_type; + int to_type = connecting_type; + Color from_color; + Color to_color = connecting_color; + + if (connecting_from_output) { + from_pos += graph_node_from->get_output_port_position(connecting_from_port_index); + from_type = graph_node_from->get_output_port_type(connecting_from_port_index); + from_color = graph_node_from->get_output_port_color(connecting_from_port_index); + } else { + from_pos += graph_node_from->get_input_port_position(connecting_from_port_index); + from_type = graph_node_from->get_input_port_type(connecting_from_port_index); + from_color = graph_node_from->get_input_port_color(connecting_from_port_index); + } + + if (connecting_target_valid) { + GraphNode *graph_node_to = Object::cast_to(get_node_or_null(NodePath(connecting_target_node))); + ERR_FAIL_NULL(graph_node_to); + if (connecting_from_output) { + to_type = graph_node_to->get_input_port_type(connecting_target_port_index); + to_color = graph_node_to->get_input_port_color(connecting_target_port_index); } else { - pos = graph_node_from->get_input_port_position(connecting_index) * zoom; - } - pos += graph_node_from->get_position(); - - Vector2 to_pos = connecting_to; - Color line_color = connecting_color; - - // Draw the line to the mouse cursor brighter when it's over a valid target port. - if (connecting_target) { - line_color.r += CONNECTING_TARGET_LINE_COLOR_BRIGHTENING; - line_color.g += CONNECTING_TARGET_LINE_COLOR_BRIGHTENING; - line_color.b += CONNECTING_TARGET_LINE_COLOR_BRIGHTENING; + to_type = graph_node_to->get_output_port_type(connecting_target_port_index); + to_color = graph_node_to->get_output_port_color(connecting_target_port_index); } - if (!connecting_out) { - SWAP(pos, to_pos); - } - _draw_connection_line(top_layer, pos, to_pos, line_color, line_color, lines_thickness, zoom); + // Highlight the line to the mouse cursor when it's over a valid target port. + from_color = from_color.blend(theme_cache.connection_valid_target_tint_color); + to_color = to_color.blend(theme_cache.connection_valid_target_tint_color); } - if (box_selecting) { - top_layer->draw_rect(box_selecting_rect, theme_cache.selection_fill); - top_layer->draw_rect(box_selecting_rect, theme_cache.selection_stroke, false); + if (!connecting_from_output) { + SWAP(from_pos, to_pos); + SWAP(from_type, to_type); + SWAP(from_color, to_color); } + + PackedVector2Array line_points = get_connection_line(from_pos * zoom, to_pos * zoom); + dragged_connection_line->set_points(line_points); + + Ref line_material = dragged_connection_line->get_material(); + if (line_material.is_null()) { + line_material.instantiate(); + line_material->set_shader(connections_shader); + dragged_connection_line->set_material(line_material); + } + + float line_width = _get_shader_line_width(); + line_material->set_shader_parameter("line_width", line_width); + line_material->set_shader_parameter("from_type", from_type); + line_material->set_shader_parameter("to_type", to_type); + line_material->set_shader_parameter("rim_color", theme_cache.connection_rim_color); + + Ref line_gradient = memnew(Gradient); + dragged_connection_line->set_width(line_width); + line_gradient->set_color(0, from_color); + line_gradient->set_color(1, to_color); + + dragged_connection_line->set_gradient(line_gradient); } void GraphEdit::_minimap_draw() { @@ -1060,31 +1289,17 @@ void GraphEdit::_minimap_draw() { } // Draw node connections. - for (const Connection &E : connections) { - Node *from = get_node(NodePath(E.from_node)); - GraphNode *graph_node_from = Object::cast_to(from); - if (!graph_node_from) { - continue; - } + for (const Ref &c : connections) { + Vector2 from_position = minimap->_convert_from_graph_position(c->_cache.from_pos * zoom - graph_offset) + minimap_offset; + Vector2 to_position = minimap->_convert_from_graph_position(c->_cache.to_pos * zoom - graph_offset) + minimap_offset; + Color from_color = c->_cache.from_color; + Color to_color = c->_cache.to_color; - Node *node_to = get_node(NodePath(E.to_node)); - GraphNode *graph_node_to = Object::cast_to(node_to); - if (!graph_node_to) { - continue; + if (c->activity > 0) { + from_color = from_color.lerp(theme_cache.activity_color, c->activity); + to_color = to_color.lerp(theme_cache.activity_color, c->activity); } - - Vector2 from_port_position = graph_node_from->get_position_offset() * zoom + graph_node_from->get_output_port_position(E.from_port) * zoom; - Vector2 from_position = minimap->_convert_from_graph_position(from_port_position - graph_offset) + minimap_offset; - Color from_color = graph_node_from->get_output_port_color(E.from_port); - Vector2 to_port_position = graph_node_to->get_position_offset() * zoom + graph_node_to->get_input_port_position(E.to_port) * zoom; - Vector2 to_position = minimap->_convert_from_graph_position(to_port_position - graph_offset) + minimap_offset; - Color to_color = graph_node_to->get_input_port_color(E.to_port); - - if (E.activity > 0) { - from_color = from_color.lerp(theme_cache.activity_color, E.activity); - to_color = to_color.lerp(theme_cache.activity_color, E.activity); - } - _draw_connection_line(minimap, from_position, to_position, from_color, to_color, 0.5, minimap->_convert_from_graph_position(Vector2(zoom, zoom)).length()); + _draw_minimap_connection_line(minimap, from_position, to_position, from_color, to_color); } // Draw the "camera" viewport. @@ -1175,7 +1390,15 @@ void GraphEdit::gui_input(const Ref &p_ev) { return; } + // Highlight the connection close to the mouse cursor. Ref mm = p_ev; + if (mm.is_valid()) { + Ref new_highlighted_connection = get_closest_connection_at_point(mm->get_position()); + if (new_highlighted_connection != hovered_connection) { + connections_layer->queue_redraw(); + } + hovered_connection = new_highlighted_connection; + } if (mm.is_valid() && dragging) { if (!moving_selection) { @@ -1201,6 +1424,7 @@ void GraphEdit::gui_input(const Ref &p_ev) { } } + // Box selection logic. if (mm.is_valid() && box_selecting) { box_selecting_to = mm->get_position(); @@ -1281,10 +1505,10 @@ void GraphEdit::gui_input(const Ref &p_ev) { dragging = false; - top_layer->queue_redraw(); minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); } // Node selection logic. @@ -1430,29 +1654,56 @@ void GraphEdit::gui_input(const Ref &p_ev) { void GraphEdit::_pan_callback(Vector2 p_scroll_vec, Ref p_event) { h_scrollbar->set_value(h_scrollbar->get_value() - p_scroll_vec.x); v_scrollbar->set_value(v_scrollbar->get_value() - p_scroll_vec.y); + + connections_layer->queue_redraw(); } void GraphEdit::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref p_event) { + // We need to invalidate all connections since we don't know whether + // the user is zooming/panning at the same time. + _invalidate_connection_line_cache(); + set_zoom_custom(zoom * p_zoom_factor, p_origin); } void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity) { - for (Connection &E : connections) { - if (E.from_node == p_from && E.from_port == p_from_port && E.to_node == p_to && E.to_port == p_to_port) { - if (!Math::is_equal_approx(E.activity, p_activity)) { + for (Ref &c : connection_map[p_from]) { + if (c->from_node == p_from && c->from_port == p_from_port && c->to_node == p_to && c->to_port == p_to_port) { + if (!Math::is_equal_approx(c->activity, p_activity)) { // Update only if changed. - top_layer->queue_redraw(); minimap->queue_redraw(); + c->_cache.dirty = true; connections_layer->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); } - E.activity = p_activity; + c->activity = p_activity; return; } } } +void GraphEdit::reset_all_connection_activity() { + bool changed = false; + for (Ref &conn : connections) { + if (conn->activity > 0) { + changed = true; + conn->_cache.dirty = true; + } + conn->activity = 0; + } + if (changed) { + connections_layer->queue_redraw(); + } +} + void GraphEdit::clear_connections() { + for (Ref &c : connections) { + c->_cache.line->queue_free(); + } + connections.clear(); + connection_map.clear(); + minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); @@ -1462,10 +1713,10 @@ void GraphEdit::force_connection_drag_end() { ERR_FAIL_COND_MSG(!connecting, "Drag end requested without active drag!"); connecting = false; connecting_valid = false; - top_layer->queue_redraw(); minimap->queue_redraw(); queue_redraw(); connections_layer->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); emit_signal(SNAME("connection_drag_ended")); } @@ -1497,7 +1748,8 @@ void GraphEdit::set_zoom_custom(float p_zoom, const Vector2 &p_center) { Vector2 scrollbar_offset = (Vector2(h_scrollbar->get_value(), v_scrollbar->get_value()) + p_center) / zoom; zoom = p_zoom; - top_layer->queue_redraw(); + + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); zoom_minus_button->set_disabled(zoom == zoom_min); zoom_plus_button->set_disabled(zoom == zoom_max); @@ -1590,15 +1842,42 @@ void GraphEdit::remove_valid_left_disconnect_type(int p_type) { } TypedArray GraphEdit::_get_connection_list() const { - List conns; - get_connection_list(&conns); + List> conns = get_connection_list(); + TypedArray arr; - for (const Connection &E : conns) { + for (const Ref &conn : conns) { Dictionary d; - d["from_node"] = E.from_node; - d["from_port"] = E.from_port; - d["to_node"] = E.to_node; - d["to_port"] = E.to_port; + d["from_node"] = conn->from_node; + d["from_port"] = conn->from_port; + d["to_node"] = conn->to_node; + d["to_port"] = conn->to_port; + arr.push_back(d); + } + return arr; +} + +Dictionary GraphEdit::_get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance) const { + Dictionary ret; + Ref c = get_closest_connection_at_point(p_point, p_max_distance); + if (c.is_valid()) { + ret["from_node"] = c->from_node; + ret["from_port"] = c->from_port; + ret["to_node"] = c->to_node; + ret["to_port"] = c->to_port; + } + return ret; +} + +TypedArray GraphEdit::_get_connections_intersecting_with_rect(const Rect2 &p_rect) const { + List> intersecting_connections = get_connections_intersecting_with_rect(p_rect); + + TypedArray arr; + for (const Ref &conn : intersecting_connections) { + Dictionary d; + d["from_node"] = conn->from_node; + d["from_port"] = conn->from_port; + d["to_node"] = conn->to_node; + d["to_port"] = conn->to_port; arr.push_back(d); } return arr; @@ -1622,6 +1901,16 @@ void GraphEdit::_update_zoom_label() { zoom_label->set_text(zoom_text); } +void GraphEdit::_invalidate_connection_line_cache() { + for (Ref &c : connections) { + c->_cache.dirty = true; + } +} + +float GraphEdit::_get_shader_line_width() { + return lines_thickness * theme_cache.base_scale + 4.0; +} + void GraphEdit::add_valid_connection_type(int p_type, int p_with_type) { ConnectionType ct(p_type, p_with_type); valid_connection_types.insert(ct); @@ -1806,6 +2095,15 @@ bool GraphEdit::is_showing_arrange_button() const { return show_arrange_button; } +void GraphEdit::override_connections_shader(const Ref &p_shader) { + connections_shader = p_shader; + + _invalidate_connection_line_cache(); + connections_layer->queue_redraw(); + minimap->queue_redraw(); + callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); +} + void GraphEdit::_minimap_toggled() { if (is_minimap_enabled()) { minimap->set_visible(true); @@ -1817,6 +2115,8 @@ void GraphEdit::_minimap_toggled() { void GraphEdit::set_connection_lines_curvature(float p_curvature) { lines_curvature = p_curvature; + _invalidate_connection_line_cache(); + connections_layer->queue_redraw(); queue_redraw(); } @@ -1825,10 +2125,13 @@ float GraphEdit::get_connection_lines_curvature() const { } void GraphEdit::set_connection_lines_thickness(float p_thickness) { + ERR_FAIL_COND_MSG(p_thickness < 0, "Connection lines thickness must be greater than or equal to 0."); if (lines_thickness == p_thickness) { return; } lines_thickness = p_thickness; + _invalidate_connection_line_cache(); + connections_layer->queue_redraw(); queue_redraw(); } @@ -1841,6 +2144,8 @@ void GraphEdit::set_connection_lines_antialiased(bool p_antialiased) { return; } lines_antialiased = p_antialiased; + _invalidate_connection_line_cache(); + connections_layer->queue_redraw(); queue_redraw(); } @@ -1870,6 +2175,8 @@ void GraphEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("disconnect_node", "from_node", "from_port", "to_node", "to_port"), &GraphEdit::disconnect_node); ClassDB::bind_method(D_METHOD("set_connection_activity", "from_node", "from_port", "to_node", "to_port", "amount"), &GraphEdit::set_connection_activity); ClassDB::bind_method(D_METHOD("get_connection_list"), &GraphEdit::_get_connection_list); + ClassDB::bind_method(D_METHOD("get_closest_connection_at_point", "point", "max_distance"), &GraphEdit::_get_closest_connection_at_point, DEFVAL(4.0)); + ClassDB::bind_method(D_METHOD("get_connections_intersecting_with_rect", "rect"), &GraphEdit::_get_connections_intersecting_with_rect); ClassDB::bind_method(D_METHOD("clear_connections"), &GraphEdit::clear_connections); ClassDB::bind_method(D_METHOD("force_connection_drag_end"), &GraphEdit::force_connection_drag_end); ClassDB::bind_method(D_METHOD("get_scroll_offset"), &GraphEdit::get_scroll_offset); @@ -1971,7 +2278,7 @@ void GraphEdit::_bind_methods() { ADD_GROUP("Connection Lines", "connection_lines"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_curvature"), "set_connection_lines_curvature", "get_connection_lines_curvature"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness", PROPERTY_HINT_NONE, "suffix:px"), "set_connection_lines_thickness", "get_connection_lines_thickness"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness", PROPERTY_HINT_RANGE, "0,100,0.1,suffix:px"), "set_connection_lines_thickness", "get_connection_lines_thickness"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "connection_lines_antialiased"), "set_connection_lines_antialiased", "is_connection_lines_antialiased"); ADD_GROUP("Zoom", ""); @@ -2025,6 +2332,9 @@ void GraphEdit::_bind_methods() { BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, grid_minor); BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, GraphEdit, activity_color, "activity"); + BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, connection_hover_tint_color); + BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, connection_valid_target_tint_color); + BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, connection_rim_color); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, selection_fill); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, selection_stroke); @@ -2056,21 +2366,33 @@ GraphEdit::GraphEdit() { panner.instantiate(); panner->set_callbacks(callable_mp(this, &GraphEdit::_pan_callback), callable_mp(this, &GraphEdit::_zoom_callback)); - top_layer = memnew(GraphEditFilter(this)); + top_layer = memnew(Control); add_child(top_layer, false, INTERNAL_MODE_BACK); - top_layer->set_mouse_filter(MOUSE_FILTER_PASS); + top_layer->set_mouse_filter(MOUSE_FILTER_IGNORE); top_layer->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); top_layer->connect("draw", callable_mp(this, &GraphEdit::_top_layer_draw)); - top_layer->connect("gui_input", callable_mp(this, &GraphEdit::_top_layer_input)); top_layer->connect("focus_exited", callable_mp(panner.ptr(), &ViewPanner::release_pan_key)); connections_layer = memnew(Control); add_child(connections_layer, false, INTERNAL_MODE_FRONT); - connections_layer->connect("draw", callable_mp(this, &GraphEdit::_connections_layer_draw)); + connections_layer->connect("draw", callable_mp(this, &GraphEdit::_update_connections)); connections_layer->set_name("_connection_layer"); connections_layer->set_disable_visibility_clip(true); // Necessary, so it can draw freely and be offset. connections_layer->set_mouse_filter(MOUSE_FILTER_IGNORE); + top_connection_layer = memnew(GraphEditFilter(this)); + add_child(top_connection_layer, false, INTERNAL_MODE_BACK); + + connections_shader = default_connections_shader; + + top_connection_layer->set_mouse_filter(MOUSE_FILTER_PASS); + top_connection_layer->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); + top_connection_layer->connect("gui_input", callable_mp(this, &GraphEdit::_top_connection_layer_input)); + + dragged_connection_line = memnew(Line2D); + dragged_connection_line->set_texture_mode(Line2D::LINE_TEXTURE_STRETCH); + top_connection_layer->add_child(dragged_connection_line); + h_scrollbar = memnew(HScrollBar); h_scrollbar->set_name("_h_scroll"); top_layer->add_child(h_scrollbar); diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h index 31cb495bf8d..e24f039e84d 100644 --- a/scene/gui/graph_edit.h +++ b/scene/gui/graph_edit.h @@ -39,6 +39,7 @@ class GraphEdit; class GraphEditArranger; class HScrollBar; class Label; +class Line2D; class PanelContainer; class SpinBox; class ViewPanner; @@ -112,12 +113,25 @@ class GraphEdit : public Control { GDCLASS(GraphEdit, Control); public: - struct Connection { + struct Connection : RefCounted { StringName from_node; StringName to_node; int from_port = 0; int to_port = 0; float activity = 0.0; + + private: + struct Cache { + bool dirty = true; + Vector2 from_pos; // In graph space. + Vector2 to_pos; // In graph space. + Color from_color; + Color to_color; + Rect2 aabb; // In local screen space. + Line2D *line = nullptr; // In local screen space. + } _cache; + + friend class GraphEdit; }; // Should be in sync with ControlScheme in ViewPanner. @@ -184,15 +198,15 @@ private: GridPattern grid_pattern = GRID_PATTERN_LINES; bool connecting = false; - String connecting_from; - bool connecting_out = false; - int connecting_index = 0; + StringName connecting_from_node; + bool connecting_from_output = false; int connecting_type = 0; Color connecting_color; - bool connecting_target = false; - Vector2 connecting_to; - StringName connecting_target_to; - int connecting_target_index = 0; + Vector2 connecting_to_point; // In local screen space. + bool connecting_target_valid = false; + StringName connecting_target_node; + int connecting_from_port_index = 0; + int connecting_target_port_index = 0; bool just_disconnected = false; bool connecting_valid = false; @@ -222,18 +236,28 @@ private: bool right_disconnects = false; bool updating = false; bool awaiting_scroll_offset_update = false; - List connections; - float lines_thickness = 2.0f; + List> connections; + HashMap>> connection_map; + Ref hovered_connection; + + float lines_thickness = 4.0f; float lines_curvature = 0.5f; bool lines_antialiased = true; PanelContainer *menu_panel = nullptr; HBoxContainer *menu_hbox = nullptr; Control *connections_layer = nullptr; - GraphEditFilter *top_layer = nullptr; + + GraphEditFilter *top_connection_layer = nullptr; // Draws a dragged connection. Necessary since the connection line shader can't be applied to the whole top layer. + Line2D *dragged_connection_line = nullptr; + Control *top_layer = nullptr; // Used for drawing the box selection rect. Contains the minimap, menu panel and the scrollbars. + GraphEditMinimap *minimap = nullptr; + static Ref default_connections_shader; + Ref connections_shader; + Ref arranger; HashSet valid_connection_types; @@ -248,6 +272,10 @@ private: Color grid_minor; Color activity_color; + Color connection_hover_tint_color; + Color connection_valid_target_tint_color; + Color connection_rim_color; + Color selection_fill; Color selection_stroke; @@ -274,30 +302,35 @@ private: void _zoom_plus(); void _update_zoom_label(); - void _draw_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_zoom); - void _graph_element_selected(Node *p_node); void _graph_element_deselected(Node *p_node); void _graph_element_moved_to_front(Node *p_node); void _graph_element_resized(Vector2 p_new_minsize, Node *p_node); void _graph_element_moved(Node *p_node); void _graph_node_slot_updated(int p_index, Node *p_node); + void _graph_node_rect_changed(GraphNode *p_node); void _update_scroll(); void _update_scroll_offset(); void _scroll_moved(double); virtual void gui_input(const Ref &p_ev) override; - void _top_layer_input(const Ref &p_ev); + void _top_connection_layer_input(const Ref &p_ev); + + float _get_shader_line_width(); + void _draw_minimap_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color); + void _invalidate_connection_line_cache(); + void _update_top_connection_layer(); + void _update_connections(); + + void _top_layer_draw(); + void _minimap_draw(); + void _draw_grid(); bool is_in_port_hotzone(const Vector2 &p_pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left); - void _top_layer_draw(); - void _connections_layer_draw(); - void _minimap_draw(); - - void _draw_grid(); - TypedArray _get_connection_list() const; + Dictionary _get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance = 4.0) const; + TypedArray _get_connections_intersecting_with_rect(const Rect2 &p_rect) const; friend class GraphEditFilter; bool _filter_input(const Point2 &p_point); @@ -313,6 +346,7 @@ private: #ifndef DISABLE_DEPRECATED bool _is_arrange_nodes_button_hidden_bind_compat_81582() const; void _set_arrange_nodes_button_hidden_bind_compat_81582(bool p_enable); + PackedVector2Array _get_connection_line_bind_compat_86158(const Vector2 &p_from, const Vector2 &p_to); #endif protected: @@ -336,6 +370,9 @@ protected: GDVIRTUAL4R(bool, _is_node_hover_valid, StringName, int, StringName, int); public: + static void init_shaders(); + static void finish_shaders(); + virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override; PackedStringArray get_configuration_warnings() const override; @@ -344,12 +381,17 @@ public: bool is_node_connected(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); void disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); void clear_connections(); - void force_connection_drag_end(); - virtual PackedVector2Array get_connection_line(const Vector2 &p_from, const Vector2 &p_to); + void force_connection_drag_end(); + const List> &get_connection_list() const; + virtual PackedVector2Array get_connection_line(const Vector2 &p_from, const Vector2 &p_to) const; + Ref get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance = 4.0) const; + List> get_connections_intersecting_with_rect(const Rect2 &p_rect) const; + virtual bool is_node_hover_valid(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); void set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity); + void reset_all_connection_activity(); void add_valid_connection_type(int p_type, int p_with_type); void remove_valid_connection_type(int p_type, int p_with_type); @@ -392,10 +434,10 @@ public: void set_show_arrange_button(bool p_hidden); bool is_showing_arrange_button() const; - GraphEditFilter *get_top_layer() const { return top_layer; } + Control *get_top_layer() const { return top_layer; } GraphEditMinimap *get_minimap() const { return minimap; } - void get_connection_list(List *r_connections) const; + void override_connections_shader(const Ref &p_shader); void set_right_disconnects(bool p_enable); bool is_right_disconnects_enabled() const; diff --git a/scene/gui/graph_edit_arranger.cpp b/scene/gui/graph_edit_arranger.cpp index 29c3056b3b8..49998beb424 100644 --- a/scene/gui/graph_edit_arranger.cpp +++ b/scene/gui/graph_edit_arranger.cpp @@ -65,8 +65,7 @@ void GraphEditArranger::arrange_nodes() { float gap_v = 100.0f; float gap_h = 100.0f; - List connection_list; - graph_edit->get_connection_list(&connection_list); + List> connection_list = graph_edit->get_connection_list(); for (int i = graph_edit->get_child_count() - 1; i >= 0; i--) { GraphNode *graph_element = Object::cast_to(graph_edit->get_child(i)); @@ -77,15 +76,16 @@ void GraphEditArranger::arrange_nodes() { if (graph_element->is_selected() || arrange_entire_graph) { selected_nodes.insert(graph_element->get_name()); HashSet s; - for (List::Element *E = connection_list.front(); E; E = E->next()) { - GraphNode *p_from = Object::cast_to(node_names[E->get().from_node]); - if (E->get().to_node == graph_element->get_name() && (p_from->is_selected() || arrange_entire_graph) && E->get().to_node != E->get().from_node) { + + for (const Ref &connection : connection_list) { + GraphNode *p_from = Object::cast_to(node_names[connection->from_node]); + if (connection->to_node == graph_element->get_name() && (p_from->is_selected() || arrange_entire_graph) && connection->to_node != connection->from_node) { if (!s.has(p_from->get_name())) { s.insert(p_from->get_name()); } - String s_connection = String(p_from->get_name()) + " " + String(E->get().to_node); + String s_connection = String(p_from->get_name()) + " " + String(connection->to_node); StringName _connection(s_connection); - Pair ports(E->get().from_port, E->get().to_port); + Pair ports(connection->from_port, connection->to_port); port_info.insert(_connection, ports); } } @@ -437,31 +437,30 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Strin float threshold = p_current_threshold; if (p_v == p_w) { int min_order = MAX_ORDER; - GraphEdit::Connection incoming; - List connection_list; - graph_edit->get_connection_list(&connection_list); - for (List::Element *E = connection_list.front(); E; E = E->next()) { - if (E->get().to_node == p_w) { - ORDER(E->get().from_node, r_layers); + Ref incoming; + List> connection_list = graph_edit->get_connection_list(); + for (const Ref &connection : connection_list) { + if (connection->to_node == p_w) { + ORDER(connection->from_node, r_layers); if (min_order > order) { min_order = order; - incoming = E->get(); + incoming = connection; } } } - if (incoming.from_node != StringName()) { - GraphNode *gnode_from = Object::cast_to(r_node_names[incoming.from_node]); + if (incoming.is_valid()) { + GraphNode *gnode_from = Object::cast_to(r_node_names[incoming->from_node]); GraphNode *gnode_to = Object::cast_to(r_node_names[p_w]); - Vector2 pos_from = gnode_from->get_output_port_position(incoming.from_port) * graph_edit->get_zoom(); - Vector2 pos_to = gnode_to->get_input_port_position(incoming.to_port) * graph_edit->get_zoom(); + Vector2 pos_from = gnode_from->get_output_port_position(incoming->from_port) * graph_edit->get_zoom(); + Vector2 pos_to = gnode_to->get_input_port_position(incoming->to_port) * graph_edit->get_zoom(); // If connected block node is selected, calculate thershold or add current block to list. if (gnode_from->is_selected()) { - Vector2 connected_block_pos = r_node_positions[r_root[incoming.from_node]]; + Vector2 connected_block_pos = r_node_positions[r_root[incoming->from_node]]; if (connected_block_pos.y != FLT_MAX) { //Connected block is placed, calculate threshold. - threshold = connected_block_pos.y + (real_t)r_inner_shift[incoming.from_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y; + threshold = connected_block_pos.y + (real_t)r_inner_shift[incoming->from_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y; } } } @@ -469,31 +468,30 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Strin if (threshold == FLT_MIN && (StringName)r_align[p_w] == p_v) { // This time, pick an outgoing edge and repeat as above! int min_order = MAX_ORDER; - GraphEdit::Connection outgoing; - List connection_list; - graph_edit->get_connection_list(&connection_list); - for (List::Element *E = connection_list.front(); E; E = E->next()) { - if (E->get().from_node == p_w) { - ORDER(E->get().to_node, r_layers); + Ref outgoing; + List> connection_list = graph_edit->get_connection_list(); + for (const Ref &connection : connection_list) { + if (connection->from_node == p_w) { + ORDER(connection->to_node, r_layers); if (min_order > order) { min_order = order; - outgoing = E->get(); + outgoing = connection; } } } - if (outgoing.to_node != StringName()) { + if (outgoing.is_valid()) { GraphNode *gnode_from = Object::cast_to(r_node_names[p_w]); - GraphNode *gnode_to = Object::cast_to(r_node_names[outgoing.to_node]); - Vector2 pos_from = gnode_from->get_output_port_position(outgoing.from_port) * graph_edit->get_zoom(); - Vector2 pos_to = gnode_to->get_input_port_position(outgoing.to_port) * graph_edit->get_zoom(); + GraphNode *gnode_to = Object::cast_to(r_node_names[outgoing->to_node]); + Vector2 pos_from = gnode_from->get_output_port_position(outgoing->from_port) * graph_edit->get_zoom(); + Vector2 pos_to = gnode_to->get_input_port_position(outgoing->to_port) * graph_edit->get_zoom(); // If connected block node is selected, calculate thershold or add current block to list. if (gnode_to->is_selected()) { - Vector2 connected_block_pos = r_node_positions[r_root[outgoing.to_node]]; + Vector2 connected_block_pos = r_node_positions[r_root[outgoing->to_node]]; if (connected_block_pos.y != FLT_MAX) { //Connected block is placed. Calculate threshold - threshold = connected_block_pos.y + (real_t)r_inner_shift[outgoing.to_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y; + threshold = connected_block_pos.y + (real_t)r_inner_shift[outgoing->to_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y; } } } diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 111d6447a02..64a1c72f9d9 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -1184,7 +1184,9 @@ void register_scene_types() { } if (RenderingServer::get_singleton()) { - ColorPicker::init_shaders(); // RenderingServer needs to exist for this to succeed. + // RenderingServer needs to exist for this to succeed. + ColorPicker::init_shaders(); + GraphEdit::init_shaders(); } SceneDebugger::initialize(); @@ -1236,6 +1238,7 @@ void unregister_scene_types() { ParticleProcessMaterial::finish_shaders(); CanvasItemMaterial::finish_shaders(); ColorPicker::finish_shaders(); + GraphEdit::finish_shaders(); SceneStringNames::free(); OS::get_singleton()->benchmark_end_measure("Scene", "Unregister Types"); diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp index 005a88d3917..02774959dfc 100644 --- a/scene/theme/default_theme.cpp +++ b/scene/theme/default_theme.cpp @@ -1161,6 +1161,9 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_color("selection_fill", "GraphEdit", Color(1, 1, 1, 0.3)); theme->set_color("selection_stroke", "GraphEdit", Color(1, 1, 1, 0.8)); theme->set_color("activity", "GraphEdit", Color(1, 1, 1)); + theme->set_color("connection_hover_tint_color", "GraphEdit", Color(0, 0, 0, 0.3)); + theme->set_color("connection_valid_target_tint_color", "GraphEdit", Color(1, 1, 1, 0.4)); + theme->set_color("connection_rim_color", "GraphEdit", style_normal_color); // Visual Node Ports