diff options
-rw-r--r-- | src/quick/scenegraph/qsgcurveprocessor.cpp | 640 | ||||
-rw-r--r-- | src/quick/scenegraph/qsgcurveprocessor_p.h | 3 | ||||
-rw-r--r-- | src/quick/scenegraph/util/qquadpath.cpp | 123 | ||||
-rw-r--r-- | src/quick/scenegraph/util/qquadpath_p.h | 33 | ||||
-rw-r--r-- | src/quickshapes/qquickshapecurverenderer.cpp | 3 | ||||
-rw-r--r-- | src/quickshapes/qquickshapecurverenderer_p.h | 1 | ||||
-rw-r--r-- | tests/baseline/scenegraph/data/shape/shape_intersecting1.qml | 72 | ||||
-rw-r--r-- | tests/baseline/scenegraph/data/shape/shape_intersecting2.qml | 73 | ||||
-rw-r--r-- | tests/manual/painterpathquickshape/CMakeLists.txt | 2 | ||||
-rw-r--r-- | tests/manual/painterpathquickshape/ControlPanel.qml | 21 | ||||
-rw-r--r-- | tests/manual/painterpathquickshape/ControlledShape.qml | 2 | ||||
-rw-r--r-- | tests/manual/painterpathquickshape/Intersect.qml | 125 | ||||
-rw-r--r-- | tests/manual/painterpathquickshape/Intersect2.qml | 136 | ||||
-rw-r--r-- | tests/manual/painterpathquickshape/main.qml | 8 |
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" } |