aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamuel Fadel <samuelfadel@gmail.com>2016-04-04 17:16:08 -0300
committerSamuel Fadel <samuelfadel@gmail.com>2016-04-04 17:16:08 -0300
commit0bdc4110ac010685ee3e81552041bc553d848294 (patch)
treede0f68c45bdf85b14d830796239ef14ce0a8e411
parent0615b37b56f3c2ffaf46255808a60f16b1d5be7c (diff)
LinePlot: working properly (and updates settings).
Added the several options to the bundling (from CUBu) as properties of the LinePlot component, which are set from the options UI. In addition, many changes to the UI regarding those options. Added a new shortcut to hide options (for cleaner demos).
-rw-r--r--lineplot.cpp382
-rw-r--r--lineplot.h119
-rw-r--r--main.cpp53
-rw-r--r--main.h4
-rw-r--r--main_view.qml627
5 files changed, 857 insertions, 328 deletions
diff --git a/lineplot.cpp b/lineplot.cpp
index b4d7412..83f6e12 100644
--- a/lineplot.cpp
+++ b/lineplot.cpp
@@ -19,86 +19,124 @@
#include "geometry.h"
#include "scatterplot.h"
+static const int SAMPLES = 128;
+
LinePlot::LinePlot(QQuickItem *parent)
: QQuickFramebufferObject(parent)
, m_sx(0, 1, 0, 1)
, m_sy(0, 1, 0, 1)
- , m_xyChanged(false)
+ , m_linesChanged(false)
, m_valuesChanged(false)
, m_colorScaleChanged(false)
+
+ // Q_PROPERTY's
+ , m_iterations(15)
+ , m_kernelSize(32)
+ , m_smoothingFactor(0.2)
+ , m_smoothingIterations(1)
+
+ , m_blockEndpoints(true)
+ , m_endsIterations(0)
+ , m_endsKernelSize(32)
+ , m_endsSmoothingFactor(0.5)
+
+ , m_edgeSampling(15)
+ , m_advectionSpeed(0.5)
+ , m_relaxation(0)
, m_bundleGPU(true)
{
+ setFlag(QQuickItem::ItemHasContents);
setTextureFollowsItemSize(false);
}
-void LinePlot::setColorScale(const ColorScale *colorScale)
+void LinePlot::setColorScale(const ColorScale *scale)
{
- m_colorScale = colorScale;
- if (m_values.size() > 0) {
- // FIXME
- m_colorScaleChanged = true;
- update();
- }
+ m_cmap.resize(SAMPLES * 3);
+ scale->sample(SAMPLES, m_cmap.begin());
+
+ setColorScaleChanged(true);
+ update();
}
void LinePlot::bundle()
{
- Graph g(m_xy.n_rows);
- PointSet points;
+ m_gdBundlePtr.reset(new GraphDrawing);
+ *m_gdBundlePtr.get() = *m_gdPtr.get();
- for (arma::uword i = 0; i < m_xy.n_rows; i++) {
- const arma::rowvec &row = m_xy.row(i);
- points.push_back(Point2d(row[0], row[1]));
+ CPUBundling bundling(std::min(width(), height()));
+ bundling.setInput(m_gdBundlePtr.get());
- if (i > 0) {
- g(i - 1, i) = m_values[i];
- g(i, i - 1) = m_values[i];
- }
- }
+ bundling.niter = m_iterations;
+ bundling.h = m_kernelSize;
+ bundling.lambda = m_smoothingFactor;
+ bundling.liter = m_smoothingIterations;
- m_gdPtr.reset(new GraphDrawing);
- m_gdPtr.get()->build(&g, &points);
+ bundling.block_endpoints = m_blockEndpoints;
+ bundling.niter_ms = m_endsIterations;
+ bundling.h_ms = m_endsKernelSize;
+ bundling.lambda_ends = m_endsSmoothingFactor;
+
+ bundling.spl = m_edgeSampling;
+ bundling.eps = m_advectionSpeed;
+ // TODO: use m_relaxation as lerp param towards original (without bundling)
- CPUBundling bundling(std::min(width(), height()));
- bundling.setInput(m_gdPtr.get());
if (m_bundleGPU) {
bundling.bundleGPU();
} else {
bundling.bundleCPU();
}
+
+ setLinesChanged(true);
}
-void LinePlot::setXY(const arma::mat &xy)
+void LinePlot::setLines(const arma::uvec &indices, const arma::mat &Y)
{
- if (xy.n_cols != 2) {
+ if (indices.n_elem % 2 != 0 || Y.n_cols != 2) {
return;
}
- m_xy = xy;
- m_xyChanged = true;
- emit xyChanged(m_xy);
+ m_lines = Y.rows(indices);
+
+ // Build the line plot's internal representation: a graph where each
+ // endpoint of a line is a node, each line is a link...
+ Graph g(Y.n_rows);
+ PointSet points;
+
+ m_sx.setRange(0, width());
+ m_sy.setRange(0, height());
+ for (arma::uword i = 0; i < Y.n_rows; i++) {
+ points.push_back(Point2d(m_sx(Y(i, 0)), m_sy(Y(i, 1))));
+ }
+
+ for (arma::uword k = 0; k < m_values.size(); k++) {
+ arma::uword i = indices(2*k + 0),
+ j = indices(2*k + 1);
- // Build the line plot's internal representation: graph where each endpoint
- // of a line is a node, each line is a link; and then bundle the graph's
- // edges
+ g(i, j) = g(j, i) = m_values[k];
+ }
+
+ m_gdPtr.reset(new GraphDrawing);
+ m_gdPtr.get()->build(&g, &points);
+
+ // ... then bundle the edges
bundle();
+ emit linesChanged(m_lines);
update();
}
void LinePlot::setValues(const arma::vec &values)
{
- if (m_xy.n_rows > 0
- && (values.n_elem > 0 && values.n_elem != m_xy.n_rows)) {
+ if (m_lines.n_rows > 0
+ && (values.n_elem > 0 && values.n_elem != m_lines.n_rows)) {
return;
}
m_values.resize(values.n_elem);
- LinearScale<float> scale(values.min(), values.max(), 0, 1.0f);
- std::transform(values.begin(), values.end(), m_values.begin(), scale);
+ std::copy(values.begin(), values.end(), m_values.begin());
emit valuesChanged(values);
- m_valuesChanged = true;
+ setValuesChanged(true);
update();
}
@@ -111,6 +149,162 @@ void LinePlot::setScale(const LinearScale<float> &sx,
update();
}
+// Q_PROPERTY's
+void LinePlot::setIterations(int iterations) {
+ if (m_iterations == iterations) {
+ return;
+ }
+
+ m_iterations = iterations;
+ emit iterationsChanged(m_iterations);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setKernelSize(float kernelSize)
+{
+ if (m_kernelSize == kernelSize) {
+ return;
+ }
+
+ m_kernelSize = kernelSize;
+ emit kernelSizeChanged(m_kernelSize);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setSmoothingFactor(float smoothingFactor)
+{
+ if (m_smoothingFactor == smoothingFactor) {
+ return;
+ }
+
+ m_smoothingFactor = smoothingFactor;
+ emit smoothingFactorChanged(m_smoothingFactor);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setSmoothingIterations(int smoothingIterations)
+{
+ if (m_smoothingIterations == smoothingIterations) {
+ return;
+ }
+
+ m_smoothingIterations = smoothingIterations;
+ emit smoothingIterationsChanged(m_smoothingIterations);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setBlockEndpoints(bool blockEndpoints)
+{
+ if (m_blockEndpoints == blockEndpoints) {
+ return;
+ }
+
+ m_blockEndpoints = blockEndpoints;
+ emit blockEndpointsChanged(m_blockEndpoints);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setEndsIterations(int endsIterations)
+{
+ if (m_endsIterations == endsIterations) {
+ return;
+ }
+
+ m_endsIterations = endsIterations;
+ emit endsIterationsChanged(m_endsIterations);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setEndsKernelSize(float endsKernelSize)
+{
+ if (m_endsKernelSize == endsKernelSize) {
+ return;
+ }
+
+ m_endsKernelSize = endsKernelSize;
+ emit endsKernelSizeChanged(m_endsKernelSize);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setEndsSmoothingFactor(float endsSmoothingFactor)
+{
+ if (m_endsSmoothingFactor == endsSmoothingFactor) {
+ return;
+ }
+
+ m_endsSmoothingFactor = endsSmoothingFactor;
+ emit endsSmoothingFactorChanged(m_endsSmoothingFactor);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setEdgeSampling(float edgeSampling)
+{
+ if (m_edgeSampling == edgeSampling) {
+ return;
+ }
+
+ m_edgeSampling = edgeSampling;
+ emit edgeSamplingChanged(m_edgeSampling);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setAdvectionSpeed(float advectionSpeed)
+{
+ if (m_advectionSpeed == advectionSpeed) {
+ return;
+ }
+
+ m_advectionSpeed = advectionSpeed;
+ emit advectionSpeedChanged(m_advectionSpeed);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setRelaxation(float relaxation)
+{
+ if (m_relaxation == relaxation) {
+ return;
+ }
+
+ m_relaxation = relaxation;
+ emit relaxationChanged(m_relaxation);
+
+ bundle();
+ update();
+}
+
+void LinePlot::setBundleGPU(bool bundleGPU)
+{
+ if (m_bundleGPU == bundleGPU) {
+ return;
+ }
+
+ m_bundleGPU = bundleGPU;
+ emit bundleGPUChanged(m_bundleGPU);
+
+ bundle();
+ update();
+}
+
// ----------------------------------------------------------------------------
class LinePlotRenderer
@@ -133,23 +327,21 @@ private:
void updatePoints();
void updateValues();
void updateColormap();
- void updateTransform();
void copyPolylines(const GraphDrawing *gd);
QSize m_size;
std::vector<float> m_points;
- const std::vector<float> *m_values;
+ const std::vector<float> *m_values, *m_cmap;
std::vector<int> m_offsets;
- float m_alpha, m_beta;
- GLfloat m_transform[4][4];
- LinearScale<float> m_sx, m_sy;
QQuickWindow *m_window; // used to reset OpenGL state (as per docs)
QOpenGLFunctions gl;
QOpenGLShaderProgram *m_program;
GLuint m_VBOs[2], m_colormapTex;
QOpenGLVertexArrayObject m_VAO;
+ GLfloat m_transform[4][4];
+ LinearScale<float> m_sx, m_sy;
bool m_pointsChanged, m_valuesChanged, m_colormapChanged;
};
@@ -159,13 +351,12 @@ QQuickFramebufferObject::Renderer *LinePlot::createRenderer() const
}
LinePlotRenderer::LinePlotRenderer()
- : m_sx(0, 1, 0, 1)
- , m_sy(0, 1, 0, 1)
- , gl(QOpenGLContext::currentContext())
+ : gl(QOpenGLContext::currentContext())
+ , m_sx(0.0f, 1.0f, 0.0f, 1.0f)
+ , m_sy(0.0f, 1.0f, 0.0f, 1.0f)
{
std::fill(&m_transform[0][0], &m_transform[0][0] + 16, 0.0f);
m_transform[3][3] = 1.0f;
-
setupShaders();
setupVAOs();
setupTextures();
@@ -236,12 +427,39 @@ void LinePlotRenderer::setupTextures()
LinePlotRenderer::~LinePlotRenderer()
{
+ gl.glDeleteBuffers(2, m_VBOs);
+ gl.glDeleteTextures(1, &m_colormapTex);
+
delete m_program;
}
QOpenGLFramebufferObject *LinePlotRenderer::createFramebufferObject(const QSize &size)
{
m_size = size;
+ GLfloat w = m_size.width(), h = m_size.height();
+
+ GLfloat rangeOffset = Scatterplot::PADDING / w;
+ m_sx.setDomain(0, w);
+ m_sx.setRange(rangeOffset, 1.0f - rangeOffset);
+ GLfloat sx = 2.0f * m_sx.slope();
+ GLfloat tx = 2.0f * m_sx.offset() - 1.0f;
+
+ rangeOffset = Scatterplot::PADDING / h;
+ m_sy.setDomain(0, h);
+ m_sy.setRange(1.0f - rangeOffset, rangeOffset); // inverted on purpose
+ GLfloat sy = 2.0f * m_sy.slope();
+ GLfloat ty = 2.0f * m_sy.offset() - 1.0f;
+
+ // The transform matrix should be this (but transposed -- column major):
+ // [ sx 0.0f 0.0f tx ]
+ // [ 0.0f sy 0.0f ty ]
+ // [ 0.0f 0.0f 0.0f 0.0f ]
+ // [ 0.0f 0.0f 0.0f 1.0f ]
+ m_transform[0][0] = sx;
+ m_transform[1][1] = sy;
+ m_transform[3][0] = tx;
+ m_transform[3][1] = ty;
+
return QQuickFramebufferObject::Renderer::createFramebufferObject(m_size);
}
@@ -262,20 +480,31 @@ void LinePlotRenderer::render()
updateColormap();
}
+ if (m_offsets.size() < 2) {
+ // Nothing to draw
+ return;
+ }
+
m_program->bind();
- m_program->setUniformValue("transform", m_transform);
gl.glActiveTexture(GL_TEXTURE0);
gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex);
m_program->setUniformValue("colormap", 0);
+ m_program->setUniformValue("transform", m_transform);
gl.glClearColor(0, 0, 0, 0);
gl.glClear(GL_COLOR_BUFFER_BIT);
m_VAO.bind();
- for (int i = 0; i < m_offsets.size() - 1; i++) {
- gl.glDrawArrays(GL_LINES, m_offsets[i], m_offsets[i + 1]);
+ gl.glEnable(GL_LINE_SMOOTH);
+ gl.glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
+ gl.glEnable(GL_BLEND);
+ gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ for (int i = 1; i < m_offsets.size(); i++) {
+ gl.glDrawArrays(GL_LINE_STRIP, m_offsets[i - 1], m_offsets[i] - m_offsets[i - 1]);
}
+ gl.glDisable(GL_LINE_SMOOTH);
+ gl.glDisable(GL_BLEND);
m_VAO.release();
m_program->release();
@@ -290,22 +519,20 @@ void LinePlotRenderer::copyPolylines(const GraphDrawing *gd)
int pointsNum = 0;
m_offsets.clear();
+ m_offsets.reserve(gd->draw_order.size() + 1);
m_offsets.push_back(0);
- for (std::pair<float, const GraphDrawing::Polyline *> p : gd->draw_order) {
+ for (auto &p : gd->draw_order) {
pointsNum += p.second->size();
m_offsets.push_back(pointsNum);
}
- m_points.resize(2 * pointsNum);
- arma::uword i = 0, k = 0;
- for (std::pair<float, const GraphDrawing::Polyline *> p : gd->draw_order) {
- for (arma::uword j = 0; j < p.second->size(); j++) {
- m_points[i + j + 0] = (*p.second)[j].x;
- m_points[i + j + 1] = (*p.second)[j].y;
+ m_points.clear();
+ m_points.reserve(2 * pointsNum);
+ for (auto &p : gd->draw_order) {
+ for (auto &point : *p.second) {
+ m_points.push_back(point.x);
+ m_points.push_back(point.y);
}
-
- i += p.second->size();
- k++;
}
}
@@ -313,54 +540,29 @@ void LinePlotRenderer::synchronize(QQuickFramebufferObject *item)
{
LinePlot *plot = static_cast<LinePlot *>(item);
- m_pointsChanged = plot->xyChanged();
+ m_pointsChanged = plot->linesChanged();
m_valuesChanged = plot->valuesChanged();
m_colormapChanged = plot->colorScaleChanged();
- copyPolylines(plot->graphDrawing());
+ if (m_pointsChanged) {
+ copyPolylines(plot->bundleGraphDrawing());
+ }
m_values = &(plot->values());
- m_sx = plot->scaleX();
- m_sy = plot->scaleY();
+ m_cmap = &(plot->colorScale());
m_window = plot->window();
// Reset so that we have the correct values by the next synchronize()
- plot->setXYChanged(false);
+ plot->setLinesChanged(false);
plot->setValuesChanged(false);
plot->setColorScaleChanged(false);
}
-void LinePlotRenderer::updateTransform()
-{
- GLfloat w = m_size.width(), h = m_size.height();
-
- GLfloat rangeOffset = Scatterplot::PADDING / w;
- m_sx.setRange(rangeOffset, 1.0f - rangeOffset);
- GLfloat sx = 2.0f * m_sx.slope();
- GLfloat tx = 2.0f * m_sx.offset() - 1.0f;
-
- rangeOffset = Scatterplot::PADDING / h;
- m_sy.setRange(1.0f - rangeOffset, rangeOffset);
- GLfloat sy = 2.0f * m_sy.slope();
- GLfloat ty = 2.0f * m_sy.offset() - 1.0f;
-
- // The transform matrix should be this (but transposed -- column major):
- // [ sx 0.0f 0.0f -tx ]
- // [ 0.0f sy 0.0f -ty ]
- // [ 0.0f 0.0f 0.0f 0.0f ]
- // [ 0.0f 0.0f 0.0f 1.0f ]
- m_transform[0][0] = sx;
- m_transform[1][1] = sy;
- m_transform[3][0] = tx;
- m_transform[3][1] = ty;
-}
-
void LinePlotRenderer::updatePoints()
{
gl.glBindBuffer(GL_ARRAY_BUFFER, m_VBOs[0]);
gl.glBufferData(GL_ARRAY_BUFFER, m_points.size() * sizeof(float),
m_points.data(), GL_DYNAMIC_DRAW);
- updateTransform();
m_pointsChanged = false;
}
@@ -376,8 +578,8 @@ void LinePlotRenderer::updateValues()
void LinePlotRenderer::updateColormap()
{
gl.glBindTexture(GL_TEXTURE_2D, m_colormapTex);
- //gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_cmap->size() / 3, 1, 0, GL_RGB,
- // GL_FLOAT, m_cmap->data());
+ gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_cmap->size() / 3, 1, 0, GL_RGB,
+ GL_FLOAT, m_cmap->data());
m_colormapChanged = false;
}
diff --git a/lineplot.h b/lineplot.h
index 060171d..bc6340c 100644
--- a/lineplot.h
+++ b/lineplot.h
@@ -18,27 +18,46 @@ class LinePlot
: public QQuickFramebufferObject
{
Q_OBJECT
+
+ // Main bundling
+ Q_PROPERTY(int iterations READ iterations WRITE setIterations NOTIFY iterationsChanged)
+ Q_PROPERTY(float kernelSize READ kernelSize WRITE setKernelSize NOTIFY kernelSizeChanged)
+ Q_PROPERTY(float smoothingFactor READ smoothingFactor WRITE setSmoothingFactor NOTIFY smoothingFactorChanged)
+ Q_PROPERTY(int smoothingIterations READ smoothingIterations WRITE setSmoothingIterations NOTIFY smoothingIterationsChanged)
+
+ // Ends bundling
+ Q_PROPERTY(bool blockEndpoints READ blockEndpoints WRITE setBlockEndpoints NOTIFY blockEndpointsChanged)
+ Q_PROPERTY(int endsIterations READ endsIterations WRITE setEndsIterations NOTIFY endsIterationsChanged)
+ Q_PROPERTY(float endsKernelSize READ endsKernelSize WRITE setEndsKernelSize NOTIFY endsKernelSizeChanged)
+ Q_PROPERTY(float endsSmoothingFactor READ endsSmoothingFactor WRITE setEndsSmoothingFactor NOTIFY endsSmoothingFactorChanged)
+
+ // General bundling options
+ Q_PROPERTY(float edgeSampling READ edgeSampling WRITE setEdgeSampling NOTIFY edgeSamplingChanged)
+ Q_PROPERTY(float advectionSpeed READ advectionSpeed WRITE setAdvectionSpeed NOTIFY advectionSpeedChanged)
+ Q_PROPERTY(float relaxation READ relaxation WRITE setRelaxation NOTIFY relaxationChanged)
+ Q_PROPERTY(bool bundleGPU READ bundleGPU WRITE setBundleGPU NOTIFY bundleGPUChanged)
+
public:
static const int PADDING = 20;
LinePlot(QQuickItem *parent = 0);
- void setColorScale(const ColorScale *colorScale);
- void setAutoScale(bool autoScale);
+ void setColorScale(const ColorScale *scale);
- const GraphDrawing *graphDrawing() const { return m_gdPtr.get(); }
- const std::vector<float> &values() const { return m_values; }
+ const GraphDrawing *bundleGraphDrawing() const { return m_gdBundlePtr.get(); }
+ const std::vector<float> &values() const { return m_values; }
+ const std::vector<float> &colorScale() const { return m_cmap; }
LinearScale<float> scaleX() const { return m_sx; }
LinearScale<float> scaleY() const { return m_sy; }
Renderer *createRenderer() const;
- bool xyChanged() const { return m_xyChanged; }
- bool valuesChanged() const { return m_valuesChanged; }
+ bool linesChanged() const { return m_linesChanged; }
+ bool valuesChanged() const { return m_valuesChanged; }
bool colorScaleChanged() const { return m_colorScaleChanged; }
- void setXYChanged(bool xyChanged) {
- m_xyChanged = xyChanged;
+ void setLinesChanged(bool linesChanged) {
+ m_linesChanged = linesChanged;
}
void setValuesChanged(bool valuesChanged) {
m_valuesChanged = valuesChanged;
@@ -47,15 +66,66 @@ public:
m_colorScaleChanged = colorScaleChanged;
}
+ // Q_PROPERTY's
+ int iterations() const { return m_iterations; }
+ float kernelSize() const { return m_kernelSize; }
+ float smoothingFactor() const { return m_smoothingFactor; }
+ int smoothingIterations() const { return m_smoothingIterations; }
+
+ void setIterations(int iterations);
+ void setKernelSize(float kernelSize);
+ void setSmoothingFactor(float smoothingFactor);
+ void setSmoothingIterations(int smoothingIterations);
+
+ bool blockEndpoints() const { return m_blockEndpoints; }
+ int endsIterations() const { return m_endsIterations; }
+ float endsKernelSize() const { return m_endsKernelSize; }
+ float endsSmoothingFactor() const { return m_endsSmoothingFactor; }
+
+ void setBlockEndpoints(bool blockEndpoints);
+ void setEndsIterations(int endsIterations);
+ void setEndsKernelSize(float endsKernelSize);
+ void setEndsSmoothingFactor(float endsSmoothingFactor);
+
+ float edgeSampling() const { return m_edgeSampling; }
+ float advectionSpeed() const { return m_advectionSpeed; }
+ float relaxation() const { return m_relaxation; }
+ bool bundleGPU() const { return m_bundleGPU; }
+
+ void setEdgeSampling(float edgeSampling);
+ void setAdvectionSpeed(float advectionSpeed);
+ void setRelaxation(float relaxation);
+ void setBundleGPU(bool bundleGPU);
+
signals:
- void xyChanged(const arma::mat &xy);
+ void linesChanged(const arma::mat &xy);
void valuesChanged(const arma::vec &values) const;
void scaleChanged(const LinearScale<float> &sx,
const LinearScale<float> &sy) const;
+ // Q_PROPERTY's
+ void iterationsChanged(int iterations) const;
+ void kernelSizeChanged(float kernelSize) const;
+ void smoothingFactorChanged(float smoothingFactor) const;
+ void smoothingIterationsChanged(int smoothingIterations) const;
+
+ void blockEndpointsChanged(bool blockEndpoints) const;
+ void endsIterationsChanged(int endsIterations) const;
+ void endsKernelSizeChanged(float endsKernelSize) const;
+ void endsSmoothingFactorChanged(float endsSmoothingFactor) const;
+
+ void edgeSamplingChanged(float edgeSampling) const;
+ void advectionSpeedChanged(float advectionSpeed) const;
+ void relaxationChanged(float relaxation) const;
+ void bundleGPUChanged(bool bundleGPU) const;
+
public slots:
- void setXY(const arma::mat &xy);
+ // Lines are two consecutive elements in 'indices' (ref. points in 'Y')
+ void setLines(const arma::uvec &indices, const arma::mat &Y);
+
+ // One value for each line
void setValues(const arma::vec &values);
+
void setScale(const LinearScale<float> &sx,
const LinearScale<float> &sy);
@@ -63,21 +133,32 @@ private:
void bundle();
// Data
- arma::mat m_xy;
+ arma::mat m_lines;
std::vector<float> m_values;
// Visuals
- const ColorScale *m_colorScale;
-
- void autoScale();
- bool m_autoScale;
+ std::vector<float> m_cmap;
LinearScale<float> m_sx, m_sy;
-
- std::unique_ptr<GraphDrawing> m_gdPtr;
+ std::unique_ptr<GraphDrawing> m_gdPtr, m_gdBundlePtr;
// Internal state
- bool m_xyChanged, m_valuesChanged, m_colorScaleChanged;
- bool m_bundleGPU;
+ bool m_linesChanged, m_valuesChanged, m_colorScaleChanged;
+
+ // Q_PROPERTY's
+ int m_iterations;
+ float m_kernelSize;
+ float m_smoothingFactor;
+ int m_smoothingIterations;
+
+ bool m_blockEndpoints;
+ int m_endsIterations;
+ float m_endsKernelSize;
+ float m_endsSmoothingFactor;
+
+ float m_edgeSampling;
+ float m_advectionSpeed;
+ float m_relaxation;
+ bool m_bundleGPU;
};
#endif // LINEPLOT_H
diff --git a/main.cpp b/main.cpp
index c7a6f5c..bf9042a 100644
--- a/main.cpp
+++ b/main.cpp
@@ -153,9 +153,10 @@ int main(int argc, char **argv)
// Initialize pointers to visual components
m->cpPlot = engine.rootObjects()[0]->findChild<Scatterplot *>("cpPlot");
m->rpPlot = engine.rootObjects()[0]->findChild<Scatterplot *>("rpPlot");
+ m->splat = engine.rootObjects()[0]->findChild<VoronoiSplat *>("splat");
+ m->bundlePlot = engine.rootObjects()[0]->findChild<LinePlot *>("bundlePlot");
m->cpColormap = engine.rootObjects()[0]->findChild<Colormap *>("cpColormap");
m->rpColormap = engine.rootObjects()[0]->findChild<Colormap *>("rpColormap");
- m->splat = engine.rootObjects()[0]->findChild<VoronoiSplat *>("splat");
m->cpBarChart = engine.rootObjects()[0]->findChild<BarChart *>("cpBarChart");
m->rpBarChart = engine.rootObjects()[0]->findChild<BarChart *>("rpBarChart");
TransitionControl *plotTC = engine.rootObjects()[0]->findChild<TransitionControl *>("plotTC");
@@ -170,6 +171,20 @@ int main(int argc, char **argv)
QObject::connect(m->cpPlot, &Scatterplot::xyInteractivelyChanged,
m, &Main::setCP);
+ // Keep both scatterplots, the splat and line plot scaled equally and
+ // relative to the full plot
+ MapScaleHandler mapScaleHandler;
+ QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged,
+ m->cpPlot, &Scatterplot::setScale);
+ QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged,
+ m->rpPlot, &Scatterplot::setScale);
+ QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged,
+ m->splat, &VoronoiSplat::setScale);
+ QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged,
+ m->bundlePlot, &LinePlot::setScale);
+ QObject::connect(m->projectionHistory, &ProjectionHistory::currentMapChanged,
+ &mapScaleHandler, &MapScaleHandler::scaleToMap);
+
// Update projection as the cp are modified (either directly in the
// manipulationHandler object or interactively in cpPlot
ManipulationHandler manipulationHandler(X, cpIndices);
@@ -183,18 +198,32 @@ int main(int argc, char **argv)
// ... and update visual components whenever the history changes
QObject::connect(m->projectionHistory, &ProjectionHistory::currentMapChanged,
m, &Main::updateMap);
-
- // Keep both scatterplots and the splat scaled equally and relative to the
- // full plot
- MapScaleHandler mapScaleHandler;
- QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged,
- m->cpPlot, &Scatterplot::setScale);
- QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged,
- m->rpPlot, &Scatterplot::setScale);
- QObject::connect(&mapScaleHandler, &MapScaleHandler::scaleChanged,
- m->splat, &VoronoiSplat::setScale);
QObject::connect(m->projectionHistory, &ProjectionHistory::currentMapChanged,
- &mapScaleHandler, &MapScaleHandler::scaleToMap);
+ [m](const arma::mat &Y) {
+ // ... and bundling
+ const arma::mat &unreliability = m->projectionHistory->unreliability();
+ arma::uvec indicesLargest = arma::sort_index(unreliability, "descending");
+ auto numLargest = Y.n_rows * 0.1f;
+ indicesLargest = indicesLargest.subvec(0, numLargest-1);
+ m->bundlePlot->setValues(unreliability(indicesLargest));
+
+ const arma::uvec &cpIndices = m->projectionHistory->cpIndices();
+ arma::uvec CPs = cpIndices(indicesLargest / unreliability.n_rows);
+
+ const arma::uvec &rpIndices = m->projectionHistory->rpIndices();
+ arma::uvec RPs = indicesLargest;
+ RPs.transform([&unreliability](arma::uword v) {
+ return v % unreliability.n_rows;
+ });
+ RPs = rpIndices(RPs);
+
+ arma::uvec indices(CPs.n_elem + RPs.n_elem);
+ for (arma::uword i = 0; i < CPs.n_elem; i++) {
+ indices(2*i + 0) = CPs(i);
+ indices(2*i + 1) = RPs(i);
+ }
+ m->bundlePlot->setLines(indices, Y);
+ });
// Linking between selections
SelectionHandler cpSelectionHandler(cpIndices.n_elem);
diff --git a/main.h b/main.h
index 830c742..143d49e 100644
--- a/main.h
+++ b/main.h
@@ -12,6 +12,7 @@
#include "numericrange.h"
#include "barchart.h"
#include "colormap.h"
+#include "lineplot.h"
#include "scatterplot.h"
#include "voronoisplat.h"
@@ -96,6 +97,7 @@ public:
cpPlot->setColorScale(colorScaleCPs.get());
cpBarChart->setColorScale(colorScaleCPs.get());
cpColormap->setColorScale(colorScaleCPs.get());
+ bundlePlot->setColorScale(colorScaleCPs.get());
}
Q_INVOKABLE void setRPColorScale(ColorScaleType colorScaleType) {
@@ -122,6 +124,7 @@ public:
Colormap *cpColormap, *rpColormap;
Scatterplot *cpPlot, *rpPlot;
VoronoiSplat *splat;
+ LinePlot *bundlePlot;
// Color scales in use
std::unique_ptr<ColorScale> colorScaleCPs, colorScaleRPs;
@@ -188,6 +191,7 @@ private:
, cpPlot(0)
, rpPlot(0)
, splat(0)
+ , bundlePlot(0)
, projectionHistory(0)
{
}
diff --git a/main_view.qml b/main_view.qml
index bce7005..60a85aa 100644
--- a/main_view.qml
+++ b/main_view.qml
@@ -14,9 +14,6 @@ ApplicationWindow {
Component.onCompleted: {
setX(Screen.width / 2 - width / 2);
setY(Screen.height / 2 - height / 2);
-
- this.minimumWidth = width;
- this.minimumHeight = height;
}
menuBar: MenuBar {
@@ -38,6 +35,11 @@ ApplicationWindow {
MenuItem { action: selectRPsAction }
MenuItem { action: selectCPsAction }
}
+
+ Menu {
+ title: "View"
+ MenuItem { action: toggleOptionsAction }
+ }
}
statusBar: StatusBar {
@@ -78,28 +80,28 @@ ApplicationWindow {
anchors.fill: parent
}
- Scatterplot {
- id: rpPlot
- objectName: "rpPlot"
+ LinePlot {
+ id: bundlePlot
+ objectName: "bundlePlot"
x: parent.x
y: parent.y
z: 1
anchors.fill: parent
- glyphSize: 3.0
}
Scatterplot {
- id: cpPlot
- objectName: "cpPlot"
+ id: rpPlot
+ objectName: "rpPlot"
x: parent.x
y: parent.y
z: 2
anchors.fill: parent
+ glyphSize: 3.0
}
- LinePlot {
- id: linePlot
- objectName: "linePlot"
+ Scatterplot {
+ id: cpPlot
+ objectName: "cpPlot"
x: parent.x
y: parent.y
z: 3
@@ -237,234 +239,434 @@ ApplicationWindow {
}
// Options panel
- ColumnLayout {
- Layout.alignment: Qt.AlignTop | Qt.AlignLeft
-
- GroupBox {
- Layout.fillWidth: true
- title: "Control points"
- checkable: true
- __checkbox.onClicked: {
- cpPlot.visible = this.checked;
-
- if (this.checked) {
- cpPlot.z = 0;
- rpPlot.z = 0;
- } else {
- cpPlot.z = 0;
- rpPlot.z = 1;
- }
- }
-
- ColumnLayout {
- GroupBox {
- flat: true
- title: "Colors"
-
- GridLayout {
- columns: 2
+ RowLayout {
+ id: optionsPanel
- Label { text: "Map to:" }
- ComboBox {
- id: cpPlotMetricComboBox
- model: metricsModel
- }
+ ColumnLayout {
+ Layout.alignment: Qt.AlignTop | Qt.AlignLeft
- Label { text: "Color map:" }
- ComboBox {
- id: cpPlotColormapCombo
- model: colormapModel
- onActivated:
- Main.setCPColorScale(model.get(index).value);
- }
+ GroupBox {
+ Layout.fillWidth: true
+ title: "Control points"
+ checkable: true
+ __checkbox.onClicked: {
+ cpPlot.visible = this.checked;
+
+ if (this.checked) {
+ cpPlot.z = 0;
+ rpPlot.z = 0;
+ } else {
+ cpPlot.z = 0;
+ rpPlot.z = 1;
}
}
- GroupBox {
- flat: true
- title: "Glyphs"
-
- GridLayout {
- columns: 2
-
- Label { text: "Size:" }
- SpinBox {
- id: cpGlyphSizeSpinBox
- maximumValue: 100
- minimumValue: 6
- decimals: 1
- stepSize: 1
- value: cpPlot.glyphSize
- onValueChanged: cpPlot.glyphSize = this.value
+ ColumnLayout {
+ GroupBox {
+ flat: true
+ title: "Colors"
+
+ GridLayout {
+ columns: 2
+
+ Label { text: "Map to:" }
+ ComboBox {
+ id: cpPlotMetricComboBox
+ model: metricsModel
+ }
+
+ Label { text: "Color map:" }
+ ComboBox {
+ id: cpPlotColormapCombo
+ model: colormapModel
+ onActivated:
+ Main.setCPColorScale(model.get(index).value);
+ }
}
+ }
- Label { text: "Opacity:" }
- Slider {
- id: cpPlotOpacitySlider
- tickmarksEnabled: true
- stepSize: 0.1
- maximumValue: 1
- minimumValue: 0
- value: cpPlot.opacity
- onValueChanged: cpPlot.opacity = this.value
+ GroupBox {
+ flat: true
+ title: "Glyphs"
+
+ GridLayout {
+ columns: 2
+
+ Label { text: "Size:" }
+ SpinBox {
+ id: cpGlyphSizeSpinBox
+ maximumValue: 100
+ minimumValue: 6
+ decimals: 1
+ stepSize: 1
+ value: cpPlot.glyphSize
+ onValueChanged: cpPlot.glyphSize = this.value
+ }
+
+ Label { text: "Opacity:" }
+ Slider {
+ id: cpPlotOpacitySlider
+ tickmarksEnabled: true
+ stepSize: 0.1
+ maximumValue: 1
+ minimumValue: 0
+ value: cpPlot.opacity
+ onValueChanged: cpPlot.opacity = this.value
+ }
}
}
}
}
- }
-
- GroupBox {
- Layout.fillWidth: true
- title: "Regular points"
- checked: true
- checkable: true
- __checkbox.onClicked: {
- rpPlot.visible = this.checked;
- splat.visible = this.checked;
- }
-
- ColumnLayout {
- GroupBox {
- flat: true
- title: "Colors"
-
- GridLayout {
- columns: 2
- Label { text: "Map to:" }
- ComboBox {
- id: rpPlotMetricComboBox
- model: metricsModel
- }
-
- Label { text: "Color map:" }
- ComboBox {
- id: rpPlotColormapCombo
- model: colormapModel
- onActivated:
- Main.setRPColorScale(model.get(index).value);
- }
- }
+ GroupBox {
+ Layout.fillWidth: true
+ title: "Regular points"
+ checked: true
+ checkable: true
+ __checkbox.onClicked: {
+ rpPlot.visible = this.checked;
+ splat.visible = this.checked;
}
- GroupBox {
- flat: true
- title: "Splat"
-
- GridLayout {
- columns: 2
-
- Label { text: "Blur (α):" }
- SpinBox {
- id: alphaSpinBox
- maximumValue: 100
- minimumValue: 1
- value: splat.alpha
- decimals: 2
- stepSize: 1
- onValueChanged: splat.alpha = this.value
+ ColumnLayout {
+ GroupBox {
+ flat: true
+ title: "Colors"
+
+ GridLayout {
+ columns: 2
+
+ Label { text: "Map to:" }
+ ComboBox {
+ id: rpPlotMetricComboBox
+ model: metricsModel
+ }
+
+ Label { text: "Color map:" }
+ ComboBox {
+ id: rpPlotColormapCombo
+ model: colormapModel
+ onActivated:
+ Main.setRPColorScale(model.get(index).value);
+ }
}
+ }
- Label { text: "Radius (β):" }
- SpinBox {
- id: betaSpinBox
- maximumValue: 100
- minimumValue: 1
- value: splat.beta
- decimals: 2
- stepSize: 1
- onValueChanged: splat.beta = this.value
+ GroupBox {
+ flat: true
+ title: "Splat"
+
+ GridLayout {
+ columns: 2
+
+ Label { text: "Blur (α):" }
+ SpinBox {
+ id: alphaSpinBox
+ maximumValue: 100
+ minimumValue: 1
+ value: splat.alpha
+ decimals: 2
+ stepSize: 1
+ onValueChanged: splat.alpha = this.value
+ }
+
+ Label { text: "Radius (β):" }
+ SpinBox {
+ id: betaSpinBox
+ maximumValue: 100
+ minimumValue: 1
+ value: splat.beta
+ decimals: 2
+ stepSize: 1
+ onValueChanged: splat.beta = this.value
+ }
+
+ Label { text: "Opacity:" }
+ Slider {
+ id: splatOpacitySlider
+ tickmarksEnabled: true
+ stepSize: 0.1
+ maximumValue: 1
+ minimumValue: 0
+ value: splat.opacity
+ onValueChanged: splat.opacity = this.value
+ }
}
+ }
- Label { text: "Opacity:" }
- Slider {
- id: splatOpacitySlider
- tickmarksEnabled: true
- stepSize: 0.1
- maximumValue: 1
- minimumValue: 0
- value: splat.opacity
- onValueChanged: splat.opacity = this.value
+ GroupBox {
+ flat: true
+ title: "Glyphs"
+
+ GridLayout {
+ columns: 2
+
+ Label { text: "Size:" }
+ SpinBox {
+ id: rpGlyphSizeSpinBox
+ maximumValue: 100
+ minimumValue: 2
+ decimals: 1
+ stepSize: 1
+ value: rpPlot.glyphSize
+ onValueChanged: rpPlot.glyphSize = this.value
+ }
+
+ Label { text: "Opacity:" }
+ Slider {
+ id: rpPlotOpacitySlider
+ tickmarksEnabled: true
+ stepSize: 0.1
+ maximumValue: 1
+ minimumValue: 0
+ value: rpPlot.opacity
+ onValueChanged: rpPlot.opacity = this.value
+ }
}
}
}
+ }
- GroupBox {
- flat: true
- title: "Glyphs"
-
- GridLayout {
- columns: 2
-
- Label { text: "Size:" }
- SpinBox {
- id: rpGlyphSizeSpinBox
- maximumValue: 100
- minimumValue: 2
- decimals: 1
- stepSize: 1
- value: rpPlot.glyphSize
- onValueChanged: rpPlot.glyphSize = this.value
+ GroupBox {
+ Layout.fillWidth: true
+ id: metricsGroupBox
+ title: "Projection metrics"
+ property RadioButton current: currentMetricRadioButton
+
+ Column {
+ ExclusiveGroup { id: wrtMetricsGroup }
+
+ RadioButton {
+ id: currentMetricRadioButton
+ text: "Current"
+ exclusiveGroup: wrtMetricsGroup
+ checked: true
+ onClicked: {
+ if (!Main.setObserverType(Main.ObserverCurrent)) {
+ metricsGroupBox.current.checked = true;
+ } else {
+ metricsGroupBox.current = this;
+ }
}
-
- Label { text: "Opacity:" }
- Slider {
- id: rpPlotOpacitySlider
- tickmarksEnabled: true
- stepSize: 0.1
- maximumValue: 1
- minimumValue: 0
- value: rpPlot.opacity
- onValueChanged: rpPlot.opacity = this.value
+ }
+ RadioButton {
+ id: diffPreviousMetricRadioButton
+ text: "Diff. to previous"
+ exclusiveGroup: wrtMetricsGroup
+ onClicked: {
+ if (!Main.setObserverType(Main.ObserverDiffPrevious)) {
+ metricsGroupBox.current.checked = true;
+ } else {
+ metricsGroupBox.current = this;
+ }
+ }
+ }
+ RadioButton {
+ id: diffFirstMetricRadioButton
+ text: "Diff. to first"
+ exclusiveGroup: wrtMetricsGroup
+ onClicked: {
+ if (!Main.setObserverType(Main.ObserverDiffFirst)) {
+ metricsGroupBox.current.checked = true;
+ } else {
+ metricsGroupBox.current = this;
+ }
}
}
}
}
}
- GroupBox {
- Layout.fillWidth: true
- id: metricsGroupBox
- title: "Projection metrics"
- property RadioButton current: currentMetricRadioButton
-
- Column {
- ExclusiveGroup { id: wrtMetricsGroup }
-
- RadioButton {
- id: currentMetricRadioButton
- text: "Current"
- exclusiveGroup: wrtMetricsGroup
- checked: true
- onClicked: {
- if (!Main.setObserverType(Main.ObserverCurrent)) {
- metricsGroupBox.current.checked = true;
- } else {
- metricsGroupBox.current = this;
+ ColumnLayout {
+ Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+
+ GroupBox {
+ Layout.fillWidth: true
+ id: bundlingGroupBox
+ title: "Bundling"
+ checked: true
+ checkable: true
+ __checkbox.onClicked: {
+ bundlePlot.visible = this.checked;
+ }
+
+ ColumnLayout {
+ GroupBox {
+ flat: true
+ title: "Main bundling"
+
+ GridLayout {
+ columns: 2
+
+ Label { text: "Iterations:" }
+ SpinBox {
+ maximumValue: 100
+ minimumValue: 0
+ decimals: 0
+ stepSize: 1
+ value: bundlePlot.iterations
+ onValueChanged: bundlePlot.iterations = this.value
+ }
+
+ Label { text: "Kernel size:" }
+ SpinBox {
+ maximumValue: 100
+ minimumValue: 3
+ decimals: 1
+ stepSize: 1
+ value: bundlePlot.kernelSize
+ onValueChanged: bundlePlot.kernelSize = this.value
+ }
+
+ Label { text: "Smoothing factor:" }
+ Slider {
+ tickmarksEnabled: true
+ stepSize: 0.1
+ maximumValue: 1
+ minimumValue: 0
+ value: bundlePlot.smoothingFactor
+ onValueChanged: bundlePlot.smoothingFactor = this.value
+ }
+
+ Label { text: "Smoothing iterations:" }
+ SpinBox {
+ maximumValue: 100
+ minimumValue: 0
+ decimals: 0
+ stepSize: 1
+ value: bundlePlot.smoothingIterations
+ onValueChanged: bundlePlot.smoothingIterations = this.value
+ }
}
}
- }
- RadioButton {
- id: diffPreviousMetricRadioButton
- text: "Diff. to previous"
- exclusiveGroup: wrtMetricsGroup
- onClicked: {
- if (!Main.setObserverType(Main.ObserverDiffPrevious)) {
- metricsGroupBox.current.checked = true;
- } else {
- metricsGroupBox.current = this;
+
+ GroupBox {
+ flat: true
+ title: "Ends bundling"
+
+ GridLayout {
+ columns: 2
+
+ CheckBox {
+ Layout.columnSpan: 2
+ text: "Block endpoints"
+ checked: bundlePlot.blockEndpoints
+ onClicked: bundlePlot.blockEndpoints = this.checked
+ }
+
+ Label { text: "Iterations:" }
+ SpinBox {
+ maximumValue: 100
+ minimumValue: 0
+ decimals: 0
+ stepSize: 1
+ value: bundlePlot.endsIterations
+ onValueChanged: bundlePlot.endsIterations = this.value
+ }
+
+ Label { text: "Kernel size:" }
+ SpinBox {
+ maximumValue: 100
+ minimumValue: 3
+ decimals: 1
+ stepSize: 1
+ value: bundlePlot.endsKernelSize
+ onValueChanged: bundlePlot.endsKernelSize = this.value
+ }
+
+ Label { text: "Smoothing factor:" }
+ Slider {
+ tickmarksEnabled: true
+ stepSize: 0.1
+ maximumValue: 1
+ minimumValue: 0
+ value: bundlePlot.endsSmoothingFactor
+ onValueChanged: bundlePlot.endsSmoothingFactor = this.value
+ }
}
}
- }
- RadioButton {
- id: diffFirstMetricRadioButton
- text: "Diff. to first"
- exclusiveGroup: wrtMetricsGroup
- onClicked: {
- if (!Main.setObserverType(Main.ObserverDiffFirst)) {
- metricsGroupBox.current.checked = true;
- } else {
- metricsGroupBox.current = this;
+
+ GroupBox {
+ flat: true
+ title: "General"
+
+ GridLayout {
+ columns: 2
+
+ Label { text: "Opacity:" }
+ Slider {
+ tickmarksEnabled: true
+ stepSize: 0.1
+ maximumValue: 1
+ minimumValue: 0
+ value: bundlePlot.opacity
+ onValueChanged: bundlePlot.opacity = this.value
+ }
+
+ Label { text: "Edge sampling:" }
+ SpinBox {
+ maximumValue: 100
+ minimumValue: 3
+ decimals: 1
+ stepSize: 1
+ value: bundlePlot.edgeSampling
+ onValueChanged: bundlePlot.edgeSampling = this.value
+ }
+
+ Label { text: "Advection speed:" }
+ Slider {
+ tickmarksEnabled: true
+ stepSize: 0.1
+ maximumValue: 1
+ minimumValue: 0
+ value: bundlePlot.advectionSpeed
+ onValueChanged: bundlePlot.advectionSpeed = this.value
+ }
+
+ Label { text: "Density estimation:" }
+ RowLayout {
+ ExclusiveGroup { id: densityEstimationGroup }
+ RadioButton {
+ text: "Exact"
+ exclusiveGroup: densityEstimationGroup
+ }
+ RadioButton {
+ text: "Fast"
+ exclusiveGroup: densityEstimationGroup
+ checked: true
+ }
+ }
+
+ Label { text: "Bundle shape:" }
+ RowLayout {
+ ExclusiveGroup { id: bundleShapeGroup }
+ RadioButton {
+ text: "FDEB"
+ exclusiveGroup: bundleShapeGroup
+ checked: true
+ }
+ RadioButton {
+ text: "HEB"
+ exclusiveGroup: bundleShapeGroup
+ }
+ }
+
+ Label { text: "Relaxation:" }
+ Slider {
+ tickmarksEnabled: true
+ stepSize: 0.1
+ maximumValue: 1
+ minimumValue: 0
+ value: bundlePlot.relaxation
+ onValueChanged: bundlePlot.relaxation = this.value
+ }
+
+ CheckBox {
+ Layout.columnSpan: 2
+ text: "Use GPU"
+ checked: bundlePlot.bundleGPU
+ onClicked: bundlePlot.bundleGPU = this.checked
+ }
}
}
}
@@ -562,6 +764,17 @@ ApplicationWindow {
}
Action {
+ id: toggleOptionsAction
+ text: "&Options"
+ shortcut: "Ctrl+O"
+ checkable: true
+ checked: true
+ onToggled: {
+ optionsPanel.visible = this.checked;
+ }
+ }
+
+ Action {
id: selectCPsAction
text: "&Control points"
shortcut: "C"