From 54ecc33ca96fab43cf11c0a8ec3906b77bcc29f0 Mon Sep 17 00:00:00 2001 From: karliss Date: Wed, 3 Jun 2020 18:36:44 +0300 Subject: [PATCH] Rewrite part of graph layout code (#2207) * Rewrite node placement and edge routing parts of graph layout code * Document the high level structure of layout algorithm * Tighter layout and less edge crossings * Better worst case memory and CPU usage --- docs/Doxyfile | 6 +- .../graph_grid_layout/graph_grid.svg | 664 ++++++ .../graph_parent_placement.svg | 2104 +++++++++++++++++ src/Cutter.pro | 7 +- src/common/BinaryTrees.h | 419 ++++ src/common/LinkedListPool.h | 170 ++ src/widgets/DisassemblyWidget.h | 2 +- src/widgets/GraphGridLayout.cpp | 1381 +++++++---- src/widgets/GraphGridLayout.h | 143 +- src/widgets/GraphLayout.h | 6 +- 10 files changed, 4446 insertions(+), 456 deletions(-) create mode 100644 docs/doxygen-images/graph_grid_layout/graph_grid.svg create mode 100644 docs/doxygen-images/graph_grid_layout/graph_parent_placement.svg create mode 100644 src/common/BinaryTrees.h create mode 100644 src/common/LinkedListPool.h diff --git a/docs/Doxyfile b/docs/Doxyfile index 34dbcec2..5761f58e 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -762,7 +762,7 @@ WARNINGS = YES # will automatically be disabled. # The default value is: YES. -WARN_IF_UNDOCUMENTED = YES +WARN_IF_UNDOCUMENTED = NO # If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some parameters @@ -950,7 +950,7 @@ EXAMPLE_RECURSIVE = NO # that contain images that are to be included in the documentation (see the # \image command). -IMAGE_PATH = +IMAGE_PATH = doxygen-images/graph_grid_layout # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program @@ -1127,7 +1127,7 @@ IGNORE_PREFIX = # If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output # The default value is: YES. -GENERATE_HTML = NO +GENERATE_HTML = YES # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of diff --git a/docs/doxygen-images/graph_grid_layout/graph_grid.svg b/docs/doxygen-images/graph_grid_layout/graph_grid.svg new file mode 100644 index 00000000..274f9327 --- /dev/null +++ b/docs/doxygen-images/graph_grid_layout/graph_grid.svg @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + 0 + 1 + 0 + 1 + 2 + 2 + 3 + 3 + 0 + 0 + 1 + 1 + 2 + column + edge column + row + edge row + + + + + + + + + + + + + + (2, 1) + (1, 0) + (0, 1) + + segment offset + + + + + + + negativesegment offset + + + + diff --git a/docs/doxygen-images/graph_grid_layout/graph_parent_placement.svg b/docs/doxygen-images/graph_grid_layout/graph_parent_placement.svg new file mode 100644 index 00000000..9b87b81f --- /dev/null +++ b/docs/doxygen-images/graph_grid_layout/graph_parent_placement.svg @@ -0,0 +1,2104 @@ + + + + + + image/svg+xml + + Cutter graph export + + + + + Cutter graph export + Generated with Qt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +        + + +        + + +        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Placing parent in the middle of bounds + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Placing parent between direct children + + + + + + + + + + Placing subtress as close as possiblebased on exact shape + + + + + + + + + + + Using bounding box of smallest subtree + + diff --git a/src/Cutter.pro b/src/Cutter.pro index 950dfea7..8512c803 100644 --- a/src/Cutter.pro +++ b/src/Cutter.pro @@ -562,6 +562,7 @@ HEADERS += \ common/BugReporting.h \ common/HighDpiPixmap.h \ widgets/GraphLayout.h \ + widgets/GraphGridLayout.h \ widgets/HexWidget.h \ common/SelectionHighlight.h \ common/Decompiler.h \ @@ -574,9 +575,11 @@ HEADERS += \ common/IOModesController.h \ common/SettingsUpgrade.h \ dialogs/LayoutManager.h \ - common/CutterLayout.h + common/CutterLayout.h \ + common/BinaryTrees.h \ + common/LinkedListPool.h -GRAPHVIZ_HEADERS = widgets/GraphGridLayout.h +GRAPHVIZ_HEADERS = widgets/GraphvizLayout.h FORMS += \ dialogs/AboutDialog.ui \ diff --git a/src/common/BinaryTrees.h b/src/common/BinaryTrees.h new file mode 100644 index 00000000..56fef37b --- /dev/null +++ b/src/common/BinaryTrees.h @@ -0,0 +1,419 @@ +#ifndef BINARY_TREES_H +#define BINARY_TREES_H + +/** \file BinaryTrees.h + * \brief Utilities to simplify creation of specialized augmented binary trees. + */ + +#include +#include +#include +#include +#include + + +/** + * Not really a segment tree for storing segments as referred in academic literature. Can be considered a + * full, almost perfect, augmented binary tree. In the context of competitive programming often called segment tree. + * + * Child classes are expected to implement updateFromChildren(NodeType&parent, NodeType& left, NodeType& right) + * method which calculates inner node values from children nodes. + * + * \tparam NodeTypeT type of each tree element + * \tparam FinalType final child class used for curiously recurring template pattern + */ +template +class SegmentTreeBase +{ +public: + using NodePosition = size_t; + using NodeType = NodeTypeT; + + /** + * @brief Create tree with \a size leaves. + * @param size number of leaves in the tree + */ + explicit SegmentTreeBase(size_t size) + : size(size) + , nodeCount(2 * size) + , nodes(nodeCount) + {} + + /** + * @brief Create a tree with given size and initial value. + * + * Inner nodes are calculated from leaves. + * @param size number of leaves + * @param initialValue initial leave value + */ + SegmentTreeBase(size_t size, const NodeType &initialValue) + : SegmentTreeBase(size) + { + init(initialValue); + } +protected: + // Curiously recurring template pattern + FinalType &This() + { + return static_cast(*this); + } + + // Curiously recurring template pattern + const FinalType &This() const + { + return static_cast(*this); + } + + size_t leavePositionToIndex(NodePosition pos) const + { + return pos - size; + } + + NodePosition leaveIndexToPosition(size_t index) const + { + return index + size; + } + + bool isLeave(NodePosition position) const + { + return position >= size; + } + + /** + * @brief Calculate inner node values from leaves. + */ + void buildInnerNodes() + { + for (size_t i = size - 1; i > 0; i--) { + This().updateFromChildren(nodes[i], nodes[i << 1], nodes[(i << 1) | 1]); + } + } + + /** + * @brief Initialize leaves with given value. + * @param value value that will be assigned to leaves + */ + void init(const NodeType &value) + { + std::fill_n(nodes.begin() + size, size, value); + buildInnerNodes(); + } + + const size_t size; //< number of leaves and also index of left most leave + const size_t nodeCount; + std::vector nodes; +}; + +/** + * \brief Tree for point modification and range queries. + */ +template +class PointSetSegmentTree : public SegmentTreeBase +{ + using BaseType = SegmentTreeBase; +public: + using BaseType::BaseType; + + /** + * @brief Set leave \a index to \a value. + * @param index Leave index, should be in the range [0,size) + * @param value + */ + void set(size_t index, const NodeType &value) + { + auto pos = this->leaveIndexToPosition(index); + this->nodes[pos] = value; + while (pos > 1) { + auto parrent = pos >> 1; + this->This().updateFromChildren(this->nodes[parrent], this->nodes[pos], this->nodes[pos ^ 1]); + pos = parrent; + } + } + + const NodeType &valueAtPoint(size_t index) const + { + return this->nodes[this->leaveIndexToPosition(index)]; + } + + // Implement range query when necessary +}; + +class PointSetMinTree : public PointSetSegmentTree +{ + using BaseType = PointSetSegmentTree; +public: + using NodeType = int; + + using BaseType::BaseType; + + void updateFromChildren(NodeType &parent, NodeType &leftChild, NodeType &rightChild) + { + parent = std::min(leftChild, rightChild); + } + + /** + * @brief Find right most position with value than less than given in range [0; position]. + * @param position inclusive right side of query range + * @param value search for position less than this + * @return returns the position with searched property or -1 if there is no such position. + */ + int rightMostLessThan(size_t position, int value) + { + auto isGood = [&](size_t pos) { + return nodes[pos] < value; + }; + // right side exclusive range [l;r) + size_t goodSubtree = 0; + for (size_t l = leaveIndexToPosition(0), r = leaveIndexToPosition(position + 1); l < r; + l >>= 1, r >>= 1) { + if (l & 1) { + if (isGood(l)) { + // mark subtree as good but don't stop yet, there might be something good further to the right + goodSubtree = l; + } + ++l; + } + if (r & 1) { + --r; + if (isGood(r)) { + goodSubtree = r; + break; + } + } + } + if (!goodSubtree) { + return -1; + } + // find rightmost good leave + while (goodSubtree < size) { + goodSubtree = (goodSubtree << 1) + 1; + if (!isGood(goodSubtree)) { + goodSubtree ^= 1; + } + } + return leavePositionToIndex(goodSubtree); + } + + /** + * @brief Find left most position with value less than \a value in range [position; size). + * @param position inclusive left side of query range + * @param value search for position less than this + * @return returns the position with searched property or -1 if there is no such position. + */ + int leftMostLessThan(size_t position, int value) + { + auto isGood = [&](size_t pos) { + return nodes[pos] < value; + }; + // right side exclusive range [l;r) + size_t goodSubtree = 0; + for (size_t l = leaveIndexToPosition(position), r = leaveIndexToPosition(size); l < r; + l >>= 1, r >>= 1) { + if (l & 1) { + if (isGood(l)) { + goodSubtree = l; + break; + } + ++l; + } + if (r & 1) { + --r; + if (isGood(r)) { + goodSubtree = r; + // mark subtree as good but don't stop yet, there might be something good further to the left + } + } + } + if (!goodSubtree) { + return -1; + } + // find leftmost good leave + while (goodSubtree < size) { + goodSubtree = (goodSubtree << 1); + if (!isGood(goodSubtree)) { + goodSubtree ^= 1; + } + } + return leavePositionToIndex(goodSubtree); + } +}; + +/** + * \brief Tree that supports lazily applying an operation to range. + * + * Each inner node has a promise value describing an operation that needs to be applied to corresponding subtree. + * + * Child classes are expected to implement to pushDown(size_t nodePosition) method. Which applies the applies the + * operation stored in \a promise for nodePosition to the direct children nodes. + * + * \tparam NodeType type of tree nodes + * \tparam PromiseType type describing operation that needs to be applied to subtree + * \tparam FinalType child class type for CRTP. See SegmentTreeBase + */ +template +class LazySegmentTreeBase : public SegmentTreeBase +{ + using BaseType = SegmentTreeBase; +public: + /** + * @param size Number of tree leaves. + * @param neutralPromise Promise value that doesn't modify tree nodes. + */ + LazySegmentTreeBase(size_t size, const PromiseType &neutralPromise) + : BaseType(size) + , neutralPromiseElement(neutralPromise) + , promise(size, neutralPromise) + { + h = 0; + size_t v = size; + while (v) { + v >>= 1; + h++; + } + } + + LazySegmentTreeBase(size_t size, NodeType value, PromiseType neutralPromise) + : LazySegmentTreeBase(size, neutralPromise) + { + this->init(value); + } + + /** + * @brief Calculate the tree operation over the range [\a l, \a r) + * @param l inclusive range left side + * @param r exclusive range right side + * @param initialValue Initial value for aggregate operation. + * @return Tree operation calculated over the range. + */ + NodeType rangeOperation(size_t l, size_t r, NodeType initialValue) + { + NodeType result = initialValue; + l = this->leaveIndexToPosition(l); + r = this->leaveIndexToPosition(r); + pushDownFromRoot(l); + pushDownFromRoot(r - 1); + for (; l < r; l >>= 1, r >>= 1) { + if (l & 1) { + This().updateFromChildren(result, result, this->nodes[l++]); + } + if (r & 1) { + This().updateFromChildren(result, result, this->nodes[--r]); + } + } + return result; + } + +protected: + /** + * @brief Ensure that all the parents of node \a p have the operation applied. + * @param p Node position + */ + void pushDownFromRoot(typename BaseType::NodePosition p) + { + for (size_t i = h; i > 0; i--) { + This().pushDown(p >> i); + } + } + + /** + * @brief Update all the inner nodes in path from \a p to root. + * @param p node position + */ + void updateUntilRoot(typename BaseType::NodePosition p) + { + while (p > 1) { + auto parent = p >> 1; + if (promise[parent] == neutralPromiseElement) { + This().updateFromChildren(this->nodes[parent], this->nodes[p & ~size_t(1)], this->nodes[p | 1]); + } + p = parent; + } + } + + using BaseType::This; + + int h; //< Tree height + const PromiseType neutralPromiseElement; + std::vector promise; +}; + + +/** + * @brief Structure supporting range assignment and range maximum operations. + */ +class RangeAssignMaxTree : public LazySegmentTreeBase +{ + using BaseType = LazySegmentTreeBase; +public: + using ValueType = int; + RangeAssignMaxTree(size_t size, ValueType initialValue) + : BaseType(size, initialValue, 0) + { + } + + void updateFromChildren(NodeType &parent, const NodeType &left, const NodeType &right) + { + parent = std::max(left, right); + } + + void pushDown(size_t parent) + { + if (promise[parent]) { + size_t left = (parent << 1); + size_t right = (parent << 1) | 1; + nodes[left] = nodes[right] = nodes[parent]; + if (left < size) { + promise[left] = promise[parent]; + } + if (right < size) { + promise[right] = promise[parent]; + } + promise[parent] = neutralPromiseElement; + } + } + + /** + * @brief Change all the elements in range [\a left, \a right) to \a value. + * @param left inclusive range left side + * @param right exclusive right side of range + * @param value value to be assigned + */ + void setRange(size_t left, size_t right, NodeType value) + { + left = leaveIndexToPosition(left); + right = leaveIndexToPosition(right); + pushDownFromRoot(left); + pushDownFromRoot(right - 1); + for (size_t l = left, r = right; l < r; l >>= 1, r >>= 1) { + if (l & 1) { + nodes[l] = value; + if (!isLeave(l)) { + promise[l] = 1; + } + l += 1; + } + if (r & 1) { + r -= 1; + nodes[r] = value; + if (!isLeave(r)) { + promise[r] = 1; + } + } + } + updateUntilRoot(left); + updateUntilRoot(right - 1); + } + + /** + * @brief Calculate biggest value in the range [l, r) + * @param l inclusive left side of range + * @param r exclusive right side of range + * @return biggest value in given range + */ + int rangeMaximum(size_t l, size_t r) + { + return rangeOperation(l, r, std::numeric_limits::min()); + } +}; + +#endif // BINARY_TREES_H diff --git a/src/common/LinkedListPool.h b/src/common/LinkedListPool.h new file mode 100644 index 00000000..b56340fe --- /dev/null +++ b/src/common/LinkedListPool.h @@ -0,0 +1,170 @@ +#ifndef LINKED_LIST_POOL_H +#define LINKED_LIST_POOL_H + +#include +#include +#include + +/** + * @brief Pool of singly linked lists. + * + * Should not be used as general purpose container. Use only for algorithms that require linked lists ability + * to split and concatenate them. All the data is owned by LinkedListPool. + * + * In contrast to std::list and std::forward_list doesn't allocate each node separately. LinkedListPool can reserve + * all the memory for multiple lists during construction. Uses std::vector as backing container. + */ +template +class LinkedListPool +{ + using IndexType = size_t; + struct Item { + IndexType next; + T value; + }; +public: + /** + * @brief Single list within LinkedListPool. + * + * List only refers to chain of elements. Copying it doesn't copy any element. Item data is owned by + * LinkedListPool. + * + * Use LinkedListPool::makeList to create non-empty list. + */ + class List + { + IndexType head = 0; + IndexType tail = 0; + friend class LinkedListPool; + List(IndexType head, IndexType tail) + : head(head) + , tail(tail) + {} + public: + /** + * @brief Create an empty list + */ + List() = default; + }; + + /** + * @brief List iterator. + * + * Iterators don't get invalidated by adding items to list, but the items may be relocated. + */ + class ListIterator + { + IndexType index = 0; + LinkedListPool *pool = nullptr; + ListIterator(IndexType index, LinkedListPool *pool) + : index(index) + , pool(pool) + {} + + friend class LinkedListPool; + public: + using iterator_category = std::forward_iterator_tag; + using value_type = T; + using difference_type = size_t; + using pointer = T*; + using reference = T&; + ListIterator() = default; + reference operator*() + { + return pool->data[index].value; + } + ListIterator &operator++() + { + index = pool->data[index].next; + return *this; + } + ListIterator operator++(int) + { + ListIterator tmp(*this); + operator++(); + return tmp; + } + bool operator!=(const ListIterator &b) const + { + return index != b.index || pool != b.pool; + }; + /** + * @brief Test if iterator points to valid value. + */ + operator bool() const + { + return index; + } + }; + + /** + * @brief Create a linked list pool with capacity for \a initialCapacity list items. + * @param initialCapacity number of elements to preallocate. + */ + LinkedListPool(size_t initialCapacity) + : data(1) + { + data.reserve(initialCapacity + 1); // [0] element reserved + } + + /** + * @brief Create a list containing single item. + * + * Does not invalidate any iterators, but may cause item relocation when initialCapacity is exceeded. + * @param value value of element that will be inserted in the created list + * @return List containing single value \a value . + */ + List makeList(const T &value) + { + size_t position = data.size(); + data.push_back(Item{0, value}); + return {position, position}; + } + + /** + * @brief Split list and return second half. + * + * After performing the operation, list passed as argument and return list point to the same items. Modifying them + * will affect both lists. + * + * @param list The list that needs to be split. + * @param head Iterator to the first item in new list. Needs to be within \a list . + * @return Returns suffix of \a list. + */ + List splitTail(const List &list, const ListIterator &head) + { + return List {head.index, list.tail}; + } + + /** + * @brief Create list iterator from list. + * @param list + * @return Iterator pointing to the first item in the list. + */ + ListIterator head(const List &list) + { + return iteratorFromIndex(list.head); + } + + ListIterator end(const List &list) + { + return std::next(iteratorFromIndex(list.tail)); + } + + List append(const List &head, const List &tail) + { + List result{head.head, tail.tail}; + data[head.tail].next = tail.head; + return result; + } +private: + ListIterator iteratorFromIndex(IndexType index) + { + return ListIterator{ index, this }; + } + + std::vector data; +}; + + +#endif // LINKED_LIST_POOL diff --git a/src/widgets/DisassemblyWidget.h b/src/widgets/DisassemblyWidget.h index 9d43a872..0ddaa2d7 100644 --- a/src/widgets/DisassemblyWidget.h +++ b/src/widgets/DisassemblyWidget.h @@ -134,7 +134,7 @@ private: }; /** - * @class This class is used to draw the left pane of the disassembly + * This class is used to draw the left pane of the disassembly * widget. Its goal is to draw proper arrows for the jumps of the disassembly. */ class DisassemblyLeftPanel: public QFrame diff --git a/src/widgets/GraphGridLayout.cpp b/src/widgets/GraphGridLayout.cpp index f06cb843..0e57c83f 100644 --- a/src/widgets/GraphGridLayout.cpp +++ b/src/widgets/GraphGridLayout.cpp @@ -6,17 +6,142 @@ #include #include -// Vector functions -template -static void removeFromVec(std::vector &vec, T elem) -{ - vec.erase(std::remove(vec.begin(), vec.end(), elem), vec.end()); -} +#include "common/BinaryTrees.h" + + +/** @class GraphGridLayout + +Basic familiarity with graph algorithms is recommended. + +# Terms used: +- **Vertex**, **node**, **block** - read description of graph for definition. Within this text vertex and node are +used interchangeably with block due to code being written for visualizing basic block control flow graph. +- **edge** - read description of graph for definition for precise definition. +- **DAG** - directed acyclic graph, graph using directed edges which doesn't have cycles. DAG may contain loops if +following them would require going in both directions of edges. Example 1->2 1->3 3->2 is a DAG, 2->1 1->3 3->2 +isn't a DAG. +- **DFS** - depth first search, a graph traversal algorithm +- **toposort** - topological sorting, process of ordering a DAG vertices that all edges go from vertices earlier in the +toposort order to vertices later in toposort order. There are multiple algorithms for implementing toposort operation. +Single DAG can have multiple valid topological orderings, a toposort algorithm can be designed to prioritize a specific +one from all valid toposort orders. Example: for graph 1->4, 2->1, 2->3, 3->4 valid topological orders are [2,1,3,4] and +[2,3,1,4]. + +# High level structure of the algorithm +1. select subset of edges that form a DAG (remove cycles) +2. toposort the DAG +3. choose a subset of edges that form a tree and assign layers +4. assign node positions within grid using tree structure, child subtrees are placed side by side with parent on top +5. perform edge routing +6. calculate column and row pixel positions based on node sizes and amount edges between the rows + + +Contrary to many other layered graph drawing algorithm this implementation doesn't perform node reordering to minimize +edge crossing. This simplifies implementation, and preserves original control flow structure for conditional jumps ( +true jump on one side, false jump on other). Due to most of control flow being result of structured programming +constructs like if/then/else and loops, resulting layout is usually readable without node reordering within layers. + + +# Description of grid. +To simplify the layout algorithm initial steps assume that all nodes have the same size and edges are zero width. +After placing the nodes and routing the edges it is known which nodes are in in which row and column, how +many edges are between each pair of rows. Using this information positions are converted from the grid cells +to pixel coordinates. Routing 0 width edges between rows can also be interpreted as every second row and column being +reserved for edges. The row numbers in code are using first interpretation. To allow better centering of nodes one +above other each node is 2 columns wide and 1 row high. + +\image html graph_grid.svg + +# 1-2 Cycle removal and toposort + +Cycle removal and toposort are done at the same time during single DFS traversal. In case entrypoint is part of a loop +DFS started from entrypoint. This ensures that entrypoint is at the top of resulting layout if possible. Resulting +toposort order is used in many of the following layout steps that require calculating some property of a vertex based +on child property or the other way around. Using toposort order such operations can be implemented iteration through +array in either forward or reverse direction. To prevent running out of stack memory when processing large graphs +DFS is implemented non-recursively. + +# Row assignment + +Rows are assigned in toposort order from top to bottom, with nodes row being max(predecessor.row)+1. This ensures +that loop edges are only ones going from deeper levels to previous layers. + +To further simply node placement a subset of edges is selected which forms a tree. This turns DAG drawing problem +into a tree drawing problem. For each node in level n following nodes which have level exactly n+1 are greedily +assigned as child nodes in tree. If a node already has parent assigned then corresponding edge is not part of tree. + +# Node position assignment + +Since the graph has been reduced to a tree, node placement is more or less putting subtrees side by side with +parent on top. There is some room for interpretation what exactly side by side means and where exactly on top is. +Drawing the graph either too dense or too big may make it less readable so there are configuration options which allow +choosing these things resulting in more or less dense layout. + +Once the subtrees are placed side by side. Parent node can be placed either in the middle of horizontal bounds or +in the middle of direct children. First option results in narrower layout and more vertical columns. Second option +results in nodes being more spread out which may help seeing where each edge goes. + +In more compact mode two subtrees are placed side by side taking into account their shape. In wider mode +bounding box of shorter subtree is used instead of exact shape. This gives slightly sparse layout without it being too +wide. + +\image html graph_parent_placement.svg + +# Edge routing +Edge routing can be split into: main column selection, rough routing, segment offset calculation. + +Transition from source to target row is done using single vertical segment. This is called main column. + +Rough routing creates the path of edge using up to 5 segments using grid coordinates. +Due to nodes being placed in a grid. Horizontal segments of edges can't intersect with any nodes. +The path for edges is chosen so that it consists of at most 5 segments, typically resulting in sideways U shape or +square Z shape. +- short vertical segment from node to horizontal line +- move to empty column +- vertical segment between starting row and end row, an empty column can always be found, in the worst case there are empty columns at the sides of drawing +- horizontal segment to target node column +- short vertical segment connecting to target node + +There are 3 special cases: +- source and target nodes are in the same column with no nodes between - single vertical segment +- column bellow stating node is empty - segments 1-3 are merged +- column above target node is empty - segments 3-5 are merged +Vertical segment intersection with nodes is prevented using a 2d array marking which vertical segments are blocked and +naively iterating through all rows between start and end at the desired column. + +After rough routing segment offsets are calculated relative to their corresponding edge column. This ensures that +two segments don't overlap. Segment offsets within each column are assigned greedily with some heuristics for +assignment order to reduce amount of edge crossings and result in more visually pleasing output for a typical CFG +graph. +Each segment gets assigned an offset that is maximum of previously assigned offsets overlapping with current +segment + segment spacing. +Assignment order is chosen based on: +* direction of previous and last segment - helps reducing crossings and place the segments between nodes +* segment length - reduces crossing when segment endpoints have the same structure as valid parentheses expression +* edge length - establishes some kind of order when single node is connected to many edges, typically a block + with switch statement or block after switch statement. + +*/ + GraphGridLayout::GraphGridLayout(GraphGridLayout::LayoutType layoutType) : GraphLayout({}) -, layoutType(layoutType) + , layoutType(layoutType) { + switch (layoutType) { + case LayoutType::Narrow: + tightSubtreePlacement = true; + parentBetweenDirectChild = false; + break; + case LayoutType::Medium: + tightSubtreePlacement = false; + parentBetweenDirectChild = false; + break; + case LayoutType::Wide: + tightSubtreePlacement = false; + parentBetweenDirectChild = true; + break; + } } std::vector GraphGridLayout::topoSort(LayoutState &state, ut64 entry) @@ -27,14 +152,16 @@ std::vector GraphGridLayout::topoSort(LayoutState &state, ut64 entry) // * select backwards/loop edges // * perform toposort std::vector blockOrder; - // 0 - not visited - // 1 - in stack - // 2 - visited - std::unordered_map visited; + enum class State : uint8_t { + NotVisited = 0, + InStack, + Visited + }; + std::unordered_map visited; visited.reserve(state.blocks->size()); std::stack> stack; auto dfsFragment = [&visited, &blocks, &state, &stack, &blockOrder](ut64 first) { - visited[first] = 1; + visited[first] = State::InStack; stack.push({first, 0}); while (!stack.empty()) { auto v = stack.top().first; @@ -44,16 +171,16 @@ std::vector GraphGridLayout::topoSort(LayoutState &state, ut64 entry) ++stack.top().second; auto target = block.edges[edge_index].target; auto &targetState = visited[target]; - if (targetState == 0) { - targetState = 1; + if (targetState == State::NotVisited) { + targetState = State::InStack; stack.push({target, 0}); state.grid_blocks[v].dag_edge.push_back(target); - } else if (targetState == 2) { + } else if (targetState == State::Visited) { state.grid_blocks[v].dag_edge.push_back(target); } // else { targetState == 1 in stack, loop edge } } else { stack.pop(); - visited[v] = 2; + visited[v] = State::Visited; blockOrder.push_back(v); } } @@ -64,31 +191,38 @@ std::vector GraphGridLayout::topoSort(LayoutState &state, ut64 entry) // topological order. dfsFragment(entry); for (auto &blockIt : blocks) { - if (!visited[blockIt.first]) { + if (visited[blockIt.first] == State::NotVisited) { dfsFragment(blockIt.first); } } - // assign levels and select tree edges + return blockOrder; +} + +void GraphGridLayout::assignRows(GraphGridLayout::LayoutState &state, const std::vector &blockOrder) +{ for (auto it = blockOrder.rbegin(), end = blockOrder.rend(); it != end; it++) { auto &block = state.grid_blocks[*it]; - int nextLevel = block.level + 1; + int nextLevel = block.row + 1; for (auto target : block.dag_edge) { auto &targetBlock = state.grid_blocks[target]; - targetBlock.level = std::max(targetBlock.level, nextLevel); + targetBlock.row = std::max(targetBlock.row, nextLevel); } } +} + +void GraphGridLayout::selectTree(GraphGridLayout::LayoutState &state) +{ for (auto &blockIt : state.grid_blocks) { auto &block = blockIt.second; for (auto targetId : block.dag_edge) { auto &targetBlock = state.grid_blocks[targetId]; - if (!targetBlock.has_parent && targetBlock.level == block.level + 1) { + if (!targetBlock.has_parent && targetBlock.row == block.row + 1) { block.tree_edge.push_back(targetId); targetBlock.has_parent = true; } } } - return blockOrder; } void GraphGridLayout::CalculateLayout(std::unordered_map &blocks, ut64 entry, @@ -103,180 +237,89 @@ void GraphGridLayout::CalculateLayout(std::unordered_map &bloc layoutState.grid_blocks[it.first] = block; } - auto block_order = topoSort(layoutState, entry); - computeAllBlockPlacement(block_order, layoutState); + auto blockOrder = topoSort(layoutState, entry); + computeAllBlockPlacement(blockOrder, layoutState); for (auto &blockIt : blocks) { layoutState.edge[blockIt.first].resize(blockIt.second.edges.size()); - } - - // Prepare edge routing - int col_count = 1; - int row_count = 0; - for (const auto &blockIt : layoutState.grid_blocks) { - if (!blockIt.second.has_parent) { - row_count = std::max(row_count, blockIt.second.row_count); - col_count += blockIt.second.col_count; + for (size_t i = 0; i < blockIt.second.edges.size(); i++) { + layoutState.edge[blockIt.first][i].dest = blockIt.second.edges[i].target; } } - row_count += 2; - EdgesVector horiz_edges, vert_edges; - horiz_edges.resize(row_count + 1); - vert_edges.resize(row_count + 1); - Matrix edge_valid; - edge_valid.resize(row_count + 1); - for (int row = 0; row < row_count + 1; row++) { - horiz_edges[row].resize(col_count + 1); - vert_edges[row].resize(col_count + 1); - edge_valid[row].assign(col_count + 1, true); + for (const auto &edgeList : layoutState.edge) { + auto &startBlock = layoutState.grid_blocks[edgeList.first]; + startBlock.outputCount++; + for (auto &edge : edgeList.second) { + auto &targetBlock = layoutState.grid_blocks[edge.dest]; + targetBlock.inputCount++; + } } - for (auto &blockIt : layoutState.grid_blocks) { + layoutState.columns = 1; + layoutState.rows = 1; + for (auto &node : layoutState.grid_blocks) { + // count is at least index + 1 + layoutState.rows = std::max(layoutState.rows, size_t(node.second.row) + 1); + // block is 2 column wide + layoutState.columns = std::max(layoutState.columns, size_t(node.second.col) + 2); + } + + layoutState.rowHeight.assign(layoutState.rows, 0); + layoutState.columnWidth.assign(layoutState.columns, 0); + for (auto &node : layoutState.grid_blocks) { + const auto &inputBlock = blocks[node.first]; + layoutState.rowHeight[node.second.row] = std::max(inputBlock.height, + layoutState.rowHeight[node.second.row]); + layoutState.columnWidth[node.second.col] = std::max(inputBlock.width / 2, + layoutState.columnWidth[node.second.col]); + layoutState.columnWidth[node.second.col + 1] = std::max(inputBlock.width / 2, + layoutState.columnWidth[node.second.col + 1]); + } + + routeEdges(layoutState); + + convertToPixelCoordinates(layoutState, width, height); +} + +void GraphGridLayout::findMergePoints(GraphGridLayout::LayoutState &state) const +{ + for (auto &blockIt : state.grid_blocks) { auto &block = blockIt.second; - edge_valid[block.row][block.col + 1] = false; - } - - // Perform edge routing - for (ut64 blockId : block_order) { - GraphBlock &block = blocks[blockId]; - GridBlock &start = layoutState.grid_blocks[blockId]; - size_t i = 0; - for (const auto &edge : block.edges) { - GridBlock &end = layoutState.grid_blocks[edge.target]; - layoutState.edge[blockId][i++] = routeEdge(horiz_edges, vert_edges, edge_valid, start, end); - } - } - - // Compute edge counts for each row and column - std::vector col_edge_count, row_edge_count; - col_edge_count.assign(col_count + 1, 0); - row_edge_count.assign(row_count + 1, 0); - for (int row = 0; row < row_count + 1; row++) { - for (int col = 0; col < col_count + 1; col++) { - if (int(horiz_edges[row][col].size()) > row_edge_count[row]) - row_edge_count[row] = int(horiz_edges[row][col].size()); - if (int(vert_edges[row][col].size()) > col_edge_count[col]) - col_edge_count[col] = int(vert_edges[row][col].size()); - } - } - - - //Compute row and column sizes - std::vector col_width, row_height; - col_width.assign(col_count + 1, 0); - row_height.assign(row_count + 1, 0); - for (auto &blockIt : blocks) { - GraphBlock &block = blockIt.second; - GridBlock &grid_block = layoutState.grid_blocks[blockIt.first]; - if ((int(block.width / 2)) > col_width[grid_block.col]) - col_width[grid_block.col] = int(block.width / 2); - if ((int(block.width / 2)) > col_width[grid_block.col + 1]) - col_width[grid_block.col + 1] = int(block.width / 2); - if (int(block.height) > row_height[grid_block.row]) - row_height[grid_block.row] = int(block.height); - } - - // Compute row and column positions - std::vector col_x, row_y; - col_x.assign(col_count, 0); - row_y.assign(row_count, 0); - std::vector col_edge_x(col_count + 1); - std::vector row_edge_y(row_count + 1); - int x = layoutConfig.block_horizontal_margin; - for (int i = 0; i <= col_count; i++) { - col_edge_x[i] = x; - x += layoutConfig.block_horizontal_margin * col_edge_count[i]; - if (i < col_count) { - col_x[i] = x; - x += col_width[i]; - } - } - int y = layoutConfig.block_vertical_margin; - for (int i = 0; i <= row_count; i++) { - row_edge_y[i] = y; - if (!row_edge_count[i]) { - // prevent 2 blocks being put on top of each other without any space - row_edge_count[i] = 1; - } - y += layoutConfig.block_vertical_margin * row_edge_count[i]; - if (i < row_count) { - row_y[i] = y; - y += row_height[i]; - } - } - width = x + (layoutConfig.block_horizontal_margin); - height = y + (layoutConfig.block_vertical_margin); - - //Compute node positions - for (auto &blockIt : blocks) { - GraphBlock &block = blockIt.second; - GridBlock &grid_block = layoutState.grid_blocks[blockIt.first]; - auto column = grid_block.col; - auto row = grid_block.row; - block.x = int(col_x[column] + col_width[column] + - ((layoutConfig.block_horizontal_margin / 2) * col_edge_count[column + 1]) - - (block.width / 2)); - if ((block.x + block.width) > ( - col_x[column] + col_width[column] + col_width[column + 1] + - layoutConfig.block_horizontal_margin * - col_edge_count[column + 1])) { - block.x = int((col_x[column] + col_width[column] + col_width[column + 1] + - layoutConfig.block_horizontal_margin * col_edge_count[column + 1]) - block.width); - } - block.y = row_y[row]; - } - - // Compute coordinates for edges - auto position_from_middle = [](int index, int spacing, int column_count) { - return spacing * (((index & 1) ? 1 : -1) * ((index + 1) / 2) + (column_count - 1) / 2); - }; - for (auto &blockIt : blocks) { - GraphBlock &block = blockIt.second; - - size_t index = 0; - assert(block.edges.size() == layoutState.edge[block.entry].size()); - for (GridEdge &edge : layoutState.edge[block.entry]) { - if (edge.points.empty()) { - qDebug() << "Warning, unrouted edge."; - continue; + GridBlock *mergeBlock = nullptr; + int grandChildCount = 0; + for (auto edge : block.tree_edge) { + auto &targetBlock = state.grid_blocks[edge]; + if (targetBlock.tree_edge.size()) { + mergeBlock = &state.grid_blocks[targetBlock.tree_edge[0]]; } - auto start = edge.points[0]; - auto start_col = start.col; - auto last_index = edge.start_index; - // This is the start point of the edge. - auto first_pt = QPoint(col_edge_x[start_col] + - position_from_middle(last_index, layoutConfig.block_horizontal_margin, col_edge_count[start_col]) + - (layoutConfig.block_horizontal_margin / 2), - block.y + block.height); - auto last_pt = first_pt; - QPolygonF pts; - pts.append(last_pt); - - for (int i = 0; i < int(edge.points.size()); i++) { - auto end = edge.points[i]; - auto end_row = end.row; - auto end_col = end.col; - auto last_index = end.index; - QPoint new_pt; - // block_vertical_margin/2 gives the margin from block to the horizontal lines - if (start_col == end_col) - new_pt = QPoint(last_pt.x(), row_edge_y[end_row] + - position_from_middle(last_index, layoutConfig.block_vertical_margin, row_edge_count[end_row]) + - (layoutConfig.block_vertical_margin / 2)); - else - new_pt = QPoint(col_edge_x[end_col] + - position_from_middle(last_index, layoutConfig.block_horizontal_margin, col_edge_count[end_col]) + - (layoutConfig.block_horizontal_margin / 2), last_pt.y()); - pts.push_back(new_pt); - last_pt = new_pt; - start_col = end_col; + grandChildCount += targetBlock.tree_edge.size(); + } + if (!mergeBlock || grandChildCount != 1) { + continue; + } + int blocksGoingToMerge = 0; + int blockWithTreeEdge = 0; + for (auto edge : block.tree_edge) { + auto &targetBlock = state.grid_blocks[edge]; + bool goesToMerge = false; + for (auto secondEdgeTarget : targetBlock.dag_edge) { + if (secondEdgeTarget == mergeBlock->id) { + goesToMerge = true; + break; + } } - - const auto &target = blocks[edge.dest]; - auto new_pt = QPoint(last_pt.x(), target.y - 1); - pts.push_back(new_pt); - block.edges[index].polyline = pts; - index++; + if (goesToMerge) { + if (targetBlock.tree_edge.size() == 1) { + blockWithTreeEdge = blocksGoingToMerge; + } + blocksGoingToMerge++; + } else { + break; + } + } + if (blocksGoingToMerge) { + state.grid_blocks[block.tree_edge[blockWithTreeEdge]].col = blockWithTreeEdge * 2 - + (blocksGoingToMerge - 1); } } } @@ -284,257 +327,763 @@ void GraphGridLayout::CalculateLayout(std::unordered_map &bloc void GraphGridLayout::computeAllBlockPlacement(const std::vector &blockOrder, LayoutState &layoutState) const { - for (auto blockId : blockOrder) { - computeBlockPlacement(blockId, layoutState); - } - int col = 0; - for (auto blockId : blockOrder) { - if (!layoutState.grid_blocks[blockId].has_parent) { - adjustGraphLayout(layoutState.grid_blocks[blockId], layoutState.grid_blocks, col, 1); - col += layoutState.grid_blocks[blockId].col_count; - } - } -} + assignRows(layoutState, blockOrder); + selectTree(layoutState); + findMergePoints(layoutState); -// Prepare graph -// This computes the position and (row/col based) size of the block -void GraphGridLayout::computeBlockPlacement(ut64 blockId, LayoutState &layoutState) const -{ - auto &block = layoutState.grid_blocks[blockId]; - auto &blocks = layoutState.grid_blocks; - int col = 0; - int row_count = 1; - int childColumn = 0; - bool singleChild = block.tree_edge.size() == 1; - // Compute all children nodes - for (size_t i = 0; i < block.tree_edge.size(); i++) { - ut64 edge = block.tree_edge[i]; - auto &edgeb = blocks[edge]; - row_count = std::max(edgeb.row_count + 1, row_count); - childColumn = edgeb.col; - } - if (layoutType != LayoutType::Wide && block.tree_edge.size() == 2) { - auto &left = blocks[block.tree_edge[0]]; - auto &right = blocks[block.tree_edge[1]]; - if (left.tree_edge.size() == 0) { - left.col = right.col - 2; - int add = left.col < 0 ? - left.col : 0; - adjustGraphLayout(right, blocks, add, 1); - adjustGraphLayout(left, blocks, add, 1); - col = right.col_count + add; - } else if (right.tree_edge.size() == 0) { - adjustGraphLayout(left, blocks, 0, 1); - adjustGraphLayout(right, blocks, left.col + 2, 1); - col = std::max(left.col_count, right.col + 2); - } else { - adjustGraphLayout(left, blocks, 0, 1); - adjustGraphLayout(right, blocks, left.col_count, 1); - col = left.col_count + right.col_count; - } - block.col_count = std::max(2, col); - if (layoutType == LayoutType::Medium) { - block.col = (left.col + right.col) / 2; - } else { - block.col = singleChild ? childColumn : (col - 2) / 2; - } - } else { - for (ut64 edge : block.tree_edge) { - adjustGraphLayout(blocks[edge], blocks, col, 1); - col += blocks[edge].col_count; - } - if (col >= 2) { - // Place this node centered over the child nodes - block.col = singleChild ? childColumn : (col - 2) / 2; - block.col_count = col; - } else { - //No child nodes, set single node's width (nodes are 2 columns wide to allow - //centering over a branch) + // Shapes of subtrees are maintained using linked lists. Each value within list is column relative to previous row. + // This allows moving things around by changing only first value in list. + LinkedListPool sides(blockOrder.size() * 2); // *2 = two sides for each node + + // Process nodes in the order from bottom to top. Ensures that all subtrees are processed before parent node. + for (auto blockId : blockOrder) { + auto &block = layoutState.grid_blocks[blockId]; + if (block.tree_edge.size() == 0) { + block.row_count = 1; block.col = 0; - block.col_count = 2; - } - } - block.row = 0; - block.row_count = row_count; -} + block.lastRowRight = 2; + block.lastRowLeft = 0; + block.leftPosition = 0; + block.rightPosition = 2; -void GraphGridLayout::adjustGraphLayout(GridBlock &block, - std::unordered_map &blocks, int col, int row) const -{ - block.col += col; - block.row += row; - for (ut64 edge : block.tree_edge) { - adjustGraphLayout(blocks[edge], blocks, col, row); - } -} + block.leftSideShape = sides.makeList(0); + block.rightSideShape = sides.makeList(2); + } else { + auto &firstChild = layoutState.grid_blocks[block.tree_edge[0]]; + auto leftSide = firstChild.leftSideShape; // left side of block children subtrees processed so far + auto rightSide = firstChild.rightSideShape; + block.row_count = firstChild.row_count; + block.lastRowRight = firstChild.lastRowRight; + block.lastRowLeft = firstChild.lastRowLeft; + block.leftPosition = firstChild.leftPosition; + block.rightPosition = firstChild.rightPosition; + // Place children subtrees side by side + for (size_t i = 1; i < block.tree_edge.size(); i++) { + auto &child = layoutState.grid_blocks[block.tree_edge[i]]; + int minPos = INT_MIN; + int leftPos = 0; + int rightPos = 0; + auto leftIt = sides.head(rightSide); + auto rightIt = sides.head(child.leftSideShape); + int maxLeftWidth = 0; + int minRightPos = child.col; -// Edge computing stuff -bool GraphGridLayout::isEdgeMarked(EdgesVector &edges, int row, int col, int index) -{ - if (index >= int(edges[row][col].size())) - return false; - return edges[row][col][index]; -} - -void GraphGridLayout::markEdge(EdgesVector &edges, int row, int col, int index, bool used) -{ - while (int(edges[row][col].size()) <= index) - edges[row][col].push_back(false); - edges[row][col][index] = used; -} - -GraphGridLayout::GridEdge GraphGridLayout::routeEdge(EdgesVector &horiz_edges, - EdgesVector &vert_edges, - Matrix &edge_valid, GridBlock &start, GridBlock &end) const -{ - GridEdge edge; - edge.dest = end.id; - - //Find edge index for initial outgoing line - int i = 0; - while (isEdgeMarked(vert_edges, start.row + 1, start.col + 1, i)) { - i += 1; - } - markEdge(vert_edges, start.row + 1, start.col + 1, i); - edge.addPoint(start.row + 1, start.col + 1); - edge.start_index = i; - bool horiz = false; - - //Find valid column for moving vertically to the target node - int min_row, max_row; - if (end.row < (start.row + 1)) { - min_row = end.row; - max_row = start.row + 1; - } else { - min_row = start.row + 1; - max_row = end.row; - } - int col = start.col + 1; - if (min_row != max_row) { - auto checkColumn = [min_row, max_row, &edge_valid](int column) { - if (column < 0 || column >= int(edge_valid[min_row].size())) - return false; - for (int row = min_row; row < max_row; row++) { - if (!edge_valid[row][column]) { - return false; + while (leftIt && rightIt) { // process part of subtrees that touch when put side by side + leftPos += *leftIt; + rightPos += *rightIt; + minPos = std::max(minPos, leftPos - rightPos); + maxLeftWidth = std::max(maxLeftWidth, leftPos); + minRightPos = std::min(minRightPos, rightPos); + ++leftIt; + ++rightIt; } - } - return true; - }; + int rightTreeOffset = 0; + if (tightSubtreePlacement) { + rightTreeOffset = minPos; // mode a) place subtrees as close as possible + } else { + // mode b) use bounding box for shortest subtree and full shape of other side + if (leftIt) { + rightTreeOffset = maxLeftWidth - child.leftPosition; + } else { + rightTreeOffset = block.rightPosition - minRightPos; + } - if (!checkColumn(col)) { - if (checkColumn(end.col + 1)) { - col = end.col + 1; + } + // Calculate the new shape after putting the two subtrees side by side + child.col += rightTreeOffset; + if (leftIt) { + *leftIt -= (rightTreeOffset + child.lastRowRight - leftPos); + rightSide = sides.append(child.rightSideShape, sides.splitTail(rightSide, leftIt)); + } else if (rightIt) { + *rightIt += (rightPos + rightTreeOffset - block.lastRowLeft); + leftSide = sides.append(leftSide, sides.splitTail(child.leftSideShape, rightIt)); + + rightSide = child.rightSideShape; + block.lastRowRight = child.lastRowRight + rightTreeOffset; + block.lastRowLeft = child.lastRowLeft + rightTreeOffset; + } else { + rightSide = child.rightSideShape; + } + *sides.head(rightSide) += rightTreeOffset; + block.row_count = std::max(block.row_count, child.row_count); + block.leftPosition = std::min(block.leftPosition, child.leftPosition + rightTreeOffset); + block.rightPosition = std::max(block.rightPosition, rightTreeOffset + child.rightPosition); + } + + int col = 0; + // Calculate parent position + if (parentBetweenDirectChild) { + // mode a) keep one child to the left, other to the right + for (auto target : block.tree_edge) { + col += layoutState.grid_blocks[target].col; + } + col /= block.tree_edge.size(); } else { - int ofs = 0; - while (true) { - col = start.col + 1 - ofs; - if (checkColumn(col)) { - break; - } + // mode b) somewhere between left most direct child and right most, preferably in the middle of + // horizontal dimensions. Results layout looks more like single vertical line. + col = (block.rightPosition + block.leftPosition) / 2 - 1; + col = std::max(col, layoutState.grid_blocks[block.tree_edge.front()].col - 1); + col = std::min(col, layoutState.grid_blocks[block.tree_edge.back()].col + 1); + } + block.col += col; // += instead of = to keep offset calculated in previous steps + block.row_count += 1; + block.leftPosition = std::min(block.leftPosition, block.col); + block.rightPosition = std::max(block.rightPosition, block.col + 2); - col = start.col + 1 + ofs; - if (checkColumn(col)) { - break; - } + *sides.head(leftSide) -= block.col; + block.leftSideShape = sides.append(sides.makeList(block.col), leftSide); - ofs += 1; + *sides.head(rightSide) -= block.col + 2; + block.rightSideShape = sides.append(sides.makeList(block.col + 2), rightSide); + + // Keep children positions relative to parent so that moving parent moves whole subtree + for (auto target : block.tree_edge) { + auto &targetBlock = layoutState.grid_blocks[target]; + targetBlock.col -= block.col; + } + } + } + + // Calculate root positions. Typical function should have one root node that matches with entrypoint. + // There can be more of them in case of switch statement analysis failure, unreahable basic blocks or + // using the algorithm for non control flow graphs. + int nextEmptyColumn = 0; + for (auto &blockIt : layoutState.grid_blocks) { + auto &block = blockIt.second; + if (block.row == 0) { // place all the roots first + auto offset = -block.leftPosition; + block.col += nextEmptyColumn + offset; + nextEmptyColumn = block.rightPosition + offset; + } + } + // Visit all nodes top to bottom, converting relative positions to absolute. + for (auto it = blockOrder.rbegin(), end = blockOrder.rend(); it != end; it++) { + auto &block = layoutState.grid_blocks[*it]; + assert(block.col >= 0); + for (auto childId : block.tree_edge) { + auto &childBlock = layoutState.grid_blocks[childId]; + childBlock.col += block.col; + } + } +} + +void GraphGridLayout::routeEdges(GraphGridLayout::LayoutState &state) const +{ + calculateEdgeMainColumn(state); + roughRouting(state); + elaborateEdgePlacement(state); +} + +void GraphGridLayout::calculateEdgeMainColumn(GraphGridLayout::LayoutState &state) const +{ + // Find an empty column as close as possible to start or end block's column. + // Use sweep line approach processing events sorted by row top to bottom. Use an appropriate tree structure + // to contain blocks above sweep line and query for nearest column which isn't blocked by a block. + + struct Event { + size_t blockId; + size_t edgeId; + int row; + enum Type { + Edge = 0, + Block = 1 + } type; + }; + // create events + std::vector events; + events.reserve(state.grid_blocks.size() * 2); + for (const auto &it : state.grid_blocks) { + events.push_back({it.first, 0, it.second.row, Event::Block}); + const auto &inputBlock = (*state.blocks)[it.first]; + int startRow = it.second.row + 1; + + auto gridEdges = state.edge[it.first]; + gridEdges.resize(inputBlock.edges.size()); + for (size_t i = 0; i < inputBlock.edges.size(); i++) { + auto targetId = inputBlock.edges[i].target; + gridEdges[i].dest = targetId; + const auto &targetGridBlock = state.grid_blocks[targetId]; + int endRow = targetGridBlock.row; + events.push_back({it.first, i, std::max(startRow, endRow), Event::Edge}); + } + } + std::sort(events.begin(), events.end(), [](const Event &a, const Event &b) { + if (a.row != b.row) { + return a.row < b.row; + } + return static_cast(a.type) < static_cast(b.type); + }); + + // process events and choose main column for each edge + PointSetMinTree blockedColumns(state.columns + 1, -1); + for (const auto &event : events) { + if (event.type == Event::Block) { + auto block = state.grid_blocks[event.blockId]; + blockedColumns.set(block.col + 1, event.row); + } else { + auto block = state.grid_blocks[event.blockId]; + int column = block.col + 1; + auto &edge = state.edge[event.blockId][event.edgeId]; + const auto &targetBlock = state.grid_blocks[edge.dest]; + auto topRow = std::min(block.row + 1, targetBlock.row); + auto targetColumn = targetBlock.col + 1; + + // Prefer using the same column as starting node, it allows reducing amount of segments. + if (blockedColumns.valueAtPoint(column) < topRow) { + edge.mainColumn = column; + } else if (blockedColumns.valueAtPoint(targetColumn) < topRow) { // next try target block column + edge.mainColumn = targetColumn; + } else { + auto nearestLeft = blockedColumns.rightMostLessThan(column, topRow); + auto nearestRight = blockedColumns.leftMostLessThan(column, topRow); + // There should always be empty column at the sides of drawing + assert(nearestLeft != -1 && nearestRight != -1); + + // Choose closest column. Take into account distance to source and target block columns. + auto distanceLeft = column - nearestLeft + abs(targetColumn - nearestLeft); + auto distanceRight = nearestRight - column + abs(targetColumn - nearestRight); + + // For upward edges try to make a loop instead of 8 shape, + // it is slightly longer but produces less crossing. + if (targetBlock.row < block.row) { + if (targetColumn < column && blockedColumns.valueAtPoint(column + 1) < topRow && + column - targetColumn <= distanceLeft + 2) { + edge.mainColumn = column + 1; + continue; + } else if (targetColumn > column && blockedColumns.valueAtPoint(column - 1) < topRow && + targetColumn - column <= distanceRight + 2) { + edge.mainColumn = column - 1; + continue; + } + } + + if (distanceLeft != distanceRight) { + edge.mainColumn = distanceLeft < distanceRight ? nearestLeft : nearestRight; + } else { + // In case of tie choose based on edge index. Should result in true branches being mostly on one + // side, false branches on other side. + edge.mainColumn = event.edgeId < state.edge[event.blockId].size() / 2 ? nearestLeft : nearestRight; } } } } - - if (col != (start.col + 1)) { - //Not in same column, need to generate a line for moving to the correct column - int min_col, max_col; - if (col < (start.col + 1)) { - min_col = col; - max_col = start.col + 1; - } else { - min_col = start.col + 1; - max_col = col; - } - int index = findHorizEdgeIndex(horiz_edges, start.row + 1, min_col, max_col); - edge.addPoint(start.row + 1, col, index); - horiz = true; - } - - if (end.row != (start.row + 1)) { - //Not in same row, need to generate a line for moving to the correct row - if (col == (start.col + 1)) - markEdge(vert_edges, start.row + 1, start.col + 1, i, false); - int index = findVertEdgeIndex(vert_edges, col, min_row, max_row); - if (col == (start.col + 1)) - edge.start_index = index; - edge.addPoint(end.row, col, index); - horiz = false; - } - - if (col != (end.col + 1)) { - //Not in ending column, need to generate a line for moving to the correct column - int min_col, max_col; - if (col < (end.col + 1)) { - min_col = col; - max_col = end.col + 1; - } else { - min_col = end.col + 1; - max_col = col; - } - int index = findHorizEdgeIndex(horiz_edges, end.row, min_col, max_col); - edge.addPoint(end.row, end.col + 1, index); - horiz = true; - } - - //If last line was horizontal, choose the ending edge index for the incoming edge - if (horiz) { - int index = findVertEdgeIndex(vert_edges, end.col + 1, end.row, end.row); - edge.points[int(edge.points.size()) - 1].index = index; - } - - return edge; } - -int GraphGridLayout::findHorizEdgeIndex(EdgesVector &edges, int row, int min_col, int max_col) +void GraphGridLayout::roughRouting(GraphGridLayout::LayoutState &state) const { - //Find a valid index - int i = 0; - while (true) { - bool valid = true; - for (int col = min_col; col < max_col + 1; col++) - if (isEdgeMarked(edges, row, col, i)) { - valid = false; - break; - } - if (valid) - break; - i++; - } + auto getSpacingOverride = [this](int blockWidth, int edgeCount) { + int maxSpacing = blockWidth / edgeCount; + if (maxSpacing < layoutConfig.edgeHorizontalSpacing) { + return std::max(maxSpacing, 1); + } + return 0; + }; - //Mark chosen index as used - for (int col = min_col; col < max_col + 1; col++) - markEdge(edges, row, col, i); - return i; + for (auto &blockIt : state.grid_blocks) { + auto &blockEdges = state.edge[blockIt.first]; + for (size_t i = 0; i < blockEdges.size(); i++) { + auto &edge = blockEdges[i]; + const auto &start = blockIt.second; + const auto &target = state.grid_blocks[edge.dest]; + + edge.addPoint(start.row + 1, start.col + 1); + if (edge.mainColumn != start.col + 1) { + edge.addPoint(start.row + 1, start.col + 1, edge.mainColumn < start.col + 1 ? -1 : 1); + edge.addPoint(start.row + 1, edge.mainColumn, target.row <= start.row ? -2 : 0); + } + int mainColumnKind = 0; + if (edge.mainColumn < start.col + 1 && edge.mainColumn < target.col + 1) { + mainColumnKind = +2; + } else if (edge.mainColumn > start.col + 1 && edge.mainColumn > target.col + 1) { + mainColumnKind = -2; + } else if (edge.mainColumn == start.col + 1 && edge.mainColumn != target.col + 1) { + mainColumnKind = edge.mainColumn < target.col + 1 ? 1 : -1; + } else if (edge.mainColumn == target.col + 1 && edge.mainColumn != start.col + 1) { + mainColumnKind = edge.mainColumn < start.col + 1 ? 1 : -1; + } + edge.addPoint(target.row, edge.mainColumn, mainColumnKind); + if (target.col + 1 != edge.mainColumn) { + edge.addPoint(target.row, target.col + 1, target.row <= start.row ? 2 : 0); + edge.addPoint(target.row, target.col + 1, target.col + 1 < edge.mainColumn ? 1 : -1); + } + + // reduce edge spacing when there is large amount of edges connected to single block + auto startSpacingOverride = getSpacingOverride((*state.blocks)[start.id].width, start.outputCount); + auto targetSpacingOverride = getSpacingOverride((*state.blocks)[target.id].width, + target.inputCount); + edge.points.front().spacingOverride = startSpacingOverride; + edge.points.back().spacingOverride = targetSpacingOverride; + + + int length = 0; + for (size_t i = 1; i < edge.points.size(); i++) { + length += abs(edge.points[i].row - edge.points[i - 1].row) + + abs(edge.points[i].col - edge.points[i - 1].col); + } + edge.secondaryPriority = 2 * length + (target.row >= start.row ? 1 : 0); + } + } } -int GraphGridLayout::findVertEdgeIndex(EdgesVector &edges, int col, int min_row, int max_row) +namespace { +/** + * @brief Single segment of an edge. An edge can be drawn using multiple horizontal and vertical segments. + * x y meaning matches vertical segments. For horizontal segments axis are swapped. + */ +struct EdgeSegment { + int y0; + int y1; + int x; + int edgeIndex; + int secondaryPriority; + int16_t kind; + int16_t spacingOverride; //< segment spacing override, 0 if default spacing should be used +}; +struct NodeSide { + int x; + int y0; + int y1; + int size; //< block size in the x axis direction +}; +} + +/** + * @brief Calculate segment offsets relative to their column + * + * Argument naming uses terms for vertical segments, but the function can be used for horizontal segments as well. + * + * @param segments Segments that need to be processed. + * @param edgeOffsets Output argument for returning segment offsets relative to their columns. + * @param edgeColumnWidth InOut argument describing how much column with edges take. Initial value used as minimal + * value. May be increased to depending on amount of segments in each column and how tightly they are packed. + * @param nodeRightSide Right side of nodes. Used to reduce space reserved for edges by placing them between nodes. + * @param nodeLeftSide Same as right side. + * @param columnWidth + * @param H All the segmement and node coordinates y0 and y1 are expected to be in range [0;H) + * @param segmentSpacing The expected spacing between two segments in the same column. Actual spacing may be smaller + * for nodes with many edges. + */ +void calculateSegmentOffsets( + std::vector &segments, + std::vector &edgeOffsets, + std::vector &edgeColumnWidth, + std::vector &nodeRightSide, + std::vector &nodeLeftSide, + const std::vector &columnWidth, + size_t H, + int segmentSpacing) { - //Find a valid index - int i = 0; - while (true) { - bool valid = true; - for (int row = min_row; row < max_row + 1; row++) - if (isEdgeMarked(edges, row, col, i)) { - valid = false; - break; - } - if (valid) - break; - i++; + for (auto &segment : segments) { + if (segment.y0 > segment.y1) { + std::swap(segment.y0, segment.y1); + } } - //Mark chosen index as used - for (int row = min_row; row < max_row + 1; row++) - markEdge(edges, row, col, i); - return i; + std::sort(segments.begin(), segments.end(), [](const EdgeSegment & a, const EdgeSegment & b) { + if (a.x != b.x) return a.x < b.x; + if (a.kind != b.kind) return a.kind < b.kind; + auto aSize = a.y1 - a.y0; + auto bSize = b.y1 - b.y0; + if (aSize != bSize) { + if (a.kind != 1) { + return aSize < bSize; + } else { + return aSize > bSize; + } + } + if (a.kind != 1) { + return a.secondaryPriority < b.secondaryPriority; + } else { + return a.secondaryPriority > b.secondaryPriority; + } + return false; + }); + + auto compareNode = [](const NodeSide & a, const NodeSide & b) { + return a.x < b.x; + }; + sort(nodeRightSide.begin(), nodeRightSide.end(), compareNode); + sort(nodeLeftSide.begin(), nodeLeftSide.end(), compareNode); + + RangeAssignMaxTree maxSegment(H, INT_MIN); + auto nextSegmentIt = segments.begin(); + auto rightSideIt = nodeRightSide.begin(); + auto leftSideIt = nodeLeftSide.begin(); + while (nextSegmentIt != segments.end()) { + int x = nextSegmentIt->x; + + int leftColumWidth = 0; + if (x > 0) { + leftColumWidth = columnWidth[x - 1]; + } + maxSegment.setRange(0, H, -leftColumWidth); + while (rightSideIt != nodeRightSide.end() && rightSideIt->x + 1 < x) { + rightSideIt++; + } + while (rightSideIt != nodeRightSide.end() && rightSideIt->x + 1 == x) { + maxSegment.setRange(rightSideIt->y0, rightSideIt->y1 + 1, rightSideIt->size - leftColumWidth); + rightSideIt++; + } + + while (nextSegmentIt != segments.end() && nextSegmentIt->x == x && nextSegmentIt->kind <= 1) { + int y = maxSegment.rangeMaximum(nextSegmentIt->y0, nextSegmentIt->y1 + 1); + if (nextSegmentIt->kind != -2) { + y = std::max(y, 0); + } + y += nextSegmentIt->spacingOverride ? nextSegmentIt->spacingOverride : segmentSpacing; + maxSegment.setRange(nextSegmentIt->y0, nextSegmentIt->y1 + 1, y); + edgeOffsets[nextSegmentIt->edgeIndex] = y; + nextSegmentIt++; + } + + auto firstRightSideSegment = nextSegmentIt; + auto middleWidth = std::max(maxSegment.rangeMaximum(0, H), 0); + + int rightColumnWidth = 0; + if (x < static_cast(columnWidth.size())) { + rightColumnWidth = columnWidth[x]; + } + + maxSegment.setRange(0, H, -rightColumnWidth); + while (leftSideIt != nodeLeftSide.end() && leftSideIt->x < x) { + leftSideIt++; + } + while (leftSideIt != nodeLeftSide.end() && leftSideIt->x == x) { + maxSegment.setRange(leftSideIt->y0, leftSideIt->y1 + 1, leftSideIt->size - rightColumnWidth); + leftSideIt++; + } + while (nextSegmentIt != segments.end() && nextSegmentIt->x == x) { + int y = maxSegment.rangeMaximum(nextSegmentIt->y0, nextSegmentIt->y1 + 1); + y += nextSegmentIt->spacingOverride ? nextSegmentIt->spacingOverride : segmentSpacing; + maxSegment.setRange(nextSegmentIt->y0, nextSegmentIt->y1 + 1, y); + edgeOffsets[nextSegmentIt->edgeIndex] = y; + nextSegmentIt++; + } + auto rightSideMiddle = std::max(maxSegment.rangeMaximum(0, H), 0); + rightSideMiddle = std::max(rightSideMiddle, edgeColumnWidth[x] - middleWidth - segmentSpacing); + for (auto it = firstRightSideSegment; it != nextSegmentIt; ++it) { + edgeOffsets[it->edgeIndex] = middleWidth + (rightSideMiddle - edgeOffsets[it->edgeIndex]) + + segmentSpacing; + } + edgeColumnWidth[x] = middleWidth + segmentSpacing + rightSideMiddle; + } } + + +/** + * @brief Center the segments to the middle of edge columns when possible. + * @param segmentOffsets offsets relative to the left side edge column. + * @param edgeColumnWidth widths of edge columns + * @param segments either all horizontal or all vertical edge segments + * @param minSpacing spacing between segments + */ +static void centerEdges( + std::vector &segmentOffsets, + const std::vector &edgeColumnWidth, + const std::vector &segments, + int minSpacing) +{ + /* Split segments in each edge column into non intersecting chunks. Center each chunk separately. + * + * Process segment endpoints sorted by x and y. Maintain count of currently started segments. When number of + * active segments reaches 0 there is empty space between chunks. + */ + struct Event { + int x; + int y; + int index; + bool start; + }; + std::vector events; + events.reserve(segments.size() * 2); + for (const auto &segment : segments) { + auto offset = segmentOffsets[segment.edgeIndex]; + // Exclude segments which are outside edge column and between the blocks. It's hard to ensure that moving + // them doesn't cause overlap with blocks. + if (offset >= 0 && offset <= edgeColumnWidth[segment.x]) { + events.push_back({segment.x, segment.y0, segment.edgeIndex, true}); + events.push_back({segment.x, segment.y1, segment.edgeIndex, false}); + } + } + std::sort(events.begin(), events.end(), [](const Event & a, const Event & b) { + if (a.x != b.x) return a.x < b.x; + if (a.y != b.y) return a.y < b.y; + // Process segment start events before end to ensure that activeSegmentCount doesn't go negative and only + // reaches 0 at the end of chunk. + return int(a.start) > int(b.start); + }); + + auto it = events.begin(); + while (it != events.end()) { + auto chunkStart = it++; + int activeSegmentCount = 1; + int chunkWidth = 0; + while (activeSegmentCount > 0) { + activeSegmentCount += it->start ? 1 : -1; + chunkWidth = std::max(chunkWidth, segmentOffsets[it->index]); + it++; + } + // leftMost segment position includes padding on the left side so add it on the right side as well + chunkWidth += minSpacing; + + int spacing = (std::max(edgeColumnWidth[chunkStart->x], minSpacing) - chunkWidth) / 2; + for (auto segment = chunkStart; segment != it; segment++) { + if (segment->start) { + segmentOffsets[segment->index] += spacing; + } + } + } +} + +/** + * @brief Convert segment coordinates from arbitary range to continuous range starting at 0. + * @param segments + * @param leftSides + * @param rightSides + * @return Size of compressed coordinate range. + */ +static int compressCoordinates(std::vector &segments, + std::vector &leftSides, + std::vector &rightSides) +{ + std::vector positions; + positions.reserve((segments.size() + leftSides.size()) * 2); + for (const auto &segment : segments) { + positions.push_back(segment.y0); + positions.push_back(segment.y1); + } + for (const auto &segment : leftSides) { + positions.push_back(segment.y0); + positions.push_back(segment.y1); + } + // y0 and y1 in rightSides should match leftSides + + std::sort(positions.begin(), positions.end()); + auto lastUnique = std::unique(positions.begin(), positions.end()); + positions.erase(lastUnique, positions.end()); + + auto positionToIndex = [&] (int position) { + size_t index = std::lower_bound(positions.begin(), positions.end(), position) - positions.begin(); + assert(index < positions.size()); + return index; + }; + for (auto &segment : segments) { + segment.y0 = positionToIndex(segment.y0); + segment.y1 = positionToIndex(segment.y1); + } + assert(leftSides.size() == rightSides.size()); + for (size_t i = 0; i < leftSides.size(); i++) { + leftSides[i].y0 = rightSides[i].y0 = positionToIndex(leftSides[i].y0); + leftSides[i].y1 = rightSides[i].y1 = positionToIndex(leftSides[i].y1); + } + return positions.size(); +} + +void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state) const +{ + int edgeIndex = 0; + + auto segmentFromPoint = + [&edgeIndex](const Point & point, const GridEdge & edge, int y0, int y1, int x) { + EdgeSegment segment; + segment.y0 = y0; + segment.y1 = y1; + segment.x = x; + segment.edgeIndex = edgeIndex++; + segment.kind = point.kind; + segment.spacingOverride = point.spacingOverride; + segment.secondaryPriority = edge.secondaryPriority; + return segment; + }; + + + std::vector segments; + std::vector rightSides; + std::vector leftSides; + std::vector edgeOffsets; + + // Vertical segments + for (auto &edgeListIt : state.edge) { + for (const auto &edge : edgeListIt.second) { + for (size_t j = 1; j < edge.points.size(); j += 2) { + segments.push_back(segmentFromPoint(edge.points[j], edge, + edge.points[j-1].row * 2, // edges in even rows + edge.points[j].row * 2, + edge.points[j].col)); + } + } + } + for (auto &blockIt : state.grid_blocks) { + auto &node = blockIt.second; + auto width = (*state.blocks)[blockIt.first].width; + auto leftWidth = width / 2; + // not the same as leftWidth, you would think that one pixel offset isn't visible, but it is + auto rightWidth = width - leftWidth; + int row = node.row * 2 + 1; // blocks in odd rows + leftSides.push_back({node.col, row, row, leftWidth}); + rightSides.push_back({node.col + 1, row, row, rightWidth}); + } + state.edgeColumnWidth.assign(state.columns + 1, layoutConfig.blockHorizontalSpacing); + state.edgeColumnWidth[0] = state.edgeColumnWidth.back() = layoutConfig.edgeHorizontalSpacing; + edgeOffsets.resize(edgeIndex); + calculateSegmentOffsets(segments, edgeOffsets, state.edgeColumnWidth, rightSides, leftSides, + state.columnWidth, 2 * state.rows + 1, layoutConfig.edgeHorizontalSpacing); + centerEdges(edgeOffsets, state.edgeColumnWidth, segments, layoutConfig.blockHorizontalSpacing); + edgeIndex = 0; + + auto copySegmentsToEdges = [&](bool col) { + int edgeIndex = 0; + for (auto &edgeListIt : state.edge) { + for (auto &edge : edgeListIt.second) { + for (size_t j = col ? 1 : 2; j < edge.points.size(); j += 2) { + edge.points[j].offset = edgeOffsets[edgeIndex++]; + } + } + } + }; + auto oldColumnWidths = state.columnWidth; + adjustColumnWidths(state); + for (auto &segment : segments) { + auto &offset = edgeOffsets[segment.edgeIndex]; + if (segment.kind == -2) { + offset -= (state.edgeColumnWidth[segment.x - 1] / 2 + state.columnWidth[segment.x - 1]) - + oldColumnWidths[segment.x - 1]; + } else if (segment.kind == 2) { + offset += (state.edgeColumnWidth[segment.x + 1] / 2 + state.columnWidth[segment.x]) - + oldColumnWidths[segment.x]; + } + } + calculateColumnOffsets(state.columnWidth, state.edgeColumnWidth, + state.columnOffset, state.edgeColumnOffset); + copySegmentsToEdges(true); + + + // Horizontal segments + // Use exact x coordinates obtained from vertical segment placment. + segments.clear(); + leftSides.clear(); + rightSides.clear(); + + edgeIndex = 0; + for (auto &edgeListIt : state.edge) { + for (const auto &edge : edgeListIt.second) { + for (size_t j = 2; j < edge.points.size(); j += 2) { + int y0 = state.edgeColumnOffset[edge.points[j - 1].col] + edge.points[j - 1].offset; + int y1 = state.edgeColumnOffset[edge.points[j + 1].col] + edge.points[j + 1].offset; + segments.push_back(segmentFromPoint(edge.points[j], edge, y0, y1, edge.points[j].row)); + } + } + } + edgeOffsets.resize(edgeIndex); + for (auto &blockIt : state.grid_blocks) { + auto &node = blockIt.second; + auto blockWidth = (*state.blocks)[node.id].width; + int leftSide = state.edgeColumnOffset[node.col + 1] + state.edgeColumnWidth[node.col + 1] / 2 - + blockWidth / 2; + int rightSide = leftSide + blockWidth; + + int h = (*state.blocks)[blockIt.first].height; + int freeSpace = state.rowHeight[node.row] - h; + int topProfile = state.rowHeight[node.row]; + int bottomProfile = h; + if (verticalBlockAlignmentMiddle) { + topProfile -= freeSpace / 2; + bottomProfile += freeSpace / 2; + } + leftSides.push_back({node.row, leftSide, rightSide, topProfile}); + rightSides.push_back({node.row, leftSide, rightSide, bottomProfile}); + } + state.edgeRowHeight.assign(state.rows + 1, layoutConfig.blockVerticalSpacing); + state.edgeRowHeight[0] = state.edgeRowHeight.back() = layoutConfig.edgeVerticalSpacing; + edgeOffsets.resize(edgeIndex); + auto compressedCoordinates = compressCoordinates(segments, leftSides, rightSides); + calculateSegmentOffsets(segments, edgeOffsets, state.edgeRowHeight, rightSides, leftSides, + state.rowHeight, compressedCoordinates, layoutConfig.edgeVerticalSpacing); + copySegmentsToEdges(false); +} + +void GraphGridLayout::adjustColumnWidths(GraphGridLayout::LayoutState &state) const +{ + state.rowHeight.assign(state.rows, 0); + state.columnWidth.assign(state.columns, 0); + for (auto &node : state.grid_blocks) { + const auto &inputBlock = (*state.blocks)[node.first]; + state.rowHeight[node.second.row] = std::max(inputBlock.height, state.rowHeight[node.second.row]); + int edgeWidth = state.edgeColumnWidth[node.second.col + 1]; + int columnWidth = (inputBlock.width - edgeWidth) / 2; + state.columnWidth[node.second.col] = std::max(columnWidth, state.columnWidth[node.second.col]); + state.columnWidth[node.second.col + 1] = std::max(columnWidth, + state.columnWidth[node.second.col + 1]); + } +} + +int GraphGridLayout::calculateColumnOffsets(const std::vector &columnWidth, + std::vector &edgeColumnWidth, std::vector &columnOffset, + std::vector &edgeColumnOffset) +{ + assert(edgeColumnWidth.size() == columnWidth.size() + 1); + int position = 0; + edgeColumnOffset.resize(edgeColumnWidth.size()); + columnOffset.resize(columnWidth.size()); + for (size_t i = 0; i < columnWidth.size(); i++) { + edgeColumnOffset[i] = position; + position += edgeColumnWidth[i]; + columnOffset[i] = position; + position += columnWidth[i]; + } + edgeColumnOffset.back() = position; + position += edgeColumnWidth.back(); + return position; +} + +void GraphGridLayout::convertToPixelCoordinates( + GraphGridLayout::LayoutState &state, + int &width, + int &height) const +{ + // calculate row and column offsets + width = calculateColumnOffsets(state.columnWidth, state.edgeColumnWidth, + state.columnOffset, state.edgeColumnOffset); + height = calculateColumnOffsets(state.rowHeight, state.edgeRowHeight, + state.rowOffset, state.edgeRowOffset); + + // block pixel positions + for (auto &block : (*state.blocks)) { + const auto &gridBlock = state.grid_blocks[block.first]; + + block.second.x = state.edgeColumnOffset[gridBlock.col + 1] + + state.edgeColumnWidth[gridBlock.col + 1] / 2 - block.second.width / 2; + block.second.y = state.rowOffset[gridBlock.row]; + if (verticalBlockAlignmentMiddle) { + block.second.y += (state.rowHeight[gridBlock.row] - block.second.height) / 2; + } + } + // edge pixel positions + for (auto &it : (*state.blocks)) { + auto &block = it.second; + for (size_t i = 0; i < block.edges.size(); i++) { + auto &resultEdge = block.edges[i]; + const auto &target = (*state.blocks)[resultEdge.target]; + resultEdge.polyline.clear(); + resultEdge.polyline.push_back(QPointF(0, block.y + block.height)); + + const auto &edge = state.edge[it.first][i]; + for (size_t j = 1; j < edge.points.size(); j++) { + if (j & 1) { // vertical segment + int column = edge.points[j].col; + int x = state.edgeColumnOffset[column] + edge.points[j].offset; + resultEdge.polyline.back().setX(x); + resultEdge.polyline.push_back(QPointF(x, 0)); + } else { // horizontal segment + int row = edge.points[j].row; + int y = state.edgeRowOffset[row] + edge.points[j].offset; + resultEdge.polyline.back().setY(y); + resultEdge.polyline.push_back(QPointF(0, y)); + } + } + resultEdge.polyline.back().setY(target.y); + } + } +} + diff --git a/src/widgets/GraphGridLayout.h b/src/widgets/GraphGridLayout.h index 9e6b1525..34b9a179 100644 --- a/src/widgets/GraphGridLayout.h +++ b/src/widgets/GraphGridLayout.h @@ -3,7 +3,12 @@ #include "core/Cutter.h" #include "GraphLayout.h" +#include "common/LinkedListPool.h" + +/** + * @brief Graph layout algorithm on layered graph layout approach. For simplicity all the nodes are placed in a grid. + */ class GraphGridLayout : public GraphLayout { public: @@ -20,42 +25,53 @@ public: int &height) const override; private: LayoutType layoutType; + /// false - use bounding box for smallest subtree when placing them side by side + bool tightSubtreePlacement = false; + /// true if code should try to place parent between direct children as much as possible + bool parentBetweenDirectChild = false; + /// false if blocks in rows should be aligned at top, true for middle alignment + bool verticalBlockAlignmentMiddle = false; struct GridBlock { ut64 id; - std::vector tree_edge; // subset of outgoing edges that form a tree - std::vector dag_edge; // subset of outgoing edges that form a tree + std::vector tree_edge; //!< subset of outgoing edges that form a tree + std::vector dag_edge; //!< subset of outgoing edges that form a dag std::size_t has_parent = false; - int level = 0; + int inputCount = 0; + int outputCount = 0; - // Number of rows in block + /// Number of rows in subtree int row_count = 0; - // Number of columns in block - int col_count = 0; - // Column in which the block is + /// Column in which the block is int col = 0; - // Row in which the block is + /// Row in which the block is int row = 0; + + int lastRowLeft; //!< left side of subtree last row + int lastRowRight; //!< right side of subtree last row + int leftPosition; //!< left side of subtree + int rightPosition; //!< right side of subtree + LinkedListPool::List leftSideShape; + LinkedListPool::List rightSideShape; }; struct Point { - int row; //point[0] - int col; //point[1] - int index; //point[2] + int row; + int col; + int offset; + int16_t kind; + int16_t spacingOverride; }; struct GridEdge { ut64 dest; + int mainColumn = -1; std::vector points; - int start_index = 0; - QPolygonF polyline; + int secondaryPriority; - void addPoint(int row, int col, int index = 0) + void addPoint(int row, int col, int16_t kind = 0) { - Point point = {row, col, 0}; - this->points.push_back(point); - if (int(this->points.size()) > 1) - this->points[this->points.size() - 2].index = index; + this->points.push_back({row, col, 0, kind, 0}); } }; @@ -63,29 +79,92 @@ private: std::unordered_map grid_blocks; std::unordered_map *blocks = nullptr; std::unordered_map> edge; + size_t rows = -1; + size_t columns = -1; + std::vector columnWidth; + std::vector rowHeight; + std::vector edgeColumnWidth; + std::vector edgeRowHeight; + + std::vector columnOffset; + std::vector rowOffset; + std::vector edgeColumnOffset; + std::vector edgeRowOffset; }; using GridBlockMap = std::unordered_map; + /** + * @brief Find nodes where control flow merges after splitting. + * Sets node column offset so that after computing placement merge point is centered bellow nodes above. + */ + void findMergePoints(LayoutState &state) const; + /** + * @brief Compute node rows and columns within grid. + * @param blockOrder Nodes in the reverse topological order. + */ void computeAllBlockPlacement(const std::vector &blockOrder, LayoutState &layoutState) const; - void computeBlockPlacement(ut64 blockId, - LayoutState &layoutState) const; - void adjustGraphLayout(GridBlock &block, GridBlockMap &blocks, - int col, int row) const; + /** + * @brief Perform the topological sorting of graph nodes. + * If the graph contains loops, a subset of edges is selected. Subset of edges forming DAG are stored in + * GridBlock::dag_edge. + * @param state Graph layout state including the input graph. + * @param entry Entrypoint node. When removing loops prefer placing this node at top. + * @return Reverse topological ordering. + */ static std::vector topoSort(LayoutState &state, ut64 entry); - // Edge computing stuff - template - using Matrix = std::vector>; - using EdgesVector = Matrix>; + /** + * @brief Assign row positions to nodes. + * @param state + * @param blockOrder reverse topological ordering of nodes + */ + static void assignRows(LayoutState &state, const std::vector &blockOrder); + /** + * @brief Select subset of DAG edges that form tree. + * @param state + */ + static void selectTree(LayoutState &state); - GridEdge routeEdge(EdgesVector &horiz_edges, EdgesVector &vert_edges, - Matrix &edge_valid, GridBlock &start, GridBlock &end) const; - static int findVertEdgeIndex(EdgesVector &edges, int col, int min_row, int max_row); - static bool isEdgeMarked(EdgesVector &edges, int row, int col, int index); - static void markEdge(EdgesVector &edges, int row, int col, int index, bool used = true); - static int findHorizEdgeIndex(EdgesVector &edges, int row, int min_col, int max_col); + /** + * @brief routeEdges Route edges, expects node positions to be calculated previously. + */ + void routeEdges(LayoutState &state) const; + /** + * @brief Choose which column to use for transition from start node row to target node row. + */ + void calculateEdgeMainColumn(LayoutState &state) const; + /** + * @brief Do rough edge routing within grid using up to 5 segments. + */ + void roughRouting(LayoutState &state) const; + /** + * @brief Calculate segment placement relative to their columns. + */ + void elaborateEdgePlacement(LayoutState &state) const; + /** + * @brief Recalculate column widths, trying to compensate for the space taken by edge columns. + */ + void adjustColumnWidths(LayoutState &state) const; + /** + * @brief Calculate position of each column(or row) based on widths. + * It is assumed that columnWidth.size() + 1 = edgeColumnWidth.size() and they are interleaved. + * @param columnWidth + * @param edgeColumnWidth + * @param columnOffset + * @param edgeColumnOffset + * @return total width of all the columns + */ + static int calculateColumnOffsets(const std::vector &columnWidth, std::vector &edgeColumnWidth, + std::vector &columnOffset, std::vector &edgeColumnOffset); + /** + * @brief Final graph layout step. Convert grids cell relative positions to absolute pixel positions. + * @param state + * @param width image width output argument + * @param height image height output argument + */ + void convertToPixelCoordinates(LayoutState &state, int &width, int &height) const; }; #endif // GRAPHGRIDLAYOUT_H diff --git a/src/widgets/GraphLayout.h b/src/widgets/GraphLayout.h index 6da66e1b..c5229391 100644 --- a/src/widgets/GraphLayout.h +++ b/src/widgets/GraphLayout.h @@ -32,8 +32,10 @@ public: using Graph = std::unordered_map; struct LayoutConfig { - int block_vertical_margin = 40; - int block_horizontal_margin = 10; + int blockVerticalSpacing = 40; + int blockHorizontalSpacing = 10; + int edgeVerticalSpacing = 10; + int edgeHorizontalSpacing = 10; }; GraphLayout(const LayoutConfig &layout_config) : layoutConfig(layout_config) {}