Graph optimize placment (#2255)

Add optional placement optimization pass which tries to push everything together and ignores the grid.
This commit is contained in:
karliss 2020-07-03 20:09:37 +03:00 committed by GitHub
parent 4685f4faaf
commit 8c52627312
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1280 additions and 38 deletions

View File

@ -0,0 +1,543 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="layout_compacting.svg"
id="svg33"
viewBox="0 0 266.6575 152.92204"
version="1.2"
height="203.89606"
width="355.54333">
<metadata
id="metadata37">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
lock-margins="true"
fit-margin-bottom="10"
fit-margin-right="10"
fit-margin-left="10"
fit-margin-top="10"
inkscape:current-layer="svg33"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:cy="120.22566"
inkscape:cx="319.30409"
inkscape:zoom="2.4621622"
inkscape:snap-midpoints="true"
inkscape:guide-bbox="true"
showguides="true"
inkscape:snap-object-midpoints="true"
showgrid="false"
id="namedview35"
inkscape:window-height="1390"
inkscape:window-width="2560"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<defs
id="defs11">
<marker
orient="auto"
overflow="visible"
id="marker2914">
<path
id="path2"
stroke-width="1pt"
stroke="#005f87"
fill-rule="evenodd"
fill="#005f87"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
orient="auto"
overflow="visible"
id="marker2850">
<path
id="path5"
stroke-width="1pt"
stroke="#5f8700"
fill-rule="evenodd"
fill="#5f8700"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
orient="auto"
overflow="visible"
id="marker2786">
<path
id="path8"
stroke-width="1pt"
stroke="#e03030"
fill-rule="evenodd"
fill="#e03030"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
id="marker2786-3"
overflow="visible"
orient="auto">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
fill="#e03030"
fill-rule="evenodd"
stroke="#e03030"
stroke-width="1pt"
id="path8-6" />
</marker>
<marker
id="marker2850-7"
overflow="visible"
orient="auto">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
fill="#5f8700"
fill-rule="evenodd"
stroke="#5f8700"
stroke-width="1pt"
id="path5-5" />
</marker>
<marker
id="marker2914-1"
overflow="visible"
orient="auto">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
fill="#005f87"
fill-rule="evenodd"
stroke="#005f87"
stroke-width="1pt"
id="path2-2" />
</marker>
<marker
orient="auto"
overflow="visible"
id="marker2914-1-3">
<path
id="path2-2-6"
stroke-width="1pt"
stroke="#005f87"
fill-rule="evenodd"
fill="#005f87"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
orient="auto"
overflow="visible"
id="marker2914-1-6">
<path
id="path2-2-2"
stroke-width="1pt"
stroke="#005f87"
fill-rule="evenodd"
fill="#005f87"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
id="marker2914-8"
overflow="visible"
orient="auto">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
fill="#005f87"
fill-rule="evenodd"
stroke="#005f87"
stroke-width="1pt"
id="path2-7" />
</marker>
<marker
id="marker2786-9"
overflow="visible"
orient="auto">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
fill="#e03030"
fill-rule="evenodd"
stroke="#e03030"
stroke-width="1pt"
id="path8-2" />
</marker>
<marker
id="marker2850-0"
overflow="visible"
orient="auto">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
fill="#5f8700"
fill-rule="evenodd"
stroke="#5f8700"
stroke-width="1pt"
id="path5-2" />
</marker>
<marker
orient="auto"
overflow="visible"
id="marker2786-3-3">
<path
id="path8-6-6"
stroke-width="1pt"
stroke="#e03030"
fill-rule="evenodd"
fill="#e03030"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
orient="auto"
overflow="visible"
id="marker2850-7-1">
<path
id="path5-5-2"
stroke-width="1pt"
stroke="#5f8700"
fill-rule="evenodd"
fill="#5f8700"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
orient="auto"
overflow="visible"
id="marker2914-1-9">
<path
id="path2-2-3"
stroke-width="1pt"
stroke="#005f87"
fill-rule="evenodd"
fill="#005f87"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
transform="matrix(-0.2,0,0,-0.2,-1.2,0)" />
</marker>
<marker
id="marker2914-1-3-1"
overflow="visible"
orient="auto">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
fill="#005f87"
fill-rule="evenodd"
stroke="#005f87"
stroke-width="1pt"
id="path2-2-6-9" />
</marker>
<marker
id="marker2914-1-6-4"
overflow="visible"
orient="auto">
<path
transform="matrix(-0.2,0,0,-0.2,-1.2,0)"
d="M 0,0 5,-5 -12.5,0 5,5 Z"
fill="#005f87"
fill-rule="evenodd"
stroke="#005f87"
stroke-width="1pt"
id="path2-2-2-7" />
</marker>
</defs>
<rect
x="19.86875"
y="20.397875"
width="101.07"
height="13.331"
stroke-width="0.1395"
id="rect13"
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-linecap:square;stroke-linejoin:bevel" />
<rect
id="rect15"
height="9.3690996"
width="24.597"
y="38.912872"
x="58.105747"
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel" />
<rect
id="rect17"
height="9.3690996"
width="24.597"
y="52.641872"
x="7.5697498"
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel" />
<rect
id="rect19"
height="9.3690996"
width="24.597"
y="52.641872"
x="108.64275"
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel" />
<path
d="m 70.404751,33.721873 v 5.2398"
marker-end="url(#marker2914)"
stroke="#005f87"
stroke-width="0.75px"
id="path25"
style="fill:none" />
<path
d="m 68.750751,48.243873 v 1.8828 h -48.882 v 2.5231"
marker-end="url(#marker2786)"
stop-color="#000000"
stroke="#e03030"
stroke-miterlimit="2"
stroke-width="0.75"
style="font-variation-settings:normal;fill:none"
id="path27" />
<path
d="m 71.872751,48.256873 v 1.8696 h 49.067999 v 3.2324"
marker-end="url(#marker2850)"
stroke="#5f8700"
stroke-width="0.75px"
id="path29"
style="fill:none" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="206.91023"
y="7.5697498"
width="24.596998"
height="9.3690987"
id="rect15-3" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="181.84999"
y="20.957253"
width="24.596998"
height="28.024694"
id="rect17-5" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="234.49074"
y="21.999752"
width="24.596998"
height="9.3690987"
id="rect19-6" />
<path
sodipodi:nodetypes="cccc"
id="path27-2"
style="font-variation-settings:normal;fill:none;marker-end:url(#marker2786-3)"
stroke-width="0.75"
stroke-miterlimit="2"
stroke="#e03030"
stop-color="#000000"
marker-end="url(#marker2786-3)"
d="m 217.55525,16.90075 v 1.8828 h -24.43006 v 2.5231" />
<path
sodipodi:nodetypes="cccc"
style="fill:none;marker-end:url(#marker2850-7)"
id="path29-9"
stroke-width="0.75px"
stroke="#5f8700"
marker-end="url(#marker2850-7)"
d="m 220.67725,16.91375 v 1.8696 h 26.11198 v 3.2324" />
<rect
id="rect19-6-0"
height="9.3690987"
width="24.596998"
y="54.221745"
x="181.84999"
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel" />
<path
style="fill:none;marker-end:url(#marker2914-1)"
id="path25-7"
stroke-width="0.75px"
stroke="#005f87"
marker-end="url(#marker2914-1)"
d="m 194.14849,48.981943 v 5.2398" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="234.49074"
y="54.221745"
width="24.596998"
height="9.3690987"
id="rect19-6-0-9" />
<path
sodipodi:nodetypes="cc"
d="M 246.78923,31.368849 V 54.148584"
marker-end="url(#marker2914-1-3)"
stroke="#005f87"
stroke-width="0.75px"
id="path25-7-0"
style="fill:none;marker-end:url(#marker2914-1-3)" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="234.49074"
y="68.830635"
width="24.596998"
height="9.3690987"
id="rect19-6-0-6" />
<path
d="m 246.78923,63.590837 v 5.2398"
marker-end="url(#marker2914-1-6)"
stroke="#005f87"
stroke-width="0.75px"
id="path25-7-1"
style="fill:none;marker-end:url(#marker2914-1-6)" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-linecap:square;stroke-linejoin:bevel"
id="rect13-3"
stroke-width="0.1395"
height="13.330999"
width="101.06999"
y="86.583054"
x="21.267822" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="59.50481"
y="105.09805"
width="24.596998"
height="9.3690987"
id="rect15-7" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="45.492069"
y="120.35294"
width="24.596998"
height="9.3690987"
id="rect17-59" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="73.206848"
y="120.35294"
width="24.596998"
height="9.3690987"
id="rect19-2" />
<path
style="fill:none;marker-end:url(#marker2914-8)"
id="path25-2"
stroke-width="0.75px"
stroke="#005f87"
marker-end="url(#marker2914-8)"
d="m 71.803816,99.907053 v 5.239787" />
<path
sodipodi:nodetypes="cccc"
id="path27-8"
style="font-variation-settings:normal;fill:none;marker-end:url(#marker2786-9)"
stroke-width="0.75"
stroke-miterlimit="2"
stroke="#e03030"
stop-color="#000000"
marker-end="url(#marker2786-9)"
d="m 70.149816,114.42904 v 1.8828 H 57.91528 v 3.82797" />
<path
sodipodi:nodetypes="cccc"
style="fill:none;marker-end:url(#marker2850-0)"
id="path29-97"
stroke-width="0.75px"
stroke="#5f8700"
marker-end="url(#marker2850-0)"
d="m 73.271816,114.44204 v 1.8696 h 12.721232 v 3.23241" />
<g
transform="translate(-35.90579,-13.250133)"
id="g1598">
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="241.84329"
y="102.58134"
width="24.596998"
height="9.3690987"
id="rect15-3-8" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="216.78305"
y="115.96884"
width="24.596998"
height="28.024693"
id="rect17-5-4" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="269.4238"
y="117.01134"
width="24.596998"
height="9.3690987"
id="rect19-6-5" />
<path
sodipodi:nodetypes="cccc"
id="path27-2-0"
style="font-variation-settings:normal;fill:none;marker-end:url(#marker2786-3-3)"
stroke-width="0.75"
stroke-miterlimit="2"
stroke="#e03030"
stop-color="#000000"
marker-end="url(#marker2786-3-3)"
d="m 252.48832,111.91234 v 1.8828 h -24.43006 v 2.5231" />
<path
sodipodi:nodetypes="cccc"
style="fill:none;marker-end:url(#marker2850-7-1)"
id="path29-9-3"
stroke-width="0.75px"
stroke="#5f8700"
marker-end="url(#marker2850-7-1)"
d="m 255.61032,111.92534 v 1.8696 h 26.11198 v 3.2324" />
<rect
id="rect19-6-0-61"
height="9.3690987"
width="24.596998"
y="149.23332"
x="216.78305"
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel" />
<path
style="fill:none;marker-end:url(#marker2914-1-9)"
id="path25-7-06"
stroke-width="0.75px"
stroke="#005f87"
marker-end="url(#marker2914-1-9)"
d="m 229.08156,143.99353 v 5.2398" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="269.4238"
y="130.94421"
width="24.596998"
height="9.3690987"
id="rect19-6-0-9-3" />
<path
sodipodi:nodetypes="cc"
d="m 281.7223,126.38044 v 4.19969"
marker-end="url(#marker2914-1-3-1)"
stroke="#005f87"
stroke-width="0.75px"
id="path25-7-0-2"
style="fill:none;marker-end:url(#marker2914-1-3-1)" />
<rect
style="fill:#f5faff;fill-rule:evenodd;stroke:#91c8fa;stroke-width:0.1395;stroke-linecap:square;stroke-linejoin:bevel"
x="269.4238"
y="145.55312"
width="24.596998"
height="9.3690987"
id="rect19-6-0-6-0" />
<path
d="m 281.7223,140.31331 v 5.2398"
marker-end="url(#marker2914-1-6-4)"
stroke="#005f87"
stroke-width="0.75px"
id="path25-7-1-6"
style="fill:none;marker-end:url(#marker2914-1-6-4)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -752,7 +752,7 @@ void Configuration::setGraphSpacing(QPoint blockSpacing, QPoint edgeSpacing)
QPoint Configuration::getGraphBlockSpacing() QPoint Configuration::getGraphBlockSpacing()
{ {
return s.value("graph.blockSpacing", QPoint(10, 40)).value<QPoint>(); return s.value("graph.blockSpacing", QPoint(20, 40)).value<QPoint>();
} }
QPoint Configuration::getGraphEdgeSpacing() QPoint Configuration::getGraphEdgeSpacing()

View File

@ -23,30 +23,6 @@ class LinkedListPool
T value; T value;
}; };
public: 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. * @brief List iterator.
* *
@ -73,6 +49,11 @@ public:
{ {
return pool->data[index].value; return pool->data[index].value;
} }
pointer operator->()
{
return &pool->data[index].value;
}
ListIterator &operator++() ListIterator &operator++()
{ {
index = pool->data[index].next; index = pool->data[index].next;
@ -91,12 +72,41 @@ public:
/** /**
* @brief Test if iterator points to valid value. * @brief Test if iterator points to valid value.
*/ */
operator bool() const explicit operator bool() const
{ {
return index; return index;
} }
}; };
/**
* @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;
bool isEmpty() const
{
return head == 0;
}
};
/** /**
* @brief Create a linked list pool with capacity for \a initialCapacity list items. * @brief Create a linked list pool with capacity for \a initialCapacity list items.
* @param initialCapacity number of elements to preallocate. * @param initialCapacity number of elements to preallocate.
@ -136,6 +146,29 @@ public:
return List {head.index, list.tail}; return List {head.index, list.tail};
} }
/**
* @brief Split list and return first half.
*
* @param list The list that needs to be split.
* @param end Iterator to the first item that should not be included in returned list. Needs to be within \a list .
* @return Returns prefix of \a list.
*/
List splitHead(const List &list, const ListIterator &end)
{
if (!end) {
return list;
}
if (end.index == list.head) {
return {};
}
auto last = list.head;
while (data[last].next != end.index) {
last = data[last].next;
}
data[last].next = 0;
return List {list.head, last};
}
/** /**
* @brief Create list iterator from list. * @brief Create list iterator from list.
* @param list * @param list
@ -153,6 +186,12 @@ public:
List append(const List &head, const List &tail) List append(const List &head, const List &tail)
{ {
if (head.isEmpty()) {
return tail;
}
if (tail.isEmpty()) {
return head;
}
List result{head.head, tail.tail}; List result{head.head, tail.tail};
data[head.tail].next = tail.head; data[head.tail].next = tail.head;
return result; return result;

View File

@ -41,6 +41,12 @@ void GraphOptionsWidget::updateOptionsFromVars()
ui->maxColsSpinBox->blockSignals(true); ui->maxColsSpinBox->blockSignals(true);
ui->maxColsSpinBox->setValue(Config()->getGraphBlockMaxChars()); ui->maxColsSpinBox->setValue(Config()->getGraphBlockMaxChars());
ui->maxColsSpinBox->blockSignals(false); ui->maxColsSpinBox->blockSignals(false);
auto blockSpacing = Config()->getGraphBlockSpacing();
ui->horizontalBlockSpacing->setValue(blockSpacing.x());
ui->verticalBlockSpacing->setValue(blockSpacing.y());
auto edgeSpacing = Config()->getGraphEdgeSpacing();
ui->horizontalEdgeSpacing->setValue(edgeSpacing.x());
ui->verticalEdgeSpacing->setValue(edgeSpacing.y());
} }

View File

@ -38,6 +38,12 @@ const int DisassemblerGraphView::KEY_ZOOM_IN = Qt::Key_Plus + Qt::ControlModifie
const int DisassemblerGraphView::KEY_ZOOM_OUT = Qt::Key_Minus + Qt::ControlModifier; const int DisassemblerGraphView::KEY_ZOOM_OUT = Qt::Key_Minus + Qt::ControlModifier;
const int DisassemblerGraphView::KEY_ZOOM_RESET = Qt::Key_Equal + Qt::ControlModifier; const int DisassemblerGraphView::KEY_ZOOM_RESET = Qt::Key_Equal + Qt::ControlModifier;
#ifndef NDEBUG
#define GRAPH_GRID_DEBUG_MODES true
#else
#define GRAPH_GRID_DEBUG_MODES false
#endif
DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *seekable, DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *seekable,
MainWindow *mainWindow, QList<QAction *> additionalMenuActions) MainWindow *mainWindow, QList<QAction *> additionalMenuActions)
: GraphView(parent), : GraphView(parent),
@ -103,6 +109,18 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se
{tr("Grid narrow"), GraphView::Layout::GridNarrow} {tr("Grid narrow"), GraphView::Layout::GridNarrow}
, {tr("Grid medium"), GraphView::Layout::GridMedium} , {tr("Grid medium"), GraphView::Layout::GridMedium}
, {tr("Grid wide"), GraphView::Layout::GridWide} , {tr("Grid wide"), GraphView::Layout::GridWide}
#if GRAPH_GRID_DEBUG_MODES
, {"GridAAA", GraphView::Layout::GridAAA}
, {"GridAAB", GraphView::Layout::GridAAB}
, {"GridABA", GraphView::Layout::GridABA}
, {"GridABB", GraphView::Layout::GridABB}
, {"GridBAA", GraphView::Layout::GridBAA}
, {"GridBAB", GraphView::Layout::GridBAB}
, {"GridBBA", GraphView::Layout::GridBBA}
, {"GridBBB", GraphView::Layout::GridBBB}
#endif
#ifdef CUTTER_ENABLE_GRAPHVIZ #ifdef CUTTER_ENABLE_GRAPHVIZ
, {tr("Graphviz polyline"), GraphView::Layout::GraphvizPolyline} , {tr("Graphviz polyline"), GraphView::Layout::GraphvizPolyline}
, {tr("Graphviz ortho"), GraphView::Layout::GraphvizOrtho} , {tr("Graphviz ortho"), GraphView::Layout::GraphvizOrtho}

View File

@ -5,6 +5,7 @@
#include <queue> #include <queue>
#include <stack> #include <stack>
#include <cassert> #include <cassert>
#include <queue>
#include "common/BinaryTrees.h" #include "common/BinaryTrees.h"
@ -34,6 +35,7 @@ one from all valid toposort orders. Example: for graph 1->4, 2->1, 2->3, 3->4 va
4. assign node positions within grid using tree structure, child subtrees are placed side by side with parent on top 4. assign node positions within grid using tree structure, child subtrees are placed side by side with parent on top
5. perform edge routing 5. perform edge routing
6. calculate column and row pixel positions based on node sizes and amount edges between the rows 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 Contrary to many other layered graph drawing algorithm this implementation doesn't perform node reordering to minimize
@ -121,25 +123,56 @@ Assignment order is chosen based on:
* edge length - establishes some kind of order when single node is connected to many edges, typically a block * 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. 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) GraphGridLayout::GraphGridLayout(GraphGridLayout::LayoutType layoutType)
: GraphLayout({}) : GraphLayout({})
, layoutType(layoutType)
{ {
switch (layoutType) { switch (layoutType) {
case LayoutType::Narrow: case LayoutType::Narrow:
tightSubtreePlacement = true; tightSubtreePlacement = true;
parentBetweenDirectChild = false; parentBetweenDirectChild = false;
useLayoutOptimization = true;
break; break;
case LayoutType::Medium: case LayoutType::Medium:
tightSubtreePlacement = false; tightSubtreePlacement = false;
parentBetweenDirectChild = false; parentBetweenDirectChild = true;
useLayoutOptimization = true;
break; break;
case LayoutType::Wide: case LayoutType::Wide:
tightSubtreePlacement = false; tightSubtreePlacement = false;
parentBetweenDirectChild = true; parentBetweenDirectChild = true;
useLayoutOptimization = false;
break; break;
} }
} }
@ -225,8 +258,7 @@ void GraphGridLayout::selectTree(GraphGridLayout::LayoutState &state)
} }
} }
void GraphGridLayout::CalculateLayout(std::unordered_map<ut64, GraphBlock> &blocks, ut64 entry, void GraphGridLayout::CalculateLayout(GraphLayout::Graph &blocks, ut64 entry, int &width, int &height) const
int &width, int &height) const
{ {
LayoutState layoutState; LayoutState layoutState;
layoutState.blocks = &blocks; layoutState.blocks = &blocks;
@ -244,6 +276,7 @@ void GraphGridLayout::CalculateLayout(std::unordered_map<ut64, GraphBlock> &bloc
layoutState.edge[blockIt.first].resize(blockIt.second.edges.size()); layoutState.edge[blockIt.first].resize(blockIt.second.edges.size());
for (size_t i = 0; i < blockIt.second.edges.size(); i++) { for (size_t i = 0; i < blockIt.second.edges.size(); i++) {
layoutState.edge[blockIt.first][i].dest = blockIt.second.edges[i].target; layoutState.edge[blockIt.first][i].dest = blockIt.second.edges[i].target;
blockIt.second.edges[i].arrow = GraphEdge::Down;
} }
} }
for (const auto &edgeList : layoutState.edge) { for (const auto &edgeList : layoutState.edge) {
@ -279,6 +312,10 @@ void GraphGridLayout::CalculateLayout(std::unordered_map<ut64, GraphBlock> &bloc
routeEdges(layoutState); routeEdges(layoutState);
convertToPixelCoordinates(layoutState, width, height); convertToPixelCoordinates(layoutState, width, height);
if (useLayoutOptimization) {
optimizeLayout(layoutState);
cropToContent(blocks, width, height);
}
} }
void GraphGridLayout::findMergePoints(GraphGridLayout::LayoutState &state) const void GraphGridLayout::findMergePoints(GraphGridLayout::LayoutState &state) const
@ -318,6 +355,7 @@ void GraphGridLayout::findMergePoints(GraphGridLayout::LayoutState &state) const
} }
} }
if (blocksGoingToMerge) { if (blocksGoingToMerge) {
block.mergeBlock = mergeBlock->id;
state.grid_blocks[block.tree_edge[blockWithTreeEdge]].col = blockWithTreeEdge * 2 - state.grid_blocks[block.tree_edge[blockWithTreeEdge]].col = blockWithTreeEdge * 2 -
(blocksGoingToMerge - 1); (blocksGoingToMerge - 1);
} }
@ -847,7 +885,7 @@ static void centerEdges(
} }
/** /**
* @brief Convert segment coordinates from arbitary range to continuous range starting at 0. * @brief Convert segment coordinates from arbitrary range to continuous range starting at 0.
* @param segments * @param segments
* @param leftSides * @param leftSides
* @param rightSides * @param rightSides
@ -939,7 +977,7 @@ void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state
edgeOffsets.resize(edgeIndex); edgeOffsets.resize(edgeIndex);
calculateSegmentOffsets(segments, edgeOffsets, state.edgeColumnWidth, rightSides, leftSides, calculateSegmentOffsets(segments, edgeOffsets, state.edgeColumnWidth, rightSides, leftSides,
state.columnWidth, 2 * state.rows + 1, layoutConfig.edgeHorizontalSpacing); state.columnWidth, 2 * state.rows + 1, layoutConfig.edgeHorizontalSpacing);
centerEdges(edgeOffsets, state.edgeColumnWidth, segments, layoutConfig.blockHorizontalSpacing); centerEdges(edgeOffsets, state.edgeColumnWidth, segments, layoutConfig.edgeHorizontalSpacing);
edgeIndex = 0; edgeIndex = 0;
auto copySegmentsToEdges = [&](bool col) { auto copySegmentsToEdges = [&](bool col) {
@ -970,7 +1008,7 @@ void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state
// Horizontal segments // Horizontal segments
// Use exact x coordinates obtained from vertical segment placment. // Use exact x coordinates obtained from vertical segment placement.
segments.clear(); segments.clear();
leftSides.clear(); leftSides.clear();
rightSides.clear(); rightSides.clear();
@ -1074,7 +1112,6 @@ void GraphGridLayout::convertToPixelCoordinates(
auto &block = it.second; auto &block = it.second;
for (size_t i = 0; i < block.edges.size(); i++) { for (size_t i = 0; i < block.edges.size(); i++) {
auto &resultEdge = block.edges[i]; auto &resultEdge = block.edges[i];
const auto &target = (*state.blocks)[resultEdge.target];
resultEdge.polyline.clear(); resultEdge.polyline.clear();
resultEdge.polyline.push_back(QPointF(0, block.y + block.height)); resultEdge.polyline.push_back(QPointF(0, block.y + block.height));
@ -1092,8 +1129,559 @@ void GraphGridLayout::convertToPixelCoordinates(
resultEdge.polyline.push_back(QPointF(0, y)); resultEdge.polyline.push_back(QPointF(0, y));
} }
} }
resultEdge.polyline.back().setY(target.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<std::pair<int, int>, 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 viable solution
* @param stickWhenNotMoving variable grouping strategy
*/
static void optimizeLinearProgramPass(
size_t n,
std::vector<int> objectiveFunction,
std::vector<Constraint> inequalities,
std::vector<Constraint> equalities,
std::vector<int> &solution,
bool stickWhenNotMoving)
{
std::vector<int> 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<size_t> edgeCount(n);
LinkedListPool<size_t> edgePool(inequalities.size() * 2);
std::vector<decltype(edgePool)::List> 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<uint8_t> processed(n);
// Smallest variable value in the group relative to main one, this is used to maintain implicit x_i >= 0
// constraint
std::vector<int> 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 viable 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::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> 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 != 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);
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 viable solution
*/
static void optimizeLinearProgram(
size_t n,
const std::vector<int> &objectiveFunction,
std::vector<Constraint> inequalities,
const std::vector<Constraint> &equalities,
std::vector<int> &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<int> &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<Segment> segments,
const std::vector<int>& positions,
const std::vector<size_t>& variableGroup,
int blockCount,
int blockSpacing,
int segmentSpacing,
std::vector<Constraint> &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<int, std::pair<int, int>> 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<uint64_t, int> blockMapping;
size_t blockIndex = 0;
for (auto &blockIt : *state.blocks) {
blockMapping[blockIt.first] = blockIndex++;
}
std::vector<size_t> variableGroups(blockMapping.size());
std::iota(variableGroups.begin(), variableGroups.end(), 0);
std::vector<int> objectiveFunction;
std::vector<Constraint> inequalities;
std::vector<Constraint> equalities;
std::vector<int> 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 setViableSolution = [&](size_t variable, int value) {
solution.resize(std::max(solution.size(), variable + 1));
solution[variable] = value;
};
auto copyVariablesToPositions = [&](const std::vector<int> &solution, bool horizontal = false) {
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<Segment> 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);
setViableSolution(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});
setViableSolution(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);
for (auto v : solution) {
assert(v >= 0);
}
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);
setViableSolution(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});
setViableSolution(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);
}

