#include "GraphGridLayout.h" #include #include #include #include #include #include #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 7. [optional] layout compacting 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. # Layout compacting Doing the layout within a grid causes minimal spacing to be limited by widest and tallest block within each column and row. One common case is block with function entrypoint being wider due to function name causing wide horizontal space between branching blocks. Another case is rows in two parallel columns being aligned. \image html layout_compacting.svg Both problems are mitigated by squishing graph. Compressing in each of the two direction is done separately. The process is defined as liner program. Each variable represents a position of edge segment or node in the direction being optimized. Following constraints are used - Keep the order with nearest segments. - If the node has two outgoing edges, one to the node on left side and other to the right, keep them on the corresponding side of node's center. - For all edges keep the node which is above above. This helps when vertical block spacing is set bigger than double edge spacing and edge shadows relationship between two blocks. - Equality constraint to keep relative position between nodes and and segments directly connected to them. - Equality constraint to keep the node centered when control flow merges In the vertical direction objective function minimizes y positions of nodes and lengths of vertical segments. In the horizontal direction objective function minimizes lengths of horizontal segments. In the resulting linear program all constraints beside x_i >= 0 consist of exactly two variables: either x_i - x_j <= c_k or x_i = x_j + c_k. Since it isn't necessary get perfect solution and to avoid worst case performance current implementation isn't using a general purpose linear programming solver. Each variable is changed until constraint is reached and afterwards variables are grouped and changed together. */ GraphGridLayout::GraphGridLayout(GraphGridLayout::LayoutType layoutType) : GraphLayout({}) { switch (layoutType) { case LayoutType::Narrow: tightSubtreePlacement = true; parentBetweenDirectChild = false; useLayoutOptimization = true; break; case LayoutType::Medium: tightSubtreePlacement = false; parentBetweenDirectChild = true; useLayoutOptimization = true; break; case LayoutType::Wide: tightSubtreePlacement = false; parentBetweenDirectChild = true; useLayoutOptimization = false; break; } } std::vector GraphGridLayout::topoSort(LayoutState &state, ut64 entry) { auto &blocks = *state.blocks; // Run DFS to: // * select backwards/loop edges // * perform toposort std::vector blockOrder; 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] = State::InStack; stack.push({ first, 0 }); while (!stack.empty()) { auto v = stack.top().first; auto edge_index = stack.top().second; const auto &block = blocks[v]; if (edge_index < block.edges.size()) { ++stack.top().second; auto target = block.edges[edge_index].target; auto &targetState = visited[target]; if (targetState == State::NotVisited) { targetState = State::InStack; stack.push({ target, 0 }); state.grid_blocks[v].dag_edge.push_back(target); } 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] = State::Visited; blockOrder.push_back(v); } } }; // Start with entry so that if start of function block is part of loop it // is still kept at top unless it's impossible to do while maintaining // topological order. dfsFragment(entry); for (auto &blockIt : blocks) { if (visited[blockIt.first] == State::NotVisited) { dfsFragment(blockIt.first); } } 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.row + 1; for (auto target : block.dag_edge) { auto &targetBlock = state.grid_blocks[target]; 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.row == block.row + 1) { block.tree_edge.push_back(targetId); targetBlock.has_parent = true; } } } } void GraphGridLayout::CalculateLayout(GraphLayout::Graph &blocks, ut64 entry, int &width, int &height) const { LayoutState layoutState; layoutState.blocks = &blocks; if (blocks.empty()) { return; } if (blocks.find(entry) == blocks.end()) { entry = blocks.begin()->first; } for (auto &it : blocks) { GridBlock block; block.id = it.first; layoutState.grid_blocks[it.first] = block; } auto blockOrder = topoSort(layoutState, entry); computeAllBlockPlacement(blockOrder, layoutState); for (auto &blockIt : blocks) { layoutState.edge[blockIt.first].resize(blockIt.second.edges.size()); for (size_t i = 0; i < blockIt.second.edges.size(); i++) { layoutState.edge[blockIt.first][i].dest = blockIt.second.edges[i].target; blockIt.second.edges[i].arrow = GraphEdge::Down; } } for (const auto &edgeList : layoutState.edge) { auto &startBlock = layoutState.grid_blocks[edgeList.first]; startBlock.outputCount = edgeList.second.size(); for (auto &edge : edgeList.second) { auto &targetBlock = layoutState.grid_blocks[edge.dest]; targetBlock.inputCount++; } } 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); if (useLayoutOptimization) { optimizeLayout(layoutState); cropToContent(blocks, width, height); } } void GraphGridLayout::findMergePoints(GraphGridLayout::LayoutState &state) const { for (auto &blockIt : state.grid_blocks) { auto &block = blockIt.second; 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]]; } 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; } } if (goesToMerge) { if (targetBlock.tree_edge.size() == 1) { blockWithTreeEdge = blocksGoingToMerge; } blocksGoingToMerge++; } else { break; } } if (blocksGoingToMerge) { block.mergeBlock = mergeBlock->id; state.grid_blocks[block.tree_edge[blockWithTreeEdge]].col = blockWithTreeEdge * 2 - (blocksGoingToMerge - 1); } } } void GraphGridLayout::computeAllBlockPlacement(const std::vector &blockOrder, LayoutState &layoutState) const { assignRows(layoutState, blockOrder); selectTree(layoutState); findMergePoints(layoutState); // 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.lastRowRight = 2; block.lastRowLeft = 0; block.leftPosition = 0; block.rightPosition = 2; 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; 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; } 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; } } // 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 { // 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); *sides.head(leftSide) -= block.col; block.leftSideShape = sides.append(sides.makeList(block.col), leftSide); *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 + nextEmptyColumn; } } // 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; } } } } } void GraphGridLayout::roughRouting(GraphGridLayout::LayoutState &state) const { auto getSpacingOverride = [this](int blockWidth, int edgeCount) { if (edgeCount == 0) { return 0; } int maxSpacing = blockWidth / edgeCount; if (maxSpacing < layoutConfig.edgeHorizontalSpacing) { return std::max(maxSpacing, 1); } return 0; }; 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; if (edge.points.size() <= 2) { if (startSpacingOverride && startSpacingOverride < targetSpacingOverride) { edge.points.back().spacingOverride = startSpacingOverride; } } else { edge.points[1].spacingOverride = startSpacingOverride; } 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); } } } 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) { for (auto &segment : segments) { if (segment.y0 > segment.y1) { std::swap(segment.y0, segment.y1); } } 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 */ static void centerEdges(std::vector &segmentOffsets, const std::vector &edgeColumnWidth, const std::vector &segments) { /* 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()) { int left, right; left = right = segmentOffsets[it->index]; auto chunkStart = it++; int activeSegmentCount = 1; while (activeSegmentCount > 0) { activeSegmentCount += it->start ? 1 : -1; int offset = segmentOffsets[it->index]; left = std::min(left, offset); right = std::max(right, offset); it++; } int spacing = (edgeColumnWidth[chunkStart->x] - (right - left)) / 2 - left; for (auto segment = chunkStart; segment != it; segment++) { if (segment->start) { segmentOffsets[segment->index] += spacing; } } } } /** * @brief Convert segment coordinates from arbitrary 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); 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) { int offset = edgeOffsets[edgeIndex++]; if (col) { GraphBlock *block = nullptr; if (j == 1) { block = &(*state.blocks)[edgeListIt.first]; } else if (j + 1 == edge.points.size()) { block = &(*state.blocks)[edge.dest]; } if (block) { int blockWidth = block->width; int edgeColumWidth = state.edgeColumnWidth[edge.points[j].col]; offset = std::max(-blockWidth / 2 + edgeColumWidth / 2, offset); offset = std::min(edgeColumWidth / 2 + std::min(blockWidth, edgeColumWidth) / 2, offset); } } edge.points[j].offset = offset; } } } }; 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 placement. 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]; 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)); } } } } connectEdgeEnds(*state.blocks); } void GraphGridLayout::cropToContent(GraphLayout::Graph &graph, int &width, int &height) const { if (graph.empty()) { width = std::max(1, layoutConfig.edgeHorizontalSpacing); height = std::max(1, layoutConfig.edgeVerticalSpacing); return; } const auto &anyBlock = graph.begin()->second; int minPos[2] = { anyBlock.x, anyBlock.y }; int maxPos[2] = { anyBlock.x, anyBlock.y }; auto updateLimits = [&](int x, int y) { minPos[0] = std::min(minPos[0], x); minPos[1] = std::min(minPos[1], y); maxPos[0] = std::max(maxPos[0], x); maxPos[1] = std::max(maxPos[1], y); }; for (const auto &blockIt : graph) { auto &block = blockIt.second; updateLimits(block.x, block.y); updateLimits(block.x + block.width, block.y + block.height); for (auto &edge : block.edges) { for (auto &point : edge.polyline) { updateLimits(point.x(), point.y()); } } } minPos[0] -= layoutConfig.edgeHorizontalSpacing; minPos[1] -= layoutConfig.edgeVerticalSpacing; maxPos[0] += layoutConfig.edgeHorizontalSpacing; maxPos[1] += layoutConfig.edgeVerticalSpacing; for (auto &blockIt : graph) { auto &block = blockIt.second; block.x -= minPos[0]; block.y -= minPos[1]; for (auto &edge : block.edges) { for (auto &point : edge.polyline) { point -= QPointF(minPos[0], minPos[1]); } } } width = maxPos[0] - minPos[0]; height = maxPos[1] - minPos[1]; } void GraphGridLayout::connectEdgeEnds(GraphLayout::Graph &graph) const { for (auto &it : graph) { auto &block = it.second; for (size_t i = 0; i < block.edges.size(); i++) { auto &resultEdge = block.edges[i]; const auto &target = graph[resultEdge.target]; resultEdge.polyline[0].ry() = block.y + block.height; resultEdge.polyline.back().ry() = target.y; } } } /// Either equality or inequality x_i <= x_j + c using Constraint = std::pair, int>; /** * @brief Single pass of linear program optimizer. * Changes variables until a constraint is hit, afterwards the two variables are changed together. * @param n number of variables * @param objectiveFunction coefficients for function \f$\sum c_i x_i\f$ which needs to be minimized * @param inequalities inequality constraints \f$x_{e_i} - x_{f_i} \leq b_i\f$ * @param equalities equality constraints \f$x_{e_i} - x_{f_i} = b_i\f$ * @param solution input/output argument, returns results, needs to be initialized with a feasible * solution * @param stickWhenNotMoving variable grouping strategy */ static void optimizeLinearProgramPass(size_t n, std::vector objectiveFunction, std::vector inequalities, std::vector equalities, std::vector &solution, bool stickWhenNotMoving) { std::vector group(n); std::iota(group.begin(), group.end(), 0); // initially each variable is in it's own group assert(n == objectiveFunction.size()); assert(n == solution.size()); std::vector edgeCount(n); LinkedListPool edgePool(inequalities.size() * 2); std::vector edges(n); auto getGroup = [&](int v) { while (group[v] != v) { group[v] = group[group[v]]; v = group[v]; } return v; }; auto joinGroup = [&](int a, int b) { group[getGroup(b)] = getGroup(a); }; for (auto &constraint : inequalities) { int a = constraint.first.first; int b = constraint.first.second; size_t index = &constraint - &inequalities.front(); edges[a] = edgePool.append(edges[a], edgePool.makeList(index)); edges[b] = edgePool.append(edges[b], edgePool.makeList(index)); edgeCount[a]++; edgeCount[b]++; } std::vector processed(n); // Smallest variable value in the group relative to main one, this is used to maintain implicit // x_i >= 0 constraint std::vector groupRelativeMin(n, 0); auto joinSegmentGroups = [&](int a, int b) { a = getGroup(a); b = getGroup(b); joinGroup(a, b); edgeCount[a] += edgeCount[b]; objectiveFunction[a] += objectiveFunction[b]; int internalEdgeCount = 0; auto writeIt = edgePool.head(edges[b]); // update inequalities and remove some of the constraints between variables that are now // grouped for (auto it = edgePool.head(edges[b]); it; ++it) { auto &constraint = inequalities[*it]; int other = constraint.first.first + constraint.first.second - b; if (getGroup(other) == a) { // skip inequalities where both variables are now in the same group internalEdgeCount++; continue; } *writeIt++ = *it; // Modify the inequalities for the group being attached relative to the main variable in // the group to which it is being attached. int diff = solution[a] - solution[b]; if (b == constraint.first.first) { constraint.first.first = a; constraint.second += diff; } else { constraint.first.second = a; constraint.second -= diff; } } edges[a] = edgePool.append(edges[a], edgePool.splitHead(edges[b], writeIt)); edgeCount[a] -= internalEdgeCount; groupRelativeMin[a] = std::min(groupRelativeMin[a], groupRelativeMin[b] + solution[b] - solution[a]); }; for (auto &equality : equalities) { // process equalities, assumes that initial solution is feasible solution and matches // equality constraints int a = getGroup(equality.first.first); int b = getGroup(equality.first.second); if (a == b) { equality = { { 0, 0 }, 0 }; continue; } // always join smallest group to bigger one if (edgeCount[a] > edgeCount[b]) { std::swap(a, b); // Update the equality equation so that later variable values can be calculated by // simply iterating through them without need to check which direction the group joining // was done. std::swap(equality.first.first, equality.first.second); equality.second = -equality.second; } joinSegmentGroups(b, a); equality = { { a, b }, solution[a] - solution[b] }; processed[a] = 1; } // Priority queue for processing groups starting with currently smallest one. Doing it this way // should result in number of constraints within group doubling each time two groups are joined. // That way each constraint is processed no more than log(n) times. std::priority_queue, std::vector>, std::greater>> queue; for (size_t i = 0; i < n; i++) { if (!processed[i]) { queue.push({ edgeCount[i], i }); } } while (!queue.empty()) { int g = queue.top().second; int size = queue.top().first; queue.pop(); if ((size_t)size != edgeCount[g] || processed[g]) { continue; } int direction = objectiveFunction[g]; if (direction == 0) { continue; } // Find the first constraint which will be hit by changing the variable in the desired // direction defined by objective function. int limitingGroup = -1; int smallestMove = 0; if (direction < 0) { smallestMove = INT_MAX; for (auto it = edgePool.head(edges[g]); it; ++it) { auto &inequality = inequalities[*it]; if (g == inequality.first.second) { continue; } int other = inequality.first.second; if (getGroup(other) == g) { continue; } int move = solution[other] + inequality.second - solution[g]; if (move < smallestMove) { smallestMove = move; limitingGroup = other; } } } else { smallestMove = -solution[g] - groupRelativeMin[g]; // keep all variables >= 0 for (auto it = edgePool.head(edges[g]); it; ++it) { auto &inequality = inequalities[*it]; if (g == inequality.first.first) { continue; } int other = inequality.first.first; if (getGroup(other) == g) { continue; } int move = solution[other] - inequality.second - solution[g]; if (move > smallestMove) { smallestMove = move; limitingGroup = other; } } } assert(smallestMove != INT_MAX); if (smallestMove == INT_MAX) { // Unbound variable, this means that linear program wasn't set up correctly. // Better don't change it instead of stretching the graph to infinity. smallestMove = 0; } solution[g] += smallestMove; if (smallestMove == 0 && stickWhenNotMoving == false) { continue; } processed[g] = 1; if (limitingGroup != -1) { joinSegmentGroups(limitingGroup, g); if (!processed[limitingGroup]) { queue.push({ edgeCount[limitingGroup], limitingGroup }); } equalities.push_back({ { g, limitingGroup }, solution[g] - solution[limitingGroup] }); } // else do nothing if limited by variable >= 0 } for (auto it = equalities.rbegin(), end = equalities.rend(); it != end; ++it) { solution[it->first.first] = solution[it->first.second] + it->second; } } /** * @brief Linear programming solver * Does not guarantee optimal solution. * @param n number of variables * @param objectiveFunction coefficients for function \f$\sum c_i x_i\f$ which needs to be minimized * @param inequalities inequality constraints \f$x_{e_i} - x_{f_i} \leq b_i\f$ * @param equalities equality constraints \f$x_{e_i} - x_{f_i} = b_i\f$ * @param solution input/output argument, returns results, needs to be initialized with a feasible * solution */ static void optimizeLinearProgram(size_t n, const std::vector &objectiveFunction, std::vector inequalities, const std::vector &equalities, std::vector &solution) { // Remove redundant inequalities std::sort(inequalities.begin(), inequalities.end()); auto uniqueEnd = std::unique( inequalities.begin(), inequalities.end(), [](const Constraint &a, const Constraint &b) { return a.first == b.first; }); inequalities.erase(uniqueEnd, inequalities.end()); static const int ITERATIONS = 1; for (int i = 0; i < ITERATIONS; i++) { optimizeLinearProgramPass(n, objectiveFunction, inequalities, equalities, solution, true); // optimizeLinearProgramPass(n, objectiveFunction, inequalities, equalities, solution, // false); } } namespace { struct Segment { int x; int variableId; int y0, y1; }; } static Constraint createInequality(size_t a, int posA, size_t b, int posB, int minSpacing, const std::vector &positions) { minSpacing = std::min(minSpacing, posB - posA); return { { a, b }, posB - positions[b] - (posA - positions[a]) - minSpacing }; } /** * @brief Create inequality constraints from segments which preserves their relative order on single * axis. * * @param segments list of edge segments and block sides * @param positions initial element positions before optimization * @param blockCount number of variables representing blocks, it is assumed that segments with * variableId < \a blockCount represent one side of block. * @param variableGroup used to check if segments are part of the same edge and spacing can be * reduced * @param blockSpacing minimal spacing between blocks * @param segmentSpacing minimal spacing between two edge segments, spacing may be less if values in * \a positions are closer than this * @param inequalities output variable for resulting inequalities, values initially stored in it are * not removed */ static void createInequalitiesFromSegments(std::vector segments, const std::vector &positions, const std::vector &variableGroup, int blockCount, int blockSpacing, int segmentSpacing, std::vector &inequalities) { // map used as binary search tree y_position -> segment{variableId, x_position} // It is used to maintain which segment was last seen in the range y_position.. std::map> lastSegments; lastSegments[-1] = { -1, -1 }; std::sort(segments.begin(), segments.end(), [](const Segment &a, const Segment &b) { return a.x < b.x; }); for (auto &segment : segments) { auto startPos = lastSegments.lower_bound(segment.y0); --startPos; // should never be lastSegment.begin() because map is initialized with segment // at pos -1 auto lastSegment = startPos->second; auto it = startPos; while (it != lastSegments.end() && it->first <= segment.y1) { int prevSegmentVariable = it->second.first; int prevSegmentPos = it->second.second; if (prevSegmentVariable != -1) { int minSpacing = segmentSpacing; if (prevSegmentVariable < blockCount && segment.variableId < blockCount) { // no need to add inequality between two sides of block if (prevSegmentVariable == segment.variableId) { ++it; continue; } minSpacing = blockSpacing; } else if (variableGroup[prevSegmentVariable] == variableGroup[segment.variableId]) { minSpacing = 0; } inequalities.push_back(createInequality(prevSegmentVariable, prevSegmentPos, segment.variableId, segment.x, minSpacing, positions)); } lastSegment = it->second; ++it; } if (startPos->first < segment.y0) { startPos++; } lastSegments.erase(startPos, it); // erase segments covered by current one lastSegments[segment.y0] = { segment.variableId, segment.x }; // current segment // either current segment splitting previous one into two parts or remaining part of // partially covered segment lastSegments[segment.y1] = lastSegment; } } void GraphGridLayout::optimizeLayout(GraphGridLayout::LayoutState &state) const { std::unordered_map blockMapping; size_t blockIndex = 0; for (auto &blockIt : *state.blocks) { blockMapping[blockIt.first] = blockIndex++; } std::vector variableGroups(blockMapping.size()); std::iota(variableGroups.begin(), variableGroups.end(), 0); std::vector objectiveFunction; std::vector inequalities; std::vector equalities; std::vector solution; auto addObjective = [&](size_t a, int posA, size_t b, int posB) { objectiveFunction.resize(std::max(objectiveFunction.size(), std::max(a, b) + 1)); if (posA < posB) { objectiveFunction[b] += 1; objectiveFunction[a] -= 1; } else { objectiveFunction[a] += 1; objectiveFunction[b] -= 1; } }; auto addInequality = [&](size_t a, int posA, size_t b, int posB, int minSpacing) { inequalities.push_back(createInequality(a, posA, b, posB, minSpacing, solution)); }; auto addBlockSegmentEquality = [&](ut64 blockId, int edgeVariable, int edgeVariablePos) { int blockPos = (*state.blocks)[blockId].x; int blockVariable = blockMapping[blockId]; equalities.push_back({ { blockVariable, edgeVariable }, blockPos - edgeVariablePos }); }; auto setFeasibleSolution = [&](size_t variable, int value) { solution.resize(std::max(solution.size(), variable + 1)); solution[variable] = value; }; auto copyVariablesToPositions = [&](const std::vector &solution, bool horizontal = false) { #ifndef NDEBUG for (auto v : solution) { assert(v >= 0); } #endif size_t variableIndex = blockMapping.size(); for (auto &blockIt : *state.blocks) { auto &block = blockIt.second; for (auto &edge : blockIt.second.edges) { for (int i = 1 + int(horizontal); i < edge.polyline.size(); i += 2) { int x = solution[variableIndex++]; if (horizontal) { edge.polyline[i].ry() = x; edge.polyline[i - 1].ry() = x; } else { edge.polyline[i].rx() = x; edge.polyline[i - 1].rx() = x; } } } int blockVariable = blockMapping[blockIt.first]; (horizontal ? block.y : block.x) = solution[blockVariable]; } }; std::vector segments; segments.reserve(state.blocks->size() * 2 + state.blocks->size() * 2); size_t variableIndex = state.blocks->size(); size_t edgeIndex = 0; // horizontal segments objectiveFunction.assign(blockMapping.size(), 1); for (auto &blockIt : *state.blocks) { auto &block = blockIt.second; int blockVariable = blockMapping[blockIt.first]; for (auto &edge : block.edges) { auto &targetBlock = (*state.blocks)[edge.target]; if (block.y < targetBlock.y) { int spacing = block.height + layoutConfig.blockVerticalSpacing; inequalities.push_back({ { blockVariable, blockMapping[edge.target] }, -spacing }); } if (edge.polyline.size() < 3) { continue; } for (int i = 2; i < edge.polyline.size(); i += 2) { int y0 = edge.polyline[i - 1].x(); int y1 = edge.polyline[i].x(); if (y0 > y1) { std::swap(y0, y1); } int x = edge.polyline[i].y(); segments.push_back({ x, int(variableIndex), y0, y1 }); variableGroups.push_back(blockMapping.size() + edgeIndex); setFeasibleSolution(variableIndex, x); if (i > 2) { int prevX = edge.polyline[i - 2].y(); addObjective(variableIndex, x, variableIndex - 1, prevX); } variableIndex++; } edgeIndex++; } segments.push_back({ block.y, blockVariable, block.x, block.x + block.width }); segments.push_back( { block.y + block.height, blockVariable, block.x, block.x + block.width }); setFeasibleSolution(blockVariable, block.y); } createInequalitiesFromSegments(std::move(segments), solution, variableGroups, blockMapping.size(), layoutConfig.blockVerticalSpacing, layoutConfig.edgeVerticalSpacing, inequalities); objectiveFunction.resize(solution.size()); optimizeLinearProgram(solution.size(), objectiveFunction, inequalities, equalities, solution); copyVariablesToPositions(solution, true); connectEdgeEnds(*state.blocks); // vertical segments variableGroups.resize(blockMapping.size()); solution.clear(); equalities.clear(); inequalities.clear(); objectiveFunction.clear(); segments.clear(); variableIndex = blockMapping.size(); edgeIndex = 0; for (auto &blockIt : *state.blocks) { auto &block = blockIt.second; for (auto &edge : block.edges) { if (edge.polyline.size() < 2) { continue; } size_t firstEdgeVariable = variableIndex; for (int i = 1; i < edge.polyline.size(); i += 2) { int y0 = edge.polyline[i - 1].y(); int y1 = edge.polyline[i].y(); if (y0 > y1) { std::swap(y0, y1); } int x = edge.polyline[i].x(); segments.push_back({ x, int(variableIndex), y0, y1 }); variableGroups.push_back(blockMapping.size() + edgeIndex); setFeasibleSolution(variableIndex, x); if (i > 2) { int prevX = edge.polyline[i - 2].x(); addObjective(variableIndex, x, variableIndex - 1, prevX); } variableIndex++; } size_t lastEdgeVariableIndex = variableIndex - 1; addBlockSegmentEquality(blockIt.first, firstEdgeVariable, edge.polyline[1].x()); addBlockSegmentEquality(edge.target, lastEdgeVariableIndex, segments.back().x); edgeIndex++; } int blockVariable = blockMapping[blockIt.first]; segments.push_back({ block.x, blockVariable, block.y, block.y + block.height }); segments.push_back( { block.x + block.width, blockVariable, block.y, block.y + block.height }); setFeasibleSolution(blockVariable, block.x); } createInequalitiesFromSegments(std::move(segments), solution, variableGroups, blockMapping.size(), layoutConfig.blockHorizontalSpacing, layoutConfig.edgeHorizontalSpacing, inequalities); objectiveFunction.resize(solution.size()); // horizontal centering constraints for (auto &blockIt : *state.blocks) { auto &block = blockIt.second; int blockVariable = blockMapping[blockIt.first]; if (block.edges.size() == 2) { auto &blockLeft = (*state.blocks)[block.edges[0].target]; auto &blockRight = (*state.blocks)[block.edges[1].target]; auto middle = block.x + block.width / 2; if (blockLeft.x + blockLeft.width < middle && blockRight.x > middle) { addInequality(blockMapping[block.edges[0].target], blockLeft.x + blockLeft.width, blockVariable, middle, layoutConfig.blockHorizontalSpacing / 2); addInequality(blockVariable, middle, blockMapping[block.edges[1].target], blockRight.x, layoutConfig.blockHorizontalSpacing / 2); auto &gridBlock = state.grid_blocks[blockIt.first]; if (gridBlock.mergeBlock) { auto &mergeBlock = (*state.blocks)[gridBlock.mergeBlock]; if (mergeBlock.x + mergeBlock.width / 2 == middle) { equalities.push_back( { { blockVariable, blockMapping[gridBlock.mergeBlock] }, block.x - mergeBlock.x }); } } } } } optimizeLinearProgram(solution.size(), objectiveFunction, inequalities, equalities, solution); copyVariablesToPositions(solution); }