diff options
| author | Henning Gruendl <henning.gruendl@qt.io> | 2024-08-09 16:58:24 +0200 |
|---|---|---|
| committer | Henning Gruendl <henning.gruendl@qt.io> | 2024-08-26 13:31:42 +0200 |
| commit | e8597ac3756c350cd9ad6905f385bfef3ad24753 (patch) | |
| tree | 6398ddcfb4599add0a3caedae408795381e7d471 | |
| parent | 80b9e31a8863c9bf2a57c3d569b761d9df71cdb3 (diff) | |
QmlDesigner: Update RegularPolygonItem
Update the construction of RegularPolygonItem so it will be same as in
Figma.
Task-number: QDS-13304
Change-Id: I9f2d323eb3e01e2f39be7290fe6e6736d9e57e6d
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
| -rw-r--r-- | src/imports/components/RegularPolygonItem.qml | 241 | ||||
| -rw-r--r-- | src/imports/components/designer/RegularPolygonItemSpecifics.qml | 14 | ||||
| -rw-r--r-- | src/imports/components/qmldir | 15 |
3 files changed, 162 insertions, 108 deletions
diff --git a/src/imports/components/RegularPolygonItem.qml b/src/imports/components/RegularPolygonItem.qml index 8a0e4d2..4f44771 100644 --- a/src/imports/components/RegularPolygonItem.qml +++ b/src/imports/components/RegularPolygonItem.qml @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2021 The Qt Company Ltd. +** Copyright (C) 2024 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Quick Studio Components. @@ -50,9 +50,7 @@ Shape { The default value is 10. If radius is non-zero, the corners will be rounded, otherwise they will - be sharp. The radius can also be specified separately for each corner by - using the \l bottomLeftRadius, \l bottomRightRadius, \l topLeftRadius, and - \l topRightRadius properties. + be sharp. */ property int radius: 10 @@ -159,11 +157,11 @@ Shape { property alias dashOffset: path.dashOffset /*! - Number of sides on the polygon. + The number of sides on the polygon. */ property int sideCount: 6 - property bool __preferredRendererTypeAvailable: root.preferredRendererType !== undefined + property bool __preferredRendererTypeAvailable: root.preferredRendererType !== "undefined" property bool __curveRendererActive: root.__preferredRendererTypeAvailable && root.rendererType === Shape.CurveRenderer @@ -191,14 +189,10 @@ Shape { startY: 0 } + onWidthChanged: root.constructPolygon() + onHeightChanged: root.constructPolygon() onSideCountChanged: root.constructPolygon() - onRadiusChanged: { - // Only construct polygon if radius changed from 0 to 1 or vice versa. - if ((root.radius + root.__previousRadius) === 1) - root.constructPolygon() - - root.__previousRadius = root.radius - } + onRadiusChanged: root.constructPolygon() Component.onCompleted: { // If preferredRendererType wasn't set initially make CurveRenderer the default @@ -210,20 +204,12 @@ Shape { property real __centerX: root.width / 2 property real __centerY: root.height / 2 - property real __radius: Math.min(root.width, root.height) / 2 - - property int __previousRadius: root.radius - - property int minRadius: 0 - property int maxRadius: root.__radius * Math.cos(root.toRadians(180.0 / root.sideCount)) - - property int __actualRadius: Math.max(root.minRadius, Math.min(root.maxRadius, root.radius)) function constructPolygon() { root.clearPathElements() if (root.radius === 0) - root.constructNonRoundedPolygonPath() + root.constructPolygonPath() else root.constructRoundedPolygonPath() } @@ -232,96 +218,165 @@ Shape { return degrees * (Math.PI / 180.0) } - function constructNonRoundedPolygonPath() { - for (var cornerNumber = 0; cornerNumber < root.sideCount; cornerNumber++) { - let angleToCorner = root.toRadians(cornerNumber * (360.0 / root.sideCount)) - - if (cornerNumber === 0) { - path.startX = Qt.binding(function() { - return root.__centerX + root.__radius * Math.cos(0) - }) - path.startY = Qt.binding(function() { - return root.__centerY + root.__radius * Math.sin(0) - }) + function toDegrees(radians) { + return radians * (180.0 / Math.PI) + } + + function getPoints() { + let sliceAngle = (360.0 / root.sideCount) + // The Draftsman's Method + let a = root.width / 2 // x-radius + let b = root.height / 2 // y-radius + let points = [] + + // Go clockwise from top center + for (var corner = 0; corner < root.sideCount; corner++) { + let angleToCorner = root.toRadians(corner * sliceAngle) + // Start at top center + let x = root.__centerX + a * Math.sin(angleToCorner) + let y = root.__centerY - b * Math.cos(angleToCorner) + + points.push(Qt.point(x ,y)) + } + + return points + } + + Component { + id: myPathLine + PathLine {} + } + + Component { + id: myPathArc + PathArc {} + } + + function constructPolygonPath() { + let angle = (360.0 / root.sideCount) + let points = root.getPoints() + + for (var i = 0; i < points.length; i++) { + if (i === 0) { + path.startX = points[i].x + path.startY = points[i].y } else { - let pathLine = Qt.createQmlObject('import QtQuick 2.15; PathLine {}', path) - pathLine.x = Qt.binding(function() { - return root.__centerX + root.__radius * Math.cos(angleToCorner) - }) - pathLine.y = Qt.binding(function() { - return root.__centerY + root.__radius * Math.sin(angleToCorner) - }) + let pathLine = myPathLine.createObject(path) + pathLine.x = points[i].x + pathLine.y = points[i].y path.pathElements.push(pathLine) } } // Close the polygon - var pathLineClose = Qt.createQmlObject('import QtQuick 2.15; PathLine {}', path) - pathLineClose.x = Qt.binding(function() { return path.startX } ) - pathLineClose.y = Qt.binding(function() { return path.startY } ) + var pathLineClose = myPathLine.createObject(path) + pathLineClose.x = points[0].x + pathLineClose.y = points[0].y path.pathElements.push(pathLineClose) } - property real __halfInteriorCornerAngle: 90 - (180.0 / root.sideCount) - property real __halfCornerArcSweepAngle: 90 - root.__halfInteriorCornerAngle - property real __distanceToCornerArcCenter: root.__radius - root.__actualRadius / - Math.sin(root.toRadians(root.__halfInteriorCornerAngle)) + // https://stackoverflow.com/questions/58541430/find-intersection-point-of-two-vectors-independent-from-direction + // This function returns the length of the vector from p to the intersection point of the two lines + // both defined by a point and a vector. + function intersect(p: point, dir1: vector2d, q: point, dir2: vector2d) : real { + let r = dir1.normalized() + let s = dir2.normalized() + + let pq = Qt.vector2d(q.x - p.x, q.y - p.y) + let snv = Qt.vector2d(s.y, -s.x); + + return pq.dotProduct(snv) / r.dotProduct(snv) + } + + function wrapIndex(index, size) { + return (index + size) % size + } function constructRoundedPolygonPath() { - for (var cornerNumber = 0; cornerNumber < root.sideCount; cornerNumber++) { - let angleToCorner = cornerNumber * (360.0 / root.sideCount) - - let pathArc = Qt.createQmlObject('import QtQuick 2.15; PathArc { - property real centerX; - property real centerY }', path) - pathArc.centerX = Qt.binding(function() { - return root.__centerX + root.__distanceToCornerArcCenter - * Math.cos(root.toRadians(angleToCorner)) - }) - pathArc.centerY = Qt.binding(function() { - return root.__centerY + root.__distanceToCornerArcCenter - * Math.sin(root.toRadians(angleToCorner)) - }) - pathArc.x = Qt.binding(function() { - return pathArc.centerX + root.__actualRadius - * (Math.cos(root.toRadians(angleToCorner + root.__halfCornerArcSweepAngle))) - }) - pathArc.y = Qt.binding(function() { - return pathArc.centerY + root.__actualRadius - * (Math.sin(root.toRadians(angleToCorner + root.__halfCornerArcSweepAngle))) - }) - pathArc.radiusX = Qt.binding(function() { return root.__actualRadius }) - pathArc.radiusY = Qt.binding(function() { return root.__actualRadius }) - - if (cornerNumber === 0) { - path.startX = Qt.binding(function() { - return pathArc.centerX + root.__actualRadius - * (Math.cos(root.toRadians(angleToCorner - root.__halfCornerArcSweepAngle))) - }) - path.startY = Qt.binding(function() { - return pathArc.centerY + root.__actualRadius - * (Math.sin(root.toRadians(angleToCorner - root.__halfCornerArcSweepAngle))) - }) + let angle = (360.0 / root.sideCount) + let points = root.getPoints() + + // A list of vectors that are the bisectors of the inner angles of the polygon. + // This is used to calculate the intersection point of neighboring bisectors for a corner. + // The minimum length of the two neighboring corner bisectors intersection point is the + // maximum for the center of the circle that make up the corner radius. + let bisectors = [] + + // Create angle bisectors by using the parallelolgram rule. + for (var i = 0; i < points.length; i++) { + let a = points[root.wrapIndex(i, points.length)] + let b = points[root.wrapIndex(i - 1, points.length)] + let c = points[root.wrapIndex(i + 1, points.length)] + + let vAB = Qt.vector2d(b.x - a.x, b.y - a.y).normalized() + let vAC = Qt.vector2d(c.x - a.x, c.y - a.y).normalized() + let bisector = vAB.plus(vAC).normalized() + + bisectors.push(bisector) + } + + for (var i = 0; i < points.length; i++) { + let a = points[root.wrapIndex(i, points.length)] + let b = points[root.wrapIndex(i - 1, points.length)] + let c = points[root.wrapIndex(i + 1, points.length)] + let r = root.radius + + let vAB = Qt.vector2d(b.x - a.x, b.y - a.y) + let vAC = Qt.vector2d(c.x - a.x, c.y - a.y) + + // Calculate the intersection points of the two neighboring bisectors + let tAB = root.intersect(a, bisectors[root.wrapIndex(i, bisectors.length)], + b, bisectors[root.wrapIndex(i - 1, bisectors.length)]) + let tAC = root.intersect(a, bisectors[root.wrapIndex(i, bisectors.length)], + c, bisectors[root.wrapIndex(i + 1, bisectors.length)]) + let tMax = Math.min(tAB, tAC) + + // Angle between the two vectors AB and AC as radians + let alpha = Math.acos(vAB.dotProduct(vAC) / (vAB.length() * vAC.length())) + + // The maximum radius of the circle that can be drawn at the corner. This is another + // constraint that Figma uses to calculate the corner radius. The corner radius shouldn't + // be bigger than half of the distance between the two neighboring corners. + let maxRadius = Math.round(Qt.vector2d(c.x - b.x, c.y - b.y).length() / 2) + r = Math.min(r, maxRadius) + + // The optimal length of the corner bisector to place the center of the circle. + let cLength = r / (Math.sin(alpha / 2)) + + // Clamp c to the maximum value found from the intersection points of the bisectors. + let realC = Math.min(cLength, tMax) + + if (realC < cLength) + r = realC * Math.sin(alpha / 2) + + let t = Math.sqrt(Math.pow(realC, 2) - Math.pow(r, 2)) + + let p1 = vAB.normalized().times(t).plus(Qt.vector2d(a.x, a.y)) + let p2 = vAC.normalized().times(t).plus(Qt.vector2d(a.x, a.y)) + + if (i === 0) { + path.startX = p1.x + path.startY = p1.y } else { - let pathLine = Qt.createQmlObject('import QtQuick 2.15; PathLine {}', path) - pathLine.x = Qt.binding(function() { - return pathArc.centerX + root.__actualRadius - * (Math.cos(root.toRadians(angleToCorner - root.__halfCornerArcSweepAngle))) - }) - pathLine.y = Qt.binding(function() { - return pathArc.centerY + root.__actualRadius - * (Math.sin(root.toRadians(angleToCorner - root.__halfCornerArcSweepAngle))) - }) + let pathLine = myPathLine.createObject(path) + pathLine.x = p1.x + pathLine.y = p1.y path.pathElements.push(pathLine) } + let pathArc = myPathArc.createObject(path) + pathArc.x = p2.x + pathArc.y = p2.y + pathArc.radiusX = r + pathArc.radiusY = r + path.pathElements.push(pathArc) } // Close the polygon - var pathLineClose = Qt.createQmlObject('import QtQuick 2.15; PathLine {}', path) - pathLineClose.x = Qt.binding(function() { return path.startX} ) - pathLineClose.y = Qt.binding(function() { return path.startY} ) + var pathLineClose = myPathLine.createObject(path) + pathLineClose.x = path.startX + pathLineClose.y = path.startY path.pathElements.push(pathLineClose) } diff --git a/src/imports/components/designer/RegularPolygonItemSpecifics.qml b/src/imports/components/designer/RegularPolygonItemSpecifics.qml index 46bc42b..0b4ec81 100644 --- a/src/imports/components/designer/RegularPolygonItemSpecifics.qml +++ b/src/imports/components/designer/RegularPolygonItemSpecifics.qml @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2021 The Qt Company Ltd. +** Copyright (C) 2024 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Quick Designer Components. @@ -27,10 +27,10 @@ ** ****************************************************************************/ -import QtQuick 2.15 -import QtQuick.Layouts 1.15 -import HelperWidgets 2.0 -import StudioTheme 1.0 as StudioTheme +import QtQuick +import QtQuick.Layouts +import HelperWidgets +import StudioTheme as StudioTheme Column { anchors.left: parent.left @@ -153,8 +153,8 @@ Column { implicitWidth: StudioTheme.Values.twoControlColumnWidth + StudioTheme.Values.actionIndicatorWidth decimals: 0 - minimumValue: backendValues.minRadius.value - maximumValue: backendValues.maxRadius.value + minimumValue: 0 + maximumValue: 1000 stepSize: 1 } diff --git a/src/imports/components/qmldir b/src/imports/components/qmldir index 4fdc28d..ceb4bc9 100644 --- a/src/imports/components/qmldir +++ b/src/imports/components/qmldir @@ -1,14 +1,13 @@ +ArcArrow 1.0 ArcArrow.qml ArcItem 1.0 ArcItem.qml -PieItem 1.0 PieItem.qml -TriangleItem 1.0 TriangleItem.qml -SvgPathItem 1.0 SvgPathItem.qml +BorderItem 1.0 BorderItem.qml EllipseItem 1.0 EllipseItem.qml FlipableItem 1.0 FlipableItem.qml +GroupItem 1.0 GroupItem.qml +IsoItem 1.0 IsoItem.qml +PieItem 1.0 PieItem.qml RectangleItem 1.0 RectangleItem.qml RegularPolygonItem 1.0 RegularPolygonItem.qml -BorderItem 1.0 BorderItem.qml -IsoItem 1.0 IsoItem.qml -GroupItem 1.0 GroupItem.qml -ArcArrow 1.0 ArcArrow.qml StraightArrow 1.0 StraightArrow.qml - +SvgPathItem 1.0 SvgPathItem.qml +TriangleItem 1.0 TriangleItem.qml |