View File

@ -19,18 +19,22 @@ public:
}; };
GraphGridLayout(LayoutType layoutType = LayoutType::Medium); GraphGridLayout(LayoutType layoutType = LayoutType::Medium);
virtual void CalculateLayout(std::unordered_map<ut64, GraphBlock> &blocks, virtual void CalculateLayout(Graph &blocks,
ut64 entry, ut64 entry,
int &width, int &width,
int &height) const override; int &height) const override;
void setTightSubtreePlacement(bool enabled) { tightSubtreePlacement = enabled; }
void setParentBetweenDirectChild(bool enabled) { parentBetweenDirectChild = enabled; }
void setverticalBlockAlignmentMiddle(bool enabled) { verticalBlockAlignmentMiddle = enabled; }
void setLayoutOptimization(bool enabled) { useLayoutOptimization = enabled; }
private: private:
LayoutType layoutType;
/// false - use bounding box for smallest subtree when placing them side by side /// false - use bounding box for smallest subtree when placing them side by side
bool tightSubtreePlacement = false; bool tightSubtreePlacement = false;
/// true if code should try to place parent between direct children as much as possible /// true if code should try to place parent between direct children as much as possible
bool parentBetweenDirectChild = false; bool parentBetweenDirectChild = false;
/// false if blocks in rows should be aligned at top, true for middle alignment /// false if blocks in rows should be aligned at top, true for middle alignment
bool verticalBlockAlignmentMiddle = false; bool verticalBlockAlignmentMiddle = false;
bool useLayoutOptimization = true;
struct GridBlock { struct GridBlock {
ut64 id; ut64 id;
@ -47,6 +51,8 @@ private:
/// Row in which the block is /// Row in which the block is
int row = 0; int row = 0;
ut64 mergeBlock = 0;
int lastRowLeft; //!< left side of subtree last row int lastRowLeft; //!< left side of subtree last row
int lastRowRight; //!< right side of subtree last row int lastRowRight; //!< right side of subtree last row
int leftPosition; //!< left side of subtree int leftPosition; //!< left side of subtree
@ -165,6 +171,23 @@ private:
* @param height image height output argument * @param height image height output argument
*/ */
void convertToPixelCoordinates(LayoutState &state, int &width, int &height) const; void convertToPixelCoordinates(LayoutState &state, int &width, int &height) const;
/**
* @brief Move the graph content to top left corner and update dimensions.
* @param graph
* @param width width after cropping
* @param height height after cropping
*/
void cropToContent(Graph &graph, int &width, int &height) const;
/**
* @brief Connect edge ends to blocks by changing y.
* @param graph
*/
void connectEdgeEnds(Graph &graph) const;
/**
* @brief Reduce spacing between nodes and edges by pushing everything together ignoring the grid.
* @param state
*/
void optimizeLayout(LayoutState &state) const;
}; };
#endif // GRAPHGRIDLAYOUT_H #endif // GRAPHGRIDLAYOUT_H

