Browse Source

Automatic arrangement of nodes in VisualScript/VisualShaders editors

This PR and commit adds the functionality to arrange nodes in VisualScript/VisualShader editor. The layout generated by this 
feature is compact, with minimum crossings between connections
& uniform horizontal & vertical gaps between the nodes. 

This work has been sponsored by GSoC '21.

Full list of additions/changes:
• Added arrange_nodes() method in GraphEdit module.
    • This method computes new positions for all the selected
      nodes by forming blocks and compressing them.
      The nodes are moved to these new positions. 
    • Adding this method to GraphEdit makes it available for 
      use in VisualScript/VisualShaders editors and its other
      subclasses. 
• Button with an icon has been added to call arrange_nodes() in GraphEdit. 
    • This button is inherited by VisualScript/VisualShaders editors
       to invoke the method.
• Undo/redo is functional with this method.
    • By using signals in arrange_nodes(), position changes are registered 
       in undo/redo stack of the subclass that is using the method. 
• Metadata of the method has been updated in ClassDB
• Method description has been added to class reference of GraphEdit
Umang Kalra 4 years ago
parent
commit
12fc3f1eef

+ 8 - 0
doc/classes/GraphEdit.xml

@@ -32,6 +32,12 @@
 				Makes possible to disconnect nodes when dragging from the slot at the right if it has the specified type.
 				Makes possible to disconnect nodes when dragging from the slot at the right if it has the specified type.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="arrange_nodes">
+			<return type="void" />
+			<description>
+				Rearranges selected nodes in a layout with minimum crossings between connections and uniform horizontal and vertical gap between nodes.
+			</description>
+		</method>
 		<method name="clear_connections">
 		<method name="clear_connections">
 			<return type="void" />
 			<return type="void" />
 			<description>
 			<description>
@@ -283,6 +289,8 @@
 		<theme_item name="grid_minor" data_type="color" type="Color" default="Color(1, 1, 1, 0.05)">
 		<theme_item name="grid_minor" data_type="color" type="Color" default="Color(1, 1, 1, 0.05)">
 			Color of minor grid lines.
 			Color of minor grid lines.
 		</theme_item>
 		</theme_item>
+		<theme_item name="layout" data_type="icon" type="Texture2D">
+		</theme_item>
 		<theme_item name="minimap" data_type="icon" type="Texture2D">
 		<theme_item name="minimap" data_type="icon" type="Texture2D">
 		</theme_item>
 		</theme_item>
 		<theme_item name="minus" data_type="icon" type="Texture2D">
 		<theme_item name="minus" data_type="icon" type="Texture2D">

+ 1 - 0
editor/editor_themes.cpp

