Skip to content

Commit

Permalink
Merge pull request #242 from cneben/f/#228-tree-layout-algorithm
Browse files Browse the repository at this point in the history
F/#228 tree layout algorithm
  • Loading branch information
cneben authored Aug 15, 2024
2 parents 303e7c6 + f0e5b44 commit 669ebb1
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 39 deletions.
90 changes: 73 additions & 17 deletions samples/layouts/layouts.qml
Original file line number Diff line number Diff line change
Expand Up @@ -43,42 +43,55 @@ ApplicationWindow {
navigable : true
resizeHandlerColor: "#03a9f4"
gridThickColor: Material.theme === Material.Dark ? "#4e4e4e" : "#c1c1c1"
property var treeRoot: undefined
graph: Qan.Graph {
parent: graphView
id: graph
Component.onCompleted: {
let n1 = graph.insertNode()
n1.label = "n1"; n1.item.x=15; n1.item.y= 25
id: graphView
graphView.treeRoot = n1
n1.label = "n1"; n1.item.x = 15; n1.item.y = 25
n1.item.ratio = 0.4

let n11 = graph.insertNode()
n11.label = "n11"; n11.item.x=15; n11.item.y= 125
let n12 = graph.insertNode()
n12.label = "n12"; n12.item.x=125; n12.item.y= 125
n11.label = "n11"; n11.item.x = 115; n11.item.y = 100
let n111 = graph.insertNode()
n111.label = "n111"; n111.item.x = 215; n111.item.y = 170
let n1111 = graph.insertNode()
n1111.label = "n1111"; n1111.item.x = 315; n1111.item.y = 240

let n12 = graph.insertNode()
n12.label = "n12"; n12.item.x = 115; n12.item.y= 310
let n121 = graph.insertNode()
n121.label = "n121"; n121.item.x=125; n121.item.y= 225
n121.label = "n121"; n121.item.x = 215; n121.item.y = 380
let n122 = graph.insertNode()
n122.label = "n122"; n122.item.x=225; n122.item.y= 225

let n1211 = graph.insertNode()
n1211.label = "n1211"; n1211.item.x=125; n1211.item.y= 225
n122.label = "n122"; n122.item.x = 215; n122.item.y = 450

let n13 = graph.insertNode()
n13.label = "n13"; n13.item.x=225; n13.item.y= 125
n13.label = "n13"; n13.item.x = 115; n13.item.y = 520
let n131 = graph.insertNode()
n131.label = "n131"; n131.item.x = 225; n131.item.y = 590

graph.insertEdge(n1, n11)
graph.insertEdge(n1, n12)
graph.insertEdge(n1, n13)

graph.insertEdge(n1, n11);
graph.insertEdge(n1, n12);
graph.insertEdge(n1, n13);
graph.insertEdge(n12, n121);
graph.insertEdge(n12, n122);
graph.insertEdge(n121, n1211);
graph.insertEdge(n11, n111)
graph.insertEdge(n111, n1111)

orgTreeLayout.layout(n1);
graph.insertEdge(n12, n121)
graph.insertEdge(n12, n122)

graph.insertEdge(n13, n131)
}
Qan.OrgTreeLayout {
id: orgTreeLayout
}
Qan.RandomLayout {
id: randomLayout
layoutRect: Qt.rect(100, 100, 1000, 1000)
}
} // Qan.Graph
Menu { // Context menu demonstration
id: contextMenu
Expand All @@ -97,6 +110,49 @@ ApplicationWindow {
contextMenu.y = pos.y
contextMenu.open()
}
Pane {
anchors.top: parent.top
anchors.topMargin: 10
anchors.horizontalCenter: parent.horizontalCenter
width: 470
height: 50
padding: 2
RowLayout {
anchors.fill: parent
Label {
text: "Apply OrgTree:"
}
Button {
text: 'Random'
Material.roundedScale: Material.SmallScale
onClicked: randomLayout.layout(graphView.treeRoot)
}
Button {
text: 'Mixed'
Material.roundedScale: Material.SmallScale
onClicked: {
orgTreeLayout.layoutOrientation = Qan.OrgTreeLayout.Mixed
orgTreeLayout.layout(graphView.treeRoot);
}
}
Button {
text: 'Vertical'
Material.roundedScale: Material.SmallScale
onClicked: {
orgTreeLayout.layoutOrientation = Qan.OrgTreeLayout.Vertical
orgTreeLayout.layout(graphView.treeRoot);
}
}
Button {
text: 'Horizontal'
Material.roundedScale: Material.SmallScale
onClicked: {
orgTreeLayout.layoutOrientation = Qan.OrgTreeLayout.Horizontal
orgTreeLayout.layout(graphView.treeRoot);
}
}
}
}
} // Qan.GraphView
}

