mirror of
https://github.com/rizinorg/cutter.git
synced 2024-12-22 21:06:10 +00:00
216 lines
7.2 KiB
C++
216 lines
7.2 KiB
C++
|
#include "GraphvizLayout.h"
|
||
|
|
||
|
#include <unordered_set>
|
||
|
#include <unordered_map>
|
||
|
#include <queue>
|
||
|
#include <stack>
|
||
|
#include <cassert>
|
||
|
#include <sstream>
|
||
|
#include <iomanip>
|
||
|
#include <set>
|
||
|
|
||
|
#include <gvc.h>
|
||
|
|
||
|
GraphvizLayout::GraphvizLayout(LineType lineType, Direction direction)
|
||
|
: GraphLayout({})
|
||
|
, direction(direction)
|
||
|
, lineType(lineType)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
static GraphLayout::GraphEdge::ArrowDirection getArrowDirection(QPointF direction,
|
||
|
bool preferVertical)
|
||
|
{
|
||
|
if (abs(direction.x()) > abs(direction.y()) * (preferVertical ? 3.0 : 1.0)) {
|
||
|
if (direction.x() > 0) {
|
||
|
return GraphLayout::GraphEdge::Right;
|
||
|
} else {
|
||
|
return GraphLayout::GraphEdge::Left;
|
||
|
}
|
||
|
} else {
|
||
|
if (direction.y() > 0) {
|
||
|
return GraphLayout::GraphEdge::Down;
|
||
|
} else {
|
||
|
return GraphLayout::GraphEdge::Up;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static std::set<std::pair<ut64, ut64>> SelectLoopEdges(const GraphLayout::Graph &graph, ut64 entry)
|
||
|
{
|
||
|
std::set<std::pair<ut64, ut64>> result;
|
||
|
// Run DFS to select backwards/loop edges
|
||
|
// 0 - not visited
|
||
|
// 1 - in stack
|
||
|
// 2 - visited
|
||
|
std::unordered_map<ut64, uint8_t> visited;
|
||
|
visited.reserve(graph.size());
|
||
|
std::stack<std::pair<ut64, size_t>> stack;
|
||
|
auto dfsFragment = [&visited, &graph, &stack, &result](ut64 first) {
|
||
|
visited[first] = 1;
|
||
|
stack.push({first, 0});
|
||
|
while (!stack.empty()) {
|
||
|
auto v = stack.top().first;
|
||
|
auto edge_index = stack.top().second;
|
||
|
auto blockIt = graph.find(v);
|
||
|
if (blockIt == graph.end()) {
|
||
|
continue;
|
||
|
}
|
||
|
const auto &block = blockIt->second;
|
||
|
if (edge_index < block.edges.size()) {
|
||
|
++stack.top().second;
|
||
|
auto target = block.edges[edge_index].target;
|
||
|
auto &targetState = visited[target];
|
||
|
if (targetState == 0) {
|
||
|
targetState = 1;
|
||
|
stack.push({target, 0});
|
||
|
} else if (targetState == 1) {
|
||
|
result.insert({v, target});
|
||
|
}
|
||
|
} else {
|
||
|
stack.pop();
|
||
|
visited[v] = 2;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
dfsFragment(entry);
|
||
|
for (auto &blockIt : graph) {
|
||
|
if (!visited[blockIt.first]) {
|
||
|
dfsFragment(blockIt.first);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
void GraphvizLayout::CalculateLayout(std::unordered_map<ut64, GraphBlock> &blocks, ut64 entry,
|
||
|
int &width, int &height) const
|
||
|
{
|
||
|
//https://gitlab.com/graphviz/graphviz/issues/1441
|
||
|
#define STR(v) const_cast<char*>(v)
|
||
|
|
||
|
width = height = 10;
|
||
|
GVC_t *gvc = gvContext();
|
||
|
Agraph_t *g = agopen(STR("G"), Agdirected, nullptr);
|
||
|
|
||
|
std::unordered_map<ut64, Agnode_t *> nodes;
|
||
|
for (const auto &block : blocks) {
|
||
|
nodes[block.first] = agnode(g, nullptr, TRUE);
|
||
|
}
|
||
|
|
||
|
std::vector<std::string> strc;
|
||
|
strc.reserve(2 * blocks.size());
|
||
|
std::map<std::pair<ut64, ut64>, Agedge_t *> edges;
|
||
|
|
||
|
agsafeset(g, STR("splines"), lineType == LineType::Ortho ? STR("ortho") : STR("polyline"), STR(""));
|
||
|
switch (direction) {
|
||
|
case Direction::LR:
|
||
|
agsafeset(g, STR("rankdir"), STR("LR"), STR(""));
|
||
|
break;
|
||
|
case Direction::TB:
|
||
|
agsafeset(g, STR("rankdir"), STR("BT"), STR(""));
|
||
|
break;
|
||
|
}
|
||
|
agsafeset(g, STR("newrank"), STR("true"), STR(""));
|
||
|
// graphviz has builtin 72 dpi setting for input that differs from output
|
||
|
// it's easier to use 72 everywhere
|
||
|
const double dpi = 72.0;
|
||
|
agsafeset(g, STR("dpi"), STR("72"), STR(""));
|
||
|
|
||
|
auto widhAttr = agattr(g, AGNODE, STR("width"), STR("1"));
|
||
|
auto heightAatr = agattr(g, AGNODE, STR("height"), STR("1"));
|
||
|
agattr(g, AGNODE, STR("shape"), STR("box"));
|
||
|
agattr(g, AGNODE, STR("fixedsize"), STR("true"));
|
||
|
auto constraintAttr = agattr(g, AGEDGE, STR("constraint"), STR("1"));
|
||
|
|
||
|
std::ostringstream stream;
|
||
|
stream.imbue(std::locale::classic());
|
||
|
auto setFloatingPointAttr = [&stream](void *obj, Agsym_t *sym, double value) {
|
||
|
stream.str({});
|
||
|
stream << std::fixed << std::setw(4) << value;
|
||
|
auto str = stream.str();
|
||
|
agxset(obj, sym, STR(str.c_str()));
|
||
|
};
|
||
|
|
||
|
std::set<std::pair<ut64, ut64>> loopEdges = SelectLoopEdges(blocks, entry);
|
||
|
|
||
|
for (const auto &blockIt : blocks) {
|
||
|
auto u = nodes[blockIt.first];
|
||
|
auto &block = blockIt.second;
|
||
|
|
||
|
for (auto &edge : block.edges) {
|
||
|
auto v = nodes.find(edge.target);
|
||
|
if (v == nodes.end()) {
|
||
|
continue;
|
||
|
}
|
||
|
auto e = agedge(g, u, v->second, nullptr, TRUE);
|
||
|
edges[{blockIt.first, edge.target}] = e;
|
||
|
if (loopEdges.find({blockIt.first, edge.target}) != loopEdges.end()) {
|
||
|
agxset(e, constraintAttr, STR("0"));
|
||
|
}
|
||
|
}
|
||
|
setFloatingPointAttr(u, widhAttr, block.width / dpi);
|
||
|
setFloatingPointAttr(u, heightAatr, block.height / dpi);
|
||
|
}
|
||
|
|
||
|
gvLayout(gvc, g, "dot");
|
||
|
|
||
|
for (auto &blockIt : blocks) {
|
||
|
auto &block = blockIt.second;
|
||
|
auto u = nodes[blockIt.first];
|
||
|
|
||
|
auto pos = ND_coord(u);
|
||
|
|
||
|
auto w = ND_width(u) * dpi;
|
||
|
auto h = ND_height(u) * dpi;
|
||
|
block.x = pos.x - w / 2.0;
|
||
|
block.y = pos.y - h / 2.0;
|
||
|
width = std::max(width, block.x + block.width);
|
||
|
height = std::max(height, block.y + block.height);
|
||
|
|
||
|
for (auto &edge : block.edges) {
|
||
|
auto it = edges.find({blockIt.first, edge.target});
|
||
|
if (it != edges.end()) {
|
||
|
auto e = it->second;
|
||
|
if (auto spl = ED_spl(e)) {
|
||
|
for (int i = 0; i < 1 && i < spl->size; i++) {
|
||
|
auto bz = spl->list[i];
|
||
|
edge.polyline.reserve(bz.size + 1);
|
||
|
for (int j = 0; j < bz.size; j++) {
|
||
|
edge.polyline.push_back(QPointF(bz.list[j].x, bz.list[j].y));
|
||
|
}
|
||
|
QPointF last(0, 0);
|
||
|
if (!edge.polyline.empty()) {
|
||
|
last = edge.polyline.back();
|
||
|
}
|
||
|
if (bz.eflag) {
|
||
|
QPointF tip = QPointF(bz.ep.x, bz.ep.y);
|
||
|
edge.polyline.push_back(tip);
|
||
|
}
|
||
|
|
||
|
if (edge.polyline.size() >= 2) {
|
||
|
// make sure self loops go from bottom to top
|
||
|
if (edge.target == block.entry && edge.polyline.first().y() < edge.polyline.last().y()) {
|
||
|
std::reverse(edge.polyline.begin(), edge.polyline.end());
|
||
|
}
|
||
|
auto it = edge.polyline.rbegin();
|
||
|
QPointF direction = *it;
|
||
|
direction -= *(++it);
|
||
|
edge.arrow = getArrowDirection(direction, lineType == LineType::Polyline);
|
||
|
|
||
|
} else {
|
||
|
edge.arrow = GraphEdge::Down;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
gvFreeLayout(gvc, g);
|
||
|
agclose(g);
|
||
|
gvFreeContext(gvc);
|
||
|
#undef STR
|
||
|
}
|