mirror of
https://github.com/rizinorg/cutter.git
synced 2024-11-26 22:31:08 +00:00
Graph optimize placment (#2255)
Add optional placement optimization pass which tries to push everything together and ignores the grid.
This commit is contained in:
parent
4685f4faaf
commit
8c52627312
543
docs/doxygen-images/graph_grid_layout/layout_compacting.svg
Normal file
543
docs/doxygen-images/graph_grid_layout/layout_compacting.svg
Normal 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 |
@ -752,7 +752,7 @@ void Configuration::setGraphSpacing(QPoint blockSpacing, QPoint edgeSpacing)
|
||||
|
||||
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()
|
||||
|
@ -23,30 +23,6 @@ class LinkedListPool
|
||||
T value;
|
||||
};
|
||||
public:
|
||||
/**
|
||||
* @brief Single list within LinkedListPool.
|
||||
*
|
||||
* List only refers to chain of elements. Copying it doesn't copy any element. Item data is owned by
|
||||
* LinkedListPool.
|
||||
*
|
||||
* Use LinkedListPool::makeList to create non-empty list.
|
||||
*/
|
||||
class List
|
||||
{
|
||||
IndexType head = 0;
|
||||
IndexType tail = 0;
|
||||
friend class LinkedListPool;
|
||||
List(IndexType head, IndexType tail)
|
||||
: head(head)
|
||||
, tail(tail)
|
||||
{}
|
||||
public:
|
||||
/**
|
||||
* @brief Create an empty list
|
||||
*/
|
||||
List() = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief List iterator.
|
||||
*
|
||||
@ -73,6 +49,11 @@ public:
|
||||
{
|
||||
return pool->data[index].value;
|
||||
}
|
||||
pointer operator->()
|
||||
{
|
||||
return &pool->data[index].value;
|
||||
}
|
||||
|
||||
ListIterator &operator++()
|
||||
{
|
||||
index = pool->data[index].next;
|
||||
@ -91,12 +72,41 @@ public:
|
||||
/**
|
||||
* @brief Test if iterator points to valid value.
|
||||
*/
|
||||
operator bool() const
|
||||
explicit operator bool() const
|
||||
{
|
||||
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.
|
||||
* @param initialCapacity number of elements to preallocate.
|
||||
@ -136,6 +146,29 @@ public:
|
||||
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.
|
||||
* @param list
|
||||
@ -153,6 +186,12 @@ public:
|
||||
|
||||
List append(const List &head, const List &tail)
|
||||
{
|
||||
if (head.isEmpty()) {
|
||||
return tail;
|
||||
}
|
||||
if (tail.isEmpty()) {
|
||||
return head;
|
||||
}
|
||||
List result{head.head, tail.tail};
|
||||
data[head.tail].next = tail.head;
|
||||
return result;
|
||||
|
@ -41,6 +41,12 @@ void GraphOptionsWidget::updateOptionsFromVars()
|
||||
ui->maxColsSpinBox->blockSignals(true);
|
||||
ui->maxColsSpinBox->setValue(Config()->getGraphBlockMaxChars());
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
|
@ -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_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,
|
||||
MainWindow *mainWindow, QList<QAction *> additionalMenuActions)
|
||||
: GraphView(parent),
|
||||
@ -103,6 +109,18 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget *parent, CutterSeekable *se
|
||||
{tr("Grid narrow"), GraphView::Layout::GridNarrow}
|
||||
, {tr("Grid medium"), GraphView::Layout::GridMedium}
|
||||
, {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
|
||||
, {tr("Graphviz polyline"), GraphView::Layout::GraphvizPolyline}
|
||||
, {tr("Graphviz ortho"), GraphView::Layout::GraphvizOrtho}
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include <queue>
|
||||
#include <stack>
|
||||
#include <cassert>
|
||||
#include <queue>
|
||||
|
||||
#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
|
||||
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
|
||||
@ -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
|
||||
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({})
|
||||
, layoutType(layoutType)
|
||||
{
|
||||
switch (layoutType) {
|
||||
case LayoutType::Narrow:
|
||||
tightSubtreePlacement = true;
|
||||
parentBetweenDirectChild = false;
|
||||
useLayoutOptimization = true;
|
||||
break;
|
||||
case LayoutType::Medium:
|
||||
tightSubtreePlacement = false;
|
||||
parentBetweenDirectChild = false;
|
||||
parentBetweenDirectChild = true;
|
||||
useLayoutOptimization = true;
|
||||
break;
|
||||
case LayoutType::Wide:
|
||||
tightSubtreePlacement = false;
|
||||
parentBetweenDirectChild = true;
|
||||
useLayoutOptimization = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -225,8 +258,7 @@ void GraphGridLayout::selectTree(GraphGridLayout::LayoutState &state)
|
||||
}
|
||||
}
|
||||
|
||||
void GraphGridLayout::CalculateLayout(std::unordered_map<ut64, GraphBlock> &blocks, ut64 entry,
|
||||
int &width, int &height) const
|
||||
void GraphGridLayout::CalculateLayout(GraphLayout::Graph &blocks, ut64 entry, int &width, int &height) const
|
||||
{
|
||||
LayoutState layoutState;
|
||||
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());
|
||||
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) {
|
||||
@ -279,6 +312,10 @@ void GraphGridLayout::CalculateLayout(std::unordered_map<ut64, GraphBlock> &bloc
|
||||
routeEdges(layoutState);
|
||||
|
||||
convertToPixelCoordinates(layoutState, width, height);
|
||||
if (useLayoutOptimization) {
|
||||
optimizeLayout(layoutState);
|
||||
cropToContent(blocks, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
void GraphGridLayout::findMergePoints(GraphGridLayout::LayoutState &state) const
|
||||
@ -318,6 +355,7 @@ void GraphGridLayout::findMergePoints(GraphGridLayout::LayoutState &state) const
|
||||
}
|
||||
}
|
||||
if (blocksGoingToMerge) {
|
||||
block.mergeBlock = mergeBlock->id;
|
||||
state.grid_blocks[block.tree_edge[blockWithTreeEdge]].col = blockWithTreeEdge * 2 -
|
||||
(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 leftSides
|
||||
* @param rightSides
|
||||
@ -939,7 +977,7 @@ void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state
|
||||
edgeOffsets.resize(edgeIndex);
|
||||
calculateSegmentOffsets(segments, edgeOffsets, state.edgeColumnWidth, rightSides, leftSides,
|
||||
state.columnWidth, 2 * state.rows + 1, layoutConfig.edgeHorizontalSpacing);
|
||||
centerEdges(edgeOffsets, state.edgeColumnWidth, segments, layoutConfig.blockHorizontalSpacing);
|
||||
centerEdges(edgeOffsets, state.edgeColumnWidth, segments, layoutConfig.edgeHorizontalSpacing);
|
||||
edgeIndex = 0;
|
||||
|
||||
auto copySegmentsToEdges = [&](bool col) {
|
||||
@ -970,7 +1008,7 @@ void GraphGridLayout::elaborateEdgePlacement(GraphGridLayout::LayoutState &state
|
||||
|
||||
|
||||
// Horizontal segments
|
||||
// Use exact x coordinates obtained from vertical segment placment.
|
||||
// Use exact x coordinates obtained from vertical segment placement.
|
||||
segments.clear();
|
||||
leftSides.clear();
|
||||
rightSides.clear();
|
||||
@ -1074,7 +1112,6 @@ void GraphGridLayout::convertToPixelCoordinates(
|
||||
auto &block = it.second;
|
||||
for (size_t i = 0; i < block.edges.size(); i++) {
|
||||
auto &resultEdge = block.edges[i];
|
||||
const auto &target = (*state.blocks)[resultEdge.target];
|
||||
resultEdge.polyline.clear();
|
||||
resultEdge.polyline.push_back(QPointF(0, block.y + block.height));
|
||||
|
||||
@ -1092,8 +1129,559 @@ void GraphGridLayout::convertToPixelCoordinates(
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -19,18 +19,22 @@ public:
|
||||
};
|
||||
|
||||
GraphGridLayout(LayoutType layoutType = LayoutType::Medium);
|
||||
virtual void CalculateLayout(std::unordered_map<ut64, GraphBlock> &blocks,
|
||||
virtual void CalculateLayout(Graph &blocks,
|
||||
ut64 entry,
|
||||
int &width,
|
||||
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:
|
||||
LayoutType layoutType;
|
||||
/// false - use bounding box for smallest subtree when placing them side by side
|
||||
bool tightSubtreePlacement = false;
|
||||
/// true if code should try to place parent between direct children as much as possible
|
||||
bool parentBetweenDirectChild = false;
|
||||
/// false if blocks in rows should be aligned at top, true for middle alignment
|
||||
bool verticalBlockAlignmentMiddle = false;
|
||||
bool useLayoutOptimization = true;
|
||||
|
||||
struct GridBlock {
|
||||
ut64 id;
|
||||
@ -47,6 +51,8 @@ private:
|
||||
/// Row in which the block is
|
||||
int row = 0;
|
||||
|
||||
ut64 mergeBlock = 0;
|
||||
|
||||
int lastRowLeft; //!< left side of subtree last row
|
||||
int lastRowRight; //!< right side of subtree last row
|
||||
int leftPosition; //!< left side of subtree
|
||||
@ -165,6 +171,23 @@ private:
|
||||
* @param height image height output argument
|
||||
*/
|
||||
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
|
||||
|
@ -33,7 +33,7 @@ public:
|
||||
|
||||
struct LayoutConfig {
|
||||
int blockVerticalSpacing = 40;
|
||||
int blockHorizontalSpacing = 10;
|
||||
int blockHorizontalSpacing = 20;
|
||||
int edgeVerticalSpacing = 10;
|
||||
int edgeHorizontalSpacing = 10;
|
||||
};
|
||||
|
@ -539,6 +539,23 @@ std::unique_ptr<GraphLayout> GraphView::makeGraphLayout(GraphView::Layout layout
|
||||
case Layout::GridWide:
|
||||
result.reset(new GraphGridLayout(GraphGridLayout::LayoutType::Wide));
|
||||
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
|
||||
case Layout::GraphvizOrtho:
|
||||
result.reset(new GraphvizLayout(GraphvizLayout::LineType::Ortho,
|
||||
|
@ -42,6 +42,14 @@ public:
|
||||
GridNarrow
|
||||
, GridMedium
|
||||
, GridWide
|
||||
, GridAAA
|
||||
, GridAAB
|
||||
, GridABA
|
||||
, GridABB
|
||||
, GridBAA
|
||||
, GridBAB
|
||||
, GridBBA
|
||||
, GridBBB
|
||||
#ifdef CUTTER_ENABLE_GRAPHVIZ
|
||||
, GraphvizOrtho
|
||||
, GraphvizPolyline
|
||||
|
Loading…
Reference in New Issue
Block a user