aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Rauter <matthias.rauter@qt.io>2023-11-01 15:50:43 +0100
committerMatthias Rauter <matthias.rauter@qt.io>2024-01-07 18:55:28 +0100
commit06e42e733ed6658abbb30ba7be2e571b4533d009 (patch)
tree555c4e1d6072f196407370fd10bfed942ed49fe9
parentac78bf7074c4aa2414b4da38db5b574bec9e4b71 (diff)
Detect and remove self intersections of QQuadPath
Currently the CurveRenderer is not working when the path is self-intersecting. With this patch, the self-intersections are removed before the path is used for filling (optionally, default: on) The stroking path is untouched. The function findOverlappingCandidates finds candidates of elements that might be intersecting. Its complexity is O(n log n) and can also be used in other parts of the code where overlapping bounding triangles need to be identified. The function solveIntersections removes all intersections from a QQuadPath. If intersections are solved, the path is oriented such that the filling is on the right side of the path. If no intersections are found, the path is returned without any changes. The optional argument alwaysReorder can be used to force a reordering of the paths, such that the filling of the shape is always on the right side of the path. Intersections are found with Newtons algorithm with 9 different starting values. This is reliable in finding all intersections but the starting values could be improved/reduced to improve performance. Pick-to: 6.7 Change-Id: I088e4edfff755155521ed91114bc67f63c6e546a Reviewed-by: Paul Olav Tvete <paul.tvete@qt.io>
-rw-r--r--src/quick/scenegraph/qsgcurveprocessor.cpp640
-rw-r--r--src/quick/scenegraph/qsgcurveprocessor_p.h3
-rw-r--r--src/quick/scenegraph/util/qquadpath.cpp123
-rw-r--r--src/quick/scenegraph/util/qquadpath_p.h33
-rw-r--r--src/quickshapes/qquickshapecurverenderer.cpp3
-rw-r--r--src/quickshapes/qquickshapecurverenderer_p.h1
-rw-r--r--tests/baseline/scenegraph/data/shape/shape_intersecting1.qml72
-rw-r--r--tests/baseline/scenegraph/data/shape/shape_intersecting2.qml73
-rw-r--r--tests/manual/painterpathquickshape/CMakeLists.txt2
-rw-r--r--tests/manual/painterpathquickshape/ControlPanel.qml21
-rw-r--r--tests/manual/painterpathquickshape/ControlledShape.qml2
-rw-r--r--tests/manual/painterpathquickshape/Intersect.qml125
-rw-r--r--tests/manual/painterpathquickshape/Intersect2.qml136
-rw-r--r--tests/manual/painterpathquickshape/main.qml8
14 files changed, 1227 insertions, 15 deletions
diff --git a/src/quick/scenegraph/qsgcurveprocessor.cpp b/src/quick/scenegraph/qsgcurveprocessor.cpp
index 427a344413..6fe9b56855 100644
--- a/src/quick/scenegraph/qsgcurveprocessor.cpp
+++ b/src/quick/scenegraph/qsgcurveprocessor.cpp
@@ -10,6 +10,7 @@
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcSGCurveProcessor, "qt.quick.curveprocessor");
+Q_LOGGING_CATEGORY(lcSGCurveIntersectionSolver, "qt.quick.curveprocessor.intersections");
namespace {
// Input coordinate space is pre-mapped so that (0, 0) maps to [0, 0] in uv space.
@@ -110,6 +111,38 @@ bool checkEdge(QVector2D &p1, QVector2D &p2, QVector2D &p, float epsilon)
return determinant(p1, p2, p) <= epsilon;
}
+// Check if lines l1 and l2 are intersecting and return the respective value. Solutions are stored to
+// the optional pointer solution.
+bool lineIntersection(const LinePoints &l1, const LinePoints &l2, QList<QPair<float, float>> *solution = nullptr)
+{
+ constexpr double eps2 = 1e-5; // Epsilon for parameter space t1-t2
+
+ // see https://www.wolframalpha.com/input?i=solve%28A+%2B+t+*+B+%3D+C+%2B+s*D%3B+E+%2B+t+*+F+%3D+G+%2B+s+*+H+for+s+and+t%29
+ const float A = l1[0].x();
+ const float B = l1[1].x() - l1[0].x();
+ const float C = l2[0].x();
+ const float D = l2[1].x() - l2[0].x();
+ const float E = l1[0].y();
+ const float F = l1[1].y() - l1[0].y();
+ const float G = l2[0].y();
+ const float H = l2[1].y() - l2[0].y();
+
+ float det = D * F - B * H;
+
+ if (det == 0)
+ return false;
+
+ float s = (F * (A - C) - B * (E - G)) / det;
+ float t = (H * (A - C) - D * (E - G)) / det;
+
+ bool intersecting = (s >= eps2 && s <= 1. - eps2 && t >= eps2 && t <= 1. - eps2);
+
+ if (solution && intersecting)
+ solution->append(QPair<float, float>(t, s));
+
+ return intersecting;
+}
+
bool checkTriangleOverlap(TrianglePoints &triangle1, TrianglePoints &triangle2, float epsilon = 1.0/32)
{
@@ -173,26 +206,142 @@ bool checkTriangleContains (QVector2D pt, QVector2D v1, QVector2D v2, QVector2D
return allNegative || allPositive;
}
-// e1 is always a concave curve. e2 can be curve or line
static bool isOverlap(const QQuadPath &path, int e1, int e2)
{
const QQuadPath::Element &element1 = path.elementAt(e1);
const QQuadPath::Element &element2 = path.elementAt(e2);
- Q_ASSERT(!element1.isLine());
- TrianglePoints t1{ element1.startPoint(), element1.controlPoint(), element1.endPoint() };
-
- if (element2.isLine()) {
- LinePoints line{ element2.startPoint(), element2.endPoint() };
- return checkLineTriangleOverlap(t1, line);
+ if (element1.isLine()) {
+ LinePoints line1{ element1.startPoint(), element1.endPoint() };
+ if (element2.isLine()) {
+ LinePoints line2{ element2.startPoint(), element2.endPoint() };
+ return lineIntersection(line1, line2);
+ } else {
+ TrianglePoints t2{ element2.startPoint(), element2.controlPoint(), element2.endPoint() };
+ return checkLineTriangleOverlap(t2, line1);
+ }
} else {
- TrianglePoints t2{ element2.startPoint(), element2.controlPoint(), element2.endPoint() };
- return checkTriangleOverlap(t1, t2);
+ TrianglePoints t1{ element1.startPoint(), element1.controlPoint(), element1.endPoint() };
+ if (element2.isLine()) {
+ LinePoints line{ element2.startPoint(), element2.endPoint() };
+ return checkLineTriangleOverlap(t1, line);
+ } else {
+ TrianglePoints t2{ element2.startPoint(), element2.controlPoint(), element2.endPoint() };
+ return checkTriangleOverlap(t1, t2);
+ }
}
return false;
}
+static float angleBetween(const QVector2D v1, const QVector2D v2)
+{
+ float dot = v1.x() * v2.x() + v1.y() * v2.y();
+ float cross = v1.x() * v2.y() - v1.y() * v2.x();
+ //TODO: Optimization: Maybe we don't need the atan2 here.
+ return atan2(cross, dot);
+}
+
+static bool isIntersecting(const TrianglePoints &t1, const TrianglePoints &t2, QList<QPair<float, float>> *solutions = nullptr)
+{
+ constexpr double eps = 1e-5; // Epsilon for coordinate space x-y
+ constexpr double eps2 = 1e-5; // Epsilon for parameter space t1-t2
+ constexpr int maxIterations = 7; // Maximum iterations allowed for Newton
+
+ // Convert to double to get better accuracy.
+ QPointF td1[3] = {QPointF(t1[0].x(), t1[0].y()), QPointF(t1[1].x(), t1[1].y()), QPointF(t1[2].x(), t1[2].y())};
+ QPointF td2[3] = {QPointF(t2[0].x(), t2[0].y()), QPointF(t2[1].x(), t2[1].y()), QPointF(t2[2].x(), t2[2].y())};
+
+ // F = P1(t1) - P2(t2) where P1 and P2 are bezier curve functions.
+ // F = (0, 0) at the intersection.
+ // t is the vector of bezier curve parameters for curves P1 and P2
+ auto F = [=](QPointF t) { return
+ td1[0] * (1 - t.x()) * (1. - t.x()) + 2 * td1[1] * (1. - t.x()) * t.x() + td1[2] * t.x() * t.x() -
+ td2[0] * (1 - t.y()) * (1. - t.y()) - 2 * td2[1] * (1. - t.y()) * t.y() - td2[2] * t.y() * t.y();};
+
+ // J is the Jacobi Matrix dF/dt where F and t are both vectors of dimension 2.
+ // Storing in a QLineF for simplicity.
+ auto J = [=](QPointF t) { return QLineF(
+ td1[0].x() * (-2 * (1-t.x())) + 2 * td1[1].x() * (1 - 2 * t.x()) + td1[2].x() * 2 * t.x(),
+ -td2[0].x() * (-2 * (1-t.y())) - 2 * td2[1].x() * (1 - 2 * t.y()) - td2[2].x() * 2 * t.y(),
+ td1[0].y() * (-2 * (1-t.x())) + 2 * td1[1].y() * (1 - 2 * t.x()) + td1[2].y() * 2 * t.x(),
+ -td2[0].y() * (-2 * (1-t.y())) - 2 * td2[1].y() * (1 - 2 * t.y()) - td2[2].y() * 2 * t.y());};
+
+ // solve the equation A(as 2x2 matrix)*x = b. Returns x.
+ auto solve = [](QLineF A, QPointF b) {
+ // invert A
+ const double det = A.x1() * A.y2() - A.y1() * A.x2();
+ QLineF Ainv(A.y2() / det, -A.y1() / det, -A.x2() / det, A.x1() / det);
+ // return A^-1 * b
+ return QPointF(Ainv.x1() * b.x() + Ainv.y1() * b.y(),
+ Ainv.x2() * b.x() + Ainv.y2() * b.y());
+ };
+
+#ifdef INTERSECTION_EXTRA_DEBUG
+ qCDebug(lcSGCurveIntersectionSolver) << "Checking" << t1[0] << t1[1] << t1[2];
+ qCDebug(lcSGCurveIntersectionSolver) << " vs" << t2[0] << t2[1] << t2[2];
+#endif
+
+ // TODO: Try to figure out reasonable starting points to reach all 4 possible intersections.
+ // This works but is kinda brute forcing it.
+ const QList<QPointF> tref {{0., 0.}, {0.5, 0.}, {1., 0.}, {0., 0.5}, {0.5, 0.5}, {1., 0.5}, {0., 1.}, {0.5, 1.}, {1., 1.}};
+
+ for (auto t0 : tref) {
+ QPointF t = t0;
+ double err = 1;
+ QPointF fval = F(t);
+ int i = 0;
+
+ // TODO: Try to abort sooner, e.g. when falling out of the interval [0-1]?
+ while (err > eps && i < maxIterations) { // && t.x() >= 0 && t.x() <= 1 && t.y() >= 0 && t.y() <= 1) {
+ t = t - solve(J(t), fval);
+ fval = F(t);
+ err = qAbs(fval.x()) + qAbs(fval.y()); // Using the Manhatten length as an error indicator.
+ i++;
+#ifdef INTERSECTION_EXTRA_DEBUG
+ qCDebug(lcSGCurveIntersectionSolver) << " Newton iteration" << i << "t =" << t << "F =" << fval << "Error =" << err;
+#endif
+ }
+ if (err < eps && t.x() > 10 * eps2 && t.x() < 1. - 10 * eps2 && t.y() > 10 * eps2 && t.y() < 1. - 10 * eps2) {
+#ifdef INTERSECTION_EXTRA_DEBUG
+ qCDebug(lcSGCurveIntersectionSolver) << " Newton solution (after" << i << ")=" << t << "(" << F(t) << ")";
+#endif
+ if (solutions) {
+ bool append = true;
+ for (auto solution : *solutions) {
+ if (qAbs(solution.first - t.x()) < 10 * eps2 && qAbs(solution.second - t.y()) < 10 * eps2)
+ append = false;
+ }
+ if (append)
+ solutions->append({t.x(), t.y()});
+ }
+ else
+ return true;
+ }
+ }
+ if (solutions)
+ return solutions->size() > 0;
+ else
+ return false;
+}
+
+static bool isIntersecting(const QQuadPath &path, int e1, int e2, QList<QPair<float, float>> *solutions = nullptr)
+{
+
+ const QQuadPath::Element &elem1 = path.elementAt(e1);
+ const QQuadPath::Element &elem2 = path.elementAt(e2);
+
+ if (elem1.isLine() && elem2.isLine()) {
+ return lineIntersection(LinePoints {elem1.startPoint(), elem1.endPoint() },
+ LinePoints {elem2.startPoint(), elem2.endPoint() },
+ solutions);
+ } else {
+ return isIntersecting(TrianglePoints { elem1.startPoint(), elem1.controlPoint(), elem1.endPoint() },
+ TrianglePoints { elem2.startPoint(), elem2.controlPoint(), elem2.endPoint() },
+ solutions);
+ }
+}
+
static bool isOverlap(const QQuadPath &path, int index, const QVector2D &vertex)
{
const QQuadPath::Element &elem = path.elementAt(index);
@@ -721,7 +870,7 @@ static void handleOverlap(QQuadPath &path, int e1, const QVector2D vertex, int r
if (vertex == path.elementAt(e1).endPoint() || !isOverlap(path, e1, vertex))
return;
if (recursionLevel > 8) {
- qDebug() << "Vertex overlap: recursion level" << recursionLevel << "aborting!";
+ qCDebug(lcSGCurveIntersectionSolver) << "Vertex overlap: recursion level" << recursionLevel << "aborting!";
return;
}
@@ -779,6 +928,477 @@ void QSGCurveProcessor::solveOverlaps(QQuadPath &path)
}
}
+// A fast algorithm to find path elements that might overlap. We will only check the overlap of the
+// triangles that define the elements.
+// We will order the elements first and then pool them depending on their x-values. This should
+// reduce the complexity to O(n log n), where n is the number of elements in the path.
+QList<QPair<int, int>> QSGCurveProcessor::findOverlappingCandidates(const QQuadPath &path)
+{
+ struct bRect { float xmin; float xmax; float ymin; float ymax; };
+
+ // Calculate all bounding rectanlges
+ QList<int> elementStarts;
+ QVarLengthArray<bRect, 32> boundingRects;
+ boundingRects.reserve(path.elementCount());
+ for (int i = 0; i < path.elementCount(); i++) {
+ QQuadPath::Element e = path.elementAt(i);
+
+ bRect bR{qMin(qMin(e.startPoint().x(), e.controlPoint().x()), e.endPoint().x()),
+ qMax(qMax(e.startPoint().x(), e.controlPoint().x()), e.endPoint().x()),
+ qMin(qMin(e.startPoint().y(), e.controlPoint().y()), e.endPoint().y()),
+ qMax(qMax(e.startPoint().y(), e.controlPoint().y()), e.endPoint().y())};
+ boundingRects.append(bR);
+ elementStarts.append(i);
+ }
+
+ // Sort the bounding rectangles by x-startpoint and x-endpoint
+ auto compareXmin = [&](int i, int j){return boundingRects.at(i).xmin < boundingRects.at(j).xmin;};
+ auto compareXmax = [&](int i, int j){return boundingRects.at(i).xmax < boundingRects.at(j).xmax;};
+ std::sort(elementStarts.begin(), elementStarts.end(), compareXmin);
+ QList<int> elementEnds = elementStarts;
+ std::sort(elementEnds.begin(), elementEnds.end(), compareXmax);
+
+ QList<int> bRpool;
+ QList<QPair<int, int>> overlappingBB;
+
+ // Start from x = xmin and move towards xmax. Add a rectangle to the pool and check for
+ // intersections with all other rectangles in the pool. If a rectangles xmax is smaller
+ // than the new xmin, it can be removed from the pool.
+ while (!elementStarts.isEmpty()) {
+ int addIndex = elementStarts.takeFirst();
+ const bRect &newR = boundingRects.at(addIndex);
+
+ // First remove elements from the pool that cannot touch the new one
+ // because xmax is too small
+ while (bRpool.size() && elementEnds.size() && newR.xmin >= boundingRects.at(elementEnds.first()).xmax) {
+ int removeIndex = elementEnds.takeFirst();
+ bRpool.removeOne(removeIndex);
+ }
+
+ // Now compare the new element with all elements in the pool.
+ for (int j = 0; j < bRpool.size(); j++) {
+ int i = bRpool.at(j);
+ const bRect &r1 = boundingRects.at(i);
+ // We don't have to check for x because the pooling takes care of it.
+ //if (r1.xmax <= newR.xmin || newR.xmax <= r1.xmin)
+ // continue;
+ if (r1.ymax <= newR.ymin || newR.ymax <= r1.ymin)
+ continue;
+ // If the bounding boxes are overlapping it is a candidate for an intersection.
+ if (isOverlap(path, i, addIndex)) // Another test to see if the triangles overlap
+ overlappingBB.append(QPair<int, int>(i, addIndex));
+ }
+ bRpool.append(addIndex); //Add the new element to the pool.
+ }
+ return overlappingBB;
+}
+
+void QSGCurveProcessor::solveIntersections(QQuadPath &path, bool alwaysReorder)
+{
+ struct IntersectionData { int e1; int e2; float t1; float t2; bool in1 = false, in2 = false, out1 = false, out2 = false; };
+ QList<IntersectionData> intersections;
+
+ // Helper function to mark an intersection as handled when the
+ // path i is processed moving forward/backward
+ auto markIntersectionAsHandled = [=](IntersectionData *data, int i, bool forward) {
+ if (data->e1 == i) {
+ if (forward)
+ data->out1 = true;
+ else
+ data->in1 = true;
+ } else if (data->e2 == i){
+ if (forward)
+ data->out2 = true;
+ else
+ data->in2 = true;
+ } else {
+ Q_UNREACHABLE();
+ }
+ };
+
+ // First make a O(n log n) search for candidates.
+ const QList<QPair<int, int>> candidates = findOverlappingCandidates(path);
+
+ // Then check the candidates for actual intersections.
+ for (const auto &candidate : candidates) {
+ QList<QPair<float,float>> res;
+ int e1 = candidate.first;
+ int e2 = candidate.second;
+ if (isIntersecting(path, e1, e2, &res)) {
+ for (const auto &r : res)
+ intersections.append({e1, e2, r.first, r.second});
+ }
+ }
+
+ qCDebug(lcSGCurveIntersectionSolver) << "----- Checking for Intersections -----";
+ qCDebug(lcSGCurveIntersectionSolver) << "Found" << intersections.length() << "intersections";
+ for (const auto &i : intersections) {
+ auto p1 = path.elementAt(i.e1).pointAtFraction(i.t1);
+ auto p2 = path.elementAt(i.e2).pointAtFraction(i.t2);
+ qCDebug(lcSGCurveIntersectionSolver) << " between" << i.e1 << "and" << i.e2 << "at" << i.t1 << "/" << i.t2 << "->" << p1 << "/" << p2;
+ }
+
+ if (intersections.isEmpty() && !alwaysReorder) {
+ qCDebug(lcSGCurveIntersectionSolver) << "Returning the path unchanged.";
+ return;
+ }
+
+ // Store the starting and end elements of the subpaths to be able
+ // to jump quickly between them. Also keep a list of handled paths,
+ // so we know if we need to come back to a subpath or if it
+ // was already united with another subpath due to an intersection.
+ QList<int> subPathStartPoints;
+ QList<int> subPathEndPoints;
+ QList<bool> subPathHandled;
+ for (int i = 0; i < path.elementCount(); i++) {
+ if (path.elementAt(i).isSubpathStart())
+ subPathStartPoints.append(i);
+ if (path.elementAt(i).isSubpathEnd()) {
+ subPathEndPoints.append(i);
+ subPathHandled.append(false);
+ }
+ }
+
+ // A small helper to find the subPath of an element with index
+ auto subPathIndex = [&](int index) {
+ for (int i = 0; i < subPathStartPoints.size(); i++) {
+ if (index >= subPathStartPoints.at(i) && index <= subPathEndPoints.at(i))
+ return i;
+ }
+ return -1;
+ };
+
+ // Helper function to find a siutable starting point between start and end.
+ // A suitable starting point is where right is inside and left is outside
+ // If left is inside and right is outside it works too, just move in the
+ // other direction (forward = false).
+ auto findStart = [=](QQuadPath &path, int start, int end, int *result, bool *forward) {
+ for (int i = start; i < end; i++) {
+ int adjecent;
+ if (subPathStartPoints.contains(i))
+ adjecent = subPathEndPoints.at(subPathStartPoints.indexOf(i));
+ else
+ adjecent = i - 1;
+
+ QQuadPath::Element::FillSide fillSide = path.fillSideOf(i, 1e-4f);
+ const bool leftInside = fillSide == QQuadPath::Element::FillSideLeft;
+ const bool rightInside = fillSide == QQuadPath::Element::FillSideRight;
+ qCDebug(lcSGCurveIntersectionSolver) << "Element" << i << "/" << adjecent << "meeting point is left/right inside:" << leftInside << "/" << rightInside;
+ if (rightInside) {
+ *result = i;
+ *forward = true;
+ return true;
+ } else if (leftInside) {
+ *result = adjecent;
+ *forward = false;
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // Also store handledElements. This is used to identify and abort
+ // on errors.
+ QVarLengthArray<bool> handledElements(path.elementCount(), false);
+
+ QQuadPath fixedPath;
+ fixedPath.setFillRule(path.fillRule());
+
+ int i1 = 0;
+ float t1 = 0;
+
+ int i2 = 0;
+ float t2 = 0;
+
+ float t = 0;
+ bool forward = true;
+
+ int startedAtIndex = -1;
+ float startedAtT = -1;
+
+ if (!findStart(path, 0, path.elementCount(), &i1, &forward)) {
+ qWarning() << "No suitable start found. This should not happen. Returning the path unchanged.";
+ return;
+ }
+
+ // Helper function to start a new subpath and update temporary variables.
+ auto startNewSubPath = [&](int i, bool forward) {
+ if (forward) {
+ fixedPath.moveTo(path.elementAt(i).startPoint());
+ t = startedAtT = 0;
+ } else {
+ fixedPath.moveTo(path.elementAt(i).endPoint());
+ t = startedAtT = 1;
+ }
+ startedAtIndex = i;
+ subPathHandled[subPathIndex(i)] = true;
+ };
+ startNewSubPath(i1, forward);
+
+ // If not all interactions where found correctly, we might end up in an infinite loop.
+ // Therefore we count the total number of iterations and bail out at some point.
+ int totalIterations = 0;
+
+ do {
+ // Sanity check: Make sure that we do not process the same corner point more than once.
+ if (t == 0 || t == 1) {
+ int nextIndex = i1;
+
+ if (t == 1 && path.elementAt(i1).isSubpathEnd()) {
+ nextIndex = subPathStartPoints.at(subPathIndex(i1));
+ } else if (t == 1) {
+ nextIndex = nextIndex + 1;
+ }
+ if (handledElements[nextIndex]) {
+ qWarning() << "Revisiting an element when trying to solve intersections. This should not happen. Returning the path unchanged.";
+ return;
+ }
+ handledElements[nextIndex] = true;
+ }
+ // Sanity check: Keep an eye on total iterations
+ totalIterations++;
+
+ qCDebug(lcSGCurveIntersectionSolver) << "Checking section" << i1 << "from" << t << "going" << (forward ? "forward" : "backward");
+
+ // Find the next intersection that is as close as possible to t but in direction of processing (forward or !forward).
+ int iC = -1; //intersection candidate
+ t1 = forward? +1 : -1; //intersection candidate t-value
+ for (int j = 0; j < intersections.size(); j++) {
+ if (i1 == intersections[j].e1 &&
+ intersections[j].t1 * (forward? 1 : -1) > t * (forward? 1 : -1) &&
+ intersections[j].t1 * (forward? 1 : -1) < t1 * (forward? 1 : -1)) {
+ iC = j;
+ t1 = intersections[j].t1;
+ i2 = intersections[j].e2;
+ t2 = intersections[j].t2;
+ }
+ if (i1 == intersections[j].e2 &&
+ intersections[j].t2 * (forward?1 : -1) > t * (forward? 1 : -1) &&
+ intersections[j].t2 * (forward?1 : -1) < t1 * (forward?1 : -1)) {
+ iC = j;
+ t1 = intersections[j].t2;
+ i2 = intersections[j].e1;
+ t2 = intersections[j].t1;
+ }
+ }
+
+ if (iC < 0) {
+ qCDebug(lcSGCurveIntersectionSolver) << " No intersection found on my way. Adding the rest of the segment " << i1;
+ // If no intersection with the current element was found, just add the rest of the element
+ // to the fixed path and go on.
+ // If we reached the end (going forward) or start (going backward) of a subpath, we have
+ // to wrap aroud. Abort condition for the loop comes separately later.
+ if (forward) {
+ if (path.elementAt(i1).isLine()) {
+ fixedPath.lineTo(path.elementAt(i1).endPoint());
+ } else {
+ const QQuadPath::Element rest = path.elementAt(i1).segmentFromTo(t, 1);
+ fixedPath.quadTo(rest.controlPoint(), rest.endPoint());
+ }
+ if (path.elementAt(i1).isSubpathEnd()) {
+ int index = subPathEndPoints.indexOf(i1);
+ qCDebug(lcSGCurveIntersectionSolver) << " Going back to the start of subPath" << index;
+ i1 = subPathStartPoints.at(index);
+ } else {
+ i1++;
+ }
+ t = 0;
+ } else {
+ if (path.elementAt(i1).isLine()) {
+ fixedPath.lineTo(path.elementAt(i1).startPoint());
+ } else {
+ const QQuadPath::Element rest = path.elementAt(i1).segmentFromTo(0, t).reversed();
+ fixedPath.quadTo(rest.controlPoint(), rest.endPoint());
+ }
+ if (path.elementAt(i1).isSubpathStart()) {
+ int index = subPathStartPoints.indexOf(i1);
+ qCDebug(lcSGCurveIntersectionSolver) << " Going back to the end of subPath" << index;
+ i1 = subPathEndPoints.at(index);
+ } else {
+ i1--;
+ }
+ t = 1;
+ }
+ } else { // Here comes the part where we actually handle intersections.
+ qCDebug(lcSGCurveIntersectionSolver) << " Found an intersection at" << t1 << "with" << i2 << "at" << t2;
+
+ // Mark the subpath we intersected with as visisted. We do not have to come here explicitly again.
+ subPathHandled[subPathIndex(i2)] = true;
+
+ // Mark the path that lead us to this intersection as handled on the intersection level.
+ // Note the ! in front of forward. This is required because we move towards the intersection.
+ markIntersectionAsHandled(&intersections[iC], i1, !forward);
+
+ // Split the path from the last point to the newly found intersection.
+ // Add the part of the current segment to the fixedPath.
+ const QQuadPath::Element &elem1 = path.elementAt(i1);
+ if (elem1.isLine()) {
+ fixedPath.lineTo(elem1.pointAtFraction(t1));
+ } else {
+ QQuadPath::Element partUntilIntersection;
+ if (forward) {
+ partUntilIntersection = elem1.segmentFromTo(t, t1);
+ } else {
+ partUntilIntersection = elem1.segmentFromTo(t1, t).reversed();
+ }
+ fixedPath.quadTo(partUntilIntersection.controlPoint(), partUntilIntersection.endPoint());
+ }
+
+ // If only one unhandled path is left the decision how to proceed is trivial
+ if (intersections[iC].in1 && intersections[iC].in2 && intersections[iC].out1 && !intersections[iC].out2) {
+ i1 = intersections[iC].e2;
+ t = intersections[iC].t2;
+ forward = true;
+ } else if (intersections[iC].in1 && intersections[iC].in2 && !intersections[iC].out1 && intersections[iC].out2) {
+ i1 = intersections[iC].e1;
+ t = intersections[iC].t1;
+ forward = true;
+ } else if (intersections[iC].in1 && !intersections[iC].in2 && intersections[iC].out1 && intersections[iC].out2) {
+ i1 = intersections[iC].e2;
+ t = intersections[iC].t2;
+ forward = false;
+ } else if (!intersections[iC].in1 && intersections[iC].in2 && intersections[iC].out1 && intersections[iC].out2) {
+ i1 = intersections[iC].e1;
+ t = intersections[iC].t1;
+ forward = false;
+ } else {
+ // If no trivial path is left, calculate the intersection angle to decide which path to move forward.
+ // For winding fill we take the left most path forward, so the inside stays on the right side
+ // For odd even fill we take the right most path forward so we cut of the smallest area.
+ // We come back at the intersection and add the missing pieces as subpaths later on.
+ QVector2D tangent1 = elem1.tangentAtFraction(t1);
+ if (!forward)
+ tangent1 = -tangent1;
+ const QQuadPath::Element &elem2 = path.elementAt(i2);
+ const QVector2D tangent2 = elem2.tangentAtFraction(t2);
+ const float angle = angleBetween(-tangent1, tangent2);
+ qCDebug(lcSGCurveIntersectionSolver) << " Angle at intersection is" << angle;
+ // A small angle. Everything smaller is interpreted as tangent
+ constexpr float deltaAngle = 1e-3f;
+ if ((angle > deltaAngle && path.fillRule() == Qt::WindingFill) || (angle < -deltaAngle && path.fillRule() == Qt::OddEvenFill)) {
+ forward = true;
+ i1 = i2;
+ t = t2;
+ qCDebug(lcSGCurveIntersectionSolver) << " Next going forward from" << t2 << "on" << i2;
+ } else if ((angle < -deltaAngle && path.fillRule() == Qt::WindingFill) || (angle > deltaAngle && path.fillRule() == Qt::OddEvenFill)) {
+ forward = false;
+ i1 = i2;
+ t = t2;
+ qCDebug(lcSGCurveIntersectionSolver) << " Next going backward from" << t2 << "on" << i2;
+ } else { // this is basically a tangential touch and and no crossing. So stay on the current path, keep direction
+ qCDebug(lcSGCurveIntersectionSolver) << " Found tangent. Staying on element";
+ }
+ }
+
+ // Mark the path that takes us away from this intersection as handled on the intersection level.
+ if (!(i1 == startedAtIndex && t == startedAtT))
+ markIntersectionAsHandled(&intersections[iC], i1, forward);
+
+ // If we took all paths from an intersection it can be deleted.
+ if (intersections[iC].in1 && intersections[iC].in2 && intersections[iC].out1 && intersections[iC].out2) {
+ qCDebug(lcSGCurveIntersectionSolver) << " This intersection was processed completely and will be removed";
+ intersections.removeAt(iC);
+ }
+ }
+
+ if (i1 == startedAtIndex && t == startedAtT) {
+ // We reached the point on the subpath where we started. This subpath is done.
+ // We have to find an unhandled subpath or a new subpath that was generated by cuts/intersections.
+ qCDebug(lcSGCurveIntersectionSolver) << "Reached my starting point and try to find a new subpath.";
+
+ // Search for the next subpath to handle.
+ int nextUnhandled = -1;
+ for (int i = 0; i < subPathHandled.size(); i++) {
+ if (!subPathHandled.at(i)) {
+
+ // Not nesesarrily handled (if findStart return false) but if we find no starting
+ // point, we cannot/don't need to handle it anyway. So just mark it as handled.
+ subPathHandled[i] = true;
+
+ if (findStart(path, subPathStartPoints.at(i), subPathEndPoints.at(i), &i1, &forward)) {
+ nextUnhandled = i;
+ qCDebug(lcSGCurveIntersectionSolver) << "Found a new subpath" << i << "to be processed.";
+ startNewSubPath(i1, forward);
+ break;
+ }
+ }
+ }
+
+ // If no valid subpath is left, we have to go back to the unhandled intersections.
+ while (nextUnhandled < 0) {
+ qCDebug(lcSGCurveIntersectionSolver) << "All subpaths handled. Looking for unhandled intersections.";
+ if (intersections.isEmpty()) {
+ qCDebug(lcSGCurveIntersectionSolver) << "All intersections handled. I am done.";
+ path = fixedPath;
+ return;
+ }
+
+ IntersectionData &unhandledIntersec = intersections[0];
+ qCDebug(lcSGCurveIntersectionSolver) << "Revisiting intersection of" << unhandledIntersec.e1 << "with" << unhandledIntersec.e2;
+ qCDebug(lcSGCurveIntersectionSolver) << "Handled are" << unhandledIntersec.e1 << "in:" << unhandledIntersec.in1 << "out:" << unhandledIntersec.out1
+ << "/" << unhandledIntersec.e2 << "in:" << unhandledIntersec.in2 << "out:" << unhandledIntersec.out2;
+
+ // Searching for the correct direction to go forward.
+ // That requires that the intersection + small delta (here 1e-4)
+ // is a valid starting point (filling only on one side)
+ constexpr float deltaT = 1e-4f;
+ if (!unhandledIntersec.in1) {
+ QQuadPath::Element::FillSide fillSide = path.fillSideOf(unhandledIntersec.e1, unhandledIntersec.t1 - deltaT);
+ if (fillSide == QQuadPath::Element::FillSideLeft) {
+ fixedPath.moveTo(path.elementAt(unhandledIntersec.e1).pointAtFraction(unhandledIntersec.t1));
+ i1 = startedAtIndex = unhandledIntersec.e1;
+ t = startedAtT = unhandledIntersec.t1;
+ forward = false;
+ unhandledIntersec.in1 = true;
+ break;
+ }
+ }
+ if (!unhandledIntersec.in2) {
+ QQuadPath::Element::FillSide fillSide = path.fillSideOf(unhandledIntersec.e2, unhandledIntersec.t2 - deltaT);
+ if (fillSide == QQuadPath::Element::FillSideLeft) {
+ fixedPath.moveTo(path.elementAt(unhandledIntersec.e1).pointAtFraction(unhandledIntersec.t1));
+ i1 = startedAtIndex = unhandledIntersec.e2;
+ t = startedAtT = unhandledIntersec.t2;
+ forward = false;
+ unhandledIntersec.in2 = true;
+ break;
+ }
+ }
+ if (!unhandledIntersec.out1) {
+ QQuadPath::Element::FillSide fillSide = path.fillSideOf(unhandledIntersec.e1, unhandledIntersec.t1 + deltaT);
+ if (fillSide == QQuadPath::Element::FillSideRight) {
+ fixedPath.moveTo(path.elementAt(unhandledIntersec.e1).pointAtFraction(unhandledIntersec.t1));
+ i1 = startedAtIndex = unhandledIntersec.e1;
+ t = startedAtT = unhandledIntersec.t1;
+ forward = true;
+ unhandledIntersec.out1 = true;
+ break;
+ }
+ }
+ if (!unhandledIntersec.out2) {
+ QQuadPath::Element::FillSide fillSide = path.fillSideOf(unhandledIntersec.e2, unhandledIntersec.t2 + deltaT);
+ if (fillSide == QQuadPath::Element::FillSideRight) {
+ fixedPath.moveTo(path.elementAt(unhandledIntersec.e1).pointAtFraction(unhandledIntersec.t1));
+ i1 = startedAtIndex = unhandledIntersec.e2;
+ t = startedAtT = unhandledIntersec.t2;
+ forward = true;
+ unhandledIntersec.out2 = true;
+ break;
+ }
+ }
+ intersections.removeFirst();
+ qCDebug(lcSGCurveIntersectionSolver) << "Found no way to move forward at this intersection and removed it.";
+ }
+ }
+
+ } while (totalIterations < path.elementCount() * 50);
+ // Check the totalIterations as a sanity check. Should never be triggered.
+
+ qWarning() << "Could not solve intersections of path. This should not happen. Returning the path unchanged.";
+
+}
+
+
void QSGCurveProcessor::processStroke(const QQuadPath &strokePath,
float miterLimit,
float penWidth,
diff --git a/src/quick/scenegraph/qsgcurveprocessor_p.h b/src/quick/scenegraph/qsgcurveprocessor_p.h
index 271f15207c..442c319d01 100644
--- a/src/quick/scenegraph/qsgcurveprocessor_p.h
+++ b/src/quick/scenegraph/qsgcurveprocessor_p.h
@@ -44,6 +44,9 @@ public:
addStrokeTriangleCallback addTriangle,
int subdivisions = 3);
static void solveOverlaps(QQuadPath &path);
+ static QList<QPair<int, int>> findOverlappingCandidates(const QQuadPath &path);
+ static void solveIntersections(QQuadPath &path, bool alwaysReorder = true);
+
};
QT_END_NAMESPACE
diff --git a/src/quick/scenegraph/util/qquadpath.cpp b/src/quick/scenegraph/util/qquadpath.cpp
index 8ce818ae1f..70fca33e1c 100644
--- a/src/quick/scenegraph/util/qquadpath.cpp
+++ b/src/quick/scenegraph/util/qquadpath.cpp
@@ -203,6 +203,37 @@ QVector2D QQuadPath::Element::pointAtFraction(float t) const
}
}
+QQuadPath::Element QQuadPath::Element::segmentFromTo(float t0, float t1) const
+{
+ if (t0 <= 0 && t1 >= 1)
+ return *this;
+
+ Element part;
+ part.sp = pointAtFraction(t0);
+ part.ep = pointAtFraction(t1);
+
+ if (isLine()) {
+ part.cp = 0.5f * (part.sp + part.ep);
+ part.m_isLine = true;
+ } else {
+ // Split curve right at t0, yields { t0, rcp, endPoint } quad segment
+ const QVector2D rcp = (1 - t0) * controlPoint() + t0 * endPoint();
+ // Split that left at t1, yields { t0, lcp, t1 } quad segment
+ float segmentT = (t1 - t0) / (1 - t0);
+ part.cp = (1 - segmentT) * part.sp + segmentT * rcp;
+ }
+ return part;
+}
+
+QQuadPath::Element QQuadPath::Element::reversed() const {
+ Element swappedElement;
+ swappedElement.ep = sp;
+ swappedElement.cp = cp;
+ swappedElement.sp = ep;
+ swappedElement.m_isLine = m_isLine;
+ return swappedElement;
+}
+
float QQuadPath::Element::extent() const
{
// TBD: cache this value if we start using it a lot
@@ -333,6 +364,82 @@ bool QQuadPath::contains(const QVector2D &point) const
return (fillRule() == Qt::WindingFill ? (winding_number != 0) : ((winding_number % 2) != 0));
}
+// similar as contains. But we treat the element with the index elementIdx in a special way
+// that should be numerically more stable. The result is a contains for a point on the left
+// and for the right side of the element.
+QQuadPath::Element::FillSide QQuadPath::fillSideOf(int elementIdx, float elementT) const
+{
+ constexpr float toleranceT = 1e-3f;
+ const QVector2D point = m_elements.at(elementIdx).pointAtFraction(elementT);
+
+ int winding_number = 0;
+ for (int i = 0; i < elementCount(); i++) {
+ const Element &e = m_elements.at(i);
+ int dir = 1;
+ float y1 = e.startPoint().y();
+ float y2 = e.endPoint().y();
+ if (y2 < y1) {
+ qSwap(y1, y2);
+ dir = -1;
+ }
+ if (e.m_isLine) {
+ if (point.y() < y1 || point.y() >= y2 || y1 == y2)
+ continue;
+ const float t = (point.y() - e.startPoint().y()) / (e.endPoint().y() - e.startPoint().y());
+ const float x = e.startPoint().x() + t * (e.endPoint().x() - e.startPoint().x());
+ if ((elementIdx != i && x <= point.x()) ||
+ (elementIdx == i && x <= point.x() && qAbs(t - elementT) > toleranceT)) {
+ winding_number += dir;
+ }
+ } else {
+ y1 = qMin(y1, e.controlPoint().y());
+ y2 = qMax(y2, e.controlPoint().y());
+ if (point.y() < y1 || point.y() >= y2)
+ continue;
+ float ts[2];
+ const int numRoots = e.intersectionsAtY(point.y(), ts);
+ // Count if there is exactly one intersection to the left
+ bool oneHit = false;
+ float tForHit = -1;
+ for (int j = 0; j < numRoots; j++) {
+ const float x = e.pointAtFraction(ts[j]).x();
+ if ((elementIdx != i && x <= point.x()) ||
+ (elementIdx == i && x <= point.x() && qAbs(ts[j] - elementT) > toleranceT)) {
+ oneHit = !oneHit;
+ tForHit = ts[j];
+ }
+ }
+ if (oneHit) {
+ dir = e.tangentAtFraction(tForHit).y() < 0 ? -1 : 1;
+ winding_number += dir;
+ }
+ }
+ };
+
+ int left_winding_number = winding_number;
+ int right_winding_number = winding_number;
+
+ int dir = m_elements.at(elementIdx).tangentAtFraction(elementT).y() < 0 ? -1 : 1;
+
+ if (dir > 0) {
+ left_winding_number += dir;
+ } else {
+ right_winding_number += dir;
+ }
+
+ bool leftInside = (fillRule() == Qt::WindingFill ? (left_winding_number != 0) : ((left_winding_number % 2) != 0));
+ bool rightInside = (fillRule() == Qt::WindingFill ? (right_winding_number != 0) : ((right_winding_number % 2) != 0));
+
+ if (leftInside && rightInside)
+ return QQuadPath::Element::FillSideBoth;
+ else if (leftInside)
+ return QQuadPath::Element::FillSideLeft;
+ else if (rightInside)
+ return QQuadPath::Element::FillSideRight;
+ else
+ return QQuadPath::Element::FillSideUndetermined; //should not happen except for numerical error.
+}
+
void QQuadPath::addElement(const QVector2D &control, const QVector2D &endPoint, bool isLine)
{
if (qFuzzyCompare(currentPoint, endPoint))
@@ -494,6 +601,22 @@ QPainterPath QQuadPath::toPainterPath() const
return res;
}
+QString QQuadPath::asSvgString() const
+{
+ QString res;
+ QTextStream str(&res);
+ for (const Element &element : m_elements) {
+ if (element.isSubpathStart())
+ str << "M " << element.startPoint().x() << " " << element.startPoint().y() << " ";
+ if (element.isLine())
+ str << "L " << element.endPoint().x() << " " << element.endPoint().y() << " ";
+ else
+ str << "Q " << element.controlPoint().x() << " " << element.controlPoint().y() << " "
+ << element.endPoint().x() << " " << element.endPoint().y() << " ";
+ }
+ return res;
+}
+
// Returns a new path since doing it inline would probably be less efficient
// (technically changing it from O(n) to O(n^2))
// Note that this function should be called before splitting any elements,
diff --git a/src/quick/scenegraph/util/qquadpath_p.h b/src/quick/scenegraph/util/qquadpath_p.h
index d1d8f32eb1..7540f277a0 100644
--- a/src/quick/scenegraph/util/qquadpath_p.h
+++ b/src/quick/scenegraph/util/qquadpath_p.h
@@ -35,6 +35,11 @@ public:
{
}
+ Element (QVector2D s, QVector2D c, QVector2D e)
+ : sp(s), cp(c), ep(e), m_isSubpathStart(false), m_isSubpathEnd(false), m_isLine(false)
+ {
+ }
+
bool isSubpathStart() const
{
return m_isSubpathStart;
@@ -89,6 +94,10 @@ public:
}
}
+ Element segmentFromTo(float t0, float t1) const;
+
+ Element reversed() const;
+
int childCount() const { return m_numChildren; }
int indexOfChild(int childNumber) const
@@ -120,20 +129,35 @@ public:
m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags & ~Element::Convex);
}
+ void setFillOnRight(bool isFillOnRight)
+ {
+ if (isFillOnRight)
+ m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags | Element::FillOnRight);
+ else
+ m_curvatureFlags = Element::CurvatureFlags(m_curvatureFlags & ~Element::FillOnRight);
+ }
+
bool isControlPointOnLeft() const
{
return isPointOnLeft(cp, sp, ep);
}
- private:
- int intersectionsAtY(float y, float *fractions) const;
-
enum CurvatureFlags : quint8 {
CurvatureUndetermined = 0,
FillOnRight = 1,
Convex = 2
};
+ enum FillSide : quint8 {
+ FillSideUndetermined = 0,
+ FillSideRight = 1,
+ FillSideLeft = 2,
+ FillSideBoth = 3
+ };
+
+ private:
+ int intersectionsAtY(float y, float *fractions) const;
+
QVector2D sp;
QVector2D cp;
QVector2D ep;
@@ -190,6 +214,7 @@ public:
static QQuadPath fromPainterPath(const QPainterPath &path);
QPainterPath toPainterPath() const;
+ QString asSvgString() const;
QQuadPath subPathsClosed() const;
void addCurvatureData();
@@ -197,6 +222,7 @@ public:
QQuadPath dashed(qreal lineWidth, const QList<qreal> &dashPattern, qreal dashOffset = 0) const;
void splitElementAt(int index);
bool contains(const QVector2D &point) const;
+ Element::FillSide fillSideOf(int elementIdx, float elementT) const;
template<typename Func>
void iterateChildrenOf(Element &e, Func &&lambda)
@@ -256,7 +282,6 @@ private:
Element::CurvatureFlags coordinateOrderOfElement(const Element &element) const;
friend QDebug operator<<(QDebug, const QQuadPath &);
-
bool subPathToStart = true;
Qt::FillRule m_fillRule = Qt::OddEvenFill;
QVector2D currentPoint;
diff --git a/src/quickshapes/qquickshapecurverenderer.cpp b/src/quickshapes/qquickshapecurverenderer.cpp
index e78b9a8a40..d4bd4ea96c 100644
--- a/src/quickshapes/qquickshapecurverenderer.cpp
+++ b/src/quickshapes/qquickshapecurverenderer.cpp
@@ -417,6 +417,7 @@ void QQuickShapeCurveRenderer::updateNode()
void QQuickShapeCurveRenderer::processPath(PathData *pathData)
{
static const bool doOverlapSolving = !qEnvironmentVariableIntValue("QT_QUICKSHAPES_DISABLE_OVERLAP_SOLVER");
+ static const bool doIntersetionSolving = !qEnvironmentVariableIntValue("QT_QUICKSHAPES_DISABLE_INTERSECTION_SOLVER");
static const bool useTriangulatingStroker = qEnvironmentVariableIntValue("QT_QUICKSHAPES_TRIANGULATING_STROKER");
static const bool simplifyPath = qEnvironmentVariableIntValue("QT_QUICKSHAPES_SIMPLIFY_PATHS");
@@ -436,6 +437,8 @@ void QQuickShapeCurveRenderer::processPath(PathData *pathData)
if (pathData->isFillVisible()) {
if (pathData->fillPath.isEmpty()) {
pathData->fillPath = pathData->path.subPathsClosed();
+ if (doIntersetionSolving)
+ QSGCurveProcessor::solveIntersections(pathData->fillPath);
pathData->fillPath.addCurvatureData();
if (doOverlapSolving)
QSGCurveProcessor::solveOverlaps(pathData->fillPath);
diff --git a/src/quickshapes/qquickshapecurverenderer_p.h b/src/quickshapes/qquickshapecurverenderer_p.h
index d86cf65960..12d147d80a 100644
--- a/src/quickshapes/qquickshapecurverenderer_p.h
+++ b/src/quickshapes/qquickshapecurverenderer_p.h
@@ -118,6 +118,7 @@ private:
static NodeList addTriangulatingStrokerNodes(const PathData &pathData);
static NodeList addCurveStrokeNodes(const PathData &pathData);
+ void solveIntersections(QQuadPath &path);
QQuickItem *m_item;
QSGNode *m_rootNode = nullptr;
QVector<PathData> m_paths;
diff --git a/tests/baseline/scenegraph/data/shape/shape_intersecting1.qml b/tests/baseline/scenegraph/data/shape/shape_intersecting1.qml
new file mode 100644
index 0000000000..1501a8e1c7
--- /dev/null
+++ b/tests/baseline/scenegraph/data/shape/shape_intersecting1.qml
@@ -0,0 +1,72 @@
+// 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
+
+import QtQuick
+import QtQuick.Shapes
+
+Item {
+ width: 1200
+ height: 600
+
+ ListModel {
+ id: fillRules
+ ListElement { fillrule: ShapePath.WindingFill }
+ ListElement { fillrule: ShapePath.OddEvenFill }
+ }
+
+ ListModel {
+ id: renderers
+ ListElement { renderer: Shape.GeometryRenderer }
+ ListElement { renderer: Shape.CurveRenderer }
+ }
+
+ ListModel {
+ id: svgstrings
+ ListElement { scaleToFit: 0.11; offsetX: 20; offsetY: 20; pathString: "M 885.276 1047.5 Q 1310 -265 722.654 1071.85 Q 1100 1100 636.857 929.582 Q 100 1100 322.456 883.006 L 1003.49 134.715 Q 944.382 591.106 0 0 L 885.276 1047.5"}
+ ListElement { scaleToFit: 0.035; offsetX: -60; offsetY: 0; pathString: "M 5000 3475 Q 5600 825 2290 245 Q 3635 -30 5360 3885 Q 730 2990 4685 390 L 2445 1435 L 5000 3475" }
+ ListElement { scaleToFit: 0.11; offsetX: 10; offsetY: 10; pathString: "M 600 200 Q 1000 600 600 1000 Q 200 1200 200 600 Q 200 200 600 200 M 1000 200 Q 1400 600 1000 1000 Q 600 1200 600 600 Q 200 -200 1000 200" }
+ ListElement { scaleToFit: 0.11; offsetX: -10; offsetY: 10; pathString: "M 865 270 Q 1000 600 955 820 Q 675 690 775 525 Q 200 200 865 270 M 1000 200 Q 1400 600 1000 1000 Q 600 1200 590 635 Q 200 -200 1000 200" }
+ ListElement { scaleToFit: 0.11; offsetX: -10; offsetY: 10; pathString: "M 568.243 506.927 Q 1000 600 955 820 Q 675 690 1007.14 396.964 Q 616.417 79.1432 568.243 506.927 M 1000 200 Q 1400 600 1000 1000 Q 600 1200 590 635 Q 200 -200 1000 200" }
+ ListElement { scaleToFit: 0.11; offsetX: -10; offsetY: 10; pathString: "M 562.26 285.556 Q 728.371 717.267 955 820 Q 1037.57 642.136 1007.14 396.964 Q 616.417 79.1432 562.26 285.556 M 1000 200 Q 1400 600 1000 1000 Q 600 1200 590 635 Q 200 -200 1000 200" }
+ ListElement { scaleToFit: 0.09; offsetX: -10; offsetY: 10; pathString: "M 550.789, 661.103 Q 1435.03, -8.14249 1668.28, 799.321 Q 1100, 1100 593.639, 527.481 Q 593.639 527.481 100 1100 100 600 Q 100 600 1208.1 876.376 2316.21, 1152.75 Q 2316.21 1152.75 1433.5 906.927 550.789 661.103" }
+ ListElement { scaleToFit: 0.04; offsetX: 35; offsetY: 10; pathString: "M 846.361 1397.06 Q 2485 -660 2485 1015 Q 1598.13 4529.81 975 290 Q -69.0821 3853.43 -568.237 1473.61 L 2515.73 2262.31 L 846.361 1397.06" }
+ }
+
+ Column {
+ Repeater {
+ model: renderers
+ Column {
+ Repeater {
+ model: fillRules
+ Row {
+ Repeater {
+ model: svgstrings
+ Rectangle {
+ width: 150
+ height: 150
+ border.color: "black"
+
+ Shape {
+ preferredRendererType: renderer
+ ShapePath {
+ fillColor: renderer == Shape.CurveRenderer ? "#99483d8b" : "#99dc143c"
+ fillRule: fillrule
+ strokeWidth: 0
+ PathSvg { path: pathString }
+ }
+
+ transform: Matrix4x4 {
+ matrix: Qt.matrix4x4(scaleToFit, 0, 0, offsetX,
+ 0, scaleToFit, 0, offsetY,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/baseline/scenegraph/data/shape/shape_intersecting2.qml b/tests/baseline/scenegraph/data/shape/shape_intersecting2.qml
new file mode 100644
index 0000000000..df303d97bf
--- /dev/null
+++ b/tests/baseline/scenegraph/data/shape/shape_intersecting2.qml
@@ -0,0 +1,73 @@
+// 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
+
+import QtQuick
+import QtQuick.Shapes
+
+Item {
+ width: 1200
+ height: 600
+
+ ListModel {
+ id: fillRules
+ ListElement { fillrule: ShapePath.WindingFill }
+ ListElement { fillrule: ShapePath.OddEvenFill }
+ }
+
+ ListModel {
+ id: renderers
+ ListElement { renderer: Shape.GeometryRenderer }
+ ListElement { renderer: Shape.CurveRenderer }
+ }
+
+ // The last two paths don't work yet
+ ListModel {
+ id: svgstrings
+ ListElement { scaleToFit: 0.11; offsetX: 20; offsetY: 20; pathString: "M 200 200 Q 600 1000 1000 200 Q -100 200 200 1000 Q -100 1000 -100 500 Q -300 200 200 200 M 300 200 L 800 700 L 300 700 L 800 200 L 300 200"}
+ ListElement { scaleToFit: 0.11; offsetX: 20; offsetY: 20; pathString: "M 200 200 Q 600 1000 1000 200 Q -100 200 200 1000 Q -100 1000 -100 500 Q -300 200 200 200 M 300 200 L 300 700 L 800 200 L 800 700 L 300 200"}
+ ListElement { scaleToFit: 1; offsetX: 0; offsetY: 0; pathString: "M 128.84 103.6 L 112.71 68.78 L 64.32 19.15 L 144.33 94.34 L 47.33 98.82 Q 37.0 60.87 39.01 12.06 L 51.92 72.5 L 19.87 130.09 L 139.11 47.81 Q 110.47 70.31 113.71 87.09 "}
+ ListElement { scaleToFit: 1; offsetX: 0; offsetY: 0; pathString: "M 85.23 92.08 Q 126.6 2.98 23.72 51.94 Z M 103.97 123.27 L 145.32 76.51 Z M 134.57 28.24 L 41.44 8.54 Z M 43.48 123.1 Q 58.58 39.75 92.72 144.17 Z M 23.86 116.3 Q 5.82 83.87 84.11 130.88"}
+ ListElement { scaleToFit: 1; offsetX: 0; offsetY: 0; pathString: "M 64.9 142.96 Q 4.68 9.31 7.25 121.42 Z M 5.3 86.37 Q 31.57 11.81 34.04 5.0 Q 25.26 47.39 31.31 87.97 Z M 4.2 17.35 L 33.27 124.0 Q 111.36 33.37 8.23 118.77 L 65.78 27.48 Q 139.84 87.06 96.03 23.08"}
+ ListElement { scaleToFit: 1; offsetX: 0; offsetY: 0; pathString: "M 88.78 105.24 L 69.72 114.27 L 26.94 71.0 Q 62.05 122.52 83.1 22.44 Q 105.46 40.25 108.12 54.23 L 103.51 40.41 Q 70.16 41.09 29.88 96.16 Z M 60.49 96.92 L 14.58 112.34 Q 1.76 12.05 106.52 128.97"}
+ ListElement { scaleToFit: 1; offsetX: 0; offsetY: 0; pathString: "M 70.21 146.61 L 54.01 6.38 Z M 52.51 5.75 L 70.75 44.78 L 81.98 128.43 Z M 2.71 72.51 L 96.86 4.48 L 136.96 83.88 Z M 25.32 52.79 Q 3.59 111.11 125.15 137.23"}
+ ListElement { scaleToFit: 1; offsetX: 0; offsetY: 0; pathString: "M 131.61 147.43 L 4.5 6.5 L 110.44 40.71 Q 128.69 39.15 136.85 6.84 Q 8.9 139.71 76.35 103.86 L 77.47 43.63 L 127.06 97.65 L 14.97 66.38 Z M 47.53 0.08 L 42.02 33.25"}
+ }
+
+ Column {
+ Repeater {
+ model: renderers
+ Column {
+ Repeater {
+ model: fillRules
+ Row {
+ Repeater {
+ model: svgstrings
+ Rectangle {
+ width: 150
+ height: 150
+ border.color: "black"
+
+ Shape {
+ preferredRendererType: renderer
+ ShapePath {
+ fillColor: renderer == Shape.CurveRenderer ? "#99483d8b" : "#99dc143c"
+ fillRule: fillrule
+ strokeWidth: 0
+ PathSvg { path: pathString }
+ }
+
+ transform: Matrix4x4 {
+ matrix: Qt.matrix4x4(scaleToFit, 0, 0, offsetX,
+ 0, scaleToFit, 0, offsetY,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/manual/painterpathquickshape/CMakeLists.txt b/tests/manual/painterpathquickshape/CMakeLists.txt
index 1f92afaba5..4677fad26c 100644
--- a/tests/manual/painterpathquickshape/CMakeLists.txt
+++ b/tests/manual/painterpathquickshape/CMakeLists.txt
@@ -36,6 +36,8 @@ set(qml_resource_files
"SimpleShape.qml"
"SmallPolygon.qml"
"Squircle.qml"
+ "Intersect.qml"
+ "Intersect2.qml"
"ControlledShape.qml"
"Mussel.qml"
"Graziano.ttf"
diff --git a/tests/manual/painterpathquickshape/ControlPanel.qml b/tests/manual/painterpathquickshape/ControlPanel.qml
index bb26e658f3..3fbe33968f 100644
--- a/tests/manual/painterpathquickshape/ControlPanel.qml
+++ b/tests/manual/painterpathquickshape/ControlPanel.qml
@@ -15,6 +15,7 @@ Item {
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 fillRule: fillRule.currentValue
property alias outlineAlpha: outlineAlphaSlider.value
property real outlineWidth: cosmeticPen.checked ? outlineWidthEdit.text / scale : outlineWidthEdit.text
property alias outlineStyle: outlineStyle.currentValue
@@ -46,6 +47,7 @@ Item {
property alias cosmeticPen: cosmeticPen.checked
property alias pathAlpha: alphaSlider.value
+ property alias fillRule: fillRule.currentIndex
property alias fillColor: fillColor.color
property alias setBackground: setBackground.checked
@@ -218,6 +220,25 @@ Item {
model: [ "NoGradient", "LinearGradient", "RadialGradient", "ConicalGradient" ]
}
Label {
+ text: "Fill rule:"
+ color: "white"
+ }
+ ComboBox {
+ id: fillRule
+ textRole: "text"
+ valueRole: "style"
+ model: ListModel {
+ ListElement {
+ text: "WindingFill"
+ style: ShapePath.WindingFill
+ }
+ ListElement {
+ text: "OddEvenFill"
+ style: ShapePath.OddEvenFill
+ }
+ }
+ }
+ Label {
text: "Fill alpha(" + Math.round(alphaSlider.value*100)/100 + "):"
color: "white"
}
diff --git a/tests/manual/painterpathquickshape/ControlledShape.qml b/tests/manual/painterpathquickshape/ControlledShape.qml
index db8333c2e2..087c4084dd 100644
--- a/tests/manual/painterpathquickshape/ControlledShape.qml
+++ b/tests/manual/painterpathquickshape/ControlledShape.qml
@@ -81,7 +81,7 @@ Item {
ShapePath {
id: shapePath
- fillRule: ShapePath.WindingFill
+ fillRule: controlPanel.fillRule
fillGradient: gradients[controlPanel.gradientType]
strokeColor: controlPanel.outlineColor
fillColor: controlPanel.fillColor
diff --git a/tests/manual/painterpathquickshape/Intersect.qml b/tests/manual/painterpathquickshape/Intersect.qml
new file mode 100644
index 0000000000..ed1156f388
--- /dev/null
+++ b/tests/manual/painterpathquickshape/Intersect.qml
@@ -0,0 +1,125 @@
+// 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: p0.cx; y: p0.cy },
+ PathQuad { x: p1.cx; y: p1.cy; controlX: c0.cx; controlY: c0.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 },
+ PathQuad { x: p0.cx; y: p0.cy; controlX: c3.cx; controlY: c3.cy },
+ PathMove { x: p4.cx; y: p4.cy; },
+ PathLine { x: p5.cx; y: p5.cy; },
+ PathLine { x: p6.cx; y: p6.cy; },
+ PathLine { x: p7.cx; y: p7.cy; },
+ PathLine { x: p4.cx; y: p4.cy; }
+ ]
+
+ ControlPoint {
+ id: p0
+ cx: 200
+ cy: 200
+ }
+ ControlPoint {
+ id: c0
+ color: "blue"
+ cx: 600
+ cy: 1000
+ }
+ ControlPoint {
+ id: p1
+ cx: 1000
+ cy: 200
+ }
+ ControlPoint {
+ id: c1
+ color: "blue"
+ cx: -100
+ cy: 200
+ }
+ ControlPoint {
+ id: p2
+ color: "red"
+ cx: 200
+ cy: 1000
+ }
+ ControlPoint {
+ id: c2
+ color: "blue"
+ cx: -100
+ cy: 1000
+ }
+ ControlPoint {
+ id: p3
+ color: "red"
+ cx: -100
+ cy: 500
+ }
+ ControlPoint {
+ id: c3
+ color: "blue"
+ cx: -300
+ cy: 200
+ }
+
+ ControlPoint {
+ id: p4
+ color: "green"
+ cx: 2000
+ cy: 200
+ }
+ ControlPoint {
+ id: p5
+ color: "green"
+ cx: 2500
+ cy: 700
+ }
+ ControlPoint {
+ id: p6
+ color: "green"
+ cx: 2000
+ cy: 700
+ }
+ ControlPoint {
+ id: p7
+ color: "green"
+ cx: 2500
+ cy: 200
+ }
+
+ Text {
+ anchors.centerIn: p0
+ text: "p0"
+ }
+ Text {
+ anchors.centerIn: p1
+ text: "p1"
+ }
+ Text {
+ anchors.centerIn: p2
+ text: "p2"
+ }
+ Text {
+ anchors.centerIn: p3
+ text: "p3"
+ }
+ Text {
+ anchors.centerIn: c0
+ text: "c0"
+ }
+ Text {
+ anchors.centerIn: c1
+ text: "c1"
+ }
+ Text {
+ anchors.centerIn: c2
+ text: "c2"
+ }
+ Text {
+ anchors.centerIn: c3
+ text: "c3"
+ }
+}
diff --git a/tests/manual/painterpathquickshape/Intersect2.qml b/tests/manual/painterpathquickshape/Intersect2.qml
new file mode 100644
index 0000000000..408eaa1afb
--- /dev/null
+++ b/tests/manual/painterpathquickshape/Intersect2.qml
@@ -0,0 +1,136 @@
+// 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: p0.cx; y: p0.cy },
+ PathQuad { x: p1.cx; y: p1.cy; controlX: c0.cx; controlY: c0.cy },
+ PathQuad { x: p2.cx; y: p2.cy; controlX: c1.cx; controlY: c1.cy },
+ PathQuad { x: p0.cx; y: p0.cy; controlX: c2.cx; controlY: c2.cy },
+ PathMove { x: p3.cx; y: p3.cy },
+ PathQuad { x: p4.cx; y: p4.cy; controlX: c3.cx; controlY: c3.cy },
+ PathQuad { x: p5.cx; y: p5.cy; controlX: c4.cx; controlY: c4.cy },
+ PathQuad { x: p3.cx; y: p3.cy; controlX: c5.cx; controlY: c5.cy }
+ ]
+
+ ControlPoint {
+ id: p0
+ cx: 600
+ cy: 200
+ }
+ ControlPoint {
+ id: c0
+ color: "blue"
+ cx: 1000
+ cy: 600
+ }
+ ControlPoint {
+ id: p1
+ cx: 600
+ cy: 1000
+ }
+ ControlPoint {
+ id: c1
+ color: "blue"
+ cx: 200
+ cy: 1200
+ }
+ ControlPoint {
+ id: p2
+ cx: 200
+ cy: 600
+ }
+ ControlPoint {
+ id: c2
+ color: "blue"
+ cx: 200
+ cy: 200
+ }
+
+ ControlPoint {
+ id: p3
+ cx: 1000
+ cy: 200
+ }
+ ControlPoint {
+ id: c3
+ color: "blue"
+ cx: 1400
+ cy: 600
+ }
+ ControlPoint {
+ id: p4
+ cx: 1000
+ cy: 1000
+ }
+ ControlPoint {
+ id: c4
+ color: "blue"
+ cx: 600
+ cy: 1200
+ }
+ ControlPoint {
+ id: p5
+ cx: 600
+ cy: 600
+ }
+ ControlPoint {
+ id: c5
+ color: "blue"
+ cx: 200
+ cy: -200
+ }
+
+
+ Text {
+ anchors.centerIn: p0
+ text: "p0"
+ }
+ Text {
+ anchors.centerIn: p1
+ text: "p1"
+ }
+ Text {
+ anchors.centerIn: p2
+ text: "p2"
+ }
+ Text {
+ anchors.centerIn: p3
+ text: "p3"
+ }
+ Text {
+ anchors.centerIn: p4
+ text: "p4"
+ }
+ Text {
+ anchors.centerIn: p5
+ text: "p5"
+ }
+ Text {
+ anchors.centerIn: c0
+ text: "c0"
+ }
+ Text {
+ anchors.centerIn: c1
+ text: "c1"
+ }
+ Text {
+ anchors.centerIn: c2
+ text: "c2"
+ }
+ Text {
+ anchors.centerIn: c3
+ text: "c3"
+ }
+ Text {
+ anchors.centerIn: c4
+ text: "c4"
+ }
+ Text {
+ anchors.centerIn: c5
+ text: "c5"
+ }
+}
diff --git a/tests/manual/painterpathquickshape/main.qml b/tests/manual/painterpathquickshape/main.qml
index 36dae42639..9a5efa4ec8 100644
--- a/tests/manual/painterpathquickshape/main.qml
+++ b/tests/manual/painterpathquickshape/main.qml
@@ -40,6 +40,14 @@ Window {
source: "Squircle.qml"
}
ListElement {
+ text: "Intersect"
+ source: "Intersect.qml"
+ }
+ ListElement {
+ text: "Intersect2"
+ source: "Intersect2.qml"
+ }
+ ListElement {
text: "CubicShape"
source: "CubicShape.qml"
}