Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

F/#228 tree layout algorithm #242

Merged
merged 8 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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