962130c66c
Have a widget-based application that uses qTriangulate for fills and QTriangulatingStroker (and optionally the dash stroke processor) for strokes. The resulting triangle (strip) set is visualized on a simple QPainter canvas, offering the ability to zoom in and examine how the triangulator behaves on a number of example shapes. It is also possible to step through and only have the first N set of triangles drawn. Change-Id: I3a27d86d4ea13a63dd4be0fe81dd4b5ed6e4fa75 Reviewed-by: Andy Nichols <andy.nichols@qt.io>
419 lines
13 KiB
C++
419 lines
13 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2017 The Qt Company Ltd.
|
|
** Contact: https://www.qt.io/licensing/
|
|
**
|
|
** This file is part of the test suite of the Qt Toolkit.
|
|
**
|
|
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
|
|
** Commercial License Usage
|
|
** Licensees holding valid commercial Qt licenses may use this file in
|
|
** accordance with the commercial license agreement provided with the
|
|
** Software or, alternatively, in accordance with the terms contained in
|
|
** a written agreement between you and The Qt Company. For licensing terms
|
|
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
** information use the contact form at https://www.qt.io/contact-us.
|
|
**
|
|
** GNU General Public License Usage
|
|
** Alternatively, this file may be used under the terms of the GNU
|
|
** General Public License version 3 as published by the Free Software
|
|
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
** included in the packaging of this file. Please review the following
|
|
** information to ensure the GNU General Public License requirements will
|
|
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
|
**
|
|
** $QT_END_LICENSE$
|
|
**
|
|
****************************************************************************/
|
|
|
|
#include "triviswidget.h"
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QGroupBox>
|
|
#include <QWheelEvent>
|
|
#include <QScrollBar>
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <QTimer>
|
|
#include <QtGui/private/qtriangulator_p.h>
|
|
#include <QtGui/private/qtriangulatingstroker_p.h>
|
|
#include <QDebug>
|
|
|
|
static const int W = 100;
|
|
static const int H = 100;
|
|
static const int MAX_ZOOM = 512;
|
|
|
|
class ScrollArea : public QScrollArea {
|
|
protected:
|
|
void wheelEvent(QWheelEvent *event) override {
|
|
if (!event->modifiers().testFlag(Qt::ControlModifier))
|
|
QScrollArea::wheelEvent(event);
|
|
}
|
|
};
|
|
|
|
TriangulationVisualizer::TriangulationVisualizer(QWidget *parent)
|
|
: QWidget(parent)
|
|
{
|
|
QVBoxLayout *mainLayout = new QVBoxLayout;
|
|
|
|
QHBoxLayout *headerLayout = new QHBoxLayout;
|
|
|
|
QGroupBox *cbBox = new QGroupBox(QLatin1String("Shape"));
|
|
m_cbShape = new QComboBox;
|
|
QVBoxLayout *cbBoxLayout = new QVBoxLayout;
|
|
cbBoxLayout->addWidget(m_cbShape);
|
|
cbBox->setLayout(cbBoxLayout);
|
|
headerLayout->addWidget(cbBox);
|
|
|
|
m_lbPreview = new QLabel;
|
|
m_lbPreview->setFixedSize(W, H);
|
|
headerLayout->addWidget(m_lbPreview);
|
|
|
|
QGroupBox *typeBox = new QGroupBox(QLatin1String("Type"));
|
|
m_rdStroke = new QRadioButton(QLatin1String("Stroke"));
|
|
m_rdStroke->setChecked(true);
|
|
m_rdFill = new QRadioButton(QLatin1String("Fill"));
|
|
QVBoxLayout *typeBoxLayout = new QVBoxLayout;
|
|
typeBoxLayout->addWidget(m_rdStroke);
|
|
typeBoxLayout->addWidget(m_rdFill);
|
|
typeBox->setLayout(typeBoxLayout);
|
|
headerLayout->addWidget(typeBox);
|
|
|
|
QGroupBox *paramBox = new QGroupBox(QLatin1String("Stroke params"));
|
|
QVBoxLayout *paramBoxLayout = new QVBoxLayout;
|
|
m_spStrokeWidth = new QSpinBox;
|
|
m_spStrokeWidth->setPrefix(QLatin1String("Stroke width: "));
|
|
m_spStrokeWidth->setMinimum(1);
|
|
m_spStrokeWidth->setMaximum(32);
|
|
m_spStrokeWidth->setValue(1);
|
|
m_chDash = new QCheckBox(QLatin1String("Dash stroke"));
|
|
paramBoxLayout->addWidget(m_spStrokeWidth);
|
|
paramBoxLayout->addWidget(m_chDash);
|
|
paramBox->setLayout(paramBoxLayout);
|
|
headerLayout->addWidget(paramBox);
|
|
|
|
m_lbInfo = new QLabel;
|
|
headerLayout->addWidget(m_lbInfo);
|
|
|
|
QGroupBox *animBox = new QGroupBox(QLatin1String("Step through"));
|
|
QVBoxLayout *animBoxLayout = new QVBoxLayout;
|
|
m_chStepEnable = new QCheckBox(QLatin1String("Enable"));
|
|
m_spStepStroke = new QSpinBox;
|
|
m_spStepStroke->setPrefix(QLatin1String("Stroke steps: "));
|
|
m_spStepStroke->setMinimum(3);
|
|
m_spStepStroke->setMaximum(INT_MAX);
|
|
m_spStepStroke->setEnabled(false);
|
|
m_spStepFill = new QSpinBox;
|
|
m_spStepFill->setPrefix(QLatin1String("Fill steps: "));
|
|
m_spStepFill->setMinimum(3);
|
|
m_spStepFill->setMaximum(INT_MAX);
|
|
m_spStepFill->setEnabled(false);
|
|
animBoxLayout->addWidget(m_chStepEnable);
|
|
animBoxLayout->addWidget(m_spStepStroke);
|
|
animBoxLayout->addWidget(m_spStepFill);
|
|
animBox->setLayout(animBoxLayout);
|
|
headerLayout->addWidget(animBox);
|
|
|
|
m_canvas = new TriVisCanvas;
|
|
m_scrollArea = new ScrollArea;
|
|
m_scrollArea->setWidget(m_canvas);
|
|
m_scrollArea->setMinimumSize(W, H);
|
|
|
|
mainLayout->addLayout(headerLayout);
|
|
mainLayout->addWidget(m_scrollArea);
|
|
mainLayout->setStretchFactor(m_scrollArea, 9);
|
|
|
|
setLayout(mainLayout);
|
|
|
|
for (const QString &shapeName : m_canvas->shapes())
|
|
m_cbShape->addItem(shapeName);
|
|
|
|
connect(m_cbShape, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this]() {
|
|
m_canvas->setIndex(m_cbShape->currentIndex());
|
|
m_canvas->retriangulate();
|
|
});
|
|
connect(m_rdFill, &QRadioButton::toggled, [this]() {
|
|
m_canvas->setType(TriVisCanvas::Fill);
|
|
m_canvas->retriangulate();
|
|
});
|
|
connect(m_rdStroke, &QRadioButton::toggled, [this]() {
|
|
m_canvas->setType(TriVisCanvas::Stroke);
|
|
m_canvas->retriangulate();
|
|
});
|
|
|
|
connect(m_spStrokeWidth, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this]() {
|
|
m_canvas->setStrokeWidth(m_spStrokeWidth->value());
|
|
m_canvas->regeneratePreviews();
|
|
m_canvas->retriangulate();
|
|
});
|
|
|
|
connect(m_chDash, &QCheckBox::toggled, [this]() {
|
|
m_canvas->setDashStroke(m_chDash->isChecked());
|
|
m_canvas->regeneratePreviews();
|
|
m_canvas->retriangulate();
|
|
});
|
|
|
|
connect(m_chStepEnable, &QCheckBox::toggled, [this]() {
|
|
bool enable = m_chStepEnable->isChecked();
|
|
m_spStepStroke->setEnabled(enable);
|
|
m_spStepFill->setEnabled(enable);
|
|
if (enable)
|
|
m_canvas->setStepLimits(m_spStepStroke->value(), m_spStepFill->value());
|
|
else
|
|
m_canvas->setStepLimits(0, 0);
|
|
});
|
|
|
|
connect(m_spStepStroke, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this]() {
|
|
m_canvas->setStepLimits(m_spStepStroke->value(), m_spStepFill->value());
|
|
});
|
|
|
|
connect(m_spStepFill, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this]() {
|
|
m_canvas->setStepLimits(m_spStepStroke->value(), m_spStepFill->value());
|
|
});
|
|
|
|
connect(m_canvas, &TriVisCanvas::retriangulated, [this]() {
|
|
updateInfoLabel();
|
|
updatePreviewLabel();
|
|
});
|
|
|
|
connect(m_canvas, &TriVisCanvas::zoomChanged, [this](float oldZoom, float newZoom) {
|
|
QScrollBar *sb = m_scrollArea->horizontalScrollBar();
|
|
float x = sb->value() / oldZoom;
|
|
sb->setValue(x * newZoom);
|
|
sb = m_scrollArea->verticalScrollBar();
|
|
float y = sb->value() / oldZoom;
|
|
sb->setValue(y * newZoom);
|
|
updateInfoLabel();
|
|
|
|
});
|
|
|
|
m_canvas->retriangulate();
|
|
}
|
|
|
|
void TriangulationVisualizer::updateInfoLabel()
|
|
{
|
|
m_lbInfo->setText(QString(QStringLiteral("Type: %1\n%2 vertices (x, y)\n%3 indices\nzoom: %4\nUSE CTRL+WHEEL TO ZOOM"))
|
|
.arg(m_canvas->geomType() == TriVisCanvas::Triangles ? QLatin1String("Triangles") : QLatin1String("Triangle strips"))
|
|
.arg(m_canvas->vertexCount())
|
|
.arg(m_canvas->indexCount())
|
|
.arg(m_canvas->zoomLevel()));
|
|
}
|
|
|
|
void TriangulationVisualizer::updatePreviewLabel()
|
|
{
|
|
m_lbPreview->setPixmap(QPixmap::fromImage(m_canvas->preview()).scaled(m_lbPreview->size()));
|
|
}
|
|
|
|
const int TLX = 10;
|
|
const int TLY = 10;
|
|
|
|
TriVisCanvas::TriVisCanvas(QWidget *parent)
|
|
: QWidget(parent)
|
|
{
|
|
resize(W * m_zoom, H * m_zoom);
|
|
|
|
QPainterPath linePath;
|
|
linePath.moveTo(TLX, TLY);
|
|
linePath.lineTo(TLX + 30, TLY + 30);
|
|
m_paths << linePath;
|
|
|
|
QPainterPath rectPath;
|
|
rectPath.moveTo(TLX, TLY);
|
|
rectPath.lineTo(TLX + 30, TLY);
|
|
rectPath.lineTo(TLX + 30, TLY + 30);
|
|
rectPath.lineTo(TLX, TLY + 30);
|
|
rectPath.lineTo(TLX, TLY);
|
|
m_paths << rectPath;
|
|
|
|
QPainterPath roundRectPath;
|
|
roundRectPath.addRoundedRect(TLX, TLY, TLX + 29, TLY + 29, 5, 5);
|
|
m_paths << roundRectPath;
|
|
|
|
QPainterPath ellipsePath;
|
|
ellipsePath.addEllipse(TLX, TLY, 40, 20);
|
|
m_paths << ellipsePath;
|
|
|
|
QPainterPath cubicPath;
|
|
cubicPath.moveTo(TLX, TLY + 30);
|
|
cubicPath.cubicTo(15, 2, 40, 40, 30, 10);
|
|
m_paths << cubicPath;
|
|
|
|
QPainterPath cubicPath2;
|
|
cubicPath2.moveTo(TLX, TLY + 20);
|
|
cubicPath2.cubicTo(15, 2, 30, 30, 30, 35);
|
|
m_paths << cubicPath2;
|
|
|
|
regeneratePreviews();
|
|
}
|
|
|
|
QStringList TriVisCanvas::shapes() const
|
|
{
|
|
return QStringList()
|
|
<< "line"
|
|
<< "rect"
|
|
<< "roundedrect"
|
|
<< "ellipse"
|
|
<< "cubic curve 1"
|
|
<< "cubic curve 2";
|
|
}
|
|
|
|
void TriVisCanvas::regeneratePreviews()
|
|
{
|
|
m_strokePreviews.clear();
|
|
m_fillPreviews.clear();
|
|
for (int i = 0; i < m_paths.count(); ++i)
|
|
addPreview(i);
|
|
}
|
|
|
|
void TriVisCanvas::addPreview(int idx)
|
|
{
|
|
QPen pen(Qt::black);
|
|
pen.setWidthF(m_strokeWidth);
|
|
if (m_dashStroke)
|
|
pen.setStyle(Qt::DashLine);
|
|
|
|
QImage img(W, H, QImage::Format_RGB32);
|
|
img.fill(Qt::white);
|
|
QPainter p(&img);
|
|
p.translate(-TLX, -TLY);
|
|
p.scale(2, 2);
|
|
p.strokePath(m_paths[idx], pen);
|
|
p.end();
|
|
m_strokePreviews.append(img);
|
|
|
|
img = QImage(W, H, QImage::Format_RGB32);
|
|
img.fill(Qt::white);
|
|
p.begin(&img);
|
|
p.translate(-TLX, -TLY);
|
|
p.scale(2, 2);
|
|
p.fillPath(m_paths[idx], QBrush(Qt::gray));
|
|
p.end();
|
|
m_fillPreviews.append(img);
|
|
}
|
|
|
|
QImage TriVisCanvas::preview() const
|
|
{
|
|
if (m_type == Stroke)
|
|
return m_strokePreviews[m_idx];
|
|
else
|
|
return m_fillPreviews[m_idx];
|
|
}
|
|
|
|
static const qreal SCALE = 100;
|
|
|
|
void TriVisCanvas::retriangulate()
|
|
{
|
|
const QPainterPath &path(m_paths[m_idx]);
|
|
|
|
if (m_type == Stroke) {
|
|
const QVectorPath &vp = qtVectorPathForPath(path);
|
|
const QSize clipSize(W, H);
|
|
const QRectF clip(QPointF(0, 0), clipSize);
|
|
const qreal inverseScale = 1.0 / SCALE;
|
|
|
|
QTriangulatingStroker stroker;
|
|
stroker.setInvScale(inverseScale);
|
|
|
|
QPen pen;
|
|
pen.setWidthF(m_strokeWidth);
|
|
if (m_dashStroke)
|
|
pen.setStyle(Qt::DashLine);
|
|
|
|
if (pen.style() == Qt::SolidLine) {
|
|
stroker.process(vp, pen, clip, 0);
|
|
} else {
|
|
QDashedStrokeProcessor dashStroker;
|
|
dashStroker.setInvScale(inverseScale);
|
|
dashStroker.process(vp, pen, clip, 0);
|
|
QVectorPath dashStroke(dashStroker.points(), dashStroker.elementCount(),
|
|
dashStroker.elementTypes(), 0);
|
|
stroker.process(dashStroke, pen, clip, 0);
|
|
}
|
|
|
|
m_strokeVertices.resize(stroker.vertexCount() / 2);
|
|
if (!m_strokeVertices.isEmpty()) {
|
|
const float *vsrc = stroker.vertices();
|
|
for (int i = 0; i < m_strokeVertices.count(); ++i)
|
|
m_strokeVertices[i].set(vsrc[i * 2], vsrc[i * 2 + 1]);
|
|
}
|
|
} else {
|
|
const QVectorPath &vp = qtVectorPathForPath(path);
|
|
QTriangleSet ts = qTriangulate(vp, QTransform::fromScale(SCALE, SCALE), 1, true);
|
|
const int vertexCount = ts.vertices.count() / 2;
|
|
m_fillVertices.resize(vertexCount);
|
|
Vertex *vdst = reinterpret_cast<Vertex *>(m_fillVertices.data());
|
|
const qreal *vsrc = ts.vertices.constData();
|
|
for (int i = 0; i < vertexCount; ++i)
|
|
vdst[i].set(vsrc[i * 2] / SCALE, vsrc[i * 2 + 1] / SCALE);
|
|
|
|
m_fillIndices.resize(ts.indices.size());
|
|
if (ts.indices.type() == QVertexIndexVector::UnsignedShort) {
|
|
const quint16 *shortD = static_cast<const quint16 *>(ts.indices.data());
|
|
for (int i = 0; i < m_fillIndices.count(); ++i)
|
|
m_fillIndices[i] = shortD[i];
|
|
} else {
|
|
memcpy(m_fillIndices.data(), ts.indices.data(), ts.indices.size() * sizeof(quint32));
|
|
}
|
|
}
|
|
|
|
emit retriangulated();
|
|
update();
|
|
}
|
|
|
|
void TriVisCanvas::paintEvent(QPaintEvent *)
|
|
{
|
|
QPainter p(this);
|
|
p.fillRect(rect(), Qt::white);
|
|
|
|
if (m_type == Stroke) {
|
|
QPointF prevPt[3];
|
|
int cnt = 0;
|
|
for (int i = 0; i < m_strokeVertices.count() && (!m_strokeStepLimit || i < m_strokeStepLimit); ++i) {
|
|
auto &v = m_strokeVertices[i];
|
|
QPointF pt(v.x, v.y);
|
|
pt *= m_zoom;
|
|
if (cnt == 1 || cnt == 2)
|
|
p.drawLine(prevPt[cnt - 1], pt);
|
|
prevPt[cnt] = pt;
|
|
cnt = (cnt + 1) % 3;
|
|
if (!cnt) {
|
|
p.drawLine(pt, prevPt[cnt]);
|
|
i -= 2;
|
|
}
|
|
}
|
|
} else {
|
|
QPointF prevPt[3];
|
|
int cnt = 0;
|
|
for (int i = 0; i < m_fillIndices.count() && (!m_fillStepLimit || i < m_fillStepLimit); ++i) {
|
|
auto &v = m_fillVertices[m_fillIndices[i]];
|
|
QPointF pt(v.x, v.y);
|
|
pt *= m_zoom;
|
|
if (cnt == 1 || cnt == 2)
|
|
p.drawLine(prevPt[cnt - 1], pt);
|
|
prevPt[cnt] = pt;
|
|
cnt = (cnt + 1) % 3;
|
|
if (!cnt)
|
|
p.drawLine(pt, prevPt[cnt]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TriVisCanvas::wheelEvent(QWheelEvent *event)
|
|
{
|
|
int change = 0;
|
|
|
|
if (event->modifiers().testFlag(Qt::ControlModifier)) {
|
|
if (event->delta() > 0 && m_zoom < MAX_ZOOM) {
|
|
m_zoom += 1;
|
|
change = 1;
|
|
} else if (event->delta() < 0 && m_zoom > 1) {
|
|
m_zoom -= 1;
|
|
change = -1;
|
|
}
|
|
}
|
|
|
|
resize(W * m_zoom, H * m_zoom);
|
|
emit zoomChanged(m_zoom - change, m_zoom);
|
|
update();
|
|
}
|