Inital commit of the Komplex Hub
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2024 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Quick Studio Components.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:GPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 or (at your option) any later version
|
||||
** approved by the KDE Free Qt Foundation. The licenses are as published by
|
||||
** the Free Software Foundation and appearing in the file LICENSE.GPL3
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
/*!
|
||||
\qmltype RegularPolygon
|
||||
\inqmlmodule QtQuick.Studio.Components
|
||||
\since QtQuick.Studio.Components 1.0
|
||||
\inherits Shape
|
||||
|
||||
\brief A filled regular polygon with an optional border.
|
||||
|
||||
A regular polygon can be just a 2D polygon shaped stroke, a filling, or a stroke with filling.
|
||||
The \l strokeColor, \l strokeWidth, and \l strokeStyle properties specify the appearance of the
|
||||
outline. The \l dashPattern and \l dashOffset properties specify the appearance of dashed stroke.
|
||||
|
||||
The area inside the stroke is painted using either a solid fill color, specified using the
|
||||
\l fillColor property, or a gradient, defined using one of the \l ShapeGradient subtypes and set
|
||||
using the \l gradient property. If both a color and a gradient are specified, the gradient is
|
||||
used.
|
||||
|
||||
To create a polygon with a stroke, set the \sideCount property between 3 to 100 and the
|
||||
\l strokeWidth property greater than 0. The \l strokeWidth property specifies the width of the
|
||||
polygon stroke.The default \l sideCount value is 6 and the default \l strokeWidth value is 4.
|
||||
Setting the \l strokeWidth value to a negetive value hides the border.
|
||||
|
||||
The \l radius property specifies whether the polygon corners are rounded. Because this
|
||||
introduces curved edges to the corners, it may be appropriate to set the \c antialiasing
|
||||
property that is inherited from \l Item to improve the appearance of the stroke.
|
||||
|
||||
\section2 Example Usage
|
||||
|
||||
\image studio-regularpolygon.webp
|
||||
|
||||
The QML code looks as follows:
|
||||
|
||||
\code
|
||||
RegularPolygonItem {
|
||||
id: regularPolygon
|
||||
x: 817
|
||||
y: 404
|
||||
width: 133
|
||||
height: 133
|
||||
radius: 10
|
||||
strokeColor: "#262525"
|
||||
fillColor: "#766e6e"
|
||||
}
|
||||
|
||||
RegularPolygonItem {
|
||||
id: regularPolygon1
|
||||
x: 973
|
||||
y: 404
|
||||
width: 133
|
||||
height: 133
|
||||
sideCount: 15
|
||||
gradient: RadialGradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: "#c56060"
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "#665e5e"
|
||||
}
|
||||
focalY: regularPolygon1.height * 0.5
|
||||
focalX: regularPolygon1.width * 0.5
|
||||
focalRadius: Math.min(regularPolygon1.width, regularPolygon1.height) * 0
|
||||
centerY: regularPolygon1.height * 0.5
|
||||
centerX: regularPolygon1.width * 0.5
|
||||
centerRadius: Math.min(regularPolygon1.width, regularPolygon1.height) * 0.5
|
||||
}
|
||||
strokeColor: "#262525"
|
||||
}
|
||||
|
||||
RegularPolygonItem {
|
||||
id: regularPolygon2
|
||||
x: 817
|
||||
y: 567
|
||||
width: 133
|
||||
height: 133
|
||||
gradient: ConicalGradient {
|
||||
centerY: regularPolygon2.height * 0.5
|
||||
centerX: regularPolygon2.width * 0.5
|
||||
angle: 0
|
||||
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: "#fc8e8e"
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "#f0b9b9"
|
||||
}
|
||||
}
|
||||
joinStyle: 1
|
||||
strokeStyle: 4
|
||||
strokeColor: "#262525"
|
||||
sideCount: 10
|
||||
}
|
||||
|
||||
RegularPolygonItem {
|
||||
id: regularPolygon3
|
||||
x: 973
|
||||
y: 567
|
||||
width: 133
|
||||
height: 133
|
||||
strokeWidth: -1
|
||||
strokeColor: "#262525"
|
||||
sideCount: 7
|
||||
gradient: RadialGradient {
|
||||
GradientStop {
|
||||
position: 0.21053
|
||||
color: "#60c2c5"
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "#deb2b2"
|
||||
}
|
||||
focalY: regularPolygon3.height * 0.5
|
||||
focalX: regularPolygon3.width * 0.5
|
||||
focalRadius: Math.min(regularPolygon3.width, regularPolygon3.height) * 0
|
||||
centerY: regularPolygon3.height * 0.5
|
||||
centerX: regularPolygon3.width * 0.5
|
||||
centerRadius: Math.min(regularPolygon3.width, regularPolygon3.height) * 0.5
|
||||
}
|
||||
}
|
||||
\endcode
|
||||
|
||||
*/
|
||||
|
||||
Shape {
|
||||
id: root
|
||||
width: 200
|
||||
height: 200
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc {component-radius} {10}
|
||||
*/
|
||||
property int radius: 10
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc {component-gradient} {regular polygon}
|
||||
*/
|
||||
property alias gradient: path.fillGradient
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc component-joinStyle
|
||||
*/
|
||||
//property alias joinStyle: path.joinStyle
|
||||
property int joinStyle: ShapePath.MiterJoin //workaround for regression in Qt 6.6.1 (QDS-11845)
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc component-capStyle
|
||||
*/
|
||||
//property alias capStyle: path.capStyle
|
||||
property int capStyle: ShapePath.SquareCap //workaround for regression in Qt 6.6.1 (QDS-11845)
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc {component-strokeStyle} {regular polygon}
|
||||
*/
|
||||
//property alias strokeStyle: path.strokeStyle
|
||||
property int strokeStyle: ShapePath.SolidLine //workaround for regression in Qt 6.6.1 (QDS-11845)
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc {component-strokeWidth} {regular polygon}
|
||||
*/
|
||||
property alias strokeWidth: path.strokeWidth
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc {component-strokeColor} {regular polygon}
|
||||
*/
|
||||
property alias strokeColor: path.strokeColor
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc {component-dashPattern} {regular polygon}
|
||||
*/
|
||||
property alias dashPattern: path.dashPattern
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc {component-fillColor} {regular polygon}
|
||||
*/
|
||||
property alias fillColor: path.fillColor
|
||||
|
||||
/*!
|
||||
\include CommonItemDescriptions.qdocinc {component-dashOffset} {regular polygon}
|
||||
*/
|
||||
property alias dashOffset: path.dashOffset
|
||||
|
||||
/*!
|
||||
The number of edges on the regular polygon.
|
||||
The minimum number of edges can be 3.
|
||||
The default value is 6.
|
||||
*/
|
||||
property int sideCount: 6
|
||||
|
||||
property bool __preferredRendererTypeAvailable: root.preferredRendererType !== "undefined"
|
||||
property bool __curveRendererActive: root.__preferredRendererTypeAvailable
|
||||
&& root.rendererType === Shape.CurveRenderer
|
||||
|
||||
layer.enabled: root.antialiasing && !root.__curveRendererActive
|
||||
layer.smooth: root.antialiasing && !root.__curveRendererActive
|
||||
layer.samples: root.antialiasing && !root.__curveRendererActive ? 4 : 0
|
||||
|
||||
// This is used to make the bounding box of the item a bit bigger so it will draw sharp edges
|
||||
// in case of large stroke width instead of cutting it off.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -root.strokeWidth
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
id: path
|
||||
|
||||
capStyle: root.capStyle
|
||||
strokeStyle: root.strokeStyle
|
||||
joinStyle: root.joinStyle
|
||||
|
||||
strokeWidth: 4
|
||||
strokeColor: "red"
|
||||
startX: 0
|
||||
startY: 0
|
||||
}
|
||||
|
||||
onWidthChanged: root.constructPolygon()
|
||||
onHeightChanged: root.constructPolygon()
|
||||
onSideCountChanged: root.constructPolygon()
|
||||
onRadiusChanged: root.constructPolygon()
|
||||
|
||||
Component.onCompleted: {
|
||||
// If preferredRendererType wasn't set initially make CurveRenderer the default
|
||||
if (root.__preferredRendererTypeAvailable && root.preferredRendererType === Shape.UnknownRenderer)
|
||||
root.preferredRendererType = Shape.CurveRenderer
|
||||
|
||||
root.constructPolygon()
|
||||
}
|
||||
|
||||
property real __centerX: root.width / 2
|
||||
property real __centerY: root.height / 2
|
||||
|
||||
function constructPolygon() {
|
||||
root.clearPathElements()
|
||||
|
||||
if (root.radius === 0)
|
||||
root.constructPolygonPath()
|
||||
else
|
||||
root.constructRoundedPolygonPath()
|
||||
}
|
||||
|
||||
function toRadians(degrees) {
|
||||
return degrees * (Math.PI / 180.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 = myPathLine.createObject(path)
|
||||
pathLine.x = points[i].x
|
||||
pathLine.y = points[i].y
|
||||
path.pathElements.push(pathLine)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the polygon
|
||||
var pathLineClose = myPathLine.createObject(path)
|
||||
pathLineClose.x = points[0].x
|
||||
pathLineClose.y = points[0].y
|
||||
path.pathElements.push(pathLineClose)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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 = 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 = myPathLine.createObject(path)
|
||||
pathLineClose.x = path.startX
|
||||
pathLineClose.y = path.startY
|
||||
path.pathElements.push(pathLineClose)
|
||||
}
|
||||
|
||||
function clearPathElements() {
|
||||
for (var i = 0; i !== path.pathElements.length; ++i)
|
||||
path.pathElements[i].destroy()
|
||||
|
||||
path.pathElements = []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user