diff options
| -rw-r--r-- | examples/quick/quickshapes/shapes/cornershapes.qml | 12 | ||||
| -rw-r--r-- | src/quick/doc/src/includes/pathrectangle.qdocinc | 3 | ||||
| -rw-r--r-- | src/quick/util/qquickpath.cpp | 124 | ||||
| -rw-r--r-- | src/quickshapes/designhelpers/qquickrectangleshape.cpp | 3 | ||||
| -rw-r--r-- | tests/auto/quick/qquickpath/tst_qquickpath.cpp | 4 | ||||
| -rw-r--r-- | tests/baseline/scenegraph/data/shape/shape_inverted_corners.qml | 77 |
6 files changed, 177 insertions, 46 deletions
diff --git a/examples/quick/quickshapes/shapes/cornershapes.qml b/examples/quick/quickshapes/shapes/cornershapes.qml index 34112f51f8..565c3b6057 100644 --- a/examples/quick/quickshapes/shapes/cornershapes.qml +++ b/examples/quick/quickshapes/shapes/cornershapes.qml @@ -27,17 +27,20 @@ Rectangle { GradientStop { position: 0.7; color: "red" } } + // The radius is clamped to half of the rectangle's size, so only + // animate within that range to keep the shape moving. + readonly property real maxRadius: pathRectangle.width / 2 property real animRadius SequentialAnimation on animRadius { loops: Animation.Infinite NumberAnimation { - from: 0 - to: 200 + from: -myPath.maxRadius + to: myPath.maxRadius duration: 3000 } NumberAnimation { - from: 200 - to: 0 + from: myPath.maxRadius + to: -myPath.maxRadius duration: 3000 } PauseAnimation { @@ -46,6 +49,7 @@ Rectangle { } PathRectangle { + id: pathRectangle x: myShape.width / 5 y: x width: myShape.width - 2 * x diff --git a/src/quick/doc/src/includes/pathrectangle.qdocinc b/src/quick/doc/src/includes/pathrectangle.qdocinc index e9d56bb13b..f5264df74c 100644 --- a/src/quick/doc/src/includes/pathrectangle.qdocinc +++ b/src/quick/doc/src/includes/pathrectangle.qdocinc @@ -3,9 +3,6 @@ This property defines the corner radius used to define a rounded rectangle. -If radius is a positive value, the rectangle path will be defined as a rounded rectangle, -otherwise it will be defined as a normal rectangle. - This property may be overridden by the individual corner radius properties. \sa topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius diff --git a/src/quick/util/qquickpath.cpp b/src/quick/util/qquickpath.cpp index fa05ba0a7b..03e6deab4c 100644 --- a/src/quick/util/qquickpath.cpp +++ b/src/quick/util/qquickpath.cpp @@ -2559,6 +2559,20 @@ void QQuickPathRectangle::setStrokeAdjustment(qreal newStrokeAdjustment) /*! \include pathrectangle.qdocinc {radius-property} {QtQuick::PathRectangle} + If radius is a positive value, the rectangle path will be defined as a rounded rectangle. + If radius is zero, it will be defined as a normal rectangle. + + Since Qt 6.13, if radius is negative, the corners are "inverted": instead of the usual + convex curve, a concave, scooped-out curve of the same magnitude is used, for both the + \c PathRectangle.Rounded and \c PathRectangle.Squircle \l{cornerShape}{corner shapes}. This + has no visible effect on \c PathRectangle.Bevel corners, since a straight bevel cut looks the + same either way. Before Qt 6.13, a negative radius was treated like zero. + + The effective radius is limited to half of the smaller of the rectangle's width and height, + in both the positive and the negative direction. + + \note This differs from \l Rectangle, which draws a normal rectangle for a negative radius. + The default value is \c 0. */ @@ -2586,6 +2600,9 @@ void QQuickPathRectangle::setRadius(qreal newRadius) /*! \include pathrectangle.qdocinc {radius-properties} {PathRectangle} {qml/pathrectangle/pathrectangle.qml} {shape} + + Since Qt 6.13, a negative value defines that corner to be "inverted" (concave), as described + for \l radius. */ qreal QQuickPathRectangle::cornerRadius(Qt::Corner corner) const @@ -2812,12 +2829,16 @@ void QQuickPathRectangle::addToPath(QPainterPath &path, const QQuickPathData &da return; } - // Radii must not exceed half of the width or half of the height + // Radii must not exceed half of the width or half of the height. Negative + // radii are allowed (down to the same limit) and select an "inverted" + // (concave, scooped-out) corner instead of the usual convex one, for + // both the Rounded and Squircle corner shapes. const qreal maxDiameter = qMin(rect.width(), rect.height()); - const qreal generalRadius = qBound(qreal(0), _extra->radius, maxDiameter * 0.5); + const qreal maxRadius = maxDiameter * 0.5; + const qreal generalRadius = qBound(-maxRadius, _extra->radius, maxRadius); auto effectiveRadius = [&](Qt::Corner corner) { qreal radius = _extra->cornerData[corner].radius; - return (_extra->isRadiusSet(corner)) ? qBound(qreal(0), radius, maxDiameter * 0.5) + return (_extra->isRadiusSet(corner)) ? qBound(-maxRadius, radius, maxRadius) : generalRadius; }; const qreal rTL = effectiveRadius(Qt::TopLeftCorner); @@ -2830,7 +2851,9 @@ void QQuickPathRectangle::addToPath(QPainterPath &path, const QQuickPathData &da // the outgoing edge, as fractions of the corner radius. The corner // curves clockwise, entering horizontally and exiting vertically (a // "top-right"-style corner); the other three corners are obtained by - // rotating these offsets by 90, 180 and 270 degrees. + // rotating these offsets by 90, 180 and 270 degrees. Swapping the x and + // y of each offset mirrors the curve across the chord it spans, which + // turns the usual convex curve into the concave "inverted" one. static constexpr QPointF cornerOffsets[9] = { { 0.300, 0.000 }, { 0.473, 0.000 }, { 0.619, 0.039 }, { 0.804, 0.088 }, { 0.912, 0.196 }, { 0.961, 0.381 }, { 1.000, 0.527 }, { 1.000, 0.700 }, { 1.000, 1.000 } @@ -2843,87 +2866,112 @@ void QQuickPathRectangle::addToPath(QPainterPath &path, const QQuickPathData &da return p; }; - auto addSquircleCorner = [&](QPointF start, int steps, qreal r) { + auto addSquircleCorner = [&](QPointF start, int steps, qreal r, bool inverted) { for (int seg = 0; seg < 3; ++seg) { - QPointF c1 = start + rotate(cornerOffsets[seg * 3 + 0] * r, steps); - QPointF c2 = start + rotate(cornerOffsets[seg * 3 + 1] * r, steps); - QPointF end = start + rotate(cornerOffsets[seg * 3 + 2] * r, steps); + QPointF o1 = cornerOffsets[seg * 3 + 0]; + QPointF o2 = cornerOffsets[seg * 3 + 1]; + QPointF o3 = cornerOffsets[seg * 3 + 2]; + if (inverted) { + o1 = QPointF(o1.y(), o1.x()); + o2 = QPointF(o2.y(), o2.x()); + o3 = QPointF(o3.y(), o3.x()); + } + QPointF c1 = start + rotate(o1 * r, steps); + QPointF c2 = start + rotate(o2 * r, steps); + QPointF end = start + rotate(o3 * r, steps); path.cubicTo(c1, c2, end); } }; - path.moveTo(rect.left() + rTL, rect.top()); - if (rTR > 0) { + // Adds a circular corner at "corner". A convex corner is drawn as the + // quarter of the circle inscribed in the 2r x 2r box that extends from the + // corner towards the inside of the rectangle ("inward" gives the direction + // as unit steps), sweeping clockwise from "startAngle". An inverted corner + // is the quarter of the circle centered on the corner point itself, which + // starts 90 degrees further and sweeps counter-clockwise. + auto addRoundedCorner = [&](QPointF corner, QPointF inward, qreal startAngle, qreal r, + bool inverted) { + const QRectF box(corner - QPointF(r, r), QSizeF(2 * r, 2 * r)); + if (inverted) + path.arcTo(box, startAngle + 90, 90); + else + path.arcTo(box.translated(inward * r), startAngle, -90); + }; + + path.moveTo(rect.left() + qAbs(rTL), rect.top()); + if (rTR != 0) { + const qreal r = qAbs(rTR); + const bool inverted = rTR < 0; switch (cornerShape(Qt::TopRightCorner)) { case Bevel: - path.lineTo(QPointF(rect.right() - rTR, rect.top())); - path.lineTo(QPointF(rect.right(), rect.top() + rTR)); + path.lineTo(QPointF(rect.right() - r, rect.top())); + path.lineTo(QPointF(rect.right(), rect.top() + r)); break; case Squircle: - path.lineTo(QPointF(rect.right() - rTR, rect.top())); - addSquircleCorner(QPointF(rect.right() - rTR, rect.top()), 0, rTR); + path.lineTo(QPointF(rect.right() - r, rect.top())); + addSquircleCorner(QPointF(rect.right() - r, rect.top()), 0, r, inverted); break; default: - path.arcTo( - QRectF(QPointF(rect.right() - 2 * rTR, rect.top()), QSizeF(2 * rTR, 2 * rTR)), - 90, -90); + addRoundedCorner(rect.topRight(), QPointF(-1, 1), 90, r, inverted); break; } } else { path.lineTo(rect.topRight()); } - if (rBR > 0) { + if (rBR != 0) { + const qreal r = qAbs(rBR); + const bool inverted = rBR < 0; switch (cornerShape(Qt::BottomRightCorner)) { case Bevel: - path.lineTo(QPointF(rect.right(), rect.bottom() - rBR)); - path.lineTo(QPointF(rect.right() - rBR, rect.bottom())); + path.lineTo(QPointF(rect.right(), rect.bottom() - r)); + path.lineTo(QPointF(rect.right() - r, rect.bottom())); break; case Squircle: - path.lineTo(QPointF(rect.right(), rect.bottom() - rBR)); - addSquircleCorner(QPointF(rect.right(), rect.bottom() - rBR), 1, rBR); + path.lineTo(QPointF(rect.right(), rect.bottom() - r)); + addSquircleCorner(QPointF(rect.right(), rect.bottom() - r), 1, r, inverted); break; default: - path.arcTo(QRectF(QPointF(rect.right() - 2 * rBR, rect.bottom() - 2 * rBR), - QSizeF(2 * rBR, 2 * rBR)), - 0, -90); + addRoundedCorner(rect.bottomRight(), QPointF(-1, -1), 0, r, inverted); break; } } else { path.lineTo(rect.bottomRight()); } - if (rBL > 0) { + if (rBL != 0) { + const qreal r = qAbs(rBL); + const bool inverted = rBL < 0; switch (cornerShape(Qt::BottomLeftCorner)) { case Bevel: - path.lineTo(QPointF(rect.left() + rBL, rect.bottom())); - path.lineTo(QPointF(rect.left(), rect.bottom() - rBL)); + path.lineTo(QPointF(rect.left() + r, rect.bottom())); + path.lineTo(QPointF(rect.left(), rect.bottom() - r)); break; case Squircle: - path.lineTo(QPointF(rect.left() + rBL, rect.bottom())); - addSquircleCorner(QPointF(rect.left() + rBL, rect.bottom()), 2, rBL); + path.lineTo(QPointF(rect.left() + r, rect.bottom())); + addSquircleCorner(QPointF(rect.left() + r, rect.bottom()), 2, r, inverted); break; default: - path.arcTo( - QRectF(QPointF(rect.left(), rect.bottom() - 2 * rBL), QSizeF(2 * rBL, 2 * rBL)), - 270, -90); + addRoundedCorner(rect.bottomLeft(), QPointF(1, -1), 270, r, inverted); break; } } else { path.lineTo(rect.bottomLeft()); } - if (rTL > 0) { + if (rTL != 0) { + const qreal r = qAbs(rTL); + const bool inverted = rTL < 0; switch (cornerShape(Qt::TopLeftCorner)) { case Bevel: - path.lineTo(QPointF(rect.left(), rect.top() + rTL)); + path.lineTo(QPointF(rect.left(), rect.top() + r)); break; case Squircle: - path.lineTo(QPointF(rect.left(), rect.top() + rTL)); - addSquircleCorner(QPointF(rect.left(), rect.top() + rTL), 3, rTL); + path.lineTo(QPointF(rect.left(), rect.top() + r)); + addSquircleCorner(QPointF(rect.left(), rect.top() + r), 3, r, inverted); break; default: - path.arcTo(QRectF(rect.topLeft(), QSizeF(2 * rTL, 2 * rTL)), 180, -90); + addRoundedCorner(rect.topLeft(), QPointF(1, 1), 180, r, inverted); break; } } else { diff --git a/src/quickshapes/designhelpers/qquickrectangleshape.cpp b/src/quickshapes/designhelpers/qquickrectangleshape.cpp index 47b3af1676..cc94e2d164 100644 --- a/src/quickshapes/designhelpers/qquickrectangleshape.cpp +++ b/src/quickshapes/designhelpers/qquickrectangleshape.cpp @@ -590,6 +590,9 @@ void QQuickRectangleShape::resetDrawLeft() \include pathrectangle.qdocinc {radius-property} {QtQuick.Shapes.DesignHelpers::RectangleShape} + If radius is a positive value, the rectangle path will be defined as a rounded rectangle, + otherwise it will be defined as a normal rectangle. + The default value is \c 10. */ diff --git a/tests/auto/quick/qquickpath/tst_qquickpath.cpp b/tests/auto/quick/qquickpath/tst_qquickpath.cpp index 36d8c44d0e..2034d2d192 100644 --- a/tests/auto/quick/qquickpath/tst_qquickpath.cpp +++ b/tests/auto/quick/qquickpath/tst_qquickpath.cpp @@ -421,7 +421,9 @@ void tst_QuickPath::rectangle() void tst_QuickPath::rectangleRadii() { - // Test that the radius logic of PathRectangle is the same as Rectangle's + // Test that the radius property logic (explicit, inherited and reset per-corner values) + // of PathRectangle is the same as Rectangle's. Note that the rendering differs: a negative + // radius gives inverted corners on PathRectangle, but a normal rectangle on Rectangle. QQmlEngine engine; QQmlComponent c1(&engine); c1.setData("import QtQuick\n" diff --git a/tests/baseline/scenegraph/data/shape/shape_inverted_corners.qml b/tests/baseline/scenegraph/data/shape/shape_inverted_corners.qml new file mode 100644 index 0000000000..5b858c37e7 --- /dev/null +++ b/tests/baseline/scenegraph/data/shape/shape_inverted_corners.qml @@ -0,0 +1,77 @@ +import QtQuick +import QtQuick.Shapes + +Rectangle { + width: 320 + height: 480 + color: "lightgray" + + ListModel { + id: renderers + ListElement { renderer: Shape.GeometryRenderer } + ListElement { renderer: Shape.CurveRenderer } + } + + ListModel { + id: cornerShapes + ListElement { cornerShape: PathRectangle.Rounded } + ListElement { cornerShape: PathRectangle.Squircle } + ListElement { cornerShape: PathRectangle.Bevel } + } + + Row { + padding: 10 + Repeater { + model: renderers + Column { + id: rendererColumn + required property int renderer + spacing: 10 + Repeater { + model: cornerShapes + Shape { + id: shape + required property int cornerShape + width: 150 + height: 140 + preferredRendererType: rendererColumn.renderer + + ShapePath { + fillColor: "yellow" + strokeColor: "green" + strokeWidth: 3 + joinStyle: ShapePath.MiterJoin + + // Uniform inverted corners + PathRectangle { + x: 5; y: 0 + width: 120; height: 40 + cornerShape: shape.cornerShape + radius: -12 + } + + // Mixed inverted, convex and sharp corners + PathRectangle { + x: 5; y: 50 + width: 120; height: 40 + cornerShape: shape.cornerShape + radius: 8 + topLeftRadius: -15 + bottomRightRadius: -5 + bottomLeftRadius: 0 + } + + // Clamped to half of the height + PathRectangle { + x: 5; y: 100 + width: 120; height: 40 + cornerShape: shape.cornerShape + radius: -100 + } + } + } + } + } + } + } +} |