4 changes: 2 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ set(qan_source_files
qanTableCell.cpp
qanTableBorder.cpp
qanTableGroupItem.cpp
qanTreeLayout.cpp
qanTreeLayouts.cpp
)

set (qan_header_files
Expand Down Expand Up @@ -65,7 +65,7 @@ set (qan_header_files
qanTableCell.h
qanTableBorder.h
qanTableGroupItem.h
qanTreeLayout.cpp
qanTreeLayouts.h
QuickQanava.h
gtpo/container_adapter.h
gtpo/edge.h
Expand Down
3 changes: 2 additions & 1 deletion src/QuickQanava.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
#include "./qanBottomResizer.h"
#include "./qanNavigablePreview.h"
#include "./qanAnalysisTimeHeatMap.h"
#include "./qanTreeLayout.h"
#include "./qanTreeLayouts.h"

struct QuickQanava {
static void initialize(QQmlEngine* engine) {
Expand Down Expand Up @@ -118,6 +118,7 @@ struct QuickQanava {
qmlRegisterType<qan::RightResizer>("QuickQanava", 2, 0, "RightResizer");
qmlRegisterType<qan::BottomResizer>("QuickQanava", 2, 0, "BottomResizer");

qmlRegisterType<qan::RandomLayout>("QuickQanava", 2, 0, "RandomLayout");
qmlRegisterType<qan::OrgTreeLayout>("QuickQanava", 2, 0, "OrgTreeLayout");
} // initialize()
};
Expand Down
5 changes: 2 additions & 3 deletions src/RectGlowEffect.qml
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,11 @@ Item {
anchors.centerIn: parent
width: border.width + (glowRadius * 2)
height: border.height + (glowRadius * 2)
blurEnabled: glowEffect.visible &&
glowEffect.style !== undefined ? style.effectEnabled : false
blurEnabled: glowEffect.visible && (glowEffect.style?.effectEnabled || false)
blurMax: 30
blur: 1.
colorization: 1.0
colorizationColor: glowColor
colorizationColor: style?.effectColor ?? Qt.rgba(0.7, 0.7, 0.7, 0.7)

maskEnabled: true && glowEffect.visible
maskThresholdMin: 0.29 // Should be just below border.color
Expand Down
1 change: 1 addition & 0 deletions src/RectGroupTemplate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ Item {
exclusiveSignals: TapHandler.DoubleTap
onTapped: labelEditor.visible = true
}
MouseArea { anchors.fill: parent; cursorShape: Qt.SizeAllCursor; acceptedButtons: Qt.NoButton }
}
} // labelEditor Item
} // RowLayout: collapser + label
Expand Down
2 changes: 1 addition & 1 deletion src/qanGraph.h
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,7 @@ class Graph : public gtpo::graph<QQuickItem, qan::Node, qan::Group, qan::Edge>
*/
std::vector<const qan::Node*> collectDfs(const qan::Node& node, bool collectGroup = false) const noexcept;

//! \copydoc collectDfs()
//! Collect all out nodes of \c nodes using DFS, return an unordered set of subnodes (nodes in node are _not_ in returned set).
auto collectSubNodes(const QVector<qan::Node*> nodes, bool collectGroup = false) const noexcept -> std::unordered_set<const qan::Node*>;

private:
Expand Down
135 changes: 123 additions & 12 deletions src/qanTreeLayout.cpp → src/qanTreeLayouts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
#include <QQmlComponent>

// QuickQanava headers
#include "./qanTreeLayout.h"
#include "./qanTreeLayouts.h"


namespace qan { // ::qan
Expand Down Expand Up @@ -142,45 +142,156 @@ void NaiveTreeLayout::layout(qan::Node* root) noexcept
//-----------------------------------------------------------------------------


/* OrgTreeLayout Object Management *///----------------------------------------
RandomLayout::RandomLayout(QObject* parent) noexcept :
QObject{parent}
{
}
RandomLayout::~RandomLayout() { }

bool RandomLayout::setLayoutRect(QRectF layoutRect) noexcept
{
_layoutRect = layoutRect;
emit layoutRectChanged();
return true;
}
const QRectF RandomLayout::getLayoutRect() const noexcept { return _layoutRect; }

void RandomLayout::layout(qan::Node& root) noexcept
{
// In nodes, out nodes, adjacent nodes ?
const auto graph = root.getGraph();
if (graph == nullptr)
return;
if (root.getItem() == nullptr)
return;

// Generate a 1000x1000 layout rect centered on root if the user has not specified one
const auto rootPosition = root.getItem()->position();
const auto layoutRect = _layoutRect.isEmpty() ? QRectF{rootPosition.x() - 500, rootPosition.y() - 500, 1000., 1000.} :
_layoutRect;

auto outNodes = graph->collectSubNodes(QVector<qan::Node*>{&root}, false);
outNodes.insert(&root);
for (auto n : outNodes) {
auto node = const_cast<qan::Node*>(n);
if (node->getItem() == nullptr)
continue;
const auto nodeBr = node->getItem()->boundingRect();
qreal maxX = layoutRect.width() - nodeBr.width(); // Generate and set random x and y positions
qreal maxY = layoutRect.height() - nodeBr.height(); // within available layoutRect area
node->getItem()->setX(QRandomGenerator::global()->bounded(maxX) + layoutRect.left());
node->getItem()->setY(QRandomGenerator::global()->bounded(maxY) + layoutRect.top());
}
}

void RandomLayout::layout(qan::Node* root) noexcept
{
if (root != nullptr)
layout(*root);
}
//-----------------------------------------------------------------------------


/* OrgTreeLayout Object Management *///----------------------------------------
OrgTreeLayout::OrgTreeLayout(QObject* parent) noexcept :
QObject{parent}
{
}
OrgTreeLayout::~OrgTreeLayout() { }

bool OrgTreeLayout::setLayoutOrientation(OrgTreeLayout::LayoutOrientation layoutOrientation) noexcept {
if (_layoutOrientation != layoutOrientation) {
_layoutOrientation = layoutOrientation;
emit layoutOrientationChanged();
return true;
}
return false;
}
OrgTreeLayout::LayoutOrientation OrgTreeLayout::getLayoutOrientation() noexcept { return _layoutOrientation; }
const OrgTreeLayout::LayoutOrientation OrgTreeLayout::getLayoutOrientation() const noexcept { return _layoutOrientation; }


void OrgTreeLayout::layout(qan::Node& root, qreal xSpacing, qreal ySpacing) noexcept
{
// FIXME #228: Variant / naive Reingold-Tilford algorithm
// Note: Recursive variant of Reingold-Tilford algorithm with naive shifting (ie shifting
// based on the less space efficient sub tree bounding rect intersection...)

// Pre-condition: root must be a tree subgraph, this is not enforced in this algorithm,
// any circuit will lead to intinite recursion...

// Algorithm:
// Traverse graph DFS aligning child nodes vertically
// At a given level: `shift` next node according to previous node sub-tree BR
auto layout_rec = [xSpacing, ySpacing](auto&& self, auto& childNodes, QRectF br) -> QRectF {
//qWarning() << "layout_rec(): br=" << br;
auto layoutVert_rec = [xSpacing, ySpacing](auto&& self, auto& childNodes, QRectF br) -> QRectF {
const auto x = br.right() + xSpacing;
for (auto child: childNodes) {
//qWarning() << "layout_rec(): child.label=" << child->getLabel() << " br=" << br;
child->getItem()->setX(x);
child->getItem()->setY(br.bottom() + ySpacing);
// Take into account this level maximum width
br = br.united(child->getItem()->boundingRect().translated(child->getItem()->position()));
const auto childBr = self(self, child->get_out_nodes(), br);
br.setBottom(childBr.bottom()); // Note: Do not take full child BR into account to avoid x drifting
}
return br;
};

auto layoutHoriz_rec = [xSpacing, ySpacing](auto&& self, auto& childNodes, QRectF br) -> QRectF {
const auto y = br.bottom() + ySpacing;
for (auto child: childNodes) {
child->getItem()->setX(br.right() + xSpacing);
child->getItem()->setY(y);
// Take into account this level maximum width
br = br.united(child->getItem()->boundingRect().translated(child->getItem()->position()));
const auto prevBr = self(self, child->get_out_nodes(), br);
br = br.united(prevBr);
const auto childBr = self(self, child->get_out_nodes(), br);
br.setRight(childBr.right()); // Note: Do not take full child BR into account to avoid x drifting
}
return br;
};

auto layoutMixed_rec = [xSpacing, ySpacing, layoutHoriz_rec](auto&& self, auto& childNodes, QRectF br) -> QRectF {
auto childsAreLeafs = true;
for (const auto child: childNodes)
if (child->get_out_nodes().size() != 0) {
childsAreLeafs = false;
break;
}
if (childsAreLeafs)
return layoutHoriz_rec(self, childNodes, br);
else {
const auto x = br.right() + xSpacing;
for (auto child: childNodes) {
child->getItem()->setX(x);
child->getItem()->setY(br.bottom() + ySpacing);
// Take into account this level maximum width
br = br.united(child->getItem()->boundingRect().translated(child->getItem()->position()));
const auto childBr = self(self, child->get_out_nodes(), br);
br.setBottom(childBr.bottom()); // Note: Do not take full child BR into account to avoid x drifting
}
}
return br;
};
//qWarning() << "root.bottomRight=" << root.getItem()->boundingRect().bottomRight();
// Note: QQuickItem boundingRect is in item local CS, translate to scene CS.
layout_rec(layout_rec, root.get_out_nodes(),
root.getItem()->boundingRect().translated(root.getItem()->position()));

// Note: QQuickItem boundingRect() is in item local CS, translate to "scene" CS.
switch (getLayoutOrientation()) {
case LayoutOrientation::Undefined: return;
case LayoutOrientation::Vertical:
layoutVert_rec(layoutVert_rec, root.get_out_nodes(),
root.getItem()->boundingRect().translated(root.getItem()->position()));
break;
case LayoutOrientation::Horizontal:
layoutHoriz_rec(layoutHoriz_rec, root.get_out_nodes(),
root.getItem()->boundingRect().translated(root.getItem()->position()));
break;
case LayoutOrientation::Mixed:
layoutMixed_rec(layoutMixed_rec, root.get_out_nodes(),
root.getItem()->boundingRect().translated(root.getItem()->position()));
break;
}
}

void OrgTreeLayout::layout(qan::Node* root, qreal xSpacing, qreal ySpacing) noexcept
{
//qWarning() << "qan::OrgTreeLayout::layout(): root=" << root;
if (root != nullptr)
layout(*root, xSpacing, ySpacing);
}
Expand Down
Loading

0 comments on commit 669ebb1

Please sign in to comment.