View File

@ -33,7 +33,7 @@ public:
struct LayoutConfig { struct LayoutConfig {
int blockVerticalSpacing = 40; int blockVerticalSpacing = 40;
int blockHorizontalSpacing = 10; int blockHorizontalSpacing = 20;
int edgeVerticalSpacing = 10; int edgeVerticalSpacing = 10;
int edgeHorizontalSpacing = 10; int edgeHorizontalSpacing = 10;
}; };

View File

@ -539,6 +539,23 @@ std::unique_ptr<GraphLayout> GraphView::makeGraphLayout(GraphView::Layout layout
case Layout::GridWide: case Layout::GridWide:
result.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Wide)); result.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Wide));
break; break;
case Layout::GridAAA:
case Layout::GridAAB:
case Layout::GridABA:
case Layout::GridABB:
case Layout::GridBAA:
case Layout::GridBAB:
case Layout::GridBBA:
case Layout::GridBBB:
{
int options = static_cast<int>(layout) - static_cast<int>(Layout::GridAAA);
std::unique_ptr<GraphGridLayout> gridLayout(new GraphGridLayout());
gridLayout->setTightSubtreePlacement((options & 1) == 0);
gridLayout->setParentBetweenDirectChild((options & 2));
gridLayout->setLayoutOptimization((options & 4));
result = std::move(gridLayout);
break;
}
#ifdef CUTTER_ENABLE_GRAPHVIZ #ifdef CUTTER_ENABLE_GRAPHVIZ
case Layout::GraphvizOrtho: case Layout::GraphvizOrtho:
result.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho, result.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho,

View File

@ -42,6 +42,14 @@ public:
GridNarrow GridNarrow
, GridMedium , GridMedium
, GridWide , GridWide
, GridAAA
, GridAAB
, GridABA
, GridABB
, GridBAA
, GridBAB
, GridBBA
, GridBBB
#ifdef CUTTER_ENABLE_GRAPHVIZ #ifdef CUTTER_ENABLE_GRAPHVIZ
, GraphvizOrtho , GraphvizOrtho
, GraphvizPolyline , GraphvizPolyline