aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/quickshapes/CMakeLists.txt1
-rw-r--r--src/quickshapes/qquickshapecurverenderer.cpp1369
-rw-r--r--src/quickshapes/qquickshapecurverenderer_p.h9
-rw-r--r--src/quickshapes/qquickshapecurverenderer_p_p.h47
-rw-r--r--src/quickshapes/qt_delaunay_triangulator.cpp1439
-rw-r--r--src/quickshapes/qt_quadratic_bezier.cpp184
-rw-r--r--tests/manual/painterpathquickshape/CMakeLists.txt2
-rw-r--r--tests/manual/painterpathquickshape/ControlPanel.qml69
-rw-r--r--tests/manual/painterpathquickshape/ControlPoint.qml18
-rw-r--r--tests/manual/painterpathquickshape/ControlledShape.qml73
-rw-r--r--tests/manual/painterpathquickshape/Mussel.qml42
-rw-r--r--tests/manual/painterpathquickshape/SvgShape.qml24
-rw-r--r--tests/manual/painterpathquickshape/main.qml24
-rw-r--r--tests/manual/painterpathquickshape/svgpathloader.cpp67
-rw-r--r--tests/manual/painterpathquickshape/svgpathloader.h7
15 files changed, 1530 insertions, 1845 deletions
diff --git a/src/quickshapes/CMakeLists.txt b/src/quickshapes/CMakeLists.txt
index 99e5704b39..f5697f974b 100644
--- a/src/quickshapes/CMakeLists.txt
+++ b/src/quickshapes/CMakeLists.txt
@@ -21,7 +21,6 @@ qt_internal_add_qml_module(QuickShapesPrivate
qquickshapegenericrenderer.cpp qquickshapegenericrenderer_p.h
qquickshapesglobal.h qquickshapesglobal_p.h
qquickshapecurverenderer.cpp qquickshapecurverenderer_p.h qquickshapecurverenderer_p_p.h
- qt_delaunay_triangulator.cpp
qt_quadratic_bezier.cpp
qquickshapesoftwarerenderer.cpp qquickshapesoftwarerenderer_p.h
PUBLIC_LIBRARIES
diff --git a/src/quickshapes/qquickshapecurverenderer.cpp b/src/quickshapes/qquickshapecurverenderer.cpp
index 036a6a01cb..2ab282c6af 100644
--- a/src/quickshapes/qquickshapecurverenderer.cpp
+++ b/src/quickshapes/qquickshapecurverenderer.cpp
@@ -7,6 +7,7 @@
#include <QtGui/qvector2d.h>
#include <QtGui/private/qtriangulator_p.h>
+#include <QtGui/private/qtriangulatingstroker_p.h>
#include <QtGui/private/qrhi_p.h>
#include <QtQuick/qsgmaterial.h>
@@ -489,6 +490,18 @@ QVector2D QuadPath::Element::pointAtFraction(float t) const
}
}
+float QuadPath::Element::extent() const
+{
+ // TBD: cache this value if we start using it a lot
+ QVector2D min(qMin(sp.x(), ep.x()), qMin(sp.y(), ep.y()));
+ QVector2D max(qMax(sp.x(), ep.x()), qMax(sp.y(), ep.y()));
+ if (!isLine()) {
+ min = QVector2D(qMin(min.x(), cp.x()), qMin(min.y(), cp.y()));
+ max = QVector2D(qMax(max.x(), cp.x()), qMax(max.y(), cp.y()));
+ }
+ return (max - min).length();
+}
+
// Returns the number of intersections between element and a horizontal line at y.
// The t values of max 2 intersection(s) are stored in the fractions array
int QuadPath::Element::intersectionsAtY(float y, float *fractions) const
@@ -620,7 +633,8 @@ QuadPath::Element::CurvatureFlags QuadPath::coordinateOrderOfElement(const QuadP
QVector2D midPoint = element.midPoint();
// At the midpoint, the tangent of a quad is parallel to the baseline
QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized();
- QVector2D justRightOfMid = midPoint + normal * QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN;
+ float delta = qMin(element.extent() / 100, QQUICKSHAPECURVERENDERER_CONVEX_CHECK_ERROR_MARGIN);
+ QVector2D justRightOfMid = midPoint + (normal * delta);
bool pathContainsPoint = contains(justRightOfMid);
return pathContainsPoint ? Element::FillOnRight : Element::CurvatureFlags(0);
}
@@ -640,6 +654,7 @@ QuadPath QuadPath::fromPainterPath(const QPainterPath &path)
res.reserve(path.elementCount());
res.setFillRule(path.fillRule());
+ QPolygonF quads;
QPointF sp;
for (int i = 0; i < path.elementCount(); ++i) {
QPainterPath::Element element = path.elementAt(i);
@@ -658,9 +673,9 @@ QuadPath QuadPath::fromPainterPath(const QPainterPath &path)
ep = path.elementAt(++i);
QBezier b = QBezier::fromPoints(sp, cp1, cp2, ep);
#ifndef USE_TOQUADRATICS_IN_QBEZIER
- const QPolygonF quads = qt_toQuadratics(b);
+ qt_toQuadratics(b, &quads);
#else
- const QPolygonF quads = b.toQuadratics();
+ quads = b.toQuadratics();
#endif
for (int i = 1; i < quads.size(); i += 2) {
QVector2D cp(quads[i]);
@@ -812,6 +827,22 @@ QuadPath QuadPath::subPathsClosed() const
endElement.m_isSubpathEnd = true;
endElement.ep = m_elements[subStart].sp;
}
+
+ // ### Workaround for triangulator issue: Avoid 3-element paths
+ if (res.elementCount() == 3) {
+ res.splitElementAt(2);
+ res = res.flattened();
+ Q_ASSERT(res.elementCount() == 4);
+ }
+
+ return res;
+}
+
+QuadPath QuadPath::flattened() const
+{
+ QuadPath res;
+ res.reserve(elementCountRecursive());
+ iterateElements([&](const QuadPath::Element &element) { res.m_elements.append(element); });
return res;
}
@@ -831,7 +862,7 @@ void QuadPath::splitElementAt(qsizetype index)
quad1.m_isSubpathStart = parent.m_isSubpathStart;
quad1.m_isSubpathEnd = false;
quad1.m_curvatureFlags = parent.m_curvatureFlags;
- quad1.m_isLine = parent.m_isLine || isPointNearLine(quad1.cp, quad1.sp, quad1.ep);
+ quad1.m_isLine = parent.m_isLine; //### || isPointNearLine(quad1.cp, quad1.sp, quad1.ep);
Element &quad2 = m_childElements[newChildIndex + 1];
quad2.sp = mp;
@@ -840,7 +871,7 @@ void QuadPath::splitElementAt(qsizetype index)
quad2.m_isSubpathStart = false;
quad2.m_isSubpathEnd = parent.m_isSubpathEnd;
quad2.m_curvatureFlags = parent.m_curvatureFlags;
- quad2.m_isLine = parent.m_isLine || isPointNearLine(quad2.cp, quad2.sp, quad2.ep);
+ quad2.m_isLine = parent.m_isLine; //### || isPointNearLine(quad2.cp, quad2.sp, quad2.ep);
#ifndef QT_NO_DEBUG
if (qFuzzyCompare(quad1.sp, quad1.ep) || qFuzzyCompare(quad2.sp, quad2.ep))
@@ -1040,12 +1071,13 @@ void QQuickShapeCurveRenderer::updateNode()
return;
static const int codePath = qEnvironmentVariableIntValue("QT_QUICKSHAPES_ALTERNATIVE_CODE_PATH");
static const int overlapSolving = !qEnvironmentVariableIntValue("QT_QUICKSHAPES_DISABLE_OVERLAP_SOLVER");
+ static bool newStrokeShader = qEnvironmentVariableIntValue("QT_QUICKSHAPES_EXPERIMENTAL_STROKER");
auto addNodes = [&](const PathData &pathData, NodeList *debugNodes) {
- if (codePath == 42)
- return addPathNodesDelaunayTest(pathData, debugNodes);
- else
+ if (codePath == 1)
return addPathNodesBasic(pathData, debugNodes);
+ else
+ return addPathNodesLineShader(pathData, debugNodes);
};
for (PathData &pathData : m_paths) {
@@ -1054,14 +1086,20 @@ void QQuickShapeCurveRenderer::updateNode()
deleteAndClear(&pathData.fillNodes);
deleteAndClear(&pathData.debugNodes);
+ static bool useTriangulatingStroker = !qEnvironmentVariableIntValue("QT_QUICKSHAPES_PAINTERPATH_STROKER");
bool createStrokePath = pathData.isStrokeVisible() && !pathData.useFragmentShaderStroker();
static bool simplifyPath = qEnvironmentVariableIntValue("QT_QUICKSHAPES_SIMPLIFY_PATHS") != 0;
pathData.path = simplifyPath ? QuadPath::fromPainterPath(pathData.originalPath.simplified()) : QuadPath::fromPainterPath(pathData.originalPath);
pathData.path.setFillRule(pathData.fillRule);
- if (createStrokePath)
+ bool solidStroke = pathData.pen.style() == Qt::SolidLine && pathData.pen.isSolid();
+ if (newStrokeShader && solidStroke) {
+ pathData.qPath = pathData.path;
+ pathData.qPath.addCurvatureData(); // ### Can we unify with fill below?
+ } else if (createStrokePath) {
pathData.fillPath = pathData.path.toPainterPath(); // Without subpath closing etc.
+ }
if (pathData.isFillVisible()) {
pathData.path = pathData.path.subPathsClosed();
@@ -1072,27 +1110,33 @@ void QQuickShapeCurveRenderer::updateNode()
}
if (createStrokePath) {
- QPainterPathStroker stroker(pathData.pen);
- QPainterPath strokePath = stroker.createStroke(pathData.fillPath);
-
- // Solid strokes are sometimes created with self-overlaps in the joins,
- // causing the overlap detection to freak out. So while this is expensive
- // we have to make sure the overlaps are removed first.
- static bool simplifyStroke = qEnvironmentVariableIntValue("QT_QUICKSHAPES_DISABLE_SIMPLIFY_STROKE") == 0;
- if (pathData.pen.isSolid() && simplifyStroke)
- strokePath = strokePath.simplified();
- QuadPath strokeQuadPath = QuadPath::fromPainterPath(strokePath);
- strokeQuadPath.addCurvatureData();
- strokePath = strokeQuadPath.toPainterPath();
- if (overlapSolving)
- solveOverlaps(strokeQuadPath);
-
- PathData strokeData = pathData;
- strokeData.path = strokeQuadPath;
- strokeData.fillPath = strokePath;
- strokeData.gradientType = NoGradient;
- strokeData.fillColor = pathData.pen.color();
- pathData.strokeNodes = addNodes(strokeData, &pathData.debugNodes);
+ if (newStrokeShader && solidStroke) {
+ pathData.strokeNodes = addNodesStrokeShader(pathData, &pathData.debugNodes);
+ } else if (useTriangulatingStroker && solidStroke) {
+ pathData.strokeNodes = addStrokeNodes(pathData, &pathData.debugNodes);
+ } else {
+ QPainterPathStroker stroker(pathData.pen);
+ QPainterPath strokePath = stroker.createStroke(pathData.fillPath);
+
+ // Solid strokes are sometimes created with self-overlaps in the joins,
+ // causing the overlap detection to freak out. So while this is expensive
+ // we have to make sure the overlaps are removed first.
+ static bool simplifyStroke = qEnvironmentVariableIntValue("QT_QUICKSHAPES_DISABLE_SIMPLIFY_STROKE") == 0;
+ if (pathData.pen.style() == Qt::SolidLine && simplifyStroke)
+ strokePath = strokePath.simplified();
+ QuadPath strokeQuadPath = QuadPath::fromPainterPath(strokePath);
+ strokeQuadPath.addCurvatureData();
+ strokePath = strokeQuadPath.toPainterPath();
+ if (overlapSolving)
+ solveOverlaps(strokeQuadPath);
+
+ PathData strokeData = pathData;
+ strokeData.path = strokeQuadPath;
+ strokeData.fillPath = strokePath;
+ strokeData.gradientType = NoGradient;
+ strokeData.fillColor = pathData.pen.color();
+ pathData.strokeNodes = addNodes(strokeData, &pathData.debugNodes);
+ }
}
} else if (pathData.m_dirty & UniformsDirty) {
for (auto &pathNode : std::as_const(pathData.fillNodes))
@@ -1100,7 +1144,10 @@ void QQuickShapeCurveRenderer::updateNode()
if (pathData.pen.style() != Qt::NoPen && pathData.pen.color().alpha() > 0) {
for (auto &strokeNode : std::as_const(pathData.strokeNodes))
- static_cast<QQuickShapeLoopBlinnNode *>(strokeNode)->color = pathData.pen.color();
+ if (newStrokeShader) // #### Remove this when we have a dedicated stroke shader
+ static_cast<QQuickShapeLoopBlinnNode *>(strokeNode)->strokeColor = pathData.pen.color();
+ else
+ static_cast<QQuickShapeLoopBlinnNode *>(strokeNode)->color = pathData.pen.color();
}
}
pathData.m_dirty &= ~(GeometryDirty | UniformsDirty);
@@ -1320,34 +1367,562 @@ QSGGeometryNode *QQuickShapeCurveRenderer::addLoopBlinnNodes(const QTriangleSet
return node;
}
-static bool testTriangulation(const QList<QtPathVertex> &vertices, const QList<QtPathEdge> &edges, const QList<QtPathTriangle> &triangles)
+// Input coordinate space is pre-mapped so that (0, 0) maps to [0, 0] in uv space.
+// v1 maps to [1,0], v2 maps to [0,1]. p is the point to be mapped to uv in this space (i.e. vector from p0)
+static inline QVector2D uvForPoint(QVector2D v1, QVector2D v2, QVector2D p)
+{
+ double divisor = v1.x() * v2.y() - v2.x() * v1.y();
+
+ float u = (p.x() * v2.y() - p.y() * v2.x()) / divisor;
+ float v = (p.y() * v1.x() - p.x() * v1.y()) / divisor;
+
+ return {u, v};
+}
+
+// Find uv coordinates for the point p, for a quadratic curve from p0 to p2 with control point p1
+// also works for a line from p0 to p2, where p1 is on the inside of the path relative to the line
+static inline QVector2D curveUv(QVector2D p0, QVector2D p1, QVector2D p2, QVector2D p)
+{
+ QVector2D v1 = 2 * (p1 - p0);
+ QVector2D v2 = p2 - v1 - p0;
+ return uvForPoint(v1, v2, p - p0);
+}
+
+QVector3D QuadPath::Element::uvForPoint(QVector2D p) const
+{
+ auto uv = curveUv(sp, cp, ep, p);
+ if (m_isLine)
+ return { uv.x(), uv.y(), 0.0f };
+ else
+ return { uv.x(), uv.y(), (m_curvatureFlags & Convex) ? -1.0f : 1.0f };
+}
+
+static inline QVector2D calcNormalVector(QVector2D a, QVector2D b)
+{
+ auto v = b - a;
+ return {v.y(), -v.x()};
+}
+
+// The sign of the return value indicates which side of the line defined by a and n the point p falls
+static inline float testSideOfLineByNormal(QVector2D a, QVector2D n, QVector2D p)
+{
+ float dot = QVector2D::dotProduct(p - a, n);
+ return dot;
+};
+
+template<typename Func>
+void iteratePath(const QuadPath &path, int index, Func &&lambda)
{
- // Verify that all our edges are part of a triangle:
- bool ok = true;
- auto edgeMatch = [](const QtPathEdge &edge, quint32 v1Index, quint32 v2Index) -> bool {
- return (edge.startIndex == v1Index && edge.endIndex == v2Index)
- || (edge.startIndex == v2Index && edge.endIndex == v1Index);
+ const auto &element = path.elementAt(index);
+ if (element.childCount() == 0) {
+ lambda(element, index);
+ } else {
+ for (int i = 0; i < element.childCount(); ++i)
+ iteratePath(path, element.indexOfChild(i), lambda);
+ }
+}
+QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesLineShader(const PathData &pathData, NodeList *debugNodes)
+{
+ //qDebug() << "========= STARTING ===========" << pathData.path;
+
+
+ QVector<QSGGeometryNode *> ret;
+ const QColor &color = pathData.fillColor;
+ QPainterPath internalHull;
+ internalHull.setFillRule(pathData.path.fillRule());
+
+
+ QVector<QQuickShapeLoopBlinnNode::LoopBlinnVertex> vertexBuffer;
+ QVector<quint32> indices;
+ bool visualizeDebug = debugVisualization() & DebugCurves;
+ const float dbg = visualizeDebug ? 0.5f : 0.0f;
+ QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
+
+ QHash<QPair<float, float>, int> linePointHash;
+ QHash<QPair<float, float>, int> concaveControlPointHash;
+ QHash<QPair<float, float>, int> convexPointHash;
+
+
+ auto toRoundedPair = [](const QPointF &p) -> QPair<float, float> {
+ return qMakePair(qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f);
+ };
+
+ auto toRoundedVec2D = [](const QPointF &p) -> QVector2D {
+ return { qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f };
+ };
+
+ auto roundVec2D = [](const QVector2D &p) -> QVector2D {
+ return { qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f };
+ };
+
+ auto addVertexDataForPoint = [&vertexBuffer, dbg](const QuadPath::Element &element, const QVector2D &p) {
+ auto uv = element.uvForPoint(p);
+
+ float r = 0.0f, g = 0.0f, b = 0.0f;
+ if (element.isLine()) {
+ g = b = 1.0f;
+ } else if (element.isConvex()) {
+ r = 1.0;
+ } else { // concave
+ b = 1.0;
+ }
+
+ vertexBuffer.append({p.x(), p.y(), uv.x(), uv.y(), uv.z(), r, g, b, dbg});
+ };
+
+ auto addCurveTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
+ int currentVertex = vertexBuffer.count();
+ addVertexDataForPoint(element, sp);
+ addVertexDataForPoint(element, cp);
+ addVertexDataForPoint(element, ep);
+ indices << currentVertex << currentVertex + 1 << currentVertex + 2;
+ wfVertices.append({sp.x(), sp.y(), 1.0f, 0.0f, 0.0f}); // 0
+ wfVertices.append({cp.x(), cp.y(), 0.0f, 1.0f, 0.0f}); // 1
+ wfVertices.append({ep.x(), ep.y(), 0.0f, 0.0f, 1.0f}); // 2
+ };
+
+ // Find a point on the other side of the line
+ auto findPointOtherSide = [](const QVector2D &startPoint, const QVector2D &endPoint, const QVector2D &referencePoint){
+
+ QVector2D baseLine = endPoint - startPoint;
+ QVector2D insideVector = referencePoint - startPoint;
+ //QVector2D midPoint = (endPoint + startPoint) / 2; // ??? Should we use midPoint instead of startPoint??
+ QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()); // TODO: limit size of triangle
+
+ bool swap = QVector2D::dotProduct(insideVector, normal) < 0;
+
+ return swap ? startPoint + normal : startPoint - normal;
+ };
+
+ auto addLineTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
+ int currentVertex = vertexBuffer.count();
+ addCurveTriangle(element, sp, ep, cp);
+ // Add a triangle on the outer side of the line to get some more AA
+ // The new point replaces cp (currentVertex+1)
+ QVector2D op = findPointOtherSide(sp, ep, cp); //sp - (cp - ep);
+ wfVertices.append({op.x(), op.y(), 0.0f, 1.0f, 0.0f}); // replacing cp (1)
+ auto uv = element.uvForPoint(op);
+ vertexBuffer.append({op.x(), op.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 1.0f, dbg});
+ indices << currentVertex << currentVertex + 3 << currentVertex + 2;
+ };
+
+ auto addConvexTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
+ int currentVertex = vertexBuffer.count();
+ addCurveTriangle(element, sp, ep, cp);
+ // Add two triangles on the outer side to get some more AA
+
+ // First triangle on the line sp-cp, replacing ep (currentVertex+2)
+ {
+ QVector2D op = findPointOtherSide(sp, cp, ep); //sp - (ep - cp);
+ wfVertices.append({op.x(), op.y(), 0.0f, 0.0f, 1.0f}); // replacing ep (2)
+ auto uv = element.uvForPoint(op);
+ vertexBuffer.append({op.x(), op.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 1.0f, dbg});
+ indices << currentVertex << currentVertex + 1 << currentVertex + 3;
+ }
+
+ // Second triangle on the line ep-cp, replacing sp (currentVertex+0)
+ {
+ QVector2D op = findPointOtherSide(ep, cp, sp); //ep - (sp - cp);
+ wfVertices.append({op.x(), op.y(), 1.0f, 0.0f, 0.0f}); // replacing sp (0)
+ auto uv = element.uvForPoint(op);
+ vertexBuffer.append({op.x(), op.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 1.0f, dbg});
+ indices << currentVertex + 4 << currentVertex + 1 << currentVertex + 2;
+ }
+
+ };
+
+
+ // This is guaranteed to be in safe space (the curve will never enter the triangle)
+ // ### This is the opposite of what we really want: it's going to be extremely thin when we need it,
+ // and big when it's completely pointless, but a thicker triangle could be going into negative space
+ auto oppositePoint = [](const QVector2D &startPoint, const QVector2D &endPoint, const QVector2D &controlPoint) -> QVector2D {
+ return startPoint + 2 * (endPoint - controlPoint);
+ };
+
+ // Identical to addLineTriangle, except for how op is calculated
+ auto addConcaveTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp){
+ int currentVertex = vertexBuffer.count();
+ addCurveTriangle(element, sp, ep, cp);
+ // Add an outer triangle to give extra AA for very flat curves
+ QVector2D op = oppositePoint(sp, ep, cp);
+ // The new point replaces cp (currentVertex+1)
+ wfVertices.append({op.x(), op.y(), 0.0f, 1.0f, 0.0f}); // replacing cp (1)
+ auto uv = element.uvForPoint(op);
+ vertexBuffer.append({op.x(), op.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 1.0f, dbg});
+ indices << currentVertex << currentVertex + 3 << currentVertex + 2;
+ };
+
+ auto addFillTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3){
+ int currentVertex = vertexBuffer.count();
+ constexpr QVector3D uv(0.0, 1.0, -1.0);
+ vertexBuffer.append({p1.x(), p1.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 0.0f, dbg});
+ vertexBuffer.append({p2.x(), p2.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 0.0f, dbg});
+ vertexBuffer.append({p3.x(), p3.y(), uv.x(), uv.y(), uv.z(), 0.0f, 1.0f, 0.0f, dbg});
+ indices << currentVertex << currentVertex + 1 << currentVertex + 2;
+ wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f}); // 0
+ wfVertices.append({p3.x(), p3.y(), 0.0f, 1.0f, 0.0f}); // 1
+ wfVertices.append({p2.x(), p2.y(), 0.0f, 0.0f, 1.0f}); // 2
+ };
+
+
+
+ for (int i = 0; i < pathData.path.elementCount(); ++i)
+ iteratePath(pathData.path, i, [&](const QuadPath::Element &element, int index){
+ QPointF sp(element.startPoint().toPointF()); //### to much conversion to and from pointF
+ QPointF cp(element.controlPoint().toPointF());
+ QPointF ep(element.endPoint().toPointF());
+ if (element.isSubpathStart())
+ internalHull.moveTo(sp);
+ if (element.isLine()) {
+ internalHull.lineTo(ep);
+ //lineSegments.append(QLineF(sp, ep));
+ linePointHash.insert(toRoundedPair(sp), index);
+ } else {
+ if (element.isConvex()) {
+ internalHull.lineTo(ep);
+ addConvexTriangle(element, toRoundedVec2D(sp), toRoundedVec2D(ep), toRoundedVec2D(cp));
+ convexPointHash.insert(toRoundedPair(sp), index);
+ } else {
+ internalHull.lineTo(cp);
+ internalHull.lineTo(ep);
+ addConcaveTriangle(element, toRoundedVec2D(sp), toRoundedVec2D(ep), toRoundedVec2D(cp));
+ concaveControlPointHash.insert(toRoundedPair(cp), index);
+ }
+ }
+ });
+ //qDebug() << "Curves: i" << indices.size() << "v" << vertexBuffer.size() << "w" << wfVertices.size();
+
+ auto makeHashable = [](const QVector2D &p) -> QPair<float, float> {
+ return qMakePair(qRound(p.x() * 32.0f) / 32.0f, qRound(p.y() * 32.0f) / 32.0f);
+ };
+ // Points in p are already rounded do 1/32
+ // Returns false if the triangle needs to be split. Adds the triangle to the graphics buffers and returns true otherwise.
+
+ // TODO: Does not handle ambiguous vertices that are on multiple unrelated lines/curves
+
+ auto onSameSideOfLine = [](const QVector2D &p1, const QVector2D &p2, const QVector2D &linePoint, const QVector2D &lineNormal) {
+ float side1 = testSideOfLineByNormal(linePoint, lineNormal, p1);
+ float side2 = testSideOfLineByNormal(linePoint, lineNormal, p2);
+ return side1 * side2 >= 0;
+ };
+
+ auto pointInSafeSpace = [&](const QVector2D &p, const QuadPath::Element &element) -> bool {
+ const QVector2D a = element.startPoint();
+ const QVector2D b = element.endPoint();
+ const QVector2D c = element.controlPoint();
+ // There are "safe" areas of the curve also across the baseline: the curve can never cross:
+ // line1: the line through A and B'
+ // line2: the line through B and A'
+ // Where A' = A "mirrored" through C and B' = B "mirrored" through C
+ const QVector2D n1 = calcNormalVector(a, c + (c - b));
+ const QVector2D n2 = calcNormalVector(b, c + (c - a));
+ bool safeSideOf1 = onSameSideOfLine(p, c, a, n1);
+ bool safeSideOf2 = onSameSideOfLine(p, c, b, n2);
+ return safeSideOf1 && safeSideOf2;
+ };
+
+ auto handleTriangle = [&](const QVector2D (&p)[3]) -> bool {
+ int lineElementIndex = -1;
+ int concaveElementIndex = -1;
+ int convexElementIndex = -1;
+
+ bool foundElement = false;
+ int si = -1;
+ int ei = -1;
+ for (int i = 0; i < 3; ++i) {
+ if (auto found = linePointHash.constFind(makeHashable(p[i])); found != linePointHash.constEnd()) {
+ // check if this triangle is on a line, i.e. if one point is the sp and another is the ep of the same path element
+ const auto &element = pathData.path.elementAt(*found);
+ //qDebug() << " " << element;
+ for (int j = 0; j < 3; ++j) {
+ if (i != j && roundVec2D(element.endPoint()) == p[j]) {
+ if (foundElement)
+ return false; // More than one edge on path: must split
+ lineElementIndex = *found;
+ si = i;
+ ei = j;
+ //qDebug() << "FOUND IT!!!!" << p[i] << p[j] << lineElementIndex;
+ foundElement = true;
+ }
+ }
+ } else if (auto found = concaveControlPointHash.constFind(makeHashable(p[i])); found != concaveControlPointHash.constEnd()) {
+ // check if this triangle is on the tangent line of a concave curve,
+ // i.e if one point is the cp, and the other is sp or ep
+ // TODO: clean up duplicated code (almost the same as the lineElement path above)
+ const auto &element = pathData.path.elementAt(*found);
+ for (int j = 0; j < 3; ++j) {
+ if (i == j)
+ continue;
+ if (roundVec2D(element.startPoint()) == p[j] || roundVec2D(element.endPoint()) == p[j]) {
+ if (foundElement)
+ return false; // More than one edge on path: must split
+ concaveElementIndex = *found;
+ // The tangent line is p[i] - p[j]
+ si = i; // we may not need these
+ ei = j;
+ //qDebug() << "FOUND IT!!!!" << p[i] << p[j] << lineElementIndex;
+ foundElement = true;
+ }
+ }
+ } else if (auto found = convexPointHash.constFind(makeHashable(p[i])); found != convexPointHash.constEnd()) {
+ // check if this triangle is on a curve, i.e. if one point is the sp and another is the ep of the same path element
+ const auto &element = pathData.path.elementAt(*found);
+ for (int j = 0; j < 3; ++j) {
+ if (i != j && roundVec2D(element.endPoint()) == p[j]) {
+ if (foundElement)
+ return false; // More than one edge on path: must split
+ convexElementIndex = *found;
+ si = i;
+ ei = j;
+ //qDebug() << "FOUND IT!!!!" << p[i] << p[j] << convexElementIndex;
+ foundElement = true;
+ }
+ }
+ }
+ }
+ if (lineElementIndex != -1) {
+ int ci = (6 - si - ei) % 3; // 1+2+3 is 6, so missing number is 6-n1-n2
+ addLineTriangle(pathData.path.elementAt(lineElementIndex), p[si], p[ei], p[ci]);
+ } else if (concaveElementIndex != -1) {
+ addCurveTriangle(pathData.path.elementAt(concaveElementIndex), p[0], p[1], p[2]);
+ } else if (convexElementIndex != -1) {
+ int oi = (6 - si - ei) % 3;
+ const auto &otherPoint = p[oi];
+ const auto &element = pathData.path.elementAt(convexElementIndex);
+ // We have to test whether the triangle can cross the line TODO: use the toplevel element's safe space
+ bool safeSpace = pointInSafeSpace(otherPoint, element);
+ if (safeSpace) {
+ addCurveTriangle(element, p[0], p[1], p[2]);
+ } else {
+ //qDebug() << "Point" << otherPoint << "element" << element << "safe" << safeSpace;
+ // Find a point inside the triangle that's also in the safe space
+ QVector2D newPoint = (p[0] + p[1] + p[2]) / 3;
+ // We should calculate the point directly, but just do a lazy implementation for now:
+ for (int i = 0; i < 7; ++i) {
+ safeSpace = pointInSafeSpace(newPoint, element);
+ //qDebug() << "UNSAFE space round" << i << "checking" << newPoint << safeSpace;
+ if (safeSpace)
+ break;
+ newPoint = (p[si] + p[ei] + newPoint) / 3;
+ }
+ if (safeSpace) {
+ // Split triangle. We know the original triangle is only on one path element, so the other triangles are both fill.
+ // Curve triangle is (sp, ep, np)
+ addCurveTriangle(element, p[si], p[ei], newPoint);
+ // The other two are (sp, op, np) and (ep, op, np)
+ addFillTriangle(p[si], p[oi], newPoint);
+ addFillTriangle(p[ei], p[oi], newPoint);
+ } else {
+ // fallback to fill if we can't find a point in safe space
+ addFillTriangle(p[0], p[1], p[2]);
+ }
+ }
+
+ } else {
+ addFillTriangle(p[0], p[1], p[2]);
+ }
+ return true;
};
+ QTriangleSet triangles = qTriangulate(internalHull);
+
+ const quint32 *idxTable = static_cast<const quint32 *>(triangles.indices.data());
+ for (int triangle = 0; triangle < triangles.indices.size() / 3; ++triangle) {
+ const quint32 *idx = &idxTable[triangle * 3];
- for (const auto &edge : edges) {
- // Just do it the simplest way possible, and O(n^2) be damned
- bool found = false;
- for (const auto &triangle : triangles) {
- if (edgeMatch(edge, triangle.v1Index, triangle.v2Index)
- || edgeMatch(edge, triangle.v2Index, triangle.v3Index)
- || edgeMatch(edge, triangle.v3Index, triangle.v1Index)) {
- found = true;
- break;
+ QVector2D p[3];
+ for (int i = 0; i < 3; ++i) {
+ p[i] = toRoundedVec2D(QPointF(triangles.vertices.at(idx[i]*2), triangles.vertices.at(idx[i]*2 + 1)));
+ }
+ bool needsSplit = !handleTriangle(p);
+ if (needsSplit) {
+ QVector2D c = (p[0] + p[1] + p[2]) / 3;
+ //qDebug() << "Split!!! New point" << c;
+ for (int i = 0; i < 3; ++i) {
+ qSwap(c, p[i]);
+ //qDebug() << "Adding split triangle" << p[0] << p[1] << p[2];
+ handleTriangle(p);
+ qSwap(c, p[i]);
}
}
- if (!found) {
- qDebug() << "*** MISSING LINK ***" << edge.startIndex << edge.endIndex
- << vertices[edge.startIndex].point << vertices[edge.endIndex].point;
- ok = false;
+ }
+
+ if (indices.size() > 0) {
+ auto *node = new QQuickShapeLoopBlinnNode(pathData.gradientType);
+ node->color = color;
+ node->fillGradient = pathData.gradient;
+
+ if (pathData.useFragmentShaderStroker() && pathData.isStrokeVisible() && pathData.pen.isSolid()) {
+ node->strokeColor = pathData.pen.color();
+ node->strokeWidth = pathData.pen.widthF();
}
+
+ QSGGeometry *g = new QSGGeometry(QQuickShapeLoopBlinnNode::attributes(),
+ vertexBuffer.size(), indices.size(), QSGGeometry::UnsignedIntType);
+ node->setGeometry(g);
+ g->setDrawingMode(QSGGeometry::DrawTriangles);
+
+ //qDebug() << "gvc" << g->vertexCount();
+
+ memcpy(g->vertexData(),
+ vertexBuffer.data(),
+ g->vertexCount() * g->sizeOfVertex());
+ memcpy(g->indexData(),
+ indices.data(),
+ indices.size() * g->sizeOfIndex());
+
+ m_rootNode->appendChildNode(node);
+ ret.append(node);
+ }
+ const bool wireFrame = debugVisualization() & DebugWireframe;
+ if (wireFrame) {
+ QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
+
+ //QVarLengthArray<quint32> indices;
+
+ QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
+ wfVertices.size(),
+ indices.size(),
+ QSGGeometry::UnsignedIntType);
+ wfNode->setGeometry(wfg);
+
+ wfg->setDrawingMode(QSGGeometry::DrawTriangles);
+ memcpy(wfg->indexData(),
+ indices.data(),
+ indices.size() * wfg->sizeOfIndex());
+ memcpy(wfg->vertexData(),
+ wfVertices.data(),
+ wfg->vertexCount() * wfg->sizeOfVertex());
+
+ m_rootNode->appendChildNode(wfNode);
+ debugNodes->append(wfNode);
}
- return ok;
+
+ return ret;
+}
+
+QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addStrokeNodes(const PathData &pathData, NodeList *debugNodes)
+{
+ QVector<QSGGeometryNode *> ret;
+ const QColor &color = pathData.pen.color();
+
+ QVector<QQuickShapeLoopBlinnNode::LoopBlinnVertex> vertexBuffer;
+ QVector<quint32> indices;
+ bool visualizeDebug = debugVisualization() & DebugCurves;
+ const float dbg = visualizeDebug ? 0.5f : 0.0f;
+ QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
+
+ QTriangulatingStroker stroker;
+ const QVectorPath &vp = qtVectorPathForPath(pathData.fillPath);
+ QPen pen = pathData.pen;
+ stroker.process(vp, pen, {}, {});
+
+ auto findPointOtherSide = [](const QVector2D &startPoint, const QVector2D &endPoint, const QVector2D &referencePoint){
+
+ QVector2D baseLine = endPoint - startPoint;
+ QVector2D insideVector = referencePoint - startPoint;
+ //QVector2D midPoint = (endPoint + startPoint) / 2; // ??? Should we use midPoint instead of startPoint??
+ QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()); // TODO: limit size of triangle
+
+ bool swap = QVector2D::dotProduct(insideVector, normal) < 0;
+
+ return swap ? startPoint + normal : startPoint - normal;
+ };
+
+ static bool disableExtraTriangles = qEnvironmentVariableIntValue("QT_QUICKSHAPES_WIP_DISABLE_EXTRA_STROKE_TRIANGLES");
+
+ auto addStrokeTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3, bool){
+ int currentVertex = vertexBuffer.count();
+ //constexpr QVector3D uv(0.0, 1.0, -1.0);
+
+
+ if (p1 == p2 || p2 == p3) {
+ //qDebug() << "Skipping triangle" << p1 << p2 << p3;
+ return;
+ }
+
+
+ vertexBuffer.append({p1.x(), p1.y(), 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, dbg}); // edge start
+ vertexBuffer.append({p2.x(), p2.y(), 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, dbg}); // inner
+ vertexBuffer.append({p3.x(), p3.y(), 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, dbg}); // edge end
+ indices << currentVertex << currentVertex + 1 << currentVertex + 2;
+ wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f}); // 0
+ wfVertices.append({p2.x(), p2.y(), 0.0f, 0.1f, 0.0f}); // 1
+ wfVertices.append({p3.x(), p3.y(), 0.0f, 0.0f, 1.0f}); // 2
+
+ if (!disableExtraTriangles) {
+ // Add a triangle on the outer side of the line to get some more AA
+ // The new point replaces p2 (currentVertex+1)
+ QVector2D op = findPointOtherSide(p1, p3, p2);
+ wfVertices.append({op.x(), op.y(), 0.0f, 1.0f, 0.0f}); // replacing p2
+ vertexBuffer.append({op.x(), op.y(), 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, dbg});
+ indices << currentVertex << currentVertex + 3 << currentVertex + 2;
+ }
+ };
+
+ const int vertCount = stroker.vertexCount() / 2;
+ const float *verts = stroker.vertices();
+ for (int i = 0; i < vertCount - 2; ++i) {
+ QVector2D p[3];
+ for (int j = 0; j < 3; ++j) {
+ p[j] = QVector2D(verts[(i+j)*2], verts[(i+j)*2 + 1]);
+ }
+ bool isOdd = i % 2;
+ addStrokeTriangle(p[0], p[1], p[2], isOdd);
+ }
+
+ if (indices.size() > 0) {
+ auto *node = new QQuickShapeLoopBlinnNode(pathData.gradientType);
+ node->color = color;
+ node->fillGradient = pathData.gradient;
+
+ if (pathData.useFragmentShaderStroker() && pathData.isStrokeVisible() && pathData.pen.isSolid()) {
+ node->strokeColor = pathData.pen.color();
+ node->strokeWidth = pathData.pen.widthF();
+ }
+
+ QSGGeometry *g = new QSGGeometry(QQuickShapeLoopBlinnNode::attributes(),
+ vertexBuffer.size(), indices.size(), QSGGeometry::UnsignedIntType);
+ node->setGeometry(g);
+ g->setDrawingMode(QSGGeometry::DrawTriangles);
+
+ //qDebug() << "gvc" << g->vertexCount();
+
+ memcpy(g->vertexData(),
+ vertexBuffer.data(),
+ g->vertexCount() * g->sizeOfVertex());
+ memcpy(g->indexData(),
+ indices.data(),
+ indices.size() * g->sizeOfIndex());
+
+ m_rootNode->appendChildNode(node);
+ ret.append(node);
+ }
+ const bool wireFrame = debugVisualization() & DebugWireframe;
+ if (wireFrame) {
+ QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
+
+ //QVarLengthArray<quint32> indices;
+
+ QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
+ wfVertices.size(),
+ indices.size(),
+ QSGGeometry::UnsignedIntType);
+ wfNode->setGeometry(wfg);
+
+ wfg->setDrawingMode(QSGGeometry::DrawTriangles);
+ memcpy(wfg->indexData(),
+ indices.data(),
+ indices.size() * wfg->sizeOfIndex());
+ memcpy(wfg->vertexData(),
+ wfVertices.data(),
+ wfg->vertexCount() * wfg->sizeOfVertex());
+
+ m_rootNode->appendChildNode(wfNode);
+ debugNodes->append(wfNode);
+ }
+
+ return ret;
}
void QQuickShapeCurveRenderer::setRootNode(QQuickShapeCurveNode *node)
@@ -1377,6 +1952,8 @@ void QQuickShapeCurveRenderer::deleteAndClear(NodeList *nodeList)
nodeList->clear();
}
+namespace {
+
/*
Clever triangle overlap algorithm. Stack Overflow says:
@@ -1388,11 +1965,11 @@ void QQuickShapeCurveRenderer::deleteAndClear(NodeList *nodeList)
*/
// The sign of the determinant tells the winding order
-static inline double determinant(QVector2D &p1, QVector2D &p2, QVector2D &p3)
+static inline double determinant(const QVector2D &p1, const QVector2D &p2, const QVector2D &p3)
{
return p1.x() * (p2.y() - p3.y())
- + p2.x() * (p3.y() - p1.y())
- + p3.x() * (p1.y() - p2.y());
+ + p2.x() * (p3.y() - p1.y())
+ + p3.x() * (p1.y() - p2.y());
}
// Fix the triangle so that the determinant is positive
@@ -1499,6 +2076,522 @@ static bool isOverlap(const QuadPath &path, qsizetype index, const QVector2D &ve
return checkTriangleContains(vertex, elem.startPoint(), elem.controlPoint(), elem.endPoint());
}
+struct TriangleData
+{
+ QVector2D points[3];
+ int pathElementIndex;
+ QVector3D debugColor;
+ bool specialDebug = false;
+};
+
+QVector2D normalVector(QVector2D baseLine)
+{
+ QVector2D normal = QVector2D(-baseLine.y(), baseLine.x()).normalized();
+ return normal;
+}
+
+QVector2D normalVector(const QuadPath::Element &element, bool endSide = false)
+{
+ if (element.isLine())
+ return normalVector(element.endPoint() - element.startPoint());
+ else if (!endSide)
+ return normalVector(element.controlPoint() - element.startPoint());
+ else
+ return normalVector(element.endPoint() - element.controlPoint());
+}
+
+QVector2D angleBisector(const QuadPath::Element &element1, const QuadPath::Element &element2, float strokeMargin)
+{
+ Q_ASSERT(element1.endPoint() == element2.startPoint());
+
+
+ const auto p1 = element1.isLine() ? element1.startPoint() : element1.controlPoint();
+ const auto p2 = element1.endPoint();
+ const auto p3 = element2.isLine() ? element2.endPoint() : element2.controlPoint();
+
+ const auto v1 = (p1 - p2).normalized();
+ const auto v2 = (p3 - p2).normalized();
+ const auto bisector = v1 + v2;
+
+ if (qFuzzyIsNull(bisector.x()) && qFuzzyIsNull(bisector.y())) {
+ // v1 and v2 are almost parallel, and pointing in opposite directions
+ // angle bisector formula will give an almost null vector: use normal of bisector of normals instead
+ QVector2D n1(-v1.y(), v1.x());
+ QVector2D n2(-v2.y(), v2.x());
+ return (n2 - n1).normalized() * strokeMargin;
+ } else {
+ // We need to increase the length, so that the result covers the whole miter
+ // Using the identity sin(x/2) == sqrt((1 - cos(x)) / 2), and the fact that the
+ // dot product of two unit vectors is the cosine of the angle between them
+ // The length of the miter is w/sin(x/2) where x is the angle between the two elements
+
+ float cos2x = QVector2D::dotProduct(v1, v2);
+ cos2x = qMin(1.0f, cos2x); // Allow for float inaccuracy
+ float sine = sqrt((1.0f - cos2x) / 2);
+ sine = qMax(sine, 0.1f); // Avoid divide by zero
+
+ return bisector.normalized() * strokeMargin / sine;
+ // TODO: Make a proper bevel at that point
+ // ### This is *not* the right place to do a miter limit: The inner join needs to be as long as
+ // it needs to be, independent of the miter on the other side. (We still need to avoid a divide
+ // by zero, of course.)
+ }
+}
+
+// Returns true if p and q are on the same side of the line AB
+bool onSameSideOfLine(const QVector2D &a, const QVector2D &b, const QVector2D &p, const QVector2D &q)
+{
+ auto baseLine = a - b;
+ QVector2D n(-baseLine.y(), baseLine.x());
+ float pSide = testSideOfLineByNormal(a, n, p);
+ float qSide = testSideOfLineByNormal(a, n, q);
+ return pSide * qSide >= 0;
+}
+
+// Returns true if the entire curve triangle is on the same side of the line p1-p2
+bool controlPointInsideLine(const QuadPath::Element &element, const QVector2D &p1, const QVector2D &p2)
+{
+ bool s1 = determinant(p1, p2, element.startPoint()) < 0;
+ auto s2 = determinant(p1, p2, element.endPoint()) < 0;
+ auto s3 = determinant(p1, p2, element.controlPoint()) < 0;
+ // If all determinants have the same sign, all the points are on the same side
+ return (s1 == s2 && s2 == s3);
+}
+
+inline bool lineIntersects(const QVector2D &v1_, const QVector2D &v2_,
+ const QVector2D &u1_, const QVector2D &u2_)
+{
+#if 1
+ QPointF v1 = v1_.toPointF();
+ QPointF v2 = v2_.toPointF();
+ QPointF u1 = u1_.toPointF();
+ QPointF u2 = u2_.toPointF();
+
+ if ( (v1.x() < u1.x() && v1.x() < u2.x() && v2.x() < u1.x() && v2.x() < u2.x())
+ || (v1.x() > u1.x() && v1.x() > u2.x() && v2.x() > u1.x() && v2.x() > u2.x())
+ || (v1.y() < u1.y() && v1.y() < u2.y() && v2.y() < u1.y() && v2.y() < u2.y())
+ || (v1.y() > u1.y() && v1.y() > u2.y() && v2.y() > u1.y() && v2.y() > u2.y())) {
+ return false;
+ }
+
+ // Copied from QLineF::intersects() with some changes to ordering to speed
+ // up for our use case
+ const QPointF a = v2 - v1;
+ const QPointF b = u1 - u2;
+ const QPointF c = v1 - u1;
+
+ const qreal denominator = a.y() * b.x() - a.x() * b.y();
+ if (denominator == 0.0 || !qt_is_finite(denominator))
+ return false;
+
+ const qreal reciprocal = 1.0 / denominator;
+ qreal na = (b.y() * c.x() - b.x() * c.y()) * reciprocal;
+ if (na < 0.0 || na > 1.0)
+ return false;
+
+ qreal nb = (a.x() * c.y() - a.y() * c.x()) * reciprocal;
+ if (nb < 0.0 || nb > 1.0)
+ return false;
+
+ const QPointF intersectionPoint = v1 + a * na;
+ return intersectionPoint != v1 && intersectionPoint != v2 && intersectionPoint != u1 && intersectionPoint != u2;
+ return true;
+#else
+ QLineF v(v1.toPointF(), v2.toPointF());
+ QLineF u(u1.toPointF(), u2.toPointF());
+
+ QPointF intersectionPoint;
+ if (v.intersects(u, &intersectionPoint) == QLineF::BoundedIntersection) {
+ return (intersectionPoint != v.p1() && intersectionPoint != v.p2()
+ && intersectionPoint != u.p1() && intersectionPoint != u.p2());
+ }
+
+ return false;
+#endif
+}
+
+
+static QList<TriangleData> customTriangulator(const QuadPath &path, float margin, bool miterJoin)
+{
+ QList<TriangleData> ret;
+
+ // TODO: We repeat the same calculations for each side of each join: try to remember the values from the previous
+ // element. (The difficult part is path closures)
+
+ auto handleElement = [&ret, margin, miterJoin](const QuadPath::Element *prev, const QuadPath::Element *current, const QuadPath::Element *next, const int elementIndex) {
+ QVector2D p1, p2, p3, p4;
+
+
+ auto startNormal = normalVector(*current) * margin;
+ auto endNormal = normalVector(*current, true) * margin;
+ auto startBisector = prev ? angleBisector(*prev, *current, margin) : startNormal;
+ auto endBisector = next ? angleBisector(*current, *next, margin) : endNormal;
+
+ const auto &sp = current->startPoint();
+ const auto &ep = current->endPoint();
+
+ // p1: bisect on the "inside" of the join, p2: normal on the "outside"
+ // guess which side is which: (TODO: we should be able to know this)
+ bool bevelPointReversed = false;
+
+ if (prev) {
+ const auto &prevPoint = prev->isLine() ? prev->startPoint() : prev->controlPoint();
+ const auto &lineStart = current->startPoint();
+ const auto &lineEnd = current->isLine() ? current->endPoint() : current->controlPoint();
+ if (miterJoin) {
+ p1 = sp + startBisector;
+ p2 = sp - startBisector;
+ } else {
+ p1 = sp - startBisector;
+ p2 = sp - startNormal;
+ // p1 should be on the same side as the start point of the previous element
+ if (!onSameSideOfLine(lineStart, lineEnd, p1, prevPoint)) {
+ p1 = sp + startBisector;
+ }
+ // p2 should be on the opposite side ### TODO: optimize test, reusing intermediates from test above
+ if (onSameSideOfLine(lineStart, lineEnd, p2, prevPoint)) {
+ p2 = sp + startNormal;
+ bevelPointReversed = true;
+ }
+ }
+ } else {
+ p1 = sp + startNormal;
+ p2 = sp - startNormal;
+ }
+ auto bevelPoint = p2; // This is one corner of the "gap" triangle
+
+ // p3: bisect on the "inside" of the join, p4: normal on the "outside"
+ // For curves, we use the tangent line of the next element
+ if (next) {
+ const auto &lineStart = current->isLine() ? current->startPoint() : current->controlPoint();
+ const auto &lineEnd = current->endPoint();
+ const auto &nextPoint = next->isLine() ? next->endPoint() : next->controlPoint();
+ if (miterJoin) {
+ p3 = ep + endBisector;
+ p4 = ep - endBisector;
+ } else {
+ p3 = ep - endBisector;
+ p4 = ep - endNormal;
+ // p3 should be on the same side of our line as the end point of the next element
+ if (!onSameSideOfLine(lineStart, lineEnd, p3, nextPoint)) {
+ p3 = ep + endBisector;
+ }
+ // p4 should be on the opposite side ### TODO: optimize test, reusing intermediates from test above
+ if (onSameSideOfLine(lineStart, lineEnd, p4, nextPoint)) {
+ p4 = ep + endNormal;
+ }
+ }
+ } else {
+ p3 = ep + endNormal;
+ p4 = ep - endNormal;
+ }
+
+ //bool intersect = lineIntersects(p1, p2, p3, p4); // Check if our quadrilateral is self-intersecting
+ //### Not enough! It's also wrong if p1-p2 is completely on the wrong side of p3-p4
+ //bool ok = onSameSideOfLine(p1, p2, p3, p4) && onSameSideOfLine(p1, p2, p3, current->endPoint());
+ //### And that's still not enough, since the line can turn almost 180 degrees
+
+ // New attempt: see if the bisector intersects the normal...
+ bool notOK = lineIntersects(sp + startNormal, sp - startNormal, ep + endBisector, ep - endBisector)
+ || lineIntersects(sp + startBisector, sp - startBisector, ep + endNormal, ep - endNormal);
+
+ // TESTING: Avoid the inner bisector extending past the stroke. We need to move the points around,
+ // but for now, just accept overpainting. Only implemented for BevelJoin
+ if (notOK && !miterJoin) {
+ p1 = current->startPoint() + startNormal;
+ p2 = current->startPoint() - startNormal;
+ p3 = current->endPoint() + endNormal;
+ p4 = current->endPoint() - endNormal;
+ }
+
+ if (current->isLine()) {
+ // Rearrange so p1, p3 is on the same side of the path, and p2, p4 is on the other side
+ if (!onSameSideOfLine(current->startPoint(), current->endPoint(), p1, p3))
+ qSwap(p3, p4);
+ } else {
+ // Rearrange so that p1 and p3 are on the outside of the curve triangle, i.e. p2 and p4 are on the same side
+ // of the respective tangent line as the path
+
+
+ // p1 should be on the outside of sp-cp
+ if (onSameSideOfLine(current->startPoint(), current->controlPoint(), p1, current->endPoint()))
+ qSwap(p1, p2);
+ // p3 should be on the outside of ep-cp
+ if (onSameSideOfLine(current->endPoint(), current->controlPoint(), p3, current->startPoint()))
+ qSwap(p3, p4);
+ }
+
+
+ bool special = notOK;
+
+ /*
+ p
+ p1 p3
+
+ sp ep
+
+ p2 p4
+
+ */
+
+ //Note: p1-sp-p2 is no longer guaranteed to be on a line, so we need to add extra triangles
+
+ if (current->isLine() || controlPointInsideLine(*current, p1, p3)) {
+ float r = current->isLine() ? 0.0f : 0.5f;
+ ret.append({{p1, p2, p3}, elementIndex, {r, 1.0f, 0.0f}, special});
+ ret.append({{p2, p3, p4}, elementIndex, {r, 0.0f, 1.0f}, special});
+ if (!miterJoin) { // TODO: handle bevel for overlong miter
+ ret.append({{p1, sp, p2}, elementIndex, {r, 0.5f, 1.0f}, special});
+ ret.append({{p3, ep, p4}, elementIndex, {r, 0.5f, 1.0f}, special});
+ }
+ //qDebug() << "adding triangles" << p1 << p2 << p3 << "and" << p2 << p3 << p4;
+ } else {
+
+ const auto &p = current->controlPoint();
+ ret.append({{p1, p2, p}, elementIndex, {1.0f, 0.0f, 0.0f}, special});
+ ret.append({{p, p2, p4}, elementIndex, {1.0f, 1.0f, 0.0f}, special});
+ ret.append({{p, p3, p4}, elementIndex, {1.0f, 0.0f, 1.0f}, special});
+ if (!miterJoin) { // TODO: handle bevel for overlong miter
+ ret.append({{p1, sp, p2}, elementIndex, {0.5f, 1.0f, 0.0f}, special});
+ ret.append({{p3, ep, p4}, elementIndex, {0.5f, 1.0f, 0.0f}, special});
+ }
+ //qDebug() << "adding triangle1" << p1 << p2 << p << "triangle2" << p << p2 << p4 << "triangle3" << p << p3 << p4 ;
+
+
+ // qDebug() << "Curve element" << p1 << p2 << p << p3 << p4;
+ }
+
+ if (prev && !miterJoin) {
+ // Try to do the bevel triangle: prev bevelpoint - sp - bevelPoint
+ auto prevNormal = normalVector(*prev, true) * margin; // Do we have normals in different directions???
+ auto pbp = bevelPointReversed ? sp + prevNormal : sp - prevNormal;
+ //qDebug() << "bevel triangle" << pbp << sp << bevelPoint;
+ ret.append({{pbp, sp, bevelPoint}, -1, {}, true});
+ }
+ };
+
+
+ /*
+ Iterate through the path. Each start element has to be postponed until the end of the sub-path.
+ At that point, we have to process both the start and the end elements.
+
+ If the end element's end point is equal to the start element's start point, we treat them as connected,
+ otherwise we add null pointers
+ */
+
+ const QuadPath::Element *startElement = nullptr;
+ int startElementIndex = -1;
+ const QuadPath::Element *prevElement = nullptr;
+ //### For now, we know that this path has not been split
+ for (int i = 0; i < path.elementCount(); ++i) {
+ const auto *element = &path.elementAt(i);
+ // ### isSubPathEnd() doesn't work unless subPathsClosed() has been called
+ bool isSubPathEnd = i == path.elementCount() - 1 || path.elementAt(i + 1).isSubpathStart();
+ bool isSubPathStart = element->isSubpathStart() || i == 0; // 0 should always be subPathStart, but just to be sure
+ if (isSubPathStart) {
+ if (isSubPathEnd) {
+ // If the element is both start and end, it's a standalone
+ handleElement(nullptr, element, nullptr, i);
+ startElement = nullptr; // These should be ignored, but reset just in case
+ prevElement = nullptr;
+ } else {
+ // this element will be handled later, when we know whether the sub-path is closed
+ startElement = element;
+ startElementIndex = i;
+ prevElement = element;
+ }
+ continue;
+ }
+
+ const QuadPath::Element *nextElement = nullptr;
+ if (i != 0 && !element->isSubpathStart())
+ prevElement = &path.elementAt(i - 1);
+
+ if (isSubPathEnd) {
+ //handle the previous start element: check if it is connected to this element
+ Q_ASSERT(startElement);
+ bool subPathClosed = element->endPoint() == startElement->startPoint();
+ handleElement(subPathClosed ? element : nullptr, startElement, &path.elementAt(startElementIndex + 1), startElementIndex);
+ if (subPathClosed)
+ nextElement = startElement;
+ startElement = nullptr;
+ } else {
+ nextElement = &path.elementAt(i + 1);
+ }
+ handleElement(prevElement, element, nextElement, i);
+ prevElement = isSubPathEnd ? nullptr : element;
+ }
+ Q_ASSERT(!startElement); // Should have been handled in the loop
+
+ return ret;
+}
+
+};
+
+QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addNodesStrokeShader(const PathData &pathData, NodeList *debugNodes)
+{
+ QVector<QSGGeometryNode *> ret;
+
+ Q_UNUSED(debugNodes);
+
+ const QColor &color = pathData.pen.color();
+ Q_UNUSED(color)
+
+ QVector<QQuickShapeLoopBlinnNode::LoopBlinnVertex> vertexBuffer;
+ QVector<quint32> indices;
+ bool visualizeDebug = debugVisualization() & DebugCurves;
+ const float dbg = visualizeDebug ? 0.5f : 0.0f;
+ QVector<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
+
+ const auto &thePath = pathData.qPath;
+
+ // qDebug() << "Stroking" << thePath;
+
+
+ // TODO: calculate margin correctly for narrow angles. If it's too big we'll get into unsafe space more often
+ auto triangles = customTriangulator(thePath, float(pathData.pen.widthF()) * 1, pathData.pen.joinStyle() == Qt::MiterJoin); //### SvgMiterJoin ???
+
+ //### TODO: reduce code duplication
+ auto addVertexDataForPoint = [&vertexBuffer, &wfVertices, dbg](const QuadPath::Element &element, const QVector2D &p, const QVector3D &color, int wfIndex) {
+ auto uv = element.uvForPoint(p);
+
+ float r = color.x(), g = color.y(), b = color.z();
+
+ vertexBuffer.append({p.x(), p.y(), uv.x(), uv.y(), uv.z(), r, g, b, dbg});
+ switch (wfIndex % 3) {
+ case 0:
+ wfVertices.append({p.x(), p.y(), 1.0f, 0.0f, 0.0f});
+ break;
+ case 1:
+ wfVertices.append({p.x(), p.y(), 0.0f, 1.0f, 0.0f});
+ break;
+ case 2:
+ wfVertices.append({p.x(), p.y(), 0.0f, 0.0f, 1.0f});
+ break;
+ }
+ };
+
+ auto addCurveTriangle = [&](const QuadPath::Element &element, const QVector2D &sp, const QVector2D &ep, const QVector2D &cp, const QVector3D &color){
+ int currentVertex = vertexBuffer.count();
+ addVertexDataForPoint(element, sp, color, 0);
+ addVertexDataForPoint(element, cp, color, 1);
+ addVertexDataForPoint(element, ep, color, 2);
+ indices << currentVertex << currentVertex + 1 << currentVertex + 2;
+ };
+
+ // TESTING
+ auto addBevelTriangle = [&](const QVector2D &p1, const QVector2D &p2, const QVector2D &p3)
+ {
+ float r = 0.0f;
+ float g = 1.0f;
+ float b = 0.5f;
+
+
+ // MEGA HACK!!! Invent a curve based on the triangle, since we know that:
+ // p2 is the path join point
+ // the stroke passes through the mid points of p2-p1 and p2-p3
+
+ // we have static inline QVector2D curveUv(QVector2D p0, QVector2D p1, QVector2D p2, QVector2D p)
+ // which finds uv coordinates for the point p, for a quadratic curve from p0 to p2 with control point p1
+
+ QVector2D fp1 = p2 + (p3 - p1);
+ QVector2D fp2 = p2 - (p3 - p1);
+ QVector2D fcp = (p1 + 3 * p2 + p3) / 5; // does not matter for line
+
+ // The bevel is not strokeWidth/2 wide, it's half this triangle's height
+ // We need to shift everything down by the difference (definitely hackish)
+
+ QVector2D vh = ((p1 - p2) + (p3 - p2)) / 2;
+ float triangleHeight = vh.length();
+ float delta = (pathData.pen.widthF() - triangleHeight) / 2;
+
+ QVector2D offset = vh.normalized() * delta;
+ fp1 -= offset;
+ fp2 -= offset;
+ fcp -= offset;
+
+
+ const auto uv1 = curveUv(fp1, fcp, fp2, p1);
+ const auto uv2 = curveUv(fp1, fcp, fp2, p2);
+ const auto uv3 = curveUv(fp1, fcp, fp2, p3);
+
+ const int currentVertex = vertexBuffer.count();
+ vertexBuffer.append({p1.x(), p1.y(), uv1.x(), uv1.y(), 0.0f, r, g, b, dbg});
+ vertexBuffer.append({p2.x(), p2.y(), uv2.x(), uv2.y(), 0.0f, r, g, b, dbg});
+ vertexBuffer.append({p3.x(), p3.y(), uv3.x(), uv3.y(), 0.0f, r, g, b, dbg});
+ indices << currentVertex << currentVertex + 1 << currentVertex + 2;
+ wfVertices.append({p1.x(), p1.y(), 1.0f, 0.0f, 0.0f});
+ wfVertices.append({p2.x(), p2.y(), 0.0f, 1.0f, 0.0f});
+ wfVertices.append({p3.x(), p3.y(), 0.0f, 0.0f, 1.0f});
+ //qDebug() << "bev" << p1 << p2 << p3;
+ };
+
+ static constexpr QVector3D specialDebug{0.0f, 1.0f, 1.0f};
+
+ for (const auto &triangle : triangles) {
+ if (triangle.pathElementIndex < 0) {
+ //TESTING
+ addBevelTriangle(triangle.points[0], triangle.points[1], triangle.points[2]);
+ continue;
+ }
+ const auto &element = thePath.elementAt(triangle.pathElementIndex);
+ addCurveTriangle(element, triangle.points[0], triangle.points[1], triangle.points[2],
+ triangle.specialDebug ? specialDebug : triangle.debugColor);
+ }
+
+
+
+ if (indices.size() > 0) {
+ auto *node = new QQuickShapeLoopBlinnNode(NoGradient); // TODO: dedicated node
+ node->color = Qt::transparent;
+ //node->fillGradient = pathData.gradient;
+
+ node->strokeColor = pathData.pen.color(); // this is all you need to use the stroke shader
+ node->strokeWidth = pathData.pen.widthF();
+
+ QSGGeometry *g = new QSGGeometry(QQuickShapeLoopBlinnNode::attributes(),
+ vertexBuffer.size(), indices.size(), QSGGeometry::UnsignedIntType);
+ node->setGeometry(g);
+ g->setDrawingMode(QSGGeometry::DrawTriangles);
+
+ //qDebug() << "gvc" << g->vertexCount();
+
+ memcpy(g->vertexData(),
+ vertexBuffer.data(),
+ g->vertexCount() * g->sizeOfVertex());
+ memcpy(g->indexData(),
+ indices.data(),
+ indices.size() * g->sizeOfIndex());
+
+ m_rootNode->appendChildNode(node);
+ ret.append(node);
+ }
+ const bool wireFrame = debugVisualization() & DebugWireframe;
+ if (wireFrame) {
+ QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
+
+ //QVarLengthArray<quint32> indices;
+
+ QSGGeometry *wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
+ wfVertices.size(),
+ indices.size(),
+ QSGGeometry::UnsignedIntType);
+ wfNode->setGeometry(wfg);
+
+ wfg->setDrawingMode(QSGGeometry::DrawTriangles);
+ memcpy(wfg->indexData(),
+ indices.data(),
+ indices.size() * wfg->sizeOfIndex());
+ memcpy(wfg->vertexData(),
+ wfVertices.data(),
+ wfg->vertexCount() * wfg->sizeOfVertex());
+
+ m_rootNode->appendChildNode(wfNode);
+ debugNodes->append(wfNode);
+ }
+ return ret;
+}
+
//TODO: we could optimize by preprocessing e1, since we call this function multiple times on the same
// elements
static void handleOverlap(QuadPath &path, qsizetype e1, qsizetype e2, int recursionLevel = 0)
@@ -1624,172 +2717,4 @@ void QQuickShapeCurveRenderer::solveOverlaps(QuadPath &path)
}
}
-//#define ROUNDING
-QVector<QSGGeometryNode *> QQuickShapeCurveRenderer::addPathNodesDelaunayTest(const PathData &pathData, NodeList *debugNodes)
-{
- const QuadPath &thePath = pathData.path;
- if (thePath.elementCount() == 0)
- return QVector<QSGGeometryNode *>{};
-
- QList<QtPathVertex> vertices;
- QList<QtPathEdge> edges;
-
- // Generate the graph for the triangulator
- // Start with adding an external bounding box
-
- auto boundingRect = pathData.path.controlPointRect();
- float extraBorder = qMax(boundingRect.width(), boundingRect.height()) * 0.10; // 10% extra
- boundingRect.adjust(-extraBorder, -extraBorder, extraBorder, extraBorder);
-
- vertices << QtPathVertex{ QVector2D(boundingRect.topLeft()), 1 };
- vertices << QtPathVertex{ QVector2D(boundingRect.topRight()), 2 };
- vertices << QtPathVertex{ QVector2D(boundingRect.bottomRight()), 3 };
- vertices << QtPathVertex{ QVector2D(boundingRect.bottomLeft()), 4 };
- edges.append({ 0, 1, 1 });
- edges.append({ 1, 2, 2 });
- edges.append({ 2, 3, 3 });
- edges.append({ 3, 0, 4 });
-
- // Then add all the edges that we want to have
- // TODO: We ought actually have an index-based structure in QuadPath.
- // For now, just duplicate points
-
-#if !defined(ROUNDING)
- QHash<QPair<qreal, qreal>, int> pointHash;
-#else
- QHash<QPair<int, int>, int> pointHash;
-#endif
- thePath.iterateElements([&](const QuadPath::Element &element) {
- auto findOrInsert = [&pointHash, &vertices](const QVector2D &p) {
-#if defined(ROUNDING)
- auto key = qMakePair(QFixed::fromReal(qRound(p.x() * 32.0) / 32.0).value(),
- QFixed::fromReal(qRound(p.y() * 32.0) / 32.0).value());
-#else
- auto key = qMakePair(p.x(), p.y());
-#endif
-
- auto it = pointHash.find(key);
- if (it != pointHash.end()) {
- return it.value();
- } else {
- vertices << QtPathVertex{ p, 42};
- pointHash.insert(key, int(vertices.size() - 1));
- return int(vertices.size() - 1);
- }
- };
-
-
- quint32 startIdx = findOrInsert(element.startPoint());
- quint32 endIdx = findOrInsert(element.endPoint());
-
- edges.append({ startIdx, endIdx, 42 });
- if (!element.isLine()) {
- quint32 controlIdx = findOrInsert(element.controlPoint());
- edges.append({ startIdx, controlIdx, 42 });
- edges.append({ endIdx, controlIdx, 42 });
- }
- });
-
- //Add a "super triangle" for the Delauney algorithm
-
-{
- // Calculate the dimensions and center of the bounding box
- float width = boundingRect.width();
- float height = boundingRect.height();
- float centerX = boundingRect.center().x();
- float centerY = boundingRect.center().y();
-
- // Create a super-triangle with vertices outside the bounding box
- int l = vertices.length();
- QtPathVertex v1 = {QVector2D(centerX, centerY - 3 * height), l++}; // Top vertex
- QtPathVertex v2 = {QVector2D(centerX - 3 * width, centerY + 3 * height), l++}; // Bottom-left vertex
- QtPathVertex v3 = {QVector2D(centerX + 3 * width, centerY + 3 * height), l}; // Bottom-right vertex
-
- vertices << v1 << v2 << v3;
-
-}
-
- QList<QtPathTriangle> triangles = qtDelaunayTriangulator(vertices, edges, pathData.originalPath);
-
- if (!triangles.isEmpty())
- testTriangulation(vertices, edges, triangles);
-
- // Quick-and-dirty triangle visualisation:
-
- // Semi-transparent triangle fill to debug coverage and overlap
- auto *fillNode = new QQuickShapeLoopBlinnNode(QQuickAbstractPathRenderer::NoGradient);
-
- QSGGeometry *g = new QSGGeometry(QQuickShapeLoopBlinnNode::attributes(),
- vertices.size(), triangles.size() * 3, QSGGeometry::UnsignedIntType);
-
- fillNode->setGeometry(g);
- g->setDrawingMode(QSGGeometry::DrawTriangles);
- m_rootNode->appendChildNode(fillNode);
-
- QVector<QQuickShapeLoopBlinnNode::LoopBlinnVertex> vertexData;
- vertexData.reserve(vertices.size());
- QVector<quint32> indexData;
- indexData.reserve(triangles.size() * 3);
-
- fillNode->color = QColor(0x7f, 0xff, 0, 0x7f);
- for (const auto &v : std::as_const(vertices))
- vertexData.append({ v.point.x(), v.point.y(), 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f });
- for (const auto &t : std::as_const(triangles))
- indexData << t.v1Index << t.v2Index << t.v3Index;
-
- memcpy(g->vertexData(),
- vertexData.data(),
- g->vertexCount() * g->sizeOfVertex());
- memcpy(g->indexData(),
- indexData.data(),
- indexData.size() * g->sizeOfIndex());
-
-
- // A wireframe node to show the edges
- QQuickShapeWireFrameNode *wfNode = new QQuickShapeWireFrameNode;
- QSGGeometry *wfg = wfNode->geometry();
-
- QVarLengthArray<quint32> indices;
- int iidx = 0;
- QVarLengthArray<QQuickShapeWireFrameNode::WireFrameVertex> wfVertices;
-
- auto addWireframeVertex = [&](int vertexIndex, int index){
- float u = index % 3 == 0 ? 1.0f : 0.0f;
- float v = index % 3 == 1 ? 1.0f : 0.0f;
- float w = index % 3 == 2 ? 1.0f : 0.0f;
-
- auto pt = vertices[vertexIndex].point;
- indices.append(iidx++);
- wfVertices.append({ pt.x(), pt.y(), u, v, w });
- };
-
- for (const auto &t : std::as_const(triangles)) {
- addWireframeVertex(t.v1Index, 0);
- addWireframeVertex(t.v2Index, 1);
- addWireframeVertex(t.v3Index, 2);
- }
-
- if (wfg->indexType() != QSGGeometry::UnsignedIntType) {
- wfg = new QSGGeometry(QQuickShapeWireFrameNode::attributes(),
- wfVertices.size(),
- indices.size(),
- QSGGeometry::UnsignedIntType);
- wfNode->setGeometry(wfg);
- } else {
- wfg->allocate(wfVertices.size(), indices.size());
- }
-
- wfg->setDrawingMode(QSGGeometry::DrawTriangles);
- memcpy(wfg->indexData(),
- indices.data(),
- indices.size() * wfg->sizeOfIndex());
- memcpy(wfg->vertexData(),
- wfVertices.data(),
- wfg->vertexCount() * wfg->sizeOfVertex());
-
- m_rootNode->appendChildNode(wfNode);
- debugNodes->append(wfNode);
- return { fillNode };
-}
-
QT_END_NAMESPACE
diff --git a/src/quickshapes/qquickshapecurverenderer_p.h b/src/quickshapes/qquickshapecurverenderer_p.h
index 6916de018e..859ff000c8 100644
--- a/src/quickshapes/qquickshapecurverenderer_p.h
+++ b/src/quickshapes/qquickshapecurverenderer_p.h
@@ -59,6 +59,7 @@ public:
static QuadPath fromPainterPath(const QPainterPath &path);
QPainterPath toPainterPath() const;
QuadPath subPathsClosed() const;
+ QuadPath flattened() const;
class Element
{
@@ -131,6 +132,8 @@ public:
return QVector2D(-tan.y(), tan.x());
}
+ float extent() const;
+
private:
int intersectionsAtY(float y, float *fractions) const;
@@ -306,6 +309,7 @@ private:
QPainterPath fillPath;
QPainterPath originalPath;
QuadPath path;
+ QuadPath qPath; // TODO: better name
QColor fillColor;
Qt::FillRule fillRule = Qt::OddEvenFill;
QPen pen;
@@ -321,8 +325,9 @@ private:
void deleteAndClear(NodeList *nodeList);
QVector<QSGGeometryNode *> addPathNodesBasic(const PathData &pathData, NodeList *debugNodes);
-
- QVector<QSGGeometryNode *> addPathNodesDelaunayTest(const PathData &pathData, NodeList *debugNodes);
+ QVector<QSGGeometryNode *> addPathNodesLineShader(const PathData &pathData, NodeList *debugNodes);
+ QVector<QSGGeometryNode *> addStrokeNodes(const PathData &pathData, NodeList *debugNodes);
+ QVector<QSGGeometryNode *> addNodesStrokeShader(const PathData &pathData, NodeList *debugNodes);
QSGGeometryNode *addLoopBlinnNodes(const QTriangleSet &triangles,
const QVarLengthArray<quint32> &extraIndices,
diff --git a/src/quickshapes/qquickshapecurverenderer_p_p.h b/src/quickshapes/qquickshapecurverenderer_p_p.h
index 4904cbc2b9..40d0f3beb4 100644
--- a/src/quickshapes/qquickshapecurverenderer_p_p.h
+++ b/src/quickshapes/qquickshapecurverenderer_p_p.h
@@ -25,52 +25,9 @@ QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(lcShapeCurveRenderer);
-struct QtPathVertex
-{
- QVector2D point;
- int id;
- quint32 binX = 0;
- quint32 binY = 0;
-};
-
-struct QtPathEdge
-{
- quint32 startIndex;
- quint32 endIndex;
- int id;
-};
-
-struct QtPathTriangle
-{
- QtPathTriangle(quint32 i1, quint32 i2, quint32 i3, int d) : v1Index(i1), v2Index(i2), v3Index(i3), id(d) {}
- quint32 v1Index;
- quint32 v2Index;
- quint32 v3Index;
-
- quint32 adjacentTriangle1 = quint32(-1); // Adjacent to v1-v2
- quint32 adjacentTriangle2 = quint32(-1); // Adjacent to v2-v3
- quint32 adjacentTriangle3 = quint32(-1); // Adjacent to v3-v1
-
- // Used by triangulator
- quint32 lastSeenVertex = quint32(-1);
-
- // Should this triangle be rendered? Set to false for triangles connecting to super-triangle
- bool isValid = true;
-
- int id;
-};
-
-constexpr bool operator==(const QtPathTriangle& lhs, const QtPathTriangle& rhs)
-{
- return lhs.id == rhs.id
- && lhs.v1Index == rhs.v1Index
- && lhs.v2Index == rhs.v2Index
- && lhs.v3Index == rhs.v3Index;
-}
-
class QBezier;
-Q_QUICKSHAPES_PRIVATE_EXPORT QPolygonF qt_toQuadratics(const QBezier &b, qreal errorLimit = 0.2);
-Q_QUICKSHAPES_PRIVATE_EXPORT QList<QtPathTriangle> qtDelaunayTriangulator(const QList<QtPathVertex> &vertices, const QList<QtPathEdge> &edges, const QPainterPath &path);
+Q_QUICKSHAPES_PRIVATE_EXPORT void qt_toQuadratics(const QBezier &b, QPolygonF *out,
+ qreal errorLimit = 0.01);
QT_END_NAMESPACE
diff --git a/src/quickshapes/qt_delaunay_triangulator.cpp b/src/quickshapes/qt_delaunay_triangulator.cpp
deleted file mode 100644
index 0f52477476..0000000000
--- a/src/quickshapes/qt_delaunay_triangulator.cpp
+++ /dev/null
@@ -1,1439 +0,0 @@
-// Copyright (C) 2023 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
-
-#include "qquickshapecurverenderer_p_p.h"
-#include <QtGui/qvector2d.h>
-#include <QtGui/private/qtriangulator_p.h>
-#include <QtCore/QHash>
-#include <QtCore/QSet>
-#include <QtCore/QPair>
-#include <QtCore/QStack>
-#include <QtCore/QElapsedTimer>
-
-// #define QQUICKSHAPES_DEBUG_IMAGE 1
-
-QT_BEGIN_NAMESPACE
-
-Q_LOGGING_CATEGORY(lcShapeTiming, "qt.shape.timing");
-
-// Check if a point is inside the circumcircle of a triangle defined by three vertices (v1, v2, v3).
-// It uses the determinant of a matrix formed from the coordinates of the point and the vertices
-// to determine if the point is inside the circumcircle.
-// If the determinant is > 0, the point is inside the circumcircle.
-inline bool isPointInCircumcircle(const QVector2D &point,
- const QVector2D &v1_, const QVector2D &v2_, const QVector2D &v3_)
-{
- QVector2D v1 = v1_;
- QVector2D v2 = v2_;
- QVector2D v3 = v3_;
-
- // Vertices must be in ccw order
- if ( ( ( (v2.x() - v1.x()) * (v3.y() - v1.y()) ) - ( (v3.x() - v1.x()) * (v2.y() - v1.y()) ) ) <= 0.0f) {
- qSwap(v2, v3);
- }
-
-#if 1
- float x13 = v1.x() - v3.x();
- float y13 = v1.y() - v3.y();
- float x23 = v2.x() - v3.x();
- float y23 = v2.y() - v3.y();
- float x1p = v1.x() - point.x();
- float y1p = v1.y() - point.y();
- float x2p = v2.x() - point.x();
- float y2p = v2.y() - point.y();
-
- return (x13*x23 + y13*y23) * (x2p*y1p - x1p*y2p) < (x23*y13 - x13*y23) * (x2p*x1p + y1p*y2p);
-
-#else
- float ax = v1.x() - point.x();
- float ay = v1.y() - point.y();
- float bx = v2.x() - point.x();
- float by = v2.y() - point.y();
- float cx = v3.x() - point.x();
- float cy = v3.y() - point.y();
-
- float det = (ax * ax + ay * ay) * (bx * cy - cx * by) -
- (bx * bx + by * by) * (ax * cy - cx * ay) +
- (cx * cx + cy * cy) * (ax * by - bx * ay);
-
- return det > 0.0f;
-#endif
-}
-
-// Retrieves quad corresponding to t1 and t2 (assuming that they share one edge)
-// In the end, the new diagonal is (i1, i3) and (i1, i2, i3, i4) is the ordered vertices
-// in the quad.
-inline void findQuad(const QtPathTriangle &t1, const QtPathTriangle &t2,
- quint32 *i1, quint32 *i2, quint32 *i3, quint32 *i4)
-{
- // First find index in t1 which is not in t2 and call this i1 (with i2, and i4 completing t1)
- if (t1.v1Index != t2.v1Index && t1.v1Index != t2.v2Index && t1.v1Index != t2.v3Index) {
- (*i1) = t1.v1Index;
- (*i2) = t1.v2Index;
- (*i4) = t1.v3Index;
- } else if (t1.v2Index != t2.v1Index && t1.v2Index != t2.v2Index && t1.v2Index != t2.v3Index) {
- (*i1) = t1.v2Index;
- (*i2) = t1.v3Index;
- (*i4) = t1.v1Index;
- } else {
- Q_ASSERT(t1.v3Index != t2.v1Index && t1.v3Index != t2.v2Index && t1.v3Index != t2.v3Index);
- (*i1) = t1.v3Index;
- (*i2) = t1.v1Index;
- (*i4) = t1.v2Index;
- }
-
- // Then find (*i3) as index in t2 which is not in t1
- if (t2.v1Index != t1.v1Index && t2.v1Index != t1.v2Index && t2.v1Index != t1.v3Index) {
- (*i3) = t2.v1Index;
- } else if (t2.v2Index != t1.v1Index && t2.v2Index != t1.v2Index && t2.v2Index != t1.v3Index) {
- (*i3) = t2.v2Index;
- } else {
- Q_ASSERT(t2.v3Index != t1.v1Index && t2.v3Index != t1.v2Index && t2.v3Index != t1.v3Index);
- (*i3) = t2.v3Index;
- }
-}
-
-inline void swapDiagonal(QtPathTriangle *t1,
- QtPathTriangle *t2,
- quint32 idx = quint32(-1))
-{
- quint32 i1, i2, i3, i4;
- findQuad(*t1, *t2, &i1, &i2, &i3, &i4);
- Q_ASSERT(idx == quint32(-1) || t2->v1Index == idx || t2->v2Index == idx || t2->v3Index == idx);
-
- t2->v1Index = i1;
- t2->v2Index = i3;
- t2->v3Index = i2;
-
- t1->v1Index = i1;
- t1->v2Index = i3;
- t1->v3Index = i4;
-}
-
-inline bool isPointInTriangle(const QVector2D &v,
- const QVector2D &v1,
- const QVector2D &v2,
- const QVector2D &v3)
-{
-#if 0
- QPolygonF p;
- p.append(v1.toPointF());
- p.append(v2.toPointF());
- p.append(v3.toPointF());
- return p.containsPoint(v.toPointF(), Qt::OddEvenFill);
-#else
- if ( (v1.x() > v.x() && v2.x() > v.x() && v3.x() > v.x())
- || (v1.x() < v.x() && v2.x() < v.x() && v3.x() < v.x())
- || (v1.y() > v.y() && v2.y() > v.y() && v3.y() > v.y())
- || (v1.y() < v.y() && v2.y() < v.y() && v3.y() < v.y())) {
- return false;
- }
-
- // Barycentric scalars
- double denom = (v2.y() - v3.y()) * (v1.x() - v3.x())
- + (v3.x() - v2.x()) * (v1.y() - v3.y());
- double a = ((v2.y() - v3.y()) * (v.x() - v3.x())
- + (v3.x() - v2.x()) * (v.y() - v3.y()))
- / denom;
- double b = ((v3.y() - v1.y()) * (v.x() - v3.x())
- + (v1.x() - v3.x()) * (v.y() - v3.y()))
- / denom;
- double c = 1.0 - a - b;
-
- if (qFuzzyIsNull(a))
- a = 0.0;
- if (qFuzzyIsNull(b))
- b = 0.0;
- if (qFuzzyIsNull(c))
- c = 0.0;
-
- if (qFuzzyCompare(a, 1.0))
- a = 1.0;
- if (qFuzzyCompare(b, 1.0))
- b = 1.0;
- if (qFuzzyCompare(c, 1.0))
- c = 1.0;
-
- return a >= 0.0 && a <= 1.0
- && b >= 0.0 && b <= 1.0
- && c >= 0.0 && c <= 1.0;
-#endif
-}
-
-inline void saveDebugImage(bool shouldDraw,
- const QPainterPath &path,
- const QString &label,
- const QList<QtPathTriangle> &triangles,
- const QList<QtPathVertex> &vertices,
- const QList<quint32> &highlightedPoints = QList<quint32>{},
- const QList<quint32> &highlightedTriangles = QList<quint32>{},
- const QList<QPair<quint32, quint32> > &highlightedEdges = QList<QPair<quint32, quint32> >{})
-{
-#if defined(QQUICKSHAPES_DEBUG_IMAGE)
- shouldDraw = true;
-#endif
-
- static bool zoom = qEnvironmentVariableIntValue("QT_QUICKSHAPES_ZOOM_DEBUG_IMAGE");
-
- if (!shouldDraw)
- return;
-
- static int counter = 0;
-
- int size = 3000;
- QImage image(size, size, QImage::Format_ARGB32);
- image.fill(Qt::black);
-
- QList<QVector2D> vs;
-
- if (zoom) {
- for (auto it : highlightedPoints)
- vs.append(vertices.at(it).point);
-
- for (auto it : highlightedEdges) {
- vs.append(vertices.at(it.first).point);
- vs.append(vertices.at(it.second).point);
- }
-
- for (auto it : highlightedTriangles) {
- vs.append(vertices.at(triangles.at(it).v1Index).point);
- vs.append(vertices.at(triangles.at(it).v2Index).point);
- vs.append(vertices.at(triangles.at(it).v3Index).point);
- }
- } else {
- for (int i = 0; i < vertices.size() - 3; ++i)
- vs.append(vertices.at(i).point);
- }
-
- float minX = 0.0f;
- float maxX = 0.0f;
- float minY = 0.0f;
- float maxY = 0.0f;
- for (int i = 0; i < vs.size(); ++i) {
- QVector2D p = vs.at(i);
- if (i == 0 || minX > p.x())
- minX = p.x();
- if (i == 0 || maxX < p.x())
- maxX = p.x();
- if (i == 0 || minY > p.y())
- minY = p.y();
- if (i == 0 || maxY < p.y())
- maxY = p.y();
- }
-
- minX -= (maxX - minX) * 0.05f;
- minY -= (maxY - minY) * 0.05f;
- maxX += (maxX - minX) * 0.05f;
- maxY += (maxY - minY) * 0.05f;
-
- {
- QPainter painter(&image);
-
- float w = maxX - minX;
- float h = maxY - minY;
-
- float max = qMax(w, h);
-
- painter.save();
- painter.scale(size / max, size / max);
- painter.translate(-QPointF(minX, minY));
-
- float fontSize = 18 * max / size;
-
- QFont font = painter.font();
- font.setPixelSize(qCeil(fontSize));
-
- painter.setFont(font);
-
- if (!path.isEmpty()) {
- painter.save();
- painter.setBrush(Qt::darkGray);
- painter.drawPath(path);
- painter.setOpacity(1);
- painter.restore();
- }
-
- for (int i = 0; i < triangles.size(); ++i) {
- QtPathTriangle triangle = triangles.at(i);
- QPolygonF polygon;
- polygon.append(vertices.at(triangle.v1Index).point.toPointF());
- polygon.append(vertices.at(triangle.v2Index).point.toPointF());
- polygon.append(vertices.at(triangle.v3Index).point.toPointF());
-
- QPen pen(Qt::red, 3);
- pen.setCosmetic(true);
-
- painter.setPen(pen);
- painter.drawPolygon(polygon);
-
- painter.setPen(Qt::white);
- painter.drawText(vertices.at(triangle.v1Index).point.toPointF(),
- QString::number(triangle.v1Index));
- painter.drawText(vertices.at(triangle.v2Index).point.toPointF(),
- QString::number(triangle.v2Index));
- painter.drawText(vertices.at(triangle.v3Index).point.toPointF(),
- QString::number(triangle.v3Index));
- }
-
- for (int i = 0; i < highlightedTriangles.size(); ++i) {
- int highlightedTriangle = highlightedTriangles.at(i);
- QColor color = i > 0 ? Qt::magenta : Qt::green;
-
- QtPathTriangle triangle = triangles.at(highlightedTriangle);
- QPolygonF polygon;
- polygon.append(vertices.at(triangle.v1Index).point.toPointF());
- polygon.append(vertices.at(triangle.v2Index).point.toPointF());
- polygon.append(vertices.at(triangle.v3Index).point.toPointF());
-
- QPen pen(color, 3);
- pen.setCosmetic(true);
-
- painter.setPen(pen);
- painter.drawPolygon(polygon);
-
- }
-
- for (int i = 0; i < highlightedPoints.size(); ++i) {
- int highlightedPoint = highlightedPoints.at(i);
- QPointF point = vertices.at(highlightedPoint).point.toPointF();
-
- painter.setBrush(Qt::yellow);
-
- QPen pen(Qt::yellow, 3);
- pen.setCosmetic(true);
- painter.setPen(pen);
- painter.drawEllipse(point, 5 * max / size, 5 * max / size);
- }
-
- for (int i = 0; i < highlightedEdges.size(); ++i) {
- auto highlightedEdge = highlightedEdges.at(i);
- QLineF line(vertices.at(highlightedEdge.first).point.toPointF(),
- vertices.at(highlightedEdge.second).point.toPointF());
-
- QColor color = i == 0 ? Qt::yellow : Qt::cyan;
- QPen pen(color, 3);
- pen.setCosmetic(true);
- painter.setPen(pen);
-
- painter.drawLine(line);
-
- }
-
- painter.setPen(Qt::white);
- painter.setBrush(Qt::white);
-
- painter.restore();
-
- QFont f = painter.font();
- f.setPixelSize(18);
- painter.setFont(f);
- QFontMetricsF fm(f);
-
- painter.fillRect(QRectF(0, size - fm.height() * 3, size, fm.height() * 3), Qt::red);
- painter.drawText(QRectF(0, size - fm.height() * 3, size, fm.height() * 3),
- Qt::AlignCenter,
- QStringLiteral("%1, %2 triangles").arg(label).arg(triangles.size()));
- }
-
- image.save(QStringLiteral("delaunay%1.png").arg(counter++));
-}
-
-// Checks if the quadrilateral made up of triangle1 and triangle2 is strictly convex
-inline bool isConvexQuadrilateral(const QList<QtPathVertex> &vertices,
- const QtPathTriangle &triangle1,
- const QtPathTriangle &triangle2)
-{
- quint32 quad[4];
-
- findQuad(triangle1, triangle2, &quad[0], &quad[1], &quad[2], &quad[3]);
-
- int mask = 0;
- // Check if the cross product of all adjacent lines points the same way
- for (int i = 0; i < 4 && mask <= 2; ++i) {
- quint32 p1 = quad[i];
- quint32 p2 = quad[(i + 1) % 4];
- quint32 p3 = quad[(i + 2) % 4];
-
- QVector2D v1 = vertices.at(p2).point - vertices.at(p1).point;
- QVector2D v2 = vertices.at(p3).point - vertices.at(p2).point;
-
- // Parallel lines
- if ((qFuzzyIsNull(v1.x()) && qFuzzyIsNull(v2.x()))
- || (qFuzzyIsNull(v1.y()) && qFuzzyIsNull(v2.y()))) {
- mask = 3;
- }
-
- if (!qFuzzyIsNull(v1.x()) && qFuzzyIsNull(v2.x()) && qFuzzyIsNull(v1.y()) && qFuzzyIsNull(v2.y())) {
- float v1Slope = v1.y() / v1.x();
- float v2Slope = v2.y() / v2.x();
-
- if (mask <= 2 && (qFuzzyCompare(v1Slope, v2Slope) || qFuzzyCompare(v1Slope, -v2Slope)))
- mask = 3;
- }
-
-// if (mask <= 2 && (v1.normalized() == v2.normalized() || v1.normalized() == -v2.normalized()))
-// mask |= 3;
-
- if (mask <= 2) {
- float c = v1.x() * v2.y() - v1.y() * v2.x();
- if (c >= 0.0f)
- mask |= 1;
- else
- mask |= 2;
- }
-
-
- }
- return mask <= 2;
-}
-
-inline bool lineIntersects(const QVector2D &v1_, const QVector2D &v2_,
- const QVector2D &u1_, const QVector2D &u2_)
-{
-#if 1
- QPointF v1 = v1_.toPointF();
- QPointF v2 = v2_.toPointF();
- QPointF u1 = u1_.toPointF();
- QPointF u2 = u2_.toPointF();
-
- if ( (v1.x() < u1.x() && v1.x() < u2.x() && v2.x() < u1.x() && v2.x() < u2.x())
- || (v1.x() > u1.x() && v1.x() > u2.x() && v2.x() > u1.x() && v2.x() > u2.x())
- || (v1.y() < u1.y() && v1.y() < u2.y() && v2.y() < u1.y() && v2.y() < u2.y())
- || (v1.y() > u1.y() && v1.y() > u2.y() && v2.y() > u1.y() && v2.y() > u2.y())) {
- return false;
- }
-
- // Copied from QLineF::intersects() with some changes to ordering to speed
- // up for our use case
- const QPointF a = v2 - v1;
- const QPointF b = u1 - u2;
- const QPointF c = v1 - u1;
-
- const qreal denominator = a.y() * b.x() - a.x() * b.y();
- if (denominator == 0.0 || !qt_is_finite(denominator))
- return false;
-
- const qreal reciprocal = 1.0 / denominator;
- qreal na = (b.y() * c.x() - b.x() * c.y()) * reciprocal;
- if (na < 0.0 || na > 1.0)
- return false;
-
- qreal nb = (a.x() * c.y() - a.y() * c.x()) * reciprocal;
- if (nb < 0.0 || nb > 1.0)
- return false;
-
- const QPointF intersectionPoint = v1 + a * na;
- return intersectionPoint != v1 && intersectionPoint != v2 && intersectionPoint != u1 && intersectionPoint != u2;
- return true;
-#else
- QLineF v(v1.toPointF(), v2.toPointF());
- QLineF u(u1.toPointF(), u2.toPointF());
-
- QPointF intersectionPoint;
- if (v.intersects(u, &intersectionPoint) == QLineF::BoundedIntersection) {
- return (intersectionPoint != v.p1() && intersectionPoint != v.p2()
- && intersectionPoint != u.p1() && intersectionPoint != u.p2());
- }
-
- return false;
-#endif
-}
-
-// Returns edges (and the adjacent triangles corresponding to them) which have visibility
-// to the vertex
-static void findAdjacentTrianglesOnSameSide(const QList<QtPathVertex> &vertices,
- quint32 vertexIdx,
- const QtPathTriangle &triangle,
- QVarLengthArray<quint32, 2> *possibleTriangles,
- QVarLengthArray<QPair<quint32, quint32>, 2> *edges = nullptr)
-{
- Q_ASSERT(possibleTriangles != nullptr);
- QVector2D v = vertices.at(vertexIdx).point;
- QVector2D v1 = vertices.at(triangle.v1Index).point;
- QVector2D v2 = vertices.at(triangle.v2Index).point;
- QVector2D v3 = vertices.at(triangle.v3Index).point;
-
- bool d = ( ( ( (v2.x() - v1.x()) * (v3.y() - v1.y()) ) - ( (v3.x() - v1.x()) * (v2.y() - v1.y()) ) )) <= 0.0f;
-
- // Check v1 - v2
- if (triangle.adjacentTriangle1 != quint32(-1)) {
- bool d1 = ((v.x() - v1.x()) * (v2.y() - v1.y()) - (v.y() - v1.y()) * (v2.x() - v1.x())) <= 0.0f;
- if (d1 == d) {
- possibleTriangles->append(triangle.adjacentTriangle1);
- if (edges != nullptr) {
- if (triangle.v1Index < triangle.v2Index)
- edges->append(qMakePair(triangle.v1Index, triangle.v2Index));
- else
- edges->append(qMakePair(triangle.v2Index, triangle.v1Index));
- }
- }
- }
-
- // Check v2 - v3
- if (triangle.adjacentTriangle2 != quint32(-1)) {
- bool d2 = ((v.x() - v2.x()) * (v3.y() - v2.y()) - (v.y() - v2.y()) * (v3.x() - v2.x())) <= 0.0f;
- if (d2 == d) {
- possibleTriangles->append(triangle.adjacentTriangle2);
- if (edges != nullptr) {
- if (triangle.v2Index < triangle.v3Index)
- edges->append(qMakePair(triangle.v2Index, triangle.v3Index));
- else
- edges->append(qMakePair(triangle.v3Index, triangle.v2Index));
- }
- }
- }
-
- // Check v3 - v1
- if (triangle.adjacentTriangle3 != quint32(-1)) {
- bool d3 = ((v.x() - v3.x()) * (v1.y() - v3.y()) - (v.y() - v3.y()) * (v1.x() - v3.x())) <= 0.0f;
- if (d3 == d) {
- possibleTriangles->append(triangle.adjacentTriangle3);
- if (edges != nullptr) {
- if (triangle.v3Index < triangle.v1Index)
- edges->append(qMakePair(triangle.v3Index, triangle.v1Index));
- else
- edges->append(qMakePair(triangle.v1Index, triangle.v3Index));
- }
- }
- }
-}
-
-static quint32 findAdjacentTriangleOnSameSide(const QList<QtPathVertex> &vertices,
- quint32 vertexIdx,
- const QtPathTriangle &triangle)
-{
- QVarLengthArray<quint32, 2> possibleTriangles;
- findAdjacentTrianglesOnSameSide(vertices, vertexIdx, triangle, &possibleTriangles);
-
- if (possibleTriangles.size() == 0) {
- qCWarning(lcShapeCurveRenderer) << "Point" << vertexIdx
- << "is not on the outside of any edge of triangle"
- << triangle.v1Index << triangle.v2Index << triangle.v3Index;
- return quint32(-1);
- }
-
- return possibleTriangles.at(0);
-}
-
-// CDT using Incremental Algorithm approach.
-// the algorithm first inserts the edge's endpoints into the triangulation.
-// Then, it finds the sequence of triangles intersected by the constraint
-// edge and modifies them to include the edge.
-QList<QtPathTriangle> qtDelaunayTriangulator(const QList<QtPathVertex> &_vertices,
- const QList<QtPathEdge> &edges,
- const QPainterPath &path)
-{
- static bool saveImageOnError = qEnvironmentVariableIntValue("QT_QUICKSHAPES_SAVE_IMAGE_ON_ERROR") != 0;
- static bool drawAllImages = qEnvironmentVariableIntValue("QT_QUICKSHAPES_SAVE_DEBUG_IMAGE") != 0;
- static bool normalizeCoordinates = qEnvironmentVariableIntValue("QT_QUICKSHAPES_NORMALIZE_COORDINATES") != 0;
- static bool validateResults = qEnvironmentVariableIntValue("QT_QUICKSHAPES_VALIDATE_RESULTS");
-
- if (_vertices.isEmpty())
- return {};
-
- QList<QtPathVertex> vertices;
- if (Q_UNLIKELY(normalizeCoordinates)) {
- vertices.reserve(_vertices.size());
-
- qreal minX, minY, maxX, maxY;
- for (int i = 0; i < _vertices.size(); ++i) {
- const QtPathVertex &v = _vertices.at(i);
- if (i == 0 || v.point.x() < minX)
- minX = v.point.x();
- if (i == 0 || v.point.x() > maxX)
- maxX = v.point.x();
- if (i == 0 || v.point.y() < minY)
- minY = v.point.y();
- if (i == 0 || v.point.y() > maxY)
- maxY = v.point.y();
- }
-
- for (int i = 0; i < _vertices.size(); ++i) {
- QtPathVertex v = _vertices.at(i);
- v.point -= QVector2D(minX, minY);
- v.point /= QVector2D(maxX - minX, maxY - minY);
- vertices.append(v);
- }
- } else {
- vertices = _vertices;
- }
-
- QList<QtPathTriangle> triangulation;
- triangulation.reserve(2 * vertices.size() + 1);
-
- // Create a super-triangle
- const quint32 l = int(vertices.size());
- QtPathTriangle superTriangle = { l - 3, l - 2, l - 1, 0};
- triangulation.append(superTriangle);
-
- QElapsedTimer t;
- t.start();
-
- typedef QPair<quint32, quint32> Pair;
-
- QHash<Pair, Pair> existingEdges;
- QSet<Pair> constrainedEdges;
- existingEdges.reserve(3 * (2 * vertices.size() + 1));
-
- auto registerAdjacency = [&triangulation](quint32 a, quint32 b, quint32 triangleIdx1, quint32 triangleIdx2) {
- Q_ASSERT(triangleIdx1 < triangulation.size());
- Q_ASSERT(triangleIdx2 < triangulation.size());
-
- QtPathTriangle &t1 = triangulation[triangleIdx1];
- QtPathTriangle &t2 = triangulation[triangleIdx2];
-
- if ((t1.v1Index == a && t1.v2Index == b) || (t1.v1Index == b && t1.v2Index == a)) {
- t1.adjacentTriangle1 = triangleIdx2;
- } else if ((t1.v2Index == a && t1.v3Index == b) || (t1.v2Index == b && t1.v3Index == a)) {
- t1.adjacentTriangle2 = triangleIdx2;
- } else {
- Q_ASSERT(((t1.v3Index == a && t1.v1Index == b) || (t1.v3Index == b && t1.v1Index == a)));
- t1.adjacentTriangle3 = triangleIdx2;
- }
-
- if ((t2.v1Index == a && t2.v2Index == b) || (t2.v1Index == b && t2.v2Index == a)) {
- t2.adjacentTriangle1 = triangleIdx1;
- } else if ((t2.v2Index == a && t2.v3Index == b) || (t2.v2Index == b && t2.v3Index == a)) {
- t2.adjacentTriangle2 = triangleIdx1;
- } else {
- Q_ASSERT((t2.v3Index == a && t2.v1Index == b) || (t2.v3Index == b && t2.v1Index == a));
- t2.adjacentTriangle3 = triangleIdx1;
- }
- };
-
- auto unregisterAdjacency = [&triangulation](quint32 triangleIdx1, quint32 triangleIdx2) {
- Q_ASSERT(triangleIdx1 < triangulation.size());
- Q_ASSERT(triangleIdx2 < triangulation.size());
-
- QtPathTriangle &t1 = triangulation[triangleIdx1];
- QtPathTriangle &t2 = triangulation[triangleIdx2];
-
- if (t1.adjacentTriangle1 == triangleIdx2) {
- t1.adjacentTriangle1 = quint32(-1);
- } else if (t1.adjacentTriangle2 == triangleIdx2) {
- t1.adjacentTriangle2 = quint32(-1);
- } else {
- Q_ASSERT(t1.adjacentTriangle3 == triangleIdx2);
- t1.adjacentTriangle3 = quint32(-1);
- }
-
- if (t2.adjacentTriangle1 == triangleIdx1) {
- t2.adjacentTriangle1 = quint32(-1);
- } else if (t2.adjacentTriangle2 == triangleIdx1) {
- t2.adjacentTriangle2 = quint32(-1);
- } else {
- Q_ASSERT(t2.adjacentTriangle3 == triangleIdx1);
- t2.adjacentTriangle3 = quint32(-1);
- }
- };
-
- auto insertOrdered = [&existingEdges, &registerAdjacency](quint32 a, quint32 b, quint32 triangleIdx) {
- if (a > b)
- qSwap(a, b);
-
- Pair edge = qMakePair(a, b);
- auto it = existingEdges.find(edge);
- if (it == existingEdges.end()) {
- existingEdges.insert(edge, qMakePair(triangleIdx, quint32(-1)));
- } else {
- if (it.value().first == quint32(-1)) {
- Q_ASSERT(triangleIdx != it.value().second);
- it.value().first = triangleIdx;
- } else {
- Q_ASSERT(it.value().second == quint32(-1));
- Q_ASSERT(triangleIdx != it.value().first);
- it.value().second = triangleIdx;
- }
-
- if (it.value().first != quint32(-1) && it.value().second != quint32(-1))
- registerAdjacency(a, b, it.value().first, it.value().second);
- }
- };
-
- auto removeTriangleEdge = [&existingEdges, &unregisterAdjacency](quint32 a, quint32 b, quint32 triangleIdx) {
- if (a > b)
- qSwap(a, b);
-
- Pair edge = qMakePair(a, b);
- auto it = existingEdges.find(edge);
- Q_ASSERT(it != existingEdges.end());
-
- if (it.value().first != quint32(-1) && it.value().second != quint32(-1))
- unregisterAdjacency(it.value().first, it.value().second);
-
- // Mark that triangleIdx no longer shares the edge
- if (it.value().first == triangleIdx)
- it.value().first = quint32(-1);
- else
- it.value().second = quint32(-1);
-
-
- // No more triangles refer to this (== it was the old diagonal), so we
- // remove it from the existing edges list
- if (it.value().first == quint32(-1) && it.value().second == quint32(-1))
- it = existingEdges.erase(it);
- };
-
- insertOrdered(superTriangle.v1Index, superTriangle.v2Index, 0);
- insertOrdered(superTriangle.v2Index, superTriangle.v3Index, 0);
- insertOrdered(superTriangle.v3Index, superTriangle.v1Index, 0);
-
- // Insert vertices one by one
- // For each vertex, we search for the triangle that contains it and then split this at the
- // point. We then check the Delaunay property of the new triangles and their neighbours, and
- // swap diagonals to restore Delaunayness when needed.
- quint32 lastFormedTriangle = 0;
- for (quint32 vertexIdx = 0; vertexIdx < l - 3; ++vertexIdx) {
- const QtPathVertex &vertex = vertices.at(vertexIdx);
-
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("Adding vertex %1: %2, %3").arg(vertexIdx).arg(vertex.point.x()).arg(vertex.point.y()),
- triangulation,
- vertices,
- QList<quint32>{} << vertexIdx);
- }
-
- QStack<QPair<quint32, quint32> > adjacentTriangles;
- auto findAdjacent = [&existingEdges, &vertexIdx, &adjacentTriangles](quint32 i1, quint32 i2, quint32 triangleIdx) {
- if (i1 != vertexIdx && i2 != vertexIdx) {
-
- if (i1 > i2)
- qSwap(i1, i2);
- Pair key = qMakePair(i1, i2);
- auto it = existingEdges.find(key);
- Q_ASSERT(it.value().first == triangleIdx || it.value().second == triangleIdx);
-
- if (it.value().first != quint32(-1) && it.value().second != quint32(-1)) {
- if (it.value().first == triangleIdx)
- adjacentTriangles.append(qMakePair(it.value().second, triangleIdx));
- else
- adjacentTriangles.append(qMakePair(it.value().first, triangleIdx));
- }
- }
- };
-
- {
- quint32 triangleIdx1 = quint32(-1);
-
- // Start with the last formed triangle (or the super triangle on the first run)
- // Use Lawson's visibility walk to find the triangle containing the point:
- // First check the current triangle (starting with the last formed). If it does not
- // contain the point, the next triangle to check is the one of the adjacent triangles
- // in the direction of the point. This is done using a "visibility" check, i.e. whether
- // the point is on the exterior half-plane of the shared edge.)
- triangleIdx1 = lastFormedTriangle;
- while (triangleIdx1 != quint32(-1)
- && !isPointInTriangle(vertex.point,
- vertices[triangulation.at(triangleIdx1).v1Index].point,
- vertices[triangulation.at(triangleIdx1).v2Index].point,
- vertices[triangulation.at(triangleIdx1).v3Index].point)) {
- QtPathTriangle &triangle = triangulation[triangleIdx1];
- if (triangle.lastSeenVertex != quint32(-1) && triangle.lastSeenVertex == vertexIdx) {
- qCWarning(lcShapeCurveRenderer) << "Revisiting triangle which has previously been checked";
- triangleIdx1 = quint32(-1);
- break;
- }
-
- triangle.lastSeenVertex = vertexIdx;
-
- // If the point is not in the current triangle, we find the edge(s) where the
- // point is on the outside of the triangle. The adjacent triangle to this edge
- // is the next in the search. If there are two such edges we pick the first.
- // As long as the triangulation is a Delaunay triangulation, this should always
- // find the triangle, but we keep a fail safe in case of bugs.
- quint32 nextTriangle = findAdjacentTriangleOnSameSide(vertices,
- vertexIdx,
- triangle);
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("Marching from %1 to %2 (%3, %4, %5)").arg(triangleIdx1).arg(nextTriangle),
- triangulation,
- vertices,
- QList<quint32>{} << vertexIdx,
- QList<quint32>{} << (nextTriangle != quint32(-1) ? nextTriangle : 0) << triangleIdx1,
- QList<Pair>{});
- }
-
- triangleIdx1 = nextTriangle;
- }
-
- // Fail safe, if we failed to find the triangle using the optimized step, we do a brute
- // force search.
- if (validateResults || triangleIdx1 == quint32(-1)) {
- if (triangleIdx1 == quint32(-1))
- qCWarning(lcShapeCurveRenderer) << "Can't find triangle using Lawson. Falling back to brute force";
- quint32 otherTriangle = triangleIdx1;
- triangleIdx1 = 0;
- for (; triangleIdx1 < triangulation.size(); ++triangleIdx1) {
- const QtPathTriangle &triangle = triangulation.at(triangleIdx1);
- if (isPointInTriangle(vertex.point, vertices[triangle.v1Index].point,
- vertices[triangle.v2Index].point,
- vertices[triangle.v3Index].point)) {
- break;
- }
- }
-
- if (validateResults && otherTriangle != triangleIdx1 && triangleIdx1 != quint32(-1) && otherTriangle != quint32(-1)) {
- qCWarning(lcShapeCurveRenderer) << "Lawson matched" << otherTriangle
- << "whereas brute force matched" << triangleIdx1
- << "Lawson:" << vertices.at(triangulation.at(otherTriangle).v1Index).point
- << vertices.at(triangulation.at(otherTriangle).v2Index).point
- << vertices.at(triangulation.at(otherTriangle).v3Index).point
- << "Brute:" << vertices.at(triangulation.at(triangleIdx1).v1Index).point
- << vertices.at(triangulation.at(triangleIdx1).v2Index).point
- << vertices.at(triangulation.at(triangleIdx1).v3Index).point
- << "Testing point:" << vertices.at(vertexIdx).point;
-
- saveDebugImage(saveImageOnError,
- path,
- QStringLiteral("Two triangles containing the same point"),
- triangulation,
- vertices,
- QList<quint32>{} << vertexIdx,
- QList<quint32>{} << triangleIdx1 << otherTriangle);
- }
- }
-
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("Splitting triangle"),
- triangulation,
- vertices,
- QList<quint32>{} << vertexIdx,
- QList<quint32>{} << triangleIdx1);
- }
-
- if (triangleIdx1 >= triangulation.size()) {
- qCWarning(lcShapeCurveRenderer) << "Unable to find any triangle containing point"
- << vertex.point
- << ". Aborting.";
- return QList<QtPathTriangle>{};
- }
-
- // Split triangle into three, connecting to the new vertex
- quint32 t1v1Index;
- quint32 t1v2Index;
- quint32 t1v3Index;
- {
- QtPathTriangle &t1 = triangulation[triangleIdx1];
- t1v1Index = t1.v1Index;
- t1v2Index = t1.v2Index;
- t1v3Index = t1.v3Index;
- t1.v3Index = vertexIdx;
-
- removeTriangleEdge(t1v2Index, t1v3Index, triangleIdx1);
- removeTriangleEdge(t1v3Index, t1v1Index, triangleIdx1);
- }
-
-
- quint32 triangleIdx2 = triangulation.size();
- triangulation.append(QtPathTriangle(t1v2Index, t1v3Index, vertexIdx, triangulation.count()));
- quint32 triangleIdx3 = triangulation.size();
- triangulation.append(QtPathTriangle(t1v3Index, t1v1Index, vertexIdx, triangulation.count()));
- const QtPathTriangle &t2 = triangulation.at(triangleIdx2);
- const QtPathTriangle &t3 = triangulation.at(triangleIdx3);
-
- lastFormedTriangle = triangleIdx3;
-
- t1v3Index = vertexIdx;
-
- insertOrdered(t1v2Index, t1v3Index, triangleIdx1);
- insertOrdered(t1v3Index, t1v1Index, triangleIdx1);
-
- insertOrdered(t2.v1Index, t2.v2Index, triangleIdx2);
- insertOrdered(t2.v2Index, t2.v3Index, triangleIdx2);
- insertOrdered(t2.v3Index, t2.v1Index, triangleIdx2);
-
- insertOrdered(t3.v1Index, t3.v2Index, triangleIdx3);
- insertOrdered(t3.v2Index, t3.v3Index, triangleIdx3);
- insertOrdered(t3.v3Index, t3.v1Index, triangleIdx3);
-
- // Find triangles sharing edges with new triangles and push to stack
- findAdjacent(t1v1Index, t1v2Index, triangleIdx1);
- findAdjacent(t2.v1Index, t2.v2Index, triangleIdx2);
- findAdjacent(t3.v1Index, t3.v2Index, triangleIdx3);
- }
-
- // Check adjacent triangles to see that they do not contain the introduced vertex in their
- // circumcircle. In cases where this happens, we take the quadrilateral consisting of the
- // new triangle and its neighbour and turn it into two new triangles by swapping to the
- // other diagonal.
- while (!adjacentTriangles.isEmpty()) {
- QPair<quint32, quint32> pair = adjacentTriangles.pop();
- quint32 triangleIdx1 = pair.first;
- quint32 triangleIdx2 = pair.second;
-
- // If opposing triangle circumcircle contains the vertex we are adding, we swap
- // the diagonal of the quad made of the two opposing triangles
- QtPathTriangle &triangle1 = triangulation[triangleIdx1];
- QtPathTriangle &triangle2 = triangulation[triangleIdx2];
- if (isPointInCircumcircle(vertex.point, vertices[triangle1.v1Index].point,
- vertices[triangle1.v2Index].point,
- vertices[triangle1.v3Index].point)) {
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("About to swap diagonal"),
- triangulation,
- vertices,
- QList<quint32>{} << vertexIdx,
- QList<quint32>{} << triangleIdx1 << triangleIdx2);
- }
-
- removeTriangleEdge(triangle1.v1Index, triangle1.v2Index, triangleIdx1);
- removeTriangleEdge(triangle1.v2Index, triangle1.v3Index, triangleIdx1);
- removeTriangleEdge(triangle1.v3Index, triangle1.v1Index, triangleIdx1);
-
- removeTriangleEdge(triangle2.v1Index, triangle2.v2Index, triangleIdx2);
- removeTriangleEdge(triangle2.v2Index, triangle2.v3Index, triangleIdx2);
- removeTriangleEdge(triangle2.v3Index, triangle2.v1Index, triangleIdx2);
-
- swapDiagonal(&triangle1, &triangle2, vertexIdx);
-
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("Has swapped diagonal"),
- triangulation,
- vertices,
- QList<quint32>{} << vertexIdx,
- QList<quint32>{} << triangleIdx1 << triangleIdx2);
- }
-
- // Add back triangles sharing edges
- insertOrdered(triangle1.v1Index, triangle1.v2Index, triangleIdx1);
- insertOrdered(triangle1.v2Index, triangle1.v3Index, triangleIdx1);
- insertOrdered(triangle1.v3Index, triangle1.v1Index, triangleIdx1);
-
- insertOrdered(triangle2.v1Index, triangle2.v2Index, triangleIdx2);
- insertOrdered(triangle2.v2Index, triangle2.v3Index, triangleIdx2);
- insertOrdered(triangle2.v3Index, triangle2.v1Index, triangleIdx2);
-
- findAdjacent(triangle1.v1Index, triangle1.v2Index, triangleIdx1);
- findAdjacent(triangle1.v2Index, triangle1.v3Index, triangleIdx1);
- findAdjacent(triangle1.v3Index, triangle1.v1Index, triangleIdx1);
-
- findAdjacent(triangle2.v1Index, triangle2.v2Index, triangleIdx2);
- findAdjacent(triangle2.v2Index, triangle2.v3Index, triangleIdx2);
- findAdjacent(triangle2.v3Index, triangle2.v1Index, triangleIdx2);
- }
- }
- }
-
- qCDebug(lcShapeTiming) << "qtDelaunayTriangulator: Inserting vertices in" << t.elapsed() << "ms";
-
- // Make a look-up for constrained edges to check self-intersections
- for (const QtPathEdge &constraintEdge : edges) {
- quint32 a = constraintEdge.startIndex;
- quint32 b = constraintEdge.endIndex;
- if (a > b)
- qSwap(a, b);
- constrainedEdges.insert(qMakePair(a, b));
- }
-
- // Insert constraint edges:
- // For each constrained edge, we make sure it is in the triangulation: We first check if it
- // already happens to be, and if so we do nothing. If it is not, we check which edges in the
- // current graph it intersects, and remove these by swapping diagonals. We then go through
- // newly created edges (except the constrained edge) and swap them for the other diagonals in
- // cases where this is needed to satisfy Delaunay property.
- for (const QtPathEdge &constraintEdge : edges) {
- quint32 a = constraintEdge.startIndex;
- quint32 b = constraintEdge.endIndex;
- if (a > b)
- qSwap(a, b);
-
- // If edge already exists, we continue
- if (!existingEdges.contains(qMakePair(a, b))) {
- const QVector2D &newEdgeP1 = vertices.at(a).point;
- const QVector2D &newEdgeP2 = vertices.at(b).point;
-
- // Find all edges that intersect with [a, b]
- QList<Pair> intersectingEdges;
- static bool permitSelfIntersections = qEnvironmentVariableIntValue("QT_QUICKSHAPES_PERMIT_SELF_INTERSECTIONS") != 0;
-
- // We start search with any triangle containing 'a' as a vertex
- // ### Could we keep track of this as well in the vertex info?
- quint32 triangleIdx = 0;
- while (triangleIdx != quint32(-1)
- && triangulation.at(triangleIdx).v1Index != a
- && triangulation.at(triangleIdx).v2Index != a
- && triangulation.at(triangleIdx).v3Index != a) {
- const QtPathTriangle &triangle = triangulation.at(triangleIdx);
- triangleIdx = findAdjacentTriangleOnSameSide(vertices,
- a,
- triangle);
- }
-
- if (triangleIdx >= triangulation.size()) {
- qCWarning(lcShapeCurveRenderer)
- << "qtDelaunayTriangulator: Unable to find any triangle containing vertex"
- << a << vertices.at(a).point
- << ". Constrained edge will be missing from triangulation";
- continue;
- }
-
- // We now circle 'a' by looking at adjacent triangles on edges containing 'a' (making
- // sure we do not go backwards) until we find one which intersects [a, b]. We then
- // use the visibility walk to check triangles in the direction of 'b', recording
- // intersections as we go and moving via intersecting edges. We stop when there are no
- // more intersections.
- quint32 previousTriangleIdx = quint32(-1);
- quint32 startingTriangleIdx = triangleIdx;
- while (triangleIdx != quint32(-1)) {
- quint32 nextTriangle = quint32(-1);
- const QtPathTriangle &triangle = triangulation.at(triangleIdx);
-
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("Circling %1 to find first intersection (triangle: %2, %3, %4)").arg(a).arg(triangle.v1Index).arg(triangle.v2Index).arg(triangle.v3Index),
- triangulation,
- vertices,
- QList<quint32>{} << a,
- QList<quint32>{} << triangleIdx,
- QList<Pair>{} << qMakePair(a, b));
- }
-
- Pair existingEdge;
- quint32 adjacentTriangle;
- if (triangle.v1Index == a) {
- existingEdge = qMakePair(triangle.v2Index, triangle.v3Index);
- adjacentTriangle = triangle.adjacentTriangle2;
-
- if (triangle.adjacentTriangle1 != previousTriangleIdx
- && triangle.adjacentTriangle1 != quint32(-1)) {
- nextTriangle = triangle.adjacentTriangle1; // v1-v2
- } else {
- nextTriangle = triangle.adjacentTriangle3; // v3-v1
- }
- } else if (triangle.v2Index == a) {
- existingEdge = qMakePair(triangle.v3Index, triangle.v1Index);
- adjacentTriangle = triangle.adjacentTriangle3;
-
- if (triangle.adjacentTriangle1 != previousTriangleIdx
- && triangle.adjacentTriangle1 != quint32(-1)) {
- nextTriangle = triangle.adjacentTriangle1; // v1-v2
- } else {
- nextTriangle = triangle.adjacentTriangle2; // v2-v3
- }
- } else { // a == triangle.v3Index
- Q_ASSERT(a == triangle.v3Index);
- existingEdge = qMakePair(triangle.v1Index, triangle.v2Index);
- adjacentTriangle = triangle.adjacentTriangle1;
-
- if (triangle.adjacentTriangle3 != previousTriangleIdx
- && triangle.adjacentTriangle3 != quint32(-1)) {
- nextTriangle = triangle.adjacentTriangle3; // v3-v1
- } else {
- nextTriangle = triangle.adjacentTriangle2; // v2-v3
- }
- }
-
- const QVector2D &existingEdgeP1 = vertices.at(existingEdge.first).point;
- const QVector2D &existingEdgeP2 = vertices.at(existingEdge.second).point;
-
- if (lineIntersects(newEdgeP1, newEdgeP2, existingEdgeP1, existingEdgeP2)) {
- // Go to next phase of search, where the intersections are recorded.
- triangleIdx = adjacentTriangle;
- if (existingEdge.first > existingEdge.second)
- qSwap(existingEdge.first, existingEdge.second);
- intersectingEdges.append(existingEdge);
-
- if (!permitSelfIntersections && constrainedEdges.contains(existingEdge)) {
- qCWarning(lcShapeCurveRenderer)
- << "qtDelaunayTriangulator: Exiting early due to self-intersection."
- << "Edge 1:" << a << b << "(" << newEdgeP1 << newEdgeP2 << ")"
- << "Edge 2:" << existingEdge.first << existingEdge.second << "(" << existingEdgeP1 << existingEdgeP2 << ")";
- saveDebugImage(saveImageOnError,
- path,
- QStringLiteral("Found intersection of two constrained edges. No solution."),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{},
- QList<QPair<quint32, quint32> >{}
- << qMakePair(a, b)
- << qMakePair(existingEdge.first, existingEdge.second));
- return QList<QtPathTriangle>{};
- }
-
- break;
- }
-
- previousTriangleIdx = triangleIdx;
- triangleIdx = nextTriangle;
-
- // No intersection found
- if (triangleIdx == startingTriangleIdx)
- triangleIdx = quint32(-1);
- }
-
- // Now we march towards 'b' and record all intersections
- while (triangleIdx != quint32(-1)) {
- const QtPathTriangle &triangle = triangulation.at(triangleIdx);
-
- QVarLengthArray<quint32, 2> possibleTriangles;
- QVarLengthArray<Pair, 2> edges;
-
- findAdjacentTrianglesOnSameSide(vertices, b, triangle, &possibleTriangles, &edges);
-
- // Check edge(s) which can see 'b' for intersections
- triangleIdx = quint32(-1);
- for (int i = 0; i < edges.size(); ++i) {
- const Pair &existingEdge = edges.at(i);
-
- const QVector2D &existingEdgeP1 = vertices.at(existingEdge.first).point;
- const QVector2D &existingEdgeP2 = vertices.at(existingEdge.second).point;
-
- if (existingEdge.first != a
- && existingEdge.second != a
- && lineIntersects(newEdgeP1, newEdgeP2, existingEdgeP1, existingEdgeP2)) {
- if (!permitSelfIntersections && constrainedEdges.contains(existingEdge)) {
- qCWarning(lcShapeCurveRenderer)
- << "qtDelaunayTriangulator: Exiting early due to self-intersection."
- << "Edge 1:" << a << b << "(" << newEdgeP1 << newEdgeP2 << ")"
- << "Edge 2:" << existingEdge.first << existingEdge.second << "(" << existingEdgeP1 << existingEdgeP2 << ")";
- saveDebugImage(saveImageOnError,
- path,
- QStringLiteral("Found intersection of two constrained edges. No solution."),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{},
- QList<QPair<quint32, quint32> >{}
- << qMakePair(a, b)
- << qMakePair(existingEdge.first, existingEdge.second));
- return QList<QtPathTriangle>{};
- }
-
- intersectingEdges.append(existingEdge);
- triangleIdx = possibleTriangles[i];
-
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("Marching to %1 (triangle: %2, %3, %4)")
- .arg(b)
- .arg(triangulation.at(possibleTriangles.first()).v1Index)
- .arg(triangulation.at(possibleTriangles.first()).v2Index)
- .arg(triangulation.at(possibleTriangles.first()).v3Index),
- triangulation,
- vertices,
- QList<quint32>{} << b,
- QList<quint32>{} << triangleIdx << possibleTriangles.first(),
- QList<Pair>{} << qMakePair(a, b) << existingEdge);
- }
- }
- }
- }
-
- if (Q_UNLIKELY(validateResults)) {
- QList<Pair> intersectingEdgesFound = intersectingEdges;
- intersectingEdges.clear();
- for (auto it = existingEdges.constBegin(); it != existingEdges.constEnd(); ++it) {
- const Pair &existingEdge = it.key();
-
- const QVector2D &existingEdgeP1 = vertices.at(existingEdge.first).point;
- const QVector2D &existingEdgeP2 = vertices.at(existingEdge.second).point;
-
- if (existingEdge.first != a && existingEdge.second != a
- && existingEdge.first != b && existingEdge.second != b
- && lineIntersects(newEdgeP1, newEdgeP2, existingEdgeP1, existingEdgeP2)) {
- if (!permitSelfIntersections && constrainedEdges.contains(existingEdge)) {
- qCWarning(lcShapeCurveRenderer)
- << "qtDelaunayTriangulator: Exiting early due to self-intersection."
- << "Edge 1:" << a << b << "(" << newEdgeP1 << newEdgeP2 << ")"
- << "Edge 2:" << existingEdge.first << existingEdge.second << "(" << existingEdgeP1 << existingEdgeP2 << ")";
- saveDebugImage(saveImageOnError,
- path,
- QStringLiteral("Found intersection of two constrained edges. No solution."),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{},
- QList<QPair<quint32, quint32> >{} << qMakePair(a, b) << qMakePair(existingEdge.first, existingEdge.second));
- return QList<QtPathTriangle>{};
- }
-
- intersectingEdges.append(existingEdge);
-
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("Found intersection of constrained edge (%1 so far)").arg(intersectingEdges.size()),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{},
- QList<QPair<quint32, quint32> >{} << qMakePair(a, b) << qMakePair(existingEdge.first, existingEdge.second));
- }
- }
- }
-
- for (const Pair &intersectingEdge : intersectingEdges) {
- bool found = false;
- for (const Pair &otherIntersectingEdge : intersectingEdgesFound) {
- if (otherIntersectingEdge == intersectingEdge || qMakePair(otherIntersectingEdge.second, otherIntersectingEdge.first) == intersectingEdge) {
- found = true;
- break;
- }
- }
-
- if (!found) {
- qCWarning(lcShapeCurveRenderer) << "Intersecting edge" << intersectingEdge.first << intersectingEdge.second << "not found with smart algorithm. Constrained edge:" << a << b;
-
- saveDebugImage(saveImageOnError,
- path,
- QStringLiteral("Intersecting edge not found"),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{},
- QList<QPair<quint32, quint32> >{} << qMakePair(a, b) << qMakePair(intersectingEdge.first, intersectingEdge.second));
-
- return QList<QtPathTriangle>{};
- }
- }
-
- for (const Pair &intersectingEdge : intersectingEdgesFound) {
- bool found = false;
- for (const Pair &otherIntersectingEdge : intersectingEdges) {
- if (otherIntersectingEdge == intersectingEdge || qMakePair(otherIntersectingEdge.second, otherIntersectingEdge.first) == intersectingEdge) {
- found = true;
- break;
- }
- }
-
- if (!found) {
- qCWarning(lcShapeCurveRenderer) << "Intersecting edge" << intersectingEdge.first << intersectingEdge.second << "not found with dumb algorithm. Constrained edge:" << a << b;
-
- saveDebugImage(saveImageOnError,
- path,
- QStringLiteral("Intersecting edge not found"),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{},
- QList<QPair<quint32, quint32> >{} << qMakePair(a, b) << qMakePair(intersectingEdge.first, intersectingEdge.second));
-
- return QList<QtPathTriangle>{};
- }
- }
- }
-
- bool somethingChanged = false;
- auto it = intersectingEdges.begin();
- QSet<Pair> newlyCreatedEdges;
- while (!intersectingEdges.isEmpty()) {
- Q_ASSERT(it != intersectingEdges.end());
-
- Pair existingEdge = *it;
-
- Q_ASSERT(existingEdges.contains(existingEdge));
-
- // ### Since we are now finding edges via triangles, this information can be
- // included in the intersecting edges list and we do not need the extra lookup.
- Pair triangles = existingEdges.value(existingEdge);
-
- QtPathTriangle &triangle1 = triangulation[triangles.first];
- QtPathTriangle &triangle2 = triangulation[triangles.second];
-
- // If the triangles do not together make a convex polygon, we skip this for now
- // Otherwise, swap the diagonal
- if (isConvexQuadrilateral(vertices, triangle1, triangle2)) {
- somethingChanged = true;
-
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("About to swap diagonal"),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{} << triangles.first << triangles.second,
- QList<QPair<quint32, quint32> >{} << qMakePair(existingEdge.first, existingEdge.second));
- }
-
- // The triangles are being altered, so we have to remove them from the edge
- // list first
- removeTriangleEdge(triangle1.v1Index, triangle1.v2Index, triangles.first);
- removeTriangleEdge(triangle1.v2Index, triangle1.v3Index, triangles.first);
- removeTriangleEdge(triangle1.v3Index, triangle1.v1Index, triangles.first);
-
- removeTriangleEdge(triangle2.v1Index, triangle2.v2Index, triangles.second);
- removeTriangleEdge(triangle2.v2Index, triangle2.v3Index, triangles.second);
- removeTriangleEdge(triangle2.v3Index, triangle2.v1Index, triangles.second);
-
- // If not, we swap the diagonal of the two triangles
- swapDiagonal(&triangle1, &triangle2);
- it = intersectingEdges.erase(it);
- if (it == intersectingEdges.end()) { // Wrap over end of list
- it = intersectingEdges.begin();
- somethingChanged = false;
- }
-
- // Add back triangles sharing edges
- insertOrdered(triangle1.v1Index, triangle1.v2Index, triangles.first);
- insertOrdered(triangle1.v2Index, triangle1.v3Index, triangles.first);
- insertOrdered(triangle1.v3Index, triangle1.v1Index, triangles.first);
-
- insertOrdered(triangle2.v1Index, triangle2.v2Index, triangles.second);
- insertOrdered(triangle2.v2Index, triangle2.v3Index, triangles.second);
- insertOrdered(triangle2.v3Index, triangle2.v1Index, triangles.second);
-
- Pair newDiagonal = triangle1.v1Index < triangle1.v2Index
- ? qMakePair(triangle1.v1Index, triangle1.v2Index)
- : qMakePair(triangle1.v2Index, triangle1.v1Index);
-
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("Swapped diagonal"),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{} << triangles.first << triangles.second,
- QList<QPair<quint32, quint32> >{} << qMakePair(newDiagonal.first, newDiagonal.second));
- }
-
- // By convention, the new diagonal is now the two first vertices in the triangles
- const QVector2D &newDiagonalP1 = vertices[triangle1.v1Index].point;
- const QVector2D &newDiagonalP2 = vertices[triangle1.v2Index].point;
-
- if (newDiagonal.first != a
- && newDiagonal.second != a
- && newDiagonal.first != b
- && newDiagonal.second != b
- && lineIntersects(newEdgeP1, newEdgeP2, newDiagonalP1, newDiagonalP2)) {
- it = intersectingEdges.insert(it, newDiagonal);
- } else {
- if (newDiagonal != qMakePair(a, b) && newDiagonal != qMakePair(b, a))
- newlyCreatedEdges.insert(newDiagonal);
-
- continue; // No need to increase iterator, since we erased
- }
- } else {
- if (Q_UNLIKELY(drawAllImages)) {
- saveDebugImage(true,
- path,
- QStringLiteral("Skipping non-convex quad"),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{} << triangles.first << triangles.second,
- QList<QPair<quint32, quint32> >{} << qMakePair(existingEdge.first, existingEdge.second));
- }
- }
-
- if (++it == intersectingEdges.end()) { // Wrap over end of list
- if (!somethingChanged && !intersectingEdges.isEmpty()) {
- intersectingEdges.prepend(qMakePair(a, b)); // Just for visuals
- saveDebugImage(saveImageOnError,
- path,
- QStringLiteral("Looped through all edges with no changes. No solution."),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{},
- intersectingEdges);
- qCWarning(lcShapeCurveRenderer) << "Detected infinite loop. Exiting early.";
- break;
- }
-
- it = intersectingEdges.begin();
- somethingChanged = false;
- }
- }
-
- // Restore the delaunay properties
- for (const Pair &newlyCreatedEdge : newlyCreatedEdges) {
- auto it = existingEdges.find(newlyCreatedEdge);
- Q_ASSERT(it != existingEdges.end());
-
- Pair triangles = it.value();
-
- QtPathTriangle &triangle1 = triangulation[triangles.first];
- QtPathTriangle &triangle2 = triangulation[triangles.second];
-
- bool delaunaySatisfied =
- !isPointInCircumcircle(vertices.at(triangle1.v1Index).point,
- vertices.at(triangle2.v1Index).point,
- vertices.at(triangle2.v2Index).point,
- vertices.at(triangle2.v3Index).point)
- && !isPointInCircumcircle(vertices.at(triangle1.v2Index).point,
- vertices.at(triangle2.v1Index).point,
- vertices.at(triangle2.v2Index).point,
- vertices.at(triangle2.v3Index).point)
- && !isPointInCircumcircle(vertices.at(triangle1.v3Index).point,
- vertices.at(triangle2.v1Index).point,
- vertices.at(triangle2.v2Index).point,
- vertices.at(triangle2.v3Index).point)
- && !isPointInCircumcircle(vertices.at(triangle2.v1Index).point,
- vertices.at(triangle1.v1Index).point,
- vertices.at(triangle1.v2Index).point,
- vertices.at(triangle1.v3Index).point)
- && !isPointInCircumcircle(vertices.at(triangle2.v2Index).point,
- vertices.at(triangle1.v1Index).point,
- vertices.at(triangle1.v2Index).point,
- vertices.at(triangle1.v3Index).point)
- && !isPointInCircumcircle(vertices.at(triangle2.v3Index).point,
- vertices.at(triangle1.v1Index).point,
- vertices.at(triangle1.v2Index).point,
- vertices.at(triangle1.v3Index).point);
- if (!delaunaySatisfied) {
- removeTriangleEdge(triangle1.v1Index, triangle1.v2Index, triangles.first);
- removeTriangleEdge(triangle1.v2Index, triangle1.v3Index, triangles.first);
- removeTriangleEdge(triangle1.v3Index, triangle1.v1Index, triangles.first);
-
- removeTriangleEdge(triangle2.v1Index, triangle2.v2Index, triangles.second);
- removeTriangleEdge(triangle2.v2Index, triangle2.v3Index, triangles.second);
- removeTriangleEdge(triangle2.v3Index, triangle2.v1Index, triangles.second);
-
- // If not, we swap the diagonal of the two triangles
- swapDiagonal(&triangle1, &triangle2);
-
- // Add back triangles sharing edges
- insertOrdered(triangle1.v1Index, triangle1.v2Index, triangles.first);
- insertOrdered(triangle1.v2Index, triangle1.v3Index, triangles.first);
- insertOrdered(triangle1.v3Index, triangle1.v1Index, triangles.first);
-
- insertOrdered(triangle2.v1Index, triangle2.v2Index, triangles.second);
- insertOrdered(triangle2.v2Index, triangle2.v3Index, triangles.second);
- insertOrdered(triangle2.v3Index, triangle2.v1Index, triangles.second);
- }
- }
-
- if (Q_UNLIKELY(validateResults)) {
- if (!existingEdges.contains(qMakePair(a, b))) {
- qCWarning(lcShapeCurveRenderer)
- << "Unable to produce triangulation with edge" << a << b;
- saveDebugImage(saveImageOnError,
- path,
- QStringLiteral("Triangulation missing [%1, %2]").arg(a).arg(b),
- triangulation,
- vertices,
- QList<quint32>{},
- QList<quint32>{},
- QList<Pair>{} << qMakePair(a, b));
- }
- }
- }
- }
-
- qCDebug(lcShapeTiming) << "qtDelaunayTriangulator: Inserting edges in" << t.elapsed() << "ms";
-
- for (auto it = triangulation.begin(); it != triangulation.end(); ++it) {
- QtPathTriangle &triangle = *it;
- if (triangle.v1Index >= l - 3 || triangle.v2Index >= l - 3 || triangle.v3Index >= l - 3)
- triangle.isValid = false;
- }
-
- return triangulation;
-}
-
-QT_END_NAMESPACE
diff --git a/src/quickshapes/qt_quadratic_bezier.cpp b/src/quickshapes/qt_quadratic_bezier.cpp
index 93158010a1..6a41a67872 100644
--- a/src/quickshapes/qt_quadratic_bezier.cpp
+++ b/src/quickshapes/qt_quadratic_bezier.cpp
@@ -8,38 +8,81 @@
QT_BEGIN_NAMESPACE
-#if 0
-static bool qt_isQuadratic(const QBezier &b)
-{
- const qreal f = 3.0 / 2.0;
- const QPointF c1 = b.pt1() + f * (b.pt2() - b.pt1());
- const QPointF c2 = b.pt4() + f * (b.pt3() - b.pt4());
- return c1 == c2;
-}
-#endif
-
static qreal qt_scoreQuadratic(const QBezier &b, QPointF qcp)
{
- // Construct a cubic from the quadratic, and compare its control points to the originals'
- const QRectF bounds = b.bounds();
- qreal dim = QLineF(bounds.topLeft(), bounds.bottomRight()).length();
- if (qFuzzyIsNull(dim))
- return 0;
- const qreal f = 2.0 / 3;
- const QPointF cp1 = b.pt1() + f * (qcp - b.pt1());
- const QPointF cp2 = b.pt4() + f * (qcp - b.pt4());
- const QLineF d1(b.pt2(), cp1);
- const QLineF d2(b.pt3(), cp2);
- return qMax(d1.length(), d2.length()) / dim;
+ static bool init = false;
+ const int numSteps = 21;
+ Q_STATIC_ASSERT(numSteps % 2 == 1); // numTries must be odd
+ static qreal t2s[numSteps];
+ static qreal tmts[numSteps];
+ if (!init) {
+ // Precompute bezier factors
+ qreal t = 0.20;
+ const qreal step = (1 - (2 * t)) / (numSteps - 1);
+ for (int i = 0; i < numSteps; i++) {
+ t2s[i] = t * t;
+ tmts[i] = 2 * t * (1 - t);
+ t += step;
+ }
+ init = true;
+ }
+
+ const QPointF midPoint = b.midPoint();
+ auto distForIndex = [&](int i) -> qreal {
+ QPointF qp = (t2s[numSteps - 1 - i] * b.pt1()) + (tmts[i] * qcp) + (t2s[i] * b.pt4());
+ QPointF d = midPoint - qp;
+ return QPointF::dotProduct(d, d);
+ };
+
+ const int halfSteps = (numSteps - 1) / 2;
+ bool foundIt = false;
+ const qreal centerDist = distForIndex(halfSteps);
+ qreal minDist = centerDist;
+ // Search for the minimum in right half
+ for (int i = 0; i < halfSteps; i++) {
+ qreal tDist = distForIndex(halfSteps + 1 + i);
+ if (tDist < minDist) {
+ minDist = tDist;
+ } else {
+ foundIt = (i > 0);
+ break;
+ }
+ }
+ if (!foundIt) {
+ // Search in left half
+ minDist = centerDist;
+ for (int i = 0; i < halfSteps; i++) {
+ qreal tDist = distForIndex(halfSteps - 1 - i);
+ if (tDist < minDist) {
+ minDist = tDist;
+ } else {
+ foundIt = (i > 0);
+ break;
+ }
+ }
+ }
+ return foundIt ? minDist : centerDist;
}
-static qreal qt_quadraticForCubic(const QBezier &b, QPointF *qcp)
+static QPointF qt_quadraticForCubic(const QBezier &b)
{
const QLineF st = b.startTangent();
const QLineF et = b.endTangent();
- if (st.intersects(et, qcp) == QLineF::NoIntersection)
- *qcp = b.midPoint();
- return qt_scoreQuadratic(b, *qcp);
+ const QPointF midPoint = b.midPoint();
+ bool valid = true;
+ QPointF quadControlPoint;
+ if (st.intersects(et, &quadControlPoint) == QLineF::NoIntersection) {
+ valid = false;
+ } else {
+ // Check if intersection is on wrong side
+ const QPointF bl = b.pt4() - b.pt1();
+ const QPointF ml = midPoint - b.pt1();
+ const QPointF ql = quadControlPoint - b.pt1();
+ qreal cx1 = (ml.x() * bl.y()) - (ml.y() * bl.x());
+ qreal cx2 = (ql.x() * bl.y()) - (ql.y() * bl.x());
+ valid = (std::signbit(cx1) == std::signbit(cx2));
+ }
+ return valid ? quadControlPoint : midPoint;
}
static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
@@ -57,22 +100,34 @@ static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
const QBezier n = orig.mapBy(xf);
Q_ASSERT(n.pt1() == QPoint() && qFuzzyIsNull(float(n.pt4().y())));
- const qreal p = n.pt3().x() * n.pt2().y();
- const qreal q = n.pt4().x() * n.pt2().y();
- const qreal r = n.pt2().x() * n.pt3().y();
- const qreal s = n.pt4().x() * n.pt3().y();
-
- const qreal a = 36 * ((-3 * p) + (2 * q) + (3 * r) - s);
- if (!a)
- return 0;
- const qreal b = -18 * (((3 * p) - q) - (3 * r));
+ const qreal x2 = n.pt2().x();
+ const qreal x3 = n.pt3().x();
+ const qreal x4 = n.pt4().x();
+ const qreal y2 = n.pt2().y();
+ const qreal y3 = n.pt3().y();
+
+ const qreal p = x3 * y2;
+ const qreal q = x4 * y2;
+ const qreal r = x2 * y3;
+ const qreal s = x4 * y3;
+
+ const qreal a = 18 * ((-3 * p) + (2 * q) + (3 * r) - s);
+ if (qFuzzyIsNull(float(a))) {
+ if (std::signbit(y2) != std::signbit(y3) && qFuzzyCompare(float(x4 - x3), float(x2))) {
+ tpoints[0] = 0.5; // approx
+ return 1;
+ } else if (!a) {
+ return 0;
+ }
+ }
+ const qreal b = 18 * (((3 * p) - q) - (3 * r));
const qreal c = 18 * (r - p);
- const qreal rad = (b * b) - (2 * a * c);
+ const qreal rad = (b * b) - (4 * a * c);
if (rad < 0)
return 0;
const qreal sqr = qSqrt(rad);
- const qreal root1 = (b + sqr) / a;
- const qreal root2 = (b - sqr) / a;
+ const qreal root1 = (-b + sqr) / (2 * a);
+ const qreal root2 = (-b - sqr) / (2 * a);
int res = 0;
if (isValidRoot(root1))
@@ -86,54 +141,53 @@ static int qt_getInflectionPoints(const QBezier &orig, qreal *tpoints)
return res;
}
-static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, qreal spanlength, qreal errorLimit)
+static void qt_addToQuadratics(const QBezier &b, QPolygonF *p, int maxSplits, qreal maxDiff)
{
- Q_ASSERT((spanlength > 0) && !(spanlength > 1));
-
- QPointF qcp;
- bool isOk = (qt_quadraticForCubic(b, &qcp) < errorLimit); // error limit, careful
- if (isOk || spanlength < 0.1) {
+ QPointF qcp = qt_quadraticForCubic(b);
+ if (maxSplits <= 0 || qt_scoreQuadratic(b, qcp) < maxDiff) {
p->append(qcp);
p->append(b.pt4());
} else {
QBezier rhs = b;
QBezier lhs;
rhs.parameterSplitLeft(0.5, &lhs);
- qt_addToQuadratics(lhs, p, spanlength / 2, errorLimit);
- qt_addToQuadratics(rhs, p, spanlength / 2, errorLimit);
+ qt_addToQuadratics(lhs, p, maxSplits - 1, maxDiff);
+ qt_addToQuadratics(rhs, p, maxSplits - 1, maxDiff);
}
}
-QPolygonF qt_toQuadratics(const QBezier &b, qreal errorLimit)
+void qt_toQuadratics(const QBezier &b, QPolygonF *out, qreal errorLimit)
{
+ out->resize(0);
+ out->append(b.pt1());
+
+ {
+ // Shortcut if the cubic is really a quadratic
+ const qreal f = 3.0 / 2.0;
+ const QPointF c1 = b.pt1() + f * (b.pt2() - b.pt1());
+ const QPointF c2 = b.pt4() + f * (b.pt3() - b.pt4());
+ if (c1 == c2) {
+ out->append(c1);
+ out->append(b.pt4());
+ return;
+ }
+ }
- QPolygonF res;
- res.reserve(16);
- res.append(b.pt1());
const QRectF cpr = b.bounds();
- qreal epsilon = QLineF(cpr.topLeft(), cpr.bottomRight()).length() * 0.5 * errorLimit;
- bool degenerate = false;
- if (QLineF(b.pt2(), b.pt1()).length() < epsilon) {
- res.append(b.pt3());
- degenerate = true;
- } else if (QLineF(b.pt4(), b.pt3()).length() < epsilon) {
- res.append(b.pt2());
- degenerate = true;
- }
- if (degenerate) {
- res.append(b.pt4());
- return res;
- }
+ const QPointF dim = cpr.bottomRight() - cpr.topLeft();
+ qreal maxDiff = QPointF::dotProduct(dim, dim) * errorLimit * errorLimit; // maxdistance^2
+
qreal infPoints[2];
int numInfPoints = qt_getInflectionPoints(b, infPoints);
+ const int maxSubSplits = numInfPoints > 0 ? 2 : 3;
qreal t0 = 0;
- for (int i = 0; i < numInfPoints + 1; i++) { // #segments == #inflectionpoints + 1
+ // number of main segments == #inflectionpoints + 1
+ for (int i = 0; i < numInfPoints + 1; i++) {
qreal t1 = (i < numInfPoints) ? infPoints[i] : 1;
QBezier segment = b.bezierOnInterval(t0, t1);
- qt_addToQuadratics(segment, &res, t1 - t0, errorLimit);
+ qt_addToQuadratics(segment, out, maxSubSplits, maxDiff);
t0 = t1;
}
- return res;
}
QT_END_NAMESPACE
diff --git a/tests/manual/painterpathquickshape/CMakeLists.txt b/tests/manual/painterpathquickshape/CMakeLists.txt
index a9616564a3..67f5b142ef 100644
--- a/tests/manual/painterpathquickshape/CMakeLists.txt
+++ b/tests/manual/painterpathquickshape/CMakeLists.txt
@@ -14,7 +14,6 @@ qt_internal_add_manual_test(painterPathQuickShape
Qt::Quick
Qt::QuickPrivate
Qt::QuickShapesPrivate
- Qt::SvgPrivate
)
@@ -29,6 +28,7 @@ set(qml_resource_files
"SmallPolygon.qml"
"Squircle.qml"
"ControlledShape.qml"
+ "Mussel.qml"
"Graziano.ttf"
"CubicShape.qml"
"hand-print.svg"
diff --git a/tests/manual/painterpathquickshape/ControlPanel.qml b/tests/manual/painterpathquickshape/ControlPanel.qml
index 5276ec3a2d..1b34bb6eab 100644
--- a/tests/manual/painterpathquickshape/ControlPanel.qml
+++ b/tests/manual/painterpathquickshape/ControlPanel.qml
@@ -9,10 +9,13 @@ import QtQuick.Dialogs
Item {
property real scale: +scaleSlider.value.toFixed(4)
- property color outlineColor: enableOutline.checked ? Qt.rgba(outlineColor.color.r, outlineColor.color.g, outlineColor.color.b, pathAlpha) : Qt.rgba(0,0,0,0)
+ property color backgroundColor: setBackground.checked ? Qt.rgba(bgColor.color.r, bgColor.color.g, bgColor.color.b, 1.0) : Qt.rgba(0,0,0,0)
+
+ property color outlineColor: enableOutline.checked ? Qt.rgba(outlineColor.color.r, outlineColor.color.g, outlineColor.color.b, outlineAlpha) : Qt.rgba(0,0,0,0)
property color fillColor: Qt.rgba(fillColor.color.r, fillColor.color.g, fillColor.color.b, pathAlpha)
property alias pathAlpha: alphaSlider.value
- property alias outlineWidth: outlineWidth.value
+ property alias outlineAlpha: outlineAlphaSlider.value
+ property real outlineWidth: cosmeticPen.checked ? outlineWidth.value / scale : outlineWidth.value ** 2
property alias outlineStyle: outlineStyle.currentValue
property alias capStyle: capStyle.currentValue
property alias joinStyle: joinStyle.currentValue
@@ -27,6 +30,7 @@ Item {
property alias preferCurve: rendererLabel.preferCurve
property int subShape: pickSubShape.checked ? subShapeSelector.value : -1
+ property bool subShapeGreaterThan : pickSubShapeGreaterThan.checked
property real pathMargin: marginEdit.text
@@ -100,10 +104,10 @@ Item {
text: "Alpha"
color: "white"
}
- CheckBox { id: pickSubShape }
- Label {
+ CheckBox {
text: "Pick SVG sub-shape"
- color: "white"
+ id: pickSubShape
+ palette.windowText: "white"
}
SpinBox {
id: subShapeSelector
@@ -112,6 +116,35 @@ Item {
to: 999
editable: true
}
+ CheckBox {
+ id: pickSubShapeGreaterThan
+ visible: pickSubShape.checked
+ text: "show greater than"
+ palette.windowText: "white"
+ }
+ CheckBox {
+ id: setBackground
+ text: "Solid background"
+ palette.windowText: "white"
+ }
+ RowLayout {
+ visible: setBackground.checked
+ Rectangle {
+ id: bgColor
+ property color selectedColor: "#a9a9a9"
+ color: selectedColor
+ border.color: "black"
+ border.width: 2
+ width: 21
+ height: 21
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ bgColorDialog.open()
+ }
+ }
+ }
+ }
}
RowLayout {
Label {
@@ -256,7 +289,6 @@ Item {
}
}
}
-
Label {
text: "Outline width"
color: "white"
@@ -265,14 +297,35 @@ Item {
id: outlineWidth
Layout.fillWidth: true
from: 0.0
- to: 100.0
- value: 10.0
+ to: 10.0
+ value: Math.sqrt(10)
+ }
+ CheckBox {
+ id: cosmeticPen
+ text: "Cosmetic pen"
+ palette.windowText: "white"
+ }
+ Label {
+ text: "Outline alpha"
+ color: "white"
+ }
+ Slider {
+ id: outlineAlphaSlider
+ Layout.fillWidth: true
+ from: 0.0
+ to: 1.0
+ value: 1.0
}
}
}
}
ColorDialog {
+ id: bgColorDialog
+ selectedColor: bgColor.selectedColor
+ onAccepted: bgColor.selectedColor = selectedColor
+ }
+ ColorDialog {
id: outlineColorDialog
selectedColor: outlineColor.selectedColor
onAccepted: outlineColor.selectedColor = selectedColor
diff --git a/tests/manual/painterpathquickshape/ControlPoint.qml b/tests/manual/painterpathquickshape/ControlPoint.qml
index 190d759392..77e7e4fbff 100644
--- a/tests/manual/painterpathquickshape/ControlPoint.qml
+++ b/tests/manual/painterpathquickshape/ControlPoint.qml
@@ -19,16 +19,17 @@ Rectangle {
property point pt: Qt.point(cx, cy)
DragHandler {
+ id: handler
xAxis.minimum: -controlPanel.pathMargin
yAxis.minimum: -controlPanel.pathMargin
- }
- onXChanged: {
- cx = (x + width/2) / controlPanel.scale
- controlPanel.updatePath()
- }
- onYChanged: {
- cy = (y + height/2) / controlPanel.scale
- controlPanel.updatePath()
+ xAxis.onActiveValueChanged: {
+ cx = (x + width/2) / controlPanel.scale
+ controlPanel.updatePath()
+ }
+ yAxis.onActiveValueChanged: {
+ cy = (y + height/2) / controlPanel.scale
+ controlPanel.updatePath()
+ }
}
Component.onCompleted: {
@@ -43,5 +44,4 @@ Rectangle {
y = cy * controlPanel.scale - height/2
}
}
-
}
diff --git a/tests/manual/painterpathquickshape/ControlledShape.qml b/tests/manual/painterpathquickshape/ControlledShape.qml
index 7cdc866fd3..cf9cbaa5d8 100644
--- a/tests/manual/painterpathquickshape/ControlledShape.qml
+++ b/tests/manual/painterpathquickshape/ControlledShape.qml
@@ -13,6 +13,7 @@ Item {
property alias strokeColor: shapePath.strokeColor
property alias strokeWidth: shapePath.strokeWidth
property alias fillRule: shapePath.fillRule
+ property alias shapeTransform: shape.transform
property alias startX: shapePath.startX
property alias startY: shapePath.startY
@@ -58,48 +59,48 @@ Item {
property var gradients: [ null, linearGradient, radialGradient, conicalGradient ]
- Shape {
- id: shape
- x: 0
- y: 0
- preferredRendererType: controlPanel.preferCurve ? Shape.CurveRenderer : Shape.UnknownRenderer
- onRendererTypeChanged: {
- controlPanel.rendererName = rendererType == Shape.SoftwareRenderer ? "Software" :
- rendererType == Shape.GeometryRenderer ? "Geometry" :
- rendererType == Shape.CurveRenderer ? "Curve" : "Unknown";
- }
-
+ Item {
transform: [
Scale {
- xScale: controlPanel.scale
- yScale: controlPanel.scale
- origin.x: shape.implicitWidth / 2
- origin.y: shape.implicitHeight / 2
- }
- ]
-
- ShapePath {
- id: shapePath
- fillRule: ShapePath.WindingFill
- fillGradient: gradients[controlPanel.gradientType]
- strokeColor: controlPanel.outlineColor
- fillColor: controlPanel.fillColor
- strokeWidth: controlPanel.outlineWidth
- strokeStyle: controlPanel.outlineStyle
- joinStyle: controlPanel.joinStyle
- capStyle: controlPanel.capStyle
- }
+ xScale: controlPanel.scale
+ yScale: controlPanel.scale
+ origin.x: shape.implicitWidth / 2
+ origin.y: shape.implicitHeight / 2
+ }
+ ]
+ Shape {
+ id: shape
+ x: 0
+ y: 0
+ preferredRendererType: controlPanel.preferCurve ? Shape.CurveRenderer : Shape.UnknownRenderer
+ onRendererTypeChanged: {
+ controlPanel.rendererName = rendererType == Shape.SoftwareRenderer ? "Software" :
+ rendererType == Shape.GeometryRenderer ? "Geometry" :
+ rendererType == Shape.CurveRenderer ? "Curve" : "Unknown";
+ }
+
+ ShapePath {
+ id: shapePath
+ fillRule: ShapePath.WindingFill
+ fillGradient: gradients[controlPanel.gradientType]
+ strokeColor: controlPanel.outlineColor
+ fillColor: controlPanel.fillColor
+ strokeWidth: controlPanel.outlineWidth
+ strokeStyle: controlPanel.outlineStyle
+ joinStyle: controlPanel.joinStyle
+ capStyle: controlPanel.capStyle
+ }
- Repeater {
- model: topLevel.delegate
- onModelChanged: {
- shapePath.pathElements = []
- for (var i = 0; i < model.length; ++i)
- shapePath.pathElements.push(model[i])
+ Repeater {
+ model: topLevel.delegate
+ onModelChanged: {
+ shapePath.pathElements = []
+ for (var i = 0; i < model.length; ++i)
+ shapePath.pathElements.push(model[i])
+ }
}
}
}
-
Connections {
target: controlPanel
function onPathChanged() {
diff --git a/tests/manual/painterpathquickshape/Mussel.qml b/tests/manual/painterpathquickshape/Mussel.qml
new file mode 100644
index 0000000000..5b72d1054d
--- /dev/null
+++ b/tests/manual/painterpathquickshape/Mussel.qml
@@ -0,0 +1,42 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Shapes
+
+ControlledShape {
+ delegate: [
+ PathMove { x: p1.cx; y: p1.cy },
+ PathQuad { x: p2.cx; y: p2.cy; controlX: c1.cx; controlY: c1.cy },
+ PathQuad { x: p3.cx; y: p3.cy; controlX: c2.cx; controlY: c2.cy },
+ PathLine { x: p1.cx; y: p1.cy }
+ ]
+
+ ControlPoint {
+ id: p1
+ cx: 200
+ cy: 200
+ }
+ ControlPoint {
+ id: c1
+ color: "blue"
+ cx: 600
+ cy: 0
+ }
+ ControlPoint {
+ id: p2
+ cx: 1000
+ cy: 200
+ }
+ ControlPoint {
+ id: c2
+ color: "blue"
+ cx: 1000
+ cy: 1000
+ }
+ ControlPoint {
+ id: p3
+ cx: 200
+ cy: 1000
+ }
+}
diff --git a/tests/manual/painterpathquickshape/SvgShape.qml b/tests/manual/painterpathquickshape/SvgShape.qml
index cf27706e88..697ede2524 100644
--- a/tests/manual/painterpathquickshape/SvgShape.qml
+++ b/tests/manual/painterpathquickshape/SvgShape.qml
@@ -40,19 +40,24 @@ Item {
children = []
let first = true
- let pickOne = controlPanel.subShape
- if (pickOne < 0)
- console.debug("Creating " + pathLoader.paths.length + " SVG items")
+ let pickShape = controlPanel.subShape
+ let pickOne = pickShape >= 0 && !controlPanel.subShapeGreaterThan
+ let pickGreater = pickShape >= 0 && controlPanel.subShapeGreaterThan
+ if (pickOne)
+ console.log("Creating SVG item", pickShape, "out of", pathLoader.paths.length)
+ else if (pickGreater)
+ console.debug("Creating " + (pathLoader.paths.length - pickShape) + " SVG items")
else
- console.log("Creating SVG item", pickOne, "out of", pathLoader.paths.length)
+ console.debug("Creating " + pathLoader.paths.length + " SVG items")
for (var i = 0; i < pathLoader.paths.length; ++i) {
- if (pickOne >= 0 && pickOne !== i)
+ if ((pickOne && pickShape !== i ) || (pickGreater && i < pickShape))
continue
var s = pathLoader.paths[i]
var fillColor = pathLoader.fillColors[i]
let strokeText = "";
let strokeColor = pathLoader.strokeColors[i]
let strokeWidth = pathLoader.strokeWidths[i]
+ let transform = pathLoader.transforms[i]
if (strokeColor) {
if (!strokeWidth)
strokeWidth = "1.0" // default value defined by SVG standard
@@ -62,10 +67,11 @@ Item {
fillColor = "#00000000"
}
- var obj = Qt.createQmlObject("import QtQuick\nimport QtQuick.Shapes\n ControlledShape { "
- + "fillColor: \"" + fillColor + "\";"
- + strokeText
- + "fillRule: ShapePath.WindingFill; delegate: [ PathSvg { path: \"" + s + "\"; } ] }",
+ var obj = Qt.createQmlObject("import QtQuick\nimport QtQuick.Shapes\n ControlledShape { \n"
+ + "fillColor: \"" + fillColor + "\";\n"
+ + "shapeTransform: Matrix4x4 { matrix: Qt.matrix4x4(" + transform + "); }\n"
+ + strokeText + "\n"
+ + "fillRule: ShapePath.WindingFill; delegate: [ PathSvg { path: \"" + s + "\"; } ] }\n",
topLevel, "SvgPathComponent_" + i)
diff --git a/tests/manual/painterpathquickshape/main.qml b/tests/manual/painterpathquickshape/main.qml
index 45651f870f..9ad5d1f1a4 100644
--- a/tests/manual/painterpathquickshape/main.qml
+++ b/tests/manual/painterpathquickshape/main.qml
@@ -42,6 +42,10 @@ Window {
source: "CubicShape.qml"
}
ListElement {
+ text: "Mussel"
+ source: "Mussel.qml"
+ }
+ ListElement {
text: "Arc Direction"
source: "arcDirection.qml"
}
@@ -138,6 +142,11 @@ Window {
source: "qrc:/background.png"
smooth: true
}
+ Rectangle {
+ id: solidBackground
+ anchors.fill: flickable
+ color: controlPanel.backgroundColor
+ }
Flickable {
id: flickable
@@ -152,10 +161,9 @@ Window {
WheelHandler {
onWheel: (event)=> {
let scale = controlPanel.scale
- let posX = event.x
- let posY = event.y
- let xOff = posX - flickable.contentX
- let yOff = posY - flickable.contentY
+ // position in scaled path:
+ let posX = event.x - controlPanel.pathMargin
+ let posY = event.y - controlPanel.pathMargin
let pathX = posX / scale
let pathY = posY / scale
@@ -166,8 +174,12 @@ Window {
scale = scale / 1.1
controlPanel.setScale(scale)
- flickable.contentX = pathX * controlPanel.scale - xOff
- flickable.contentY = pathY * controlPanel.scale - yOff
+ scale = controlPanel.scale
+ let scaledPosX = pathX * scale
+ let scaledPosY = pathY * scale
+
+ flickable.contentX += scaledPosX - posX
+ flickable.contentY += scaledPosY - posY
flickable.returnToBounds()
}
}
diff --git a/tests/manual/painterpathquickshape/svgpathloader.cpp b/tests/manual/painterpathquickshape/svgpathloader.cpp
index 22dfbfba1b..612d00e7d2 100644
--- a/tests/manual/painterpathquickshape/svgpathloader.cpp
+++ b/tests/manual/painterpathquickshape/svgpathloader.cpp
@@ -5,8 +5,11 @@
#include <QFile>
#include <QPainterPath>
-#include <QtSvg/private/qsvgtinydocument_p.h>
-#include <QtSvg/private/qsvggraphics_p.h>
+#include <QXmlStreamReader>
+#include <QXmlStreamAttributes>
+#include <QStack>
+#include <QLocale>
+#include <QMatrix4x4>
SvgPathLoader::SvgPathLoader()
{
@@ -20,6 +23,7 @@ struct SvgState
QString fillColor = {};
QString strokeColor = {};
QString strokeWidth = {};
+ QMatrix4x4 transform;
};
void SvgPathLoader::loadPaths()
@@ -28,6 +32,7 @@ void SvgPathLoader::loadPaths()
m_fillColors.clear();
m_strokeColors.clear();
m_strokeWidths.clear();
+ m_transforms.clear();
if (m_source.isEmpty())
return;
@@ -49,8 +54,50 @@ void SvgPathLoader::loadPaths()
QXmlStreamAttributes attrs = reader.attributes();
if (reader.isStartElement())
states.push(currentState);
+
+ if (attrs.hasAttribute(QStringLiteral("transform"))) {
+ QString t = attrs.value(QStringLiteral("transform")).toString();
+ const bool isTranslate = t.startsWith(QStringLiteral("translate"));
+ const bool isScale = t.startsWith(QStringLiteral("scale"));
+ const bool isMatrix = t.startsWith(QStringLiteral("matrix"));
+ if (isTranslate || isScale || isMatrix) {
+ int pStart = t.indexOf(QLatin1Char('('));
+ int pEnd = t.indexOf(QLatin1Char(')'));
+ if (pStart >= 0 && pEnd > pStart + 1) {
+ t = t.mid(pStart + 1, pEnd - pStart - 1);
+ QStringList coords = t.split(QLatin1Char(','));
+ if (isMatrix && coords.size() == 6) {
+ QMatrix3x3 m;
+ m(0, 0) = coords.at(0).toDouble();
+ m(1, 0) = coords.at(1).toDouble();
+ m(2, 0) = 0.0f;
+
+ m(0, 1) = coords.at(2).toDouble();
+ m(1, 1) = coords.at(3).toDouble();
+ m(2, 1) = 0.0f;
+
+ m(0, 2) = coords.at(4).toDouble();
+ m(1, 2) = coords.at(5).toDouble();
+ m(2, 2) = 1.0f;
+
+ currentState.transform *= QMatrix4x4(m);
+ } else if (coords.size() == 2) {
+ qreal c1 = coords.first().toDouble();
+ qreal c2 = coords.last().toDouble();
+
+ if (isTranslate)
+ currentState.transform.translate(c1, c2);
+ else if (isScale)
+ currentState.transform.scale(c1, c2);
+ }
+ }
+ }
+ }
+
if (attrs.hasAttribute(QStringLiteral("fill"))) {
currentState.fillColor = attrs.value(QStringLiteral("fill")).toString();
+ if (!currentState.fillColor.startsWith("#"))
+ currentState.fillColor = "";
} else if (attrs.hasAttribute(QStringLiteral("style"))) {
QString s = attrs.value(QStringLiteral("style")).toString();
int idx = s.indexOf(QStringLiteral("fill:"));
@@ -73,6 +120,22 @@ void SvgPathLoader::loadPaths()
m_fillColors.append(currentState.fillColor);
m_strokeColors.append(currentState.strokeColor);
m_strokeWidths.append(currentState.strokeWidth);
+
+ QString t;
+ for (int i = 0; i < 4; ++i) {
+ if (i > 0)
+ t += QLatin1Char(',');
+ QVector4D row = currentState.transform.row(i);
+
+ QLocale c(QLocale::C);
+ t += QStringLiteral("%1, %2, %3, %4")
+ .arg(c.toString(row.x()))
+ .arg(c.toString(row.y()))
+ .arg(c.toString(row.z()))
+ .arg(c.toString(row.w()));
+ }
+
+ m_transforms.append(t);
if (attrs.hasAttribute(QStringLiteral("d"))) {
m_paths.append(attrs.value(QStringLiteral("d")).toString());
}
diff --git a/tests/manual/painterpathquickshape/svgpathloader.h b/tests/manual/painterpathquickshape/svgpathloader.h
index 28cd1b76bd..5fd444da2f 100644
--- a/tests/manual/painterpathquickshape/svgpathloader.h
+++ b/tests/manual/painterpathquickshape/svgpathloader.h
@@ -17,6 +17,7 @@ class SvgPathLoader : public QObject
Q_PROPERTY(QStringList fillColors READ fillColors NOTIFY pathsChanged)
Q_PROPERTY(QStringList strokeColors READ strokeColors NOTIFY pathsChanged)
Q_PROPERTY(QStringList strokeWidths READ strokeWidths NOTIFY pathsChanged)
+ Q_PROPERTY(QStringList transforms READ transforms NOTIFY pathsChanged)
public:
SvgPathLoader();
@@ -53,6 +54,11 @@ public:
return m_strokeWidths;
}
+ QStringList transforms() const
+ {
+ return m_transforms;
+ }
+
private slots:
void loadPaths();
@@ -66,6 +72,7 @@ private:
QStringList m_fillColors;
QStringList m_strokeColors;
QStringList m_strokeWidths;
+ QStringList m_transforms;
};
#endif // SVGPATHLOADER_H