@@ -1232,6 +1232,7 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) {
 	theme->set_icon("reset", "GraphEdit", theme->get_icon("ZoomReset", "EditorIcons"));
 	theme->set_icon("reset", "GraphEdit", theme->get_icon("ZoomReset", "EditorIcons"));
 	theme->set_icon("snap", "GraphEdit", theme->get_icon("SnapGrid", "EditorIcons"));
 	theme->set_icon("snap", "GraphEdit", theme->get_icon("SnapGrid", "EditorIcons"));
 	theme->set_icon("minimap", "GraphEdit", theme->get_icon("GridMinimap", "EditorIcons"));
 	theme->set_icon("minimap", "GraphEdit", theme->get_icon("GridMinimap", "EditorIcons"));
+	theme->set_icon("layout", "GraphEdit", theme->get_icon("GridLayout", "EditorIcons"));
 	theme->set_constant("bezier_len_pos", "GraphEdit", 80 * EDSCALE);
 	theme->set_constant("bezier_len_pos", "GraphEdit", 80 * EDSCALE);
 	theme->set_constant("bezier_len_neg", "GraphEdit", 160 * EDSCALE);
 	theme->set_constant("bezier_len_neg", "GraphEdit", 160 * EDSCALE);
 
 

+ 1 - 0
editor/icons/GridLayout.svg

@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m14 2.1992188v2.6152343l-2.625 1.3125v-2.6152343zm-12 4.0644531 2.625 1.3125v2.5507811l-2.625-1.3124999zm12 0v2.5507812l-2.625 1.3124999v-2.5507811zm-8 1.4550781h4v2.640625h-4zm-4 2.560547 2.625 1.3125v2.521484l-2.625-1.3125zm12 0v2.521484l-2.625 1.3125v-2.521484zm-8 1.455078h4v2.640625h-4zm1.7014535-8.109375h2.2985465v2.734375h-4.15625s-.7487346.647119-.8746377.640625c-.1310411-.0067594-1.5097373-1.4558594-1.5097373-1.4558594l-1.459375-.7296875v-2.6152343l.068419.034223s.026411-.4573464.062111-.6760553c.0346282-.2121439.1970747-.59225724.1970747-.59225724l-1.0483078-.52372301c-.0795772-.04012218-.1668141-.06276382-.2558594-.06640625-.35427845-.01325803-.64865004.27047362-.6484375.625v12c.00021484.236623.13402736.45284.34570312.558594l3.99999998 2c.086686.043505.1823067.06624.2792969.066406h6c.09699-.000166.192611-.0229.279297-.06641l4-2c.211676-.10575.345488-.321967.345703-.55859v-12c-.000468-.46423753-.488958-.76598317-.904297-.55859375l-3.869141 1.93359375h-2.9709527s.033448.4166167.015891.625c-.029188.3464401-.1950466.625-.1950468.625z" fill="#b05b5b"/><path d="m5 6s-2.21875-2.1616704-2.21875-3.2425057c0-1.0808352 0-2.6072392 2.21875-2.6072392s2.21875 1.526404 2.21875 2.6072392c0 1.0808353-2.21875 3.2425057-2.21875 3.2425057z" fill="#fff" fill-opacity=".68627"/></svg>

+ 1 - 1
modules/visual_script/visual_script_editor.h

@@ -60,7 +60,7 @@ class VisualScriptEditor : public ScriptEditorBase {
 		EDIT_CUT_NODES,
 		EDIT_CUT_NODES,
 		EDIT_PASTE_NODES,
 		EDIT_PASTE_NODES,
 		EDIT_CREATE_FUNCTION,
 		EDIT_CREATE_FUNCTION,
-		REFRESH_GRAPH
+		REFRESH_GRAPH,
 	};
 	};
 
 
 	enum PortAction {
 	enum PortAction {

+ 504 - 0
scene/gui/graph_edit.cpp

@@ -450,6 +450,7 @@ void GraphEdit::_notification(int p_what) {
 		zoom_plus->set_icon(get_theme_icon(SNAME("more")));
 		zoom_plus->set_icon(get_theme_icon(SNAME("more")));
 		snap_button->set_icon(get_theme_icon(SNAME("snap")));
 		snap_button->set_icon(get_theme_icon(SNAME("snap")));
 		minimap_button->set_icon(get_theme_icon(SNAME("minimap")));
 		minimap_button->set_icon(get_theme_icon(SNAME("minimap")));
+		layout_button->set_icon(get_theme_icon(SNAME("layout")));
 	}
 	}
 	if (p_what == NOTIFICATION_READY) {
 	if (p_what == NOTIFICATION_READY) {
 		Size2 hmin = h_scroll->get_combined_minimum_size();
 		Size2 hmin = h_scroll->get_combined_minimum_size();
@@ -1646,6 +1647,500 @@ HBoxContainer *GraphEdit::get_zoom_hbox() {
 	return zoom_hb;
 	return zoom_hb;
 }
 }
 
 
+int GraphEdit::_set_operations(SET_OPERATIONS p_operation, Set<StringName> &r_u, const Set<StringName> &r_v) {
+	switch (p_operation) {
+		case GraphEdit::IS_EQUAL: {
+			for (Set<StringName>::Element *E = r_u.front(); E; E = E->next()) {
+				if (!r_v.has(E->get()))
+					return 0;
+			}
+			return r_u.size() == r_v.size();
+		} break;
+		case GraphEdit::IS_SUBSET: {
+			if (r_u.size() == r_v.size() && !r_u.size()) {
+				return 1;
+			}
+			for (Set<StringName>::Element *E = r_u.front(); E; E = E->next()) {
+				if (!r_v.has(E->get()))
+					return 0;
+			}
+			return 1;
+		} break;
+		case GraphEdit::DIFFERENCE: {
+			for (Set<StringName>::Element *E = r_u.front(); E; E = E->next()) {
+				if (r_v.has(E->get())) {
+					r_u.erase(E->get());
+				}
+			}
+			return r_u.size();
+		} break;
+		case GraphEdit::UNION: {
+			for (Set<StringName>::Element *E = r_v.front(); E; E = E->next()) {
+				if (!r_u.has(E->get())) {
+					r_u.insert(E->get());
+				}
+			}
+			return r_v.size();
+		} break;
+		default:
+			break;
+	}
+	return -1;
+}
+
+HashMap<int, Vector<StringName>> GraphEdit::_layering(const Set<StringName> &r_selected_nodes, const HashMap<StringName, Set<StringName>> &r_upper_neighbours) {
+	HashMap<int, Vector<StringName>> l;
+
+	Set<StringName> p = r_selected_nodes, q = r_selected_nodes, u, z;
+	int current_layer = 0;
+	bool selected = false;
+
+	while (!_set_operations(GraphEdit::IS_EQUAL, q, u)) {
+		_set_operations(GraphEdit::DIFFERENCE, p, u);
+		for (const Set<StringName>::Element *E = p.front(); E; E = E->next()) {
+			Set<StringName> n = r_upper_neighbours[E->get()];
+			if (_set_operations(GraphEdit::IS_SUBSET, n, z)) {
+				Vector<StringName> t;
+				t.push_back(E->get());
+				if (!l.has(current_layer)) {
+					l.set(current_layer, Vector<StringName>{});
+				}
+				selected = true;
+				t.append_array(l[current_layer]);
+				l.set(current_layer, t);
+				Set<StringName> V;
+				V.insert(E->get());
+				_set_operations(GraphEdit::UNION, u, V);
+			}
+		}
+		if (!selected) {
+			current_layer++;
+			_set_operations(GraphEdit::UNION, z, u);
+		}
+		selected = false;
+	}
+
+	return l;
+}
+
+Vector<StringName> GraphEdit::_split(const Vector<StringName> &r_layer, const HashMap<StringName, Dictionary> &r_crossings) {
+	if (!r_layer.size()) {
+		return Vector<StringName>();
+	}
+
+	StringName p = r_layer[Math::random(0, r_layer.size() - 1)];
+	Vector<StringName> left;
+	Vector<StringName> right;
+
+	for (int i = 0; i < r_layer.size(); i++) {
+		if (p != r_layer[i]) {
+			StringName q = r_layer[i];
+			int cross_pq = r_crossings[p][q];
+			int cross_qp = r_crossings[q][p];
+			if (cross_pq > cross_qp) {
+				left.push_back(q);
+			} else {
+				right.push_back(q);
+			}
+		}
+	}
+
+	left.push_back(p);
+	left.append_array(right);
+	return left;
+}
+
+void GraphEdit::_horizontal_alignment(Dictionary &r_root, Dictionary &r_align, const HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, Set<StringName>> &r_upper_neighbours, const Set<StringName> &r_selected_nodes) {
+	for (const Set<StringName>::Element *E = r_selected_nodes.front(); E; E = E->next()) {
+		r_root[E->get()] = E->get();
+		r_align[E->get()] = E->get();
+	}
+
+	if (r_layers.size() == 1) {
+		return;
+	}
+
+	for (unsigned int i = 1; i < r_layers.size(); i++) {
+		Vector<StringName> lower_layer = r_layers[i];
+		Vector<StringName> upper_layer = r_layers[i - 1];
+		int r = -1;
+
+		for (int j = 0; j < lower_layer.size(); j++) {
+			Vector<Pair<int, StringName>> up;
+			StringName current_node = lower_layer[j];
+			for (int k = 0; k < upper_layer.size(); k++) {
+				StringName adjacent_neighbour = upper_layer[k];
+				if (r_upper_neighbours[current_node].has(adjacent_neighbour)) {
+					up.push_back(Pair<int, StringName>(k, adjacent_neighbour));
+				}
+			}
+
+			int start = up.size() / 2;
+			int end = up.size() % 2 ? start : start + 1;
+			for (int p = start; p <= end; p++) {
+				StringName Align = r_align[current_node];
+				if (Align == current_node && r < up[p].first) {
+					r_align[up[p].second] = lower_layer[j];
+					r_root[current_node] = r_root[up[p].second];
+					r_align[current_node] = r_root[up[p].second];
+					r = up[p].first;
+				}
+			}
+		}
+	}
+}
+
+void GraphEdit::_crossing_minimisation(HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, Set<StringName>> &r_upper_neighbours) {
+	if (r_layers.size() == 1) {
+		return;
+	}
+
+	for (unsigned int i = 1; i < r_layers.size(); i++) {
+		Vector<StringName> upper_layer = r_layers[i - 1];
+		Vector<StringName> lower_layer = r_layers[i];
+		HashMap<StringName, Dictionary> c;
+
+		for (int j = 0; j < lower_layer.size(); j++) {
+			StringName p = lower_layer[j];
+			Dictionary d;
+
+			for (int k = 0; k < lower_layer.size(); k++) {
+				unsigned int crossings = 0;
+				StringName q = lower_layer[k];
+
+				if (j != k) {
+					for (int h = 1; h < upper_layer.size(); h++) {
+						if (r_upper_neighbours[p].has(upper_layer[h])) {
+							for (int g = 0; g < h; g++) {
+								if (r_upper_neighbours[q].has(upper_layer[g])) {
+									crossings++;
+								}
+							}
+						}
+					}
+				}
+				d[q] = crossings;
+			}
+			c.set(p, d);
+		}
+
+		r_layers.set(i, _split(lower_layer, c));
+	}
+}
+
+void GraphEdit::_calculate_inner_shifts(Dictionary &r_inner_shifts, const Dictionary &r_root, const Dictionary &r_node_names, const Dictionary &r_align, const Set<StringName> &r_block_heads, const HashMap<StringName, Pair<int, int>> &r_port_info) {
+	for (const Set<StringName>::Element *E = r_block_heads.front(); E; E = E->next()) {
+		real_t left = 0;
+		StringName u = E->get();
+		StringName v = r_align[u];
+		while (u != v && (StringName)r_root[u] != v) {
+			String _connection = String(u) + " " + String(v);
+			GraphNode *gfrom = Object::cast_to<GraphNode>(r_node_names[u]);
+			GraphNode *gto = Object::cast_to<GraphNode>(r_node_names[v]);
+
+			Pair<int, int> ports = r_port_info[_connection];
+			int pfrom = ports.first;
+			int pto = ports.second;
+			Vector2 frompos = gfrom->get_connection_output_position(pfrom);
+			Vector2 topos = gto->get_connection_input_position(pto);
+
+			real_t s = (real_t)r_inner_shifts[u] + (frompos.y - topos.y) / zoom;
+			r_inner_shifts[v] = s;
+			left = MIN(left, s);
+
+			u = v;
+			v = (StringName)r_align[v];
+		}
+
+		u = E->get();
+		do {
+			r_inner_shifts[u] = (real_t)r_inner_shifts[u] - left;
+			u = (StringName)r_align[u];
+		} while (u != E->get());
+	}
+}
+
+float GraphEdit::_calculate_threshold(StringName p_v, StringName p_w, const Dictionary &r_node_names, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_inner_shift, real_t p_current_threshold, const HashMap<StringName, Vector2> &r_node_positions) {
+#define MAX_ORDER 2147483647
+#define ORDER(node, layers)                            \
+	for (unsigned int i = 0; i < layers.size(); i++) { \
+		int index = layers[i].find(node);              \
+		if (index > 0) {                               \
+			order = index;                             \
+			break;                                     \
+		}                                              \
+		order = MAX_ORDER;                             \
+	}
+
+	int order = MAX_ORDER;
+	float threshold = p_current_threshold;
+	if (p_v == p_w) {
+		int min_order = MAX_ORDER;
+		Connection incoming;
+		for (List<Connection>::Element *E = connections.front(); E; E = E->next()) {
+			if (E->get().to == p_w) {
+				ORDER(E->get().from, r_layers);
+				if (min_order > order) {
+					min_order = order;
+					incoming = E->get();
+				}
+			}
+		}
+
+		if (incoming.from != StringName()) {
+			GraphNode *gfrom = Object::cast_to<GraphNode>(r_node_names[incoming.from]);
+			GraphNode *gto = Object::cast_to<GraphNode>(r_node_names[p_w]);
+			Vector2 frompos = gfrom->get_connection_output_position(incoming.from_port);
+			Vector2 topos = gto->get_connection_input_position(incoming.to_port);
+
+			//If connected block node is selected, calculate thershold or add current block to list
+			if (gfrom->is_selected()) {
+				Vector2 connected_block_pos = r_node_positions[r_root[incoming.from]];
+				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] - (real_t)r_inner_shift[p_w] + frompos.y - topos.y;
+				}
+			}
+		}
+	}
+	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;
+		Connection outgoing;
+		for (List<Connection>::Element *E = connections.front(); E; E = E->next()) {
+			if (E->get().from == p_w) {
+				ORDER(E->get().to, r_layers);
+				if (min_order > order) {
+					min_order = order;
+					outgoing = E->get();
+				}
+			}
+		}
+
+		if (outgoing.to != StringName()) {
+			GraphNode *gfrom = Object::cast_to<GraphNode>(r_node_names[p_w]);
+			GraphNode *gto = Object::cast_to<GraphNode>(r_node_names[outgoing.to]);
+			Vector2 frompos = gfrom->get_connection_output_position(outgoing.from_port);
+			Vector2 topos = gto->get_connection_input_position(outgoing.to_port);
+
+			//If connected block node is selected, calculate thershold or add current block to list
+			if (gto->is_selected()) {
+				Vector2 connected_block_pos = r_node_positions[r_root[outgoing.to]];
+				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] - (real_t)r_inner_shift[p_w] + frompos.y - topos.y;
+				}
+			}
+		}
+	}
+#undef MAX_ORDER
+#undef ORDER
+	return threshold;
+}
+
+void GraphEdit::_place_block(StringName p_v, float p_delta, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_node_name, const Dictionary &r_inner_shift, Dictionary &r_sink, Dictionary &r_shift, HashMap<StringName, Vector2> &r_node_positions) {
+#define PRED(node, layers)                             \
+	for (unsigned int i = 0; i < layers.size(); i++) { \
+		int index = layers[i].find(node);              \
+		if (index > 0) {                               \
+			predecessor = layers[i][index - 1];        \
+			break;                                     \
+		}                                              \
+		predecessor = StringName();                    \
+	}
+
+	StringName predecessor;
+	StringName successor;
+	Vector2 pos = r_node_positions[p_v];
+
+	if (pos.y == FLT_MAX) {
+		pos.y = 0;
+		bool initial = false;
+		StringName w = p_v;
+		real_t threshold = FLT_MIN;
+		do {
+			PRED(w, r_layers);
+			if (predecessor != StringName()) {
+				StringName u = r_root[predecessor];
+				_place_block(u, p_delta, r_layers, r_root, r_align, r_node_name, r_inner_shift, r_sink, r_shift, r_node_positions);
+				threshold = _calculate_threshold(p_v, w, r_node_name, r_layers, r_root, r_align, r_inner_shift, threshold, r_node_positions);
+				if ((StringName)r_sink[p_v] == p_v) {
+					r_sink[p_v] = r_sink[u];
+				}
+
+				Vector2 predecessor_root_pos = r_node_positions[u];
+				Vector2 predecessor_node_size = Object::cast_to<GraphNode>(r_node_name[predecessor])->get_size();
+				if (r_sink[p_v] != r_sink[u]) {
+					real_t sc = pos.y + (real_t)r_inner_shift[w] - predecessor_root_pos.y - (real_t)r_inner_shift[predecessor] - predecessor_node_size.y - p_delta;
+					r_shift[r_sink[u]] = MIN(sc, (real_t)r_shift[r_sink[u]]);
+				} else {
+					real_t sb = predecessor_root_pos.y + (real_t)r_inner_shift[predecessor] + predecessor_node_size.y - (real_t)r_inner_shift[w] + p_delta;
+					sb = MAX(sb, threshold);
+					if (initial) {
+						pos.y = sb;
+					} else {
+						pos.y = MAX(pos.y, sb);
+					}
+					initial = false;
+				}
+			}
+			threshold = _calculate_threshold(p_v, w, r_node_name, r_layers, r_root, r_align, r_inner_shift, threshold, r_node_positions);
+			w = r_align[w];
+		} while (w != p_v);
+		r_node_positions.set(p_v, pos);
+	}
+
+#undef PRED
+}
+
+void GraphEdit::arrange_nodes() {
+	if (!arranging_graph) {
+		arranging_graph = true;
+	} else {
+		return;
+	}
+
+	Dictionary node_names;
+	Set<StringName> selected_nodes;
+
+	for (int i = get_child_count() - 1; i >= 0; i--) {
+		GraphNode *gn = Object::cast_to<GraphNode>(get_child(i));
+		if (!gn) {
+			continue;
+		}
+
+		node_names[gn->get_name()] = gn;
+	}
+
+	HashMap<StringName, Set<StringName>> upper_neighbours;
+	HashMap<StringName, Pair<int, int>> port_info;
+	Vector2 origin(FLT_MAX, FLT_MAX);
+
+	float gap_v = 100.0f;
+	float gap_h = 100.0f;
+
+	for (int i = get_child_count() - 1; i >= 0; i--) {
+		GraphNode *gn = Object::cast_to<GraphNode>(get_child(i));
+		if (!gn) {
+			continue;
+		}
+
+		if (gn->is_selected()) {
+			selected_nodes.insert(gn->get_name());
+			origin = origin < gn->get_position_offset() ? origin : gn->get_position_offset();
+			Set<StringName> s;
+			for (List<Connection>::Element *E = connections.front(); E; E = E->next()) {
+				GraphNode *p_from = Object::cast_to<GraphNode>(node_names[E->get().from]);
+				if (E->get().to == gn->get_name() && p_from->is_selected()) {
+					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);
+					StringName _connection(s_connection);
+					Pair<int, int> ports(E->get().from_port, E->get().to_port);
+					if (port_info.has(_connection)) {
+						Pair<int, int> p_ports = port_info[_connection];
+						if (p_ports.first < ports.first) {
+							ports = p_ports;
+						}
+					}
+					port_info.set(_connection, ports);
+				}
+			}
+			upper_neighbours.set(gn->get_name(), s);
+		}
+	}
+
+	HashMap<int, Vector<StringName>> layers = _layering(selected_nodes, upper_neighbours);
+	_crossing_minimisation(layers, upper_neighbours);
+
+	Dictionary root, align, sink, shift;
+	_horizontal_alignment(root, align, layers, upper_neighbours, selected_nodes);
+
+	HashMap<StringName, Vector2> new_positions;
+	Vector2 default_position(FLT_MAX, FLT_MAX);
+	Dictionary inner_shift;
+	Set<StringName> block_heads;
+
+	for (const Set<StringName>::Element *E = selected_nodes.front(); E; E = E->next()) {
+		inner_shift[E->get()] = 0.0f;
+		sink[E->get()] = E->get();
+		shift[E->get()] = FLT_MAX;
+		new_positions.set(E->get(), default_position);
+		if ((StringName)root[E->get()] == E->get()) {
+			block_heads.insert(E->get());
+		}
+	}
+
+	_calculate_inner_shifts(inner_shift, root, node_names, align, block_heads, port_info);
+
+	for (const Set<StringName>::Element *E = block_heads.front(); E; E = E->next()) {
+		_place_block(E->get(), gap_v, layers, root, align, node_names, inner_shift, sink, shift, new_positions);
+	}
+
+	for (const Set<StringName>::Element *E = block_heads.front(); E; E = E->next()) {
+		StringName u = E->get();
+		StringName prev = u;
+		float start_from = origin.y + new_positions[E->get()].y;
+		do {
+			Vector2 cal_pos;
+			cal_pos.y = start_from + (real_t)inner_shift[u];
+			new_positions.set(u, cal_pos);
+			prev = u;
+			u = align[u];
+		} while (u != E->get());
+	}
+
+	//Compute horizontal co-ordinates individually for layers to get uniform gap
+	float start_from = origin.x;
+	float largest_node_size = 0.0f;
+
+	for (unsigned int i = 0; i < layers.size(); i++) {
+		Vector<StringName> layer = layers[i];
+		for (int j = 0; j < layer.size(); j++) {
+			float current_node_size = Object::cast_to<GraphNode>(node_names[layer[j]])->get_size().x;
+			largest_node_size = MAX(largest_node_size, current_node_size);
+		}
+
+		for (int j = 0; j < layer.size(); j++) {
+			float current_node_size = Object::cast_to<GraphNode>(node_names[layer[j]])->get_size().x;
+			Vector2 cal_pos = new_positions[layer[j]];
+
+			if (current_node_size == largest_node_size) {
+				cal_pos.x = start_from;
+			} else {
+				float current_node_start_pos;
+				if (current_node_size >= largest_node_size / 2) {
+					current_node_start_pos = start_from;
+				} else {
+					current_node_start_pos = start_from + largest_node_size - current_node_size;
+				}
+				cal_pos.x = current_node_start_pos;
+			}
+			new_positions.set(layer[j], cal_pos);
+		}
+
+		start_from += largest_node_size + gap_h;
+		largest_node_size = 0.0f;
+	}
+
+	emit_signal("begin_node_move");
+	for (const Set<StringName>::Element *E = selected_nodes.front(); E; E = E->next()) {
+		GraphNode *gn = Object::cast_to<GraphNode>(node_names[E->get()]);
+		gn->set_drag(true);
+		Vector2 pos = (new_positions[E->get()]);
+
+		if (is_using_snap()) {
+			const int snap = get_snap();
+			pos = pos.snapped(Vector2(snap, snap));
+		}
+		gn->set_position_offset(pos);
+		gn->set_drag(false);
+	}
+	emit_signal("end_node_move");
+	arranging_graph = false;
+}
+
 void GraphEdit::_bind_methods() {
 void GraphEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("connect_node", "from", "from_port", "to", "to_port"), &GraphEdit::connect_node);
 	ClassDB::bind_method(D_METHOD("connect_node", "from", "from_port", "to", "to_port"), &GraphEdit::connect_node);
 	ClassDB::bind_method(D_METHOD("is_node_connected", "from", "from_port", "to", "to_port"), &GraphEdit::is_node_connected);
 	ClassDB::bind_method(D_METHOD("is_node_connected", "from", "from_port", "to", "to_port"), &GraphEdit::is_node_connected);
@@ -1707,6 +2202,8 @@ void GraphEdit::_bind_methods() {
 
 
 	ClassDB::bind_method(D_METHOD("get_zoom_hbox"), &GraphEdit::get_zoom_hbox);
 	ClassDB::bind_method(D_METHOD("get_zoom_hbox"), &GraphEdit::get_zoom_hbox);
 
 
+	ClassDB::bind_method(D_METHOD("arrange_nodes"), &GraphEdit::arrange_nodes);
+
 	ClassDB::bind_method(D_METHOD("set_selected", "node"), &GraphEdit::set_selected);
 	ClassDB::bind_method(D_METHOD("set_selected", "node"), &GraphEdit::set_selected);
 
 
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "right_disconnects"), "set_right_disconnects", "is_right_disconnects_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "right_disconnects"), "set_right_disconnects", "is_right_disconnects_enabled");
@@ -1851,6 +2348,13 @@ GraphEdit::GraphEdit() {
 	minimap_button->set_focus_mode(FOCUS_NONE);
 	minimap_button->set_focus_mode(FOCUS_NONE);
 	zoom_hb->add_child(minimap_button);
 	zoom_hb->add_child(minimap_button);
 
 
+	layout_button = memnew(Button);
+	layout_button->set_flat(true);
+	zoom_hb->add_child(layout_button);
+	layout_button->set_tooltip(RTR("Arrange nodes."));
+	layout_button->connect("pressed", callable_mp(this, &GraphEdit::arrange_nodes));
+	layout_button->set_focus_mode(FOCUS_NONE);
+
 	Vector2 minimap_size = Vector2(240, 160);
 	Vector2 minimap_size = Vector2(240, 160);
 	float minimap_opacity = 0.65;
 	float minimap_opacity = 0.65;
 
 

+ 22 - 0
scene/gui/graph_edit.h

@@ -116,6 +116,8 @@ private:
 
 
 	Button *minimap_button;
 	Button *minimap_button;
 
 
+	Button *layout_button;
+
 	HScrollBar *h_scroll;
 	HScrollBar *h_scroll;
 	VScrollBar *v_scroll;
 	VScrollBar *v_scroll;
 
 
@@ -230,6 +232,24 @@ private:
 
 
 	bool _check_clickable_control(Control *p_control, const Vector2 &pos);
 	bool _check_clickable_control(Control *p_control, const Vector2 &pos);
 
 
+	bool arranging_graph = false;
+
+	enum SET_OPERATIONS {
+		IS_EQUAL,
+		IS_SUBSET,
+		DIFFERENCE,
+		UNION,
+	};
+
+	int _set_operations(SET_OPERATIONS p_operation, Set<StringName> &r_u, const Set<StringName> &r_v);
+	HashMap<int, Vector<StringName>> _layering(const Set<StringName> &r_selected_nodes, const HashMap<StringName, Set<StringName>> &r_upper_neighbours);
+	Vector<StringName> _split(const Vector<StringName> &r_layer, const HashMap<StringName, Dictionary> &r_crossings);
+	void _horizontal_alignment(Dictionary &r_root, Dictionary &r_align, const HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, Set<StringName>> &r_upper_neighbours, const Set<StringName> &r_selected_nodes);
+	void _crossing_minimisation(HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, Set<StringName>> &r_upper_neighbours);
+	void _calculate_inner_shifts(Dictionary &r_inner_shifts, const Dictionary &r_root, const Dictionary &r_node_names, const Dictionary &r_align, const Set<StringName> &r_block_heads, const HashMap<StringName, Pair<int, int>> &r_port_info);
+	float _calculate_threshold(StringName p_v, StringName p_w, const Dictionary &r_node_names, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_inner_shift, real_t p_current_threshold, const HashMap<StringName, Vector2> &r_node_positions);
+	void _place_block(StringName p_v, float p_delta, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_node_name, const Dictionary &r_inner_shift, Dictionary &r_sink, Dictionary &r_shift, HashMap<StringName, Vector2> &r_node_positions);
+
 protected:
 protected:
 	static void _bind_methods();
 	static void _bind_methods();
 	virtual void add_child_notify(Node *p_child) override;
 	virtual void add_child_notify(Node *p_child) override;
@@ -304,6 +324,8 @@ public:
 
 
 	HBoxContainer *get_zoom_hbox();
 	HBoxContainer *get_zoom_hbox();
 
 
+	void arrange_nodes();
+
 	GraphEdit();
 	GraphEdit();
 };
 };
 
 

+ 1 - 0
scene/resources/default_theme/default_theme.cpp

@@ -953,6 +953,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
 	theme->set_icon("more", "GraphEdit", make_icon(icon_zoom_more_png));
 	theme->set_icon("more", "GraphEdit", make_icon(icon_zoom_more_png));
 	theme->set_icon("snap", "GraphEdit", make_icon(icon_snap_grid_png));
 	theme->set_icon("snap", "GraphEdit", make_icon(icon_snap_grid_png));
 	theme->set_icon("minimap", "GraphEdit", make_icon(icon_grid_minimap_png));
 	theme->set_icon("minimap", "GraphEdit", make_icon(icon_grid_minimap_png));
+	theme->set_icon("layout", "GraphEdit", make_icon(icon_grid_layout_png));
 	theme->set_stylebox("bg", "GraphEdit", make_stylebox(tree_bg_png, 4, 4, 4, 5));
 	theme->set_stylebox("bg", "GraphEdit", make_stylebox(tree_bg_png, 4, 4, 4, 5));
 	theme->set_color("grid_minor", "GraphEdit", Color(1, 1, 1, 0.05));
 	theme->set_color("grid_minor", "GraphEdit", Color(1, 1, 1, 0.05));
 	theme->set_color("grid_major", "GraphEdit", Color(1, 1, 1, 0.2));
 	theme->set_color("grid_major", "GraphEdit", Color(1, 1, 1, 0.2));

BIN
scene/resources/default_theme/icon_grid_layout.png


File diff suppressed because it is too large
+ 0 - 0
scene/resources/default_theme/theme_data.h


Some files were not shown because too many files changed in this diff