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
This commit is contained in:
parent
da339f8ffc
commit
12fc3f1eef
@ -32,6 +32,12 @@
|
||||
Makes possible to disconnect nodes when dragging from the slot at the right if it has the specified type.
|
||||
</description>
|
||||
</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">
|
||||
<return type="void" />
|
||||
<description>
|
||||
@ -283,6 +289,8 @@
|
||||
<theme_item name="grid_minor" data_type="color" type="Color" default="Color(1, 1, 1, 0.05)">
|
||||
Color of minor grid lines.
|
||||
</theme_item>
|
||||
<theme_item name="layout" data_type="icon" type="Texture2D">
|
||||
</theme_item>
|
||||
<theme_item name="minimap" data_type="icon" type="Texture2D">
|
||||
</theme_item>
|
||||
<theme_item name="minus" data_type="icon" type="Texture2D">
|
||||
|
@ -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("snap", "GraphEdit", theme->get_icon("SnapGrid", "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_neg", "GraphEdit", 160 * EDSCALE);
|
||||
|
||||
|
1
editor/icons/GridLayout.svg
Normal file
1
editor/icons/GridLayout.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 1.4 KiB |
@ -60,7 +60,7 @@ class VisualScriptEditor : public ScriptEditorBase {
|
||||
EDIT_CUT_NODES,
|
||||
EDIT_PASTE_NODES,
|
||||
EDIT_CREATE_FUNCTION,
|
||||
REFRESH_GRAPH
|
||||
REFRESH_GRAPH,
|
||||
};
|
||||
|
||||
enum PortAction {
|
||||
|
@ -450,6 +450,7 @@ void GraphEdit::_notification(int p_what) {
|
||||
zoom_plus->set_icon(get_theme_icon(SNAME("more")));
|
||||
snap_button->set_icon(get_theme_icon(SNAME("snap")));
|
||||
minimap_button->set_icon(get_theme_icon(SNAME("minimap")));
|
||||
layout_button->set_icon(get_theme_icon(SNAME("layout")));
|
||||
}
|
||||
if (p_what == NOTIFICATION_READY) {
|
||||
Size2 hmin = h_scroll->get_combined_minimum_size();
|
||||
@ -1646,6 +1647,500 @@ HBoxContainer *GraphEdit::get_zoom_hbox() {
|
||||
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() {
|
||||
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);
|
||||
@ -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("arrange_nodes"), &GraphEdit::arrange_nodes);
|
||||
|
||||
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");
|
||||
@ -1851,6 +2348,13 @@ GraphEdit::GraphEdit() {
|
||||
minimap_button->set_focus_mode(FOCUS_NONE);
|
||||
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);
|
||||
float minimap_opacity = 0.65;
|
||||
|
||||
|
@ -116,6 +116,8 @@ private:
|
||||
|
||||
Button *minimap_button;
|
||||
|
||||
Button *layout_button;
|
||||
|
||||
HScrollBar *h_scroll;
|
||||
VScrollBar *v_scroll;
|
||||
|
||||
@ -230,6 +232,24 @@ private:
|
||||
|
||||
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:
|
||||
static void _bind_methods();
|
||||
virtual void add_child_notify(Node *p_child) override;
|
||||
@ -304,6 +324,8 @@ public:
|
||||
|
||||
HBoxContainer *get_zoom_hbox();
|
||||
|
||||
void arrange_nodes();
|
||||
|
||||
GraphEdit();
|
||||
};
|
||||
|
||||
|
@ -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("snap", "GraphEdit", make_icon(icon_snap_grid_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_color("grid_minor", "GraphEdit", Color(1, 1, 1, 0.05));
|
||||
theme->set_color("grid_major", "GraphEdit", Color(1, 1, 1, 0.2));
|
||||
|
BIN
scene/resources/default_theme/icon_grid_layout.png
Normal file
BIN
scene/resources/default_theme/icon_grid_layout.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 640 B |
@ -186,6 +186,10 @@ static const unsigned char icon_grid_minimap_png[] = {
|
||||
0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd, 0x49, 0x48, 0x44, 0x52, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x10, 0x8, 0x6, 0x0, 0x0, 0x0, 0x1f, 0xf3, 0xff, 0x61, 0x0, 0x0, 0x0, 0x9, 0x70, 0x48, 0x59, 0x73, 0x0, 0x0, 0xe, 0xc3, 0x0, 0x0, 0xe, 0xc3, 0x1, 0xc7, 0x6f, 0xa8, 0x64, 0x0, 0x0, 0x0, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x0, 0x77, 0x77, 0x77, 0x2e, 0x69, 0x6e, 0x6b, 0x73, 0x63, 0x61, 0x70, 0x65, 0x2e, 0x6f, 0x72, 0x67, 0x9b, 0xee, 0x3c, 0x1a, 0x0, 0x0, 0x2, 0xd, 0x49, 0x44, 0x41, 0x54, 0x38, 0x8d, 0x75, 0x93, 0x31, 0x68, 0x14, 0x51, 0x10, 0x86, 0xbf, 0xd9, 0xd, 0xbb, 0xde, 0x76, 0x82, 0x21, 0xf8, 0xe0, 0xbc, 0x5d, 0x8b, 0x80, 0x69, 0x6c, 0xd2, 0x5a, 0x6a, 0x91, 0xc3, 0xd2, 0x46, 0x22, 0x8, 0x9, 0x89, 0x70, 0x85, 0x10, 0x41, 0xd, 0x24, 0x45, 0xb0, 0xb, 0x68, 0x11, 0x14, 0x24, 0x10, 0x22, 0x62, 0x21, 0x41, 0xe, 0x4b, 0x21, 0xa4, 0xb7, 0x49, 0x17, 0xb1, 0x8, 0xb9, 0xdd, 0xc7, 0x86, 0x33, 0x21, 0xe1, 0x3a, 0x8f, 0x64, 0x61, 0x6f, 0x2c, 0xbc, 0x3b, 0x36, 0xb9, 0xdc, 0xc0, 0x2b, 0xde, 0xcc, 0xfc, 0xf3, 0xff, 0xfc, 0xcc, 0x48, 0xa3, 0xd1, 0x78, 0x20, 0x22, 0x13, 0xbe, 0xef, 0xaf, 0xdf, 0xac, 0xd7, 0x1f, 0xe1, 0x38, 0xd3, 0xa8, 0x2a, 0xf0, 0x45, 0x6a, 0xb5, 0xcf, 0x5c, 0x11, 0xcd, 0x66, 0x33, 0x38, 0x3f, 0x3f, 0x9f, 0x13, 0x91, 0x7d, 0xb1, 0xd6, 0x6e, 0xaa, 0xea, 0xd3, 0xe0, 0xe8, 0xe8, 0xde, 0xe8, 0xee, 0xee, 0x37, 0xc0, 0xe9, 0xf6, 0x75, 0xf0, 0xfd, 0x9, 0x99, 0x9d, 0x6d, 0x15, 0x81, 0x59, 0x96, 0x3d, 0x3, 0x5e, 0x2, 0x63, 0x22, 0xf2, 0x69, 0xa4, 0x57, 0x1c, 0xdd, 0xdb, 0xfb, 0x5b, 0x0, 0x3, 0x38, 0x67, 0x41, 0x30, 0x11, 0xc7, 0xf1, 0x13, 0x0, 0x11, 0x71, 0xb2, 0x2c, 0x7b, 0xd8, 0xad, 0xad, 0x2, 0x6f, 0xb9, 0x0, 0x38, 0x3c, 0xfc, 0x5, 0x9c, 0xf6, 0xff, 0x22, 0x27, 0x27, 0xe3, 0xe3, 0x7f, 0xa, 0x3, 0x67, 0x45, 0xe4, 0xbb, 0xe7, 0x79, 0xb7, 0xc3, 0x30, 0x7c, 0xd7, 0x67, 0xe9, 0xe3, 0x67, 0x66, 0x5c, 0x60, 0x1, 0x50, 0x40, 0x51, 0x7d, 0x71, 0x6b, 0x72, 0xf2, 0x20, 0x8a, 0xa2, 0xf9, 0x28, 0x8a, 0xe6, 0x1, 0x3a, 0x9d, 0xce, 0x4f, 0x63, 0x4c, 0x3b, 0x4d, 0xd3, 0xd2, 0xc0, 0x80, 0x3c, 0xcf, 0xf, 0x92, 0xa9, 0xa9, 0x31, 0x60, 0x5, 0x58, 0x91, 0x5a, 0xed, 0xc7, 0x15, 0xfe, 0x95, 0xac, 0xb5, 0xcf, 0xf3, 0x3c, 0x3f, 0xe8, 0x25, 0x46, 0xa, 0xc5, 0xd, 0x11, 0x59, 0xb3, 0xd5, 0xea, 0x1b, 0xa0, 0x95, 0x54, 0xab, 0x5b, 0x97, 0xd1, 0x22, 0xb2, 0xa6, 0xaa, 0x6d, 0x60, 0xd, 0x58, 0xba, 0xa0, 0x20, 0xc, 0xc3, 0x65, 0xd7, 0x75, 0x23, 0xe0, 0x2e, 0xb0, 0x1, 0x5c, 0xbf, 0xf4, 0x0, 0xbe, 0xba, 0xae, 0x1b, 0x85, 0x61, 0xb8, 0x3c, 0xa0, 0x20, 0x4d, 0xd3, 0x52, 0xb9, 0x5c, 0x6e, 0xc5, 0x71, 0xbc, 0x23, 0x22, 0xd3, 0x61, 0x18, 0xde, 0x2f, 0xb2, 0x27, 0x49, 0xa2, 0xaa, 0xba, 0x53, 0x2e, 0x97, 0x5b, 0x69, 0x9a, 0x96, 0xf2, 0x3c, 0x1f, 0xf0, 0xc0, 0x5a, 0x6b, 0x5f, 0x1, 0x25, 0x86, 0x84, 0xe3, 0x38, 0x9e, 0xb5, 0x76, 0x2e, 0xcf, 0xf3, 0xfd, 0x1, 0x5, 0x22, 0xb2, 0xa1, 0xaa, 0x4b, 0x22, 0x72, 0xad, 0xcb, 0x38, 0xe0, 0x81, 0xaa, 0x7e, 0x0, 0xce, 0x44, 0xe4, 0xbd, 0xaa, 0xbe, 0xbe, 0xa0, 0xa0, 0x52, 0xa9, 0x2c, 0x7a, 0x9e, 0x17, 0x1, 0x3d, 0xe0, 0x55, 0x1e, 0x6c, 0x79, 0x9e, 0x17, 0x55, 0x2a, 0x95, 0xc5, 0x1, 0x5, 0xcd, 0x66, 0x33, 0x30, 0xc6, 0x9c, 0xc6, 0x71, 0xbc, 0x2d, 0x22, 0x8f, 0x87, 0x78, 0xb0, 0x6d, 0x8c, 0x39, 0xed, 0xae, 0x74, 0xdf, 0x83, 0x3a, 0x70, 0x9c, 0x65, 0x59, 0x23, 0x49, 0x92, 0x5, 0x11, 0x9, 0x86, 0x79, 0x20, 0x22, 0x41, 0x92, 0x24, 0xb, 0x59, 0x96, 0x35, 0x80, 0x63, 0xa0, 0x2e, 0x3d, 0xf6, 0xc2, 0x91, 0xdc, 0x0, 0x5c, 0x55, 0x5d, 0xbf, 0x4, 0x9e, 0x3, 0x72, 0xfe, 0xaf, 0xfb, 0xaa, 0xe7, 0x79, 0x1f, 0x8d, 0x31, 0x6d, 0x29, 0x36, 0xf5, 0xce, 0x14, 0xb8, 0x33, 0x44, 0xc4, 0x6f, 0xdf, 0xf7, 0xd7, 0x8d, 0x31, 0xed, 0x5e, 0xe2, 0x1f, 0xb, 0x5c, 0xe2, 0xcb, 0xd, 0x9b, 0x69, 0xcb, 0x0, 0x0, 0x0, 0x0, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
|
||||
};
|
||||
|
||||
static const unsigned char icon_grid_layout_png[] = {
|
||||
0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd, 0x49, 0x48, 0x44, 0x52, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x10, 0x8, 0x6, 0x0, 0x0, 0x0, 0x1f, 0xf3, 0xff, 0x61, 0x0, 0x0, 0x0, 0x9, 0x70, 0x48, 0x59, 0x73, 0x0, 0x0, 0xe, 0xc3, 0x0, 0x0, 0xe, 0xc3, 0x1, 0xc7, 0x6f, 0xa8, 0x64, 0x0, 0x0, 0x0, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x0, 0x77, 0x77, 0x77, 0x2e, 0x69, 0x6e, 0x6b, 0x73, 0x63, 0x61, 0x70, 0x65, 0x2e, 0x6f, 0x72, 0x67, 0x9b, 0xee, 0x3c, 0x1a, 0x0, 0x0, 0x2, 0xd, 0x49, 0x44, 0x41, 0x54, 0x38, 0x8d, 0x75, 0x93, 0x31, 0x68, 0x14, 0x51, 0x10, 0x86, 0xbf, 0xd9, 0xd, 0xbb, 0xde, 0x76, 0x82, 0x21, 0xf8, 0xe0, 0xbc, 0x5d, 0x8b, 0x80, 0x69, 0x6c, 0xd2, 0x5a, 0x6a, 0x91, 0xc3, 0xd2, 0x46, 0x22, 0x8, 0x9, 0x89, 0x70, 0x85, 0x10, 0x41, 0xd, 0x24, 0x45, 0xb0, 0xb, 0x68, 0x11, 0x14, 0x24, 0x10, 0x22, 0x62, 0x21, 0x41, 0xe, 0x4b, 0x21, 0xa4, 0xb7, 0x49, 0x17, 0xb1, 0x8, 0xb9, 0xdd, 0xc7, 0x86, 0x33, 0x21, 0xe1, 0x3a, 0x8f, 0x64, 0x61, 0x6f, 0x2c, 0xbc, 0x3b, 0x36, 0xb9, 0xdc, 0xc0, 0x2b, 0xde, 0xcc, 0xfc, 0xf3, 0xff, 0xfc, 0xcc, 0x48, 0xa3, 0xd1, 0x78, 0x20, 0x22, 0x13, 0xbe, 0xef, 0xaf, 0xdf, 0xac, 0xd7, 0x1f, 0xe1, 0x38, 0xd3, 0xa8, 0x2a, 0xf0, 0x45, 0x6a, 0xb5, 0xcf, 0x5c, 0x11, 0xcd, 0x66, 0x33, 0x38, 0x3f, 0x3f, 0x9f, 0x13, 0x91, 0x7d, 0xb1, 0xd6, 0x6e, 0xaa, 0xea, 0xd3, 0xe0, 0xe8, 0xe8, 0xde, 0xe8, 0xee, 0xee, 0x37, 0xc0, 0xe9, 0xf6, 0x75, 0xf0, 0xfd, 0x9, 0x99, 0x9d, 0x6d, 0x15, 0x81, 0x59, 0x96, 0x3d, 0x3, 0x5e, 0x2, 0x63, 0x22, 0xf2, 0x69, 0xa4, 0x57, 0x1c, 0xdd, 0xdb, 0xfb, 0x5b, 0x0, 0x3, 0x38, 0x67, 0x41, 0x30, 0x11, 0xc7, 0xf1, 0x13, 0x0, 0x11, 0x71, 0xb2, 0x2c, 0x7b, 0xd8, 0xad, 0xad, 0x2, 0x6f, 0xb9, 0x0, 0x38, 0x3c, 0xfc, 0x5, 0x9c, 0xf6, 0xff, 0x22, 0x27, 0x27, 0xe3, 0xe3, 0x7f, 0xa, 0x3, 0x67, 0x45, 0xe4, 0xbb, 0xe7, 0x79, 0xb7, 0xc3, 0x30, 0x7c, 0xd7, 0x67, 0xe9, 0xe3, 0x67, 0x66, 0x5c, 0x60, 0x1, 0x50, 0x40, 0x51, 0x7d, 0x71, 0x6b, 0x72, 0xf2, 0x20, 0x8a, 0xa2, 0xf9, 0x28, 0x8a, 0xe6, 0x1, 0x3a, 0x9d, 0xce, 0x4f, 0x63, 0x4c, 0x3b, 0x4d, 0xd3, 0xd2, 0xc0, 0x80, 0x3c, 0xcf, 0xf, 0x92, 0xa9, 0xa9, 0x31, 0x60, 0x5, 0x58, 0x91, 0x5a, 0xed, 0xc7, 0x15, 0xfe, 0x95, 0xac, 0xb5, 0xcf, 0xf3, 0x3c, 0x3f, 0xe8, 0x25, 0x46, 0xa, 0xc5, 0xd, 0x11, 0x59, 0xb3, 0xd5, 0xea, 0x1b, 0xa0, 0x95, 0x54, 0xab, 0x5b, 0x97, 0xd1, 0x22, 0xb2, 0xa6, 0xaa, 0x6d, 0x60, 0xd, 0x58, 0xba, 0xa0, 0x20, 0xc, 0xc3, 0x65, 0xd7, 0x75, 0x23, 0xe0, 0x2e, 0xb0, 0x1, 0x5c, 0xbf, 0xf4, 0x0, 0xbe, 0xba, 0xae, 0x1b, 0x85, 0x61, 0xb8, 0x3c, 0xa0, 0x20, 0x4d, 0xd3, 0x52, 0xb9, 0x5c, 0x6e, 0xc5, 0x71, 0xbc, 0x23, 0x22, 0xd3, 0x61, 0x18, 0xde, 0x2f, 0xb2, 0x27, 0x49, 0xa2, 0xaa, 0xba, 0x53, 0x2e, 0x97, 0x5b, 0x69, 0x9a, 0x96, 0xf2, 0x3c, 0x1f, 0xf0, 0xc0, 0x5a, 0x6b, 0x5f, 0x1, 0x25, 0x86, 0x84, 0xe3, 0x38, 0x9e, 0xb5, 0x76, 0x2e, 0xcf, 0xf3, 0xfd, 0x1, 0x5, 0x22, 0xb2, 0xa1, 0xaa, 0x4b, 0x22, 0x72, 0xad, 0xcb, 0x38, 0xe0, 0x81, 0xaa, 0x7e, 0x0, 0xce, 0x44, 0xe4, 0xbd, 0xaa, 0xbe, 0xbe, 0xa0, 0xa0, 0x52, 0xa9, 0x2c, 0x7a, 0x9e, 0x17, 0x1, 0x3d, 0xe0, 0x55, 0x1e, 0x6c, 0x79, 0x9e, 0x17, 0x55, 0x2a, 0x95, 0xc5, 0x1, 0x5, 0xcd, 0x66, 0x33, 0x30, 0xc6, 0x9c, 0xc6, 0x71, 0xbc, 0x2d, 0x22, 0x8f, 0x87, 0x78, 0xb0, 0x6d, 0x8c, 0x39, 0xed, 0xae, 0x74, 0xdf, 0x83, 0x3a, 0x70, 0x9c, 0x65, 0x59, 0x23, 0x49, 0x92, 0x5, 0x11, 0x9, 0x86, 0x79, 0x20, 0x22, 0x41, 0x92, 0x24, 0xb, 0x59, 0x96, 0x35, 0x80, 0x63, 0xa0, 0x2e, 0x3d, 0xf6, 0xc2, 0x91, 0xdc, 0x0, 0x5c, 0x55, 0x5d, 0xbf, 0x4, 0x9e, 0x3, 0x72, 0xfe, 0xaf, 0xfb, 0xaa, 0xe7, 0x79, 0x1f, 0x8d, 0x31, 0x6d, 0x29, 0x36, 0xf5, 0xce, 0x14, 0xb8, 0x33, 0x44, 0xc4, 0x6f, 0xdf, 0xf7, 0xd7, 0x8d, 0x31, 0xed, 0x5e, 0xe2, 0x1f, 0xb, 0x5c, 0xe2, 0xcb, 0xd, 0x9b, 0x69, 0xcb, 0x0, 0x0, 0x0, 0x0, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
|
||||
};
|
||||
|
||||
static const unsigned char icon_parent_folder_png[] = {
|
||||
0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd, 0x49, 0x48, 0x44, 0x52, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x10, 0x8, 0x4, 0x0, 0x0, 0x0, 0xb5, 0xfa, 0x37, 0xea, 0x0, 0x0, 0x0, 0x68, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0xa0, 0x33, 0xb8, 0x27, 0xfe, 0xe0, 0xfc, 0x83, 0x73, 0xf7, 0xc4, 0x71, 0x48, 0xdf, 0x11, 0x7b, 0x78, 0xe9, 0xc1, 0x3f, 0x20, 0xbc, 0xfe, 0x40, 0x12, 0x8f, 0x34, 0x4c, 0x9, 0xa6, 0xe1, 0x57, 0x80, 0x12, 0x17, 0x81, 0xf8, 0x2f, 0x58, 0xe1, 0x15, 0x34, 0x8b, 0x1e, 0x9c, 0x5, 0xa, 0x5e, 0xb8, 0x23, 0x6, 0x52, 0x70, 0x5b, 0x14, 0xac, 0xf0, 0xc, 0xaa, 0x82, 0x7d, 0xf, 0x8e, 0xde, 0x14, 0xf9, 0xcf, 0x8, 0x52, 0xc0, 0xc0, 0x70, 0x5b, 0xf4, 0xe1, 0xc9, 0x7, 0x47, 0xb1, 0xb8, 0x3, 0xaa, 0x0, 0xa, 0x48, 0x52, 0x80, 0xb0, 0xea, 0xc8, 0xc3, 0x83, 0xc, 0x83, 0xe, 0x0, 0x0, 0xb8, 0x27, 0x55, 0x4c, 0xbe, 0xc0, 0xd2, 0xac, 0x0, 0x0, 0x0, 0x0, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user