Rework GraphEdit connections (drawing, API, optimizations)

- GraphEdit now uses Line2D nodes to draw connection lines and uses a dedicated canvas item shader for them
This commit is contained in:
Hendrik Brucker 2024-01-18 16:16:17 +01:00
parent 1952f64b07
commit 9d7c2978f4
10 changed files with 700 additions and 259 deletions

View file

@ -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,

View file

@ -143,7 +143,22 @@
[b]Note:[/b] This method suppresses any other connection request signals apart from [signal connection_drag_ended].
</description>
</method>
<method name="get_connection_line">
<method name="get_closest_connection_at_point" qualifiers="const">
<return type="Dictionary" />
<param index="0" name="point" type="Vector2" />
<param index="1" name="max_distance" type="float" default="4.0" />
<description>
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]
</description>
</method>
<method name="get_connection_line" qualifiers="const">
<return type="PackedVector2Array" />
<param index="0" name="from_node" type="Vector2" />
<param index="1" name="to_node" type="Vector2" />
@ -154,7 +169,14 @@
<method name="get_connection_list" qualifiers="const">
<return type="Dictionary[]" />
<description>
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].
</description>
</method>
<method name="get_connections_intersecting_with_rect" qualifiers="const">
<return type="Dictionary[]" />
<param index="0" name="rect" type="Rect2" />
<description>
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].
</description>
</method>
<method name="get_menu_hbox">
@ -233,7 +255,7 @@
<member name="connection_lines_curvature" type="float" setter="set_connection_lines_curvature" getter="get_connection_lines_curvature" default="0.5">
The curvature of the lines between the nodes. 0 results in straight lines.
</member>
<member name="connection_lines_thickness" type="float" setter="set_connection_lines_thickness" getter="get_connection_lines_thickness" default="2.0">
<member name="connection_lines_thickness" type="float" setter="set_connection_lines_thickness" getter="get_connection_lines_thickness" default="4.0">
The thickness of the lines between the nodes.
</member>
<member name="focus_mode" type="int" setter="set_focus_mode" getter="get_focus_mode" overrides="Control" enum="Control.FocusMode" default="2" />
@ -417,7 +439,16 @@
</constants>
<theme_items>
<theme_item name="activity" data_type="color" type="Color" default="Color(1, 1, 1, 1)">
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]).
</theme_item>
<theme_item name="connection_hover_tint_color" data_type="color" type="Color" default="Color(0, 0, 0, 0.3)">
Color which is blended with the connection line when the mouse is hovering over it.
</theme_item>
<theme_item name="connection_rim_color" data_type="color" type="Color" default="Color(0.1, 0.1, 0.1, 0.6)">
Color of the rim around each connection line used for making intersecting lines more distinguishable.
</theme_item>
<theme_item name="connection_valid_target_tint_color" data_type="color" type="Color" default="Color(1, 1, 1, 0.4)">
Color which is blended with the connection line when the currently dragged connection is hovering over a valid target port.
</theme_item>
<theme_item name="grid_major" data_type="color" type="Color" default="Color(1, 1, 1, 0.2)">
Color of major grid lines/dots.

View file

@ -1367,6 +1367,10 @@ void EditorThemeManager::_populate_standard_styles(const Ref<Theme> &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)));

View file

@ -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.

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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<Connection> connections;
float lines_thickness = 2.0f;
List<Ref<Connection>> connections;
HashMap<StringName, List<Ref<Connection>>> connection_map;
Ref<Connection> 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<Shader> default_connections_shader;
Ref<Shader> connections_shader;
Ref<GraphEditArranger> arranger;
HashSet<ConnectionType, ConnectionType> 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<InputEvent> &p_ev) override;
void _top_layer_input(const Ref<InputEvent> &p_ev);
void _top_connection_layer_input(const Ref<InputEvent> &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<Dictionary> _get_connection_list() const;
Dictionary _get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance = 4.0) const;
TypedArray<Dictionary> _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<Ref<Connection>> &get_connection_list() const;
virtual PackedVector2Array get_connection_line(const Vector2 &p_from, const Vector2 &p_to) const;
Ref<Connection> get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance = 4.0) const;
List<Ref<Connection>> 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<Connection> *r_connections) const;
void override_connections_shader(const Ref<Shader> &p_shader);
void set_right_disconnects(bool p_enable);
bool is_right_disconnects_enabled() const;

View file

@ -65,8 +65,7 @@ void GraphEditArranger::arrange_nodes() {
float gap_v = 100.0f;
float gap_h = 100.0f;
List<GraphEdit::Connection> connection_list;
graph_edit->get_connection_list(&connection_list);
List<Ref<GraphEdit::Connection>> 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<GraphNode>(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<StringName> s;
for (List<GraphEdit::Connection>::Element *E = connection_list.front(); E; E = E->next()) {
GraphNode *p_from = Object::cast_to<GraphNode>(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<GraphEdit::Connection> &connection : connection_list) {
GraphNode *p_from = Object::cast_to<GraphNode>(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<int, int> ports(E->get().from_port, E->get().to_port);
Pair<int, int> 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<GraphEdit::Connection> connection_list;
graph_edit->get_connection_list(&connection_list);
for (List<GraphEdit::Connection>::Element *E = connection_list.front(); E; E = E->next()) {
if (E->get().to_node == p_w) {
ORDER(E->get().from_node, r_layers);
Ref<GraphEdit::Connection> incoming;
List<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connection_list();
for (const Ref<GraphEdit::Connection> &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<GraphNode>(r_node_names[incoming.from_node]);
if (incoming.is_valid()) {
GraphNode *gnode_from = Object::cast_to<GraphNode>(r_node_names[incoming->from_node]);
GraphNode *gnode_to = Object::cast_to<GraphNode>(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<GraphEdit::Connection> connection_list;
graph_edit->get_connection_list(&connection_list);
for (List<GraphEdit::Connection>::Element *E = connection_list.front(); E; E = E->next()) {
if (E->get().from_node == p_w) {
ORDER(E->get().to_node, r_layers);
Ref<GraphEdit::Connection> outgoing;
List<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connection_list();
for (const Ref<GraphEdit::Connection> &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<GraphNode>(r_node_names[p_w]);
GraphNode *gnode_to = Object::cast_to<GraphNode>(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<GraphNode>(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;
}
}
}

View file

@ -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");

View file

@ -1161,6 +1161,9 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &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