diff --git a/examples/iotdashboard/Skin.cpp b/examples/iotdashboard/Skin.cpp index 697a0b3b..09b7b3c3 100644 --- a/examples/iotdashboard/Skin.cpp +++ b/examples/iotdashboard/Skin.cpp @@ -325,7 +325,7 @@ Skin::Palette Skin::palette( QskSkin::ColorScheme colorScheme ) const Qt::white, 0xff4a4a4a, 0xff555555, - { { { 0.0, 0xff991100 }, { 0.2, 0xff9a7a57 }, { 0.5, 0xff3726af }, { 1.0, Qt::black } } }, + { { { 0.0, 0xff991100 }, { 0.4, 0xff9a7a57 }, { 1.0, 0xff3726af } } }, 0x10ffffff, 0xff222222 }; @@ -342,8 +342,7 @@ Skin::Palette Skin::palette( QskSkin::ColorScheme colorScheme ) const Qt::black, 0xffe5e5e5, 0xffc4c4c4, - { { { 0.0, 0xffff3122 }, { 0.2, 0xfffeeeb7 }, { 0.3, 0xffa7b0ff }, { 0.5, 0xff6776ff }, - { 1.0, Qt::black } } }, + { { { 0.0, 0xffff3122 }, { 0.4, 0xfffeeeb7 }, { 0.6, 0xffa7b0ff }, { 1.0, 0xff6776ff } } }, 0x10000000, 0xffdddddd }; diff --git a/playground/charts/CircularChartSkinlet.cpp b/playground/charts/CircularChartSkinlet.cpp index 736479a9..efdabda4 100644 --- a/playground/charts/CircularChartSkinlet.cpp +++ b/playground/charts/CircularChartSkinlet.cpp @@ -9,153 +9,10 @@ #include #include -#include +#include -#include #include -#define PAINTED_NODE 0 - -#if PAINTED_NODE - -/* - This is a fallback implementation for a user who is using an outdated - version of QSkinny where only shaders for linear gradients are available. - */ -#include -#include -#include -#include -#include - -namespace -{ - QConicalGradient qskQConicalGradient( - const QskGradientStops& stops, qreal startAngle, qreal spanAngle ) - { - QskGradientStops scaledStops; - scaledStops.reserve( stops.size() ); - - const auto ratio = qAbs( spanAngle ) / 360.0; - - if ( spanAngle > 0.0 ) - { - for ( auto it = stops.cbegin(); it != stops.cend(); ++it ) - scaledStops += { ratio * it->position(), it->color() }; - } - else - { - for ( auto it = stops.crbegin(); it != stops.crend(); ++it ) - scaledStops += { 1.0 - ratio * it->position(), it->color() }; - } - - QConicalGradient qGradient( QPointF(), startAngle ); - qGradient.setStops( qskToQGradientStops( scaledStops ) ); - - return qGradient; - } - - class PaintedArcNode : public QskPaintedNode - { - public: - void setArcData( const QRectF&, const QskArcMetrics&, - qreal, const QColor&, const QskGradient&, QQuickWindow* ); - - protected: - void paint( QPainter*, const QSize&, const void* nodeData ) override; - QskHashValue hash( const void* nodeData ) const override; - - private: - QskHashValue arcHash( const QRectF&, const QskArcMetrics&, - qreal, const QColor&, const QskGradient& ) const; - - QBrush fillBrush( const QskGradient&, const QRectF&, qreal, qreal ) const; - - struct ArcData - { - QPointF translation; - QPen pen; - QBrush brush; - QPainterPath path; - - QskHashValue hash; - }; - }; - - void PaintedArcNode::setArcData( - const QRectF& rect, const QskArcMetrics& metrics, - qreal borderWidth, const QColor& borderColor, - const QskGradient& gradient, QQuickWindow* window ) - { - const auto hash = arcHash( rect, metrics, borderWidth, borderColor, gradient ); - - const auto brush = fillBrush( gradient, rect, - metrics.startAngle(), metrics.spanAngle() ); - - QPen pen( borderColor, borderWidth ); - if ( borderWidth <= 0.0 ) - pen.setStyle( Qt::NoPen ); - - const auto path = metrics.painterPath( rect ); - const auto r = path.controlPointRect(); - - const ArcData arcData { r.topLeft(), pen, brush, path, hash }; - update( window, r, QSizeF(), &arcData ); - } - - void PaintedArcNode::paint( QPainter* painter, const QSize&, const void* nodeData ) - { - const auto arcData = reinterpret_cast< const ArcData* >( nodeData ); - - painter->setRenderHint( QPainter::Antialiasing, true ); - painter->translate( -arcData->translation ); - painter->setPen( arcData->pen ); - painter->setBrush( arcData->brush ); - painter->drawPath( arcData->path ); - } - - QskHashValue PaintedArcNode::hash( const void* nodeData ) const - { - const auto arcData = reinterpret_cast< const ArcData* >( nodeData ); - return arcData->hash; - } - - QBrush PaintedArcNode::fillBrush( const QskGradient& gradient, - const QRectF& rect, qreal startAngle, qreal spanAngle ) const - { - const auto qGradient = qskQConicalGradient( - gradient.stops(), startAngle, spanAngle ); - - const qreal sz = qMax( rect.width(), rect.height() ); - const qreal sx = rect.width() / sz; - const qreal sy = rect.height() / sz; - - QTransform t; - t.scale( sx, sy ); - t.translate( rect.center().x() / sx, rect.center().y() / sy ); - - QBrush brush( qGradient ); - brush.setTransform( t ); - - return brush; - } - - inline QskHashValue PaintedArcNode::arcHash( - const QRectF& rect, const QskArcMetrics& metrics, qreal borderWidth, - const QColor& borderColor, const QskGradient& gradient ) const - { - auto hash = metrics.hash( 6753 ); - hash = qHashBits( &rect, sizeof( rect ), hash ); - hash = qHash( borderWidth, hash ); - hash = qHash( borderColor.rgba(), hash ); - hash = gradient.hash( hash ); - - return hash; - } -} - -#endif // PAINTED_NODE - namespace { inline QskArcMetrics segmentMetrics( @@ -334,39 +191,16 @@ QSGNode* CircularChartSkinlet::updateSampleNode( const QskSkinnable* skinnable, return nullptr; } -QSGNode* CircularChartSkinlet::updateArcSegmentNode( - const QskSkinnable* skinnable, +QSGNode* CircularChartSkinlet::updateArcSegmentNode( const QskSkinnable*, QSGNode* node, qreal borderWidth, const QColor& borderColor, const QskGradient& gradient, const QskArcMetrics& metrics ) const { -#if PAINTED_NODE - auto arcNode = static_cast< PaintedArcNode* >( node ); + auto arcNode = static_cast< QskArcRenderNode* >( node ); if ( arcNode == nullptr ) - arcNode = new PaintedArcNode(); + arcNode = new QskArcRenderNode(); - const auto chart = static_cast< const CircularChart* >( skinnable ); - - arcNode->setArcData( m_data->closedArcRect, metrics, - borderWidth, borderColor, gradient, chart->window() ); -#else - Q_UNUSED( skinnable ) - - auto fillGradient = gradient; - - if ( fillGradient.type() == QskGradient::Stops ) - { - fillGradient.setStretchMode( QskGradient::StretchToSize ); - fillGradient.setConicDirection( 0.5, 0.5, - metrics.startAngle(), metrics.spanAngle() ); - } - - auto arcNode = static_cast< QskArcNode* >( node ); - if ( arcNode == nullptr ) - arcNode = new QskArcNode(); - - arcNode->setArcData( m_data->closedArcRect, metrics, - borderWidth, borderColor, fillGradient ); -#endif + arcNode->updateNode( m_data->closedArcRect, metrics, true, + borderWidth, borderColor, gradient ); return arcNode; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index de2dd310..0b774c94 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -101,6 +101,8 @@ list(APPEND SOURCES list(APPEND HEADERS nodes/QskArcNode.h + nodes/QskArcRenderer.h + nodes/QskArcRenderNode.h nodes/QskArcShadowNode.h nodes/QskBasicLinesNode.h nodes/QskBoxNode.h @@ -141,6 +143,8 @@ list(APPEND PRIVATE_HEADERS list(APPEND SOURCES nodes/QskArcNode.cpp + nodes/QskArcRenderer.cpp + nodes/QskArcRenderNode.cpp nodes/QskArcShadowNode.cpp nodes/QskBasicLinesNode.cpp nodes/QskBoxNode.cpp diff --git a/src/common/QskArcMetrics.cpp b/src/common/QskArcMetrics.cpp index 596c0bff..220630f9 100644 --- a/src/common/QskArcMetrics.cpp +++ b/src/common/QskArcMetrics.cpp @@ -21,6 +21,83 @@ static void qskRegisterArcMetrics() Q_CONSTRUCTOR_FUNCTION( qskRegisterArcMetrics ) +static inline QPainterPath qskRadialPathPath( + const QRectF& rect, qreal startAngle, qreal spanAngle, qreal width ) +{ + const auto sz = qMin( rect.width(), rect.height() ); + + const auto tx = width * rect.width() / sz; + const auto ty = width * rect.height() / sz; + + const auto innerRect = rect.adjusted( tx, ty, -tx, -ty ); + + QPainterPath path; + + if ( innerRect.isEmpty() ) + { + if ( qAbs( spanAngle ) >= 360.0 ) + { + path.addEllipse( rect ); + } + else + { + // pie + path.arcMoveTo( rect, startAngle ); + path.arcTo( rect, startAngle, spanAngle ); + path.lineTo( rect.center() ); + path.closeSubpath(); + } + } + else + { + if ( qAbs( spanAngle ) >= 360.0 ) + { + path.addEllipse( rect ); + + QPainterPath innerPath; + innerPath.addEllipse( innerRect ); + path -= innerPath; + } + else + { + /* + We need the end point of the inner arc to add the line that connects + the inner/outer arcs. As QPainterPath does not offer such a method + we insert a dummy arcMoveTo and grab the calculated position. + */ + path.arcMoveTo( innerRect, startAngle + spanAngle ); + const auto pos = path.currentPosition(); + + path.arcMoveTo( rect, startAngle ); // replaces the dummy arcMoveTo above + path.arcTo( rect, startAngle, spanAngle ); + + path.lineTo( pos ); + path.arcTo( innerRect, startAngle + spanAngle, -spanAngle ); + + path.closeSubpath(); + } + } + + return path; +} + +static inline QPainterPath qskOrthogonalPath( + const QRectF& rect, qreal startAngle, qreal spanAngle, qreal width ) +{ + const auto t2 = 0.5 * width; + const auto r = rect.adjusted( t2, t2, -t2, -t2 ); + + QPainterPath arcPath; + arcPath.arcMoveTo( r, startAngle ); + arcPath.arcTo( r, startAngle, spanAngle ); + + QPainterPathStroker stroker; + stroker.setCapStyle( Qt::FlatCap ); + stroker.setWidth( width ); + + return stroker.createStroke( arcPath ); +} + static inline qreal qskInterpolated( qreal from, qreal to, qreal ratio ) { return from + ( to - from ) * ratio; @@ -122,66 +199,26 @@ QskArcMetrics QskArcMetrics::toAbsolute( qreal radius ) const noexcept return QskArcMetrics( m_startAngle, m_spanAngle, t, Qt::AbsoluteSize ); } -QPainterPath QskArcMetrics::painterPath( const QRectF& ellipseRect ) const +QPainterPath QskArcMetrics::painterPath( const QRectF& rect, bool radial ) const { - const auto sz = qMin( ellipseRect.width(), ellipseRect.height() ); - - qreal t = m_thickness; - if ( m_relativeSize ) - t = qskEffectiveThickness( 0.5 * sz, t ); - - if ( t <= 0.0 || qFuzzyIsNull( m_spanAngle ) ) - return QPainterPath(); - - const auto tx = t * ellipseRect.width() / sz; - const auto ty = t * ellipseRect.height() / sz; - - const auto innerRect = ellipseRect.adjusted( tx, ty, -tx, -ty ); - QPainterPath path; - if ( innerRect.isEmpty() ) + if ( !qFuzzyIsNull( m_spanAngle ) ) { - if ( qAbs( m_spanAngle ) >= 360.0 ) + qreal t = m_thickness; + + if ( m_relativeSize ) { - path.addEllipse( ellipseRect ); + const auto sz = qMin( rect.width(), rect.height() ); + t = qskEffectiveThickness( 0.5 * sz, t ); } - else + + if ( t > 0.0 ) { - // pie - path.arcMoveTo( ellipseRect, m_startAngle ); - path.arcTo( ellipseRect, m_startAngle, m_spanAngle ); - path.lineTo( ellipseRect.center() ); - path.closeSubpath(); - } - } - else - { - if ( qAbs( m_spanAngle ) >= 360.0 ) - { - path.addEllipse( ellipseRect ); - - QPainterPath innerPath; - innerPath.addEllipse( innerRect ); - path -= innerPath; - } - else - { - /* - We need the end point of the inner arc to add the line that connects - the inner/outer arcs. As QPainterPath does not offer such a method - we insert a dummy arcMoveTo and grab the calculated position. - */ - path.arcMoveTo( innerRect, m_startAngle + m_spanAngle ); - const auto pos = path.currentPosition(); - - path.arcMoveTo( ellipseRect, m_startAngle ); // replaces the dummy arcMoveTo above - path.arcTo( ellipseRect, m_startAngle, m_spanAngle ); - - path.lineTo( pos ); - path.arcTo( innerRect, m_startAngle + m_spanAngle, -m_spanAngle ); - - path.closeSubpath(); + if ( radial ) + path = qskRadialPathPath( rect, m_startAngle, m_spanAngle, t ); + else + path = qskOrthogonalPath( rect, m_startAngle, m_spanAngle, t ); } } diff --git a/src/common/QskArcMetrics.h b/src/common/QskArcMetrics.h index 5424d35d..608e9d8e 100644 --- a/src/common/QskArcMetrics.h +++ b/src/common/QskArcMetrics.h @@ -29,8 +29,8 @@ class QSK_EXPORT QskArcMetrics constexpr QskArcMetrics( qreal thickness, Qt::SizeMode = Qt::AbsoluteSize ) noexcept; - constexpr QskArcMetrics( qreal startAngle, qreal spanAngle, - qreal thickness, Qt::SizeMode = Qt::AbsoluteSize ) noexcept; + constexpr QskArcMetrics( qreal startAngle, qreal spanAngle, qreal thickness, + Qt::SizeMode = Qt::AbsoluteSize ) noexcept; bool operator==( const QskArcMetrics& ) const noexcept; bool operator!=( const QskArcMetrics& ) const noexcept; @@ -62,7 +62,22 @@ class QSK_EXPORT QskArcMetrics QskArcMetrics toAbsolute( qreal radiusX, qreal radiusY ) const noexcept; QskArcMetrics toAbsolute( qreal radius ) const noexcept; - QPainterPath painterPath( const QRectF& ellipseRect ) const; + /* + The arc is interpolated by pairs of points, where one point is on + the outer and the other on the inner side of the arc. The length between + these points depends on the thickness. + + When radial is set the inner point lies on the line between the outer point + and the center of the arc. This corresponds to the lines of a conic gradient. + + Otherwise the line between the inner and outer point is orthogonal to the + tangent at the point in the middle of the arc. This is how the width + of the pen is expanded by QPainter::drawArc. + + Note, that the radial flag is irrelevant for circular arcs as the tangent + is always orthogonal to any point on the circle. + */ + QPainterPath painterPath( const QRectF& ellipseRect, bool radial = false ) const; QRectF boundingRect( const QRectF& ellipseRect ) const; QSizeF boundingSize( const QSizeF& ellipseSize ) const; diff --git a/src/nodes/QskArcNode.cpp b/src/nodes/QskArcNode.cpp index 187cd53e..c2dc3f7a 100644 --- a/src/nodes/QskArcNode.cpp +++ b/src/nodes/QskArcNode.cpp @@ -6,14 +6,14 @@ #include "QskArcNode.h" #include "QskArcMetrics.h" #include "QskArcShadowNode.h" +#include "QskArcRenderNode.h" +#include "QskArcRenderer.h" #include "QskMargins.h" #include "QskGradient.h" #include "QskShapeNode.h" -#include "QskStrokeNode.h" #include "QskSGNode.h" #include "QskShadowMetrics.h" -#include #include namespace @@ -21,8 +21,9 @@ namespace enum NodeRole { ShadowRole, - FillRole, - BorderRole + + PathRole, + ArcRole }; } @@ -43,21 +44,18 @@ static inline QskGradient qskEffectiveGradient( return gradient; } -static inline QRectF qskEffectiveRect( - const QRectF& rect, const qreal borderWidth ) +template< typename Node > +inline Node* qskInsertOrRemoveNode( QSGNode* parentNode, quint8 role, bool isValid ) { - if ( borderWidth <= 0.0 ) - return rect; + using namespace QskSGNode; - return qskValidOrEmptyInnerRect( rect, QskMargins( 0.5 * borderWidth ) ); -} + Node* oldNode = static_cast< Node* >( findChildNode( parentNode, role ) ); + Node* newNode = isValid ? ensureNode< Node >( oldNode ) : nullptr; -static void qskUpdateChildren( QSGNode* parentNode, quint8 role, QSGNode* node ) -{ - static const QVector< quint8 > roles = { ShadowRole, FillRole, BorderRole }; + static const QVector< quint8 > roles = { ShadowRole, PathRole, ArcRole }; + replaceChildNode( roles, role, parentNode, oldNode, newNode ); - auto oldNode = QskSGNode::findChildNode( parentNode, role ); - QskSGNode::replaceChildNode( roles, role, parentNode, oldNode, node ); + return newNode; } QskArcNode::QskArcNode() @@ -69,57 +67,44 @@ QskArcNode::~QskArcNode() } void QskArcNode::setArcData( const QRectF& rect, - const QskArcMetrics& arcMetrics, const QskGradient& fillGradient ) + const QskArcMetrics& arcMetrics, const QskGradient& gradient ) { - setArcData( rect, arcMetrics, 0.0, QColor(), fillGradient, {}, {} ); + setArcData( rect, arcMetrics, 0.0, QColor(), gradient, {}, {} ); } void QskArcNode::setArcData( const QRectF& rect, const QskArcMetrics& arcMetrics, - const qreal borderWidth, const QColor& borderColor, const QskGradient& fillGradient ) + const qreal borderWidth, const QColor& borderColor, const QskGradient& gradient ) { - setArcData( rect, arcMetrics, borderWidth, borderColor, fillGradient, {}, {} ); + setArcData( rect, arcMetrics, borderWidth, borderColor, gradient, {}, {} ); } void QskArcNode::setArcData( const QRectF& rect, const QskArcMetrics& arcMetrics, - const qreal borderWidth, const QColor& borderColor, const QskGradient& fillGradient, + const qreal borderWidth, const QColor& borderColor, const QskGradient& gradient, const QColor& shadowColor, const QskShadowMetrics& shadowMetrics ) { + const bool radial = false; + const auto metricsArc = arcMetrics.toAbsolute( rect.size() ); - const auto gradient = qskEffectiveGradient( fillGradient, metricsArc ); - auto shadowNode = static_cast< QskArcShadowNode* >( - QskSGNode::findChildNode( this, ShadowRole ) ); - - auto fillNode = static_cast< QskShapeNode* >( - QskSGNode::findChildNode( this, FillRole ) ); - - auto borderNode = static_cast< QskStrokeNode* >( - QskSGNode::findChildNode( this, BorderRole ) ); - - const auto arcRect = qskEffectiveRect( rect, borderWidth ); - if ( metricsArc.isNull() || arcRect.isEmpty() ) + if ( metricsArc.isNull() || rect.isEmpty() ) { - delete shadowNode; - delete fillNode; - delete borderNode; + delete QskSGNode::findChildNode( this, ShadowRole ); + delete QskSGNode::findChildNode( this, PathRole ); + delete QskSGNode::findChildNode( this, ArcRole ); + return; } - const auto isFillNodeVisible = gradient.isVisible(); - const auto isStrokeNodeVisible = ( borderWidth > 0.0 ) && ( borderColor.alpha() > 0 ); - const auto isShadowNodeVisible = isFillNodeVisible && - shadowColor.isValid() && ( shadowColor.alpha() > 0.0 ); + const auto hasFilling = gradient.isVisible(); + const auto hasBorder = ( borderWidth > 0.0 ) + && borderColor.isValid() && ( borderColor.alpha() > 0 ); + const auto hasShadow = shadowColor.isValid() && ( shadowColor.alpha() > 0 ); - const auto path = metricsArc.painterPath( arcRect ); + auto shadowNode = qskInsertOrRemoveNode< QskArcShadowNode >( + this, ShadowRole, hasFilling && hasShadow ); - if ( isShadowNodeVisible ) + if ( shadowNode ) { - if ( shadowNode == nullptr ) - { - shadowNode = new QskArcShadowNode; - QskSGNode::setNodeRole( shadowNode, ShadowRole ); - } - /* The shader of the shadow node is for circular arcs and we have some unwanted scaling issues for the spread/blur values when having ellipsoid @@ -127,55 +112,30 @@ void QskArcNode::setArcData( const QRectF& rect, const QskArcMetrics& arcMetrics and not only to its radius. TODO ... */ - const auto sm = shadowMetrics.toAbsolute( arcRect.size() ); - const auto shadowRect = sm.shadowRect( arcRect ); + const auto sm = shadowMetrics.toAbsolute( rect.size() ); + const auto shadowRect = sm.shadowRect( rect ); const auto spreadRadius = sm.spreadRadius() + 0.5 * metricsArc.thickness(); shadowNode->setShadowData( shadowRect, spreadRadius, sm.blurRadius(), metricsArc.startAngle(), metricsArc.spanAngle(), shadowColor ); } - else + + auto pathNode = qskInsertOrRemoveNode< QskShapeNode >( this, PathRole, + hasFilling && !QskArcRenderer::isGradientSupported( rect, metricsArc, gradient ) ); + + if ( pathNode ) { - delete shadowNode; - shadowNode = nullptr; + const auto path = metricsArc.painterPath( rect, radial ); + pathNode->updateNode( path, QTransform(), rect, + qskEffectiveGradient( gradient, metricsArc ) ); } - if ( isFillNodeVisible ) + auto arcNode = qskInsertOrRemoveNode< QskArcRenderNode >( + this, ArcRole, hasBorder || ( hasFilling && !pathNode ) ); + + if ( arcNode ) { - if ( fillNode == nullptr ) - { - fillNode = new QskShapeNode; - QskSGNode::setNodeRole( fillNode, FillRole ); - } - - fillNode->updateNode( path, QTransform(), arcRect, gradient ); + arcNode->updateNode( rect, metricsArc, radial, + borderWidth, borderColor, pathNode ? QskGradient() : gradient ); } - else - { - delete fillNode; - fillNode = nullptr; - } - - if ( isStrokeNodeVisible ) - { - if ( borderNode == nullptr ) - { - borderNode = new QskStrokeNode; - QskSGNode::setNodeRole( borderNode, BorderRole ); - } - - QPen pen( borderColor, borderWidth ); - pen.setCapStyle( Qt::FlatCap ); - - borderNode->updateNode( path, QTransform(), pen ); - } - else - { - delete borderNode; - borderNode = nullptr; - } - - qskUpdateChildren( this, ShadowRole, shadowNode ); - qskUpdateChildren( this, FillRole, fillNode ); - qskUpdateChildren( this, BorderRole, borderNode ); } diff --git a/src/nodes/QskArcRenderNode.cpp b/src/nodes/QskArcRenderNode.cpp new file mode 100644 index 00000000..0d7ffddd --- /dev/null +++ b/src/nodes/QskArcRenderNode.cpp @@ -0,0 +1,121 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskArcRenderNode.h" +#include "QskGradient.h" +#include "QskArcRenderer.h" +#include "QskArcMetrics.h" +#include "QskGradient.h" +#include "QskSGNode.h" +#include "QskFillNodePrivate.h" + +class QskArcRenderNodePrivate final : public QskFillNodePrivate +{ + public: + inline void resetValues() { hash = 0; } + + QskHashValue hash = 0; +}; + +QskArcRenderNode::QskArcRenderNode() + : QskFillNode( *new QskArcRenderNodePrivate ) +{ +} + +QskArcRenderNode::~QskArcRenderNode() +{ +} + +void QskArcRenderNode::updateNode( const QRectF& rect, + const QskArcMetrics& metrics, const QskGradient& gradient ) +{ + updateNode( rect, metrics, false, 0.0, QColor(), gradient ); +} + +void QskArcRenderNode::updateNode( const QRectF& rect, + const QskArcMetrics& metrics, qreal borderWidth, const QColor& borderColor ) +{ + updateNode( rect, metrics, false, borderWidth, borderColor, QskGradient() ); +} + +void QskArcRenderNode::updateNode( + const QRectF& rect, const QskArcMetrics& arcMetrics, bool radial, + qreal borderWidth, const QColor& borderColor, const QskGradient& gradient ) +{ + Q_D( QskArcRenderNode ); + + const auto metrics = arcMetrics.toAbsolute( rect.size() ); + const auto borderMax = 0.5 * metrics.thickness(); + + const bool hasFill = gradient.isVisible() && ( borderWidth < borderMax ); + const bool hasBorder = ( borderWidth > 0.0 ) + && borderColor.isValid() && ( borderColor.alpha() > 0 ); + + if ( rect.isEmpty() || metrics.isNull() || !( hasFill || hasBorder ) ) + { + d->resetValues(); + QskSGNode::resetGeometry( this ); + + return; + } + + borderWidth = qMin( borderWidth, borderMax ); + + QskHashValue hash = 3496; + + hash = qHashBits( &rect, sizeof( QRectF ), hash ); + hash = qHash( borderWidth, hash ); + hash = qHashBits( &borderColor, sizeof( borderColor ), hash ); + hash = metrics.hash( hash ); + hash = gradient.hash( hash ); + hash = qHash( radial, hash ); + + if ( hash == d->hash ) + return; + + d->hash = hash; + + auto coloring = QskFillNode::Polychrome; + +#if 0 + if ( !( hasFill && hasBorder ) ) + { + if ( hasBorder || ( hasFill && gradient.isMonochrome() ) ) + coloring = QskFillNode::Monochrome; + } +#endif + + auto& geometry = *this->geometry(); + + if ( coloring == QskFillNode::Polychrome ) + { + setColoring( coloring ); + + QskArcRenderer::renderArc( rect, metrics, radial, + borderWidth, gradient, borderColor, geometry ); + } + else + { + if ( hasFill ) + { + setColoring( gradient.rgbStart() ); + + QskArcRenderer::renderArc( rect, metrics, radial, + borderWidth, gradient, borderColor, geometry ); + } + else + { + setColoring( borderColor ); + + QskArcRenderer::renderArc( rect, metrics, radial, + borderWidth, gradient, borderColor, geometry ); + } + } + + markDirty( QSGNode::DirtyGeometry ); + markDirty( QSGNode::DirtyMaterial ); + + geometry.markVertexDataDirty(); +} diff --git a/src/nodes/QskArcRenderNode.h b/src/nodes/QskArcRenderNode.h new file mode 100644 index 00000000..f0d248ab --- /dev/null +++ b/src/nodes/QskArcRenderNode.h @@ -0,0 +1,37 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#ifndef QSK_ARC_RENDER_NODE_H +#define QSK_ARC_RENDER_NODE_H + +#include "QskGlobal.h" +#include "QskFillNode.h" + +class QskGradient; +class QskArcMetrics; + +class QskArcRenderNodePrivate; + +class QSK_EXPORT QskArcRenderNode : public QskFillNode +{ + using Inherited = QskFillNode; + + public: + QskArcRenderNode(); + ~QskArcRenderNode() override; + + void updateNode( const QRectF&, const QskArcMetrics&, const QskGradient& ); + + void updateNode( const QRectF&, const QskArcMetrics&, + qreal borderWidth, const QColor& borderColor ); + + void updateNode( const QRectF&, const QskArcMetrics&, bool radial, + qreal borderWidth, const QColor& borderColor, const QskGradient& ); + + private: + Q_DECLARE_PRIVATE( QskArcRenderNode ) +}; + +#endif diff --git a/src/nodes/QskArcRenderer.cpp b/src/nodes/QskArcRenderer.cpp new file mode 100644 index 00000000..1a7963ce --- /dev/null +++ b/src/nodes/QskArcRenderer.cpp @@ -0,0 +1,554 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskArcRenderer.h" +#include "QskArcMetrics.h" +#include "QskGradient.h" +#include "QskVertex.h" +#include "QskBoxColorMap.h" +#include "QskRgbValue.h" + +#include +#include + +static inline QskVertex::Line* qskAllocateLines( + QSGGeometry& geometry, int lineCount ) +{ + geometry.allocate( 2 * lineCount ); // 2 points per line + return reinterpret_cast< QskVertex::Line* >( geometry.vertexData() ); +} + +static inline QskVertex::ColoredLine* qskAllocateColoredLines( + QSGGeometry& geometry, int lineCount ) +{ + geometry.allocate( 2 * lineCount ); // 2 points per line + return reinterpret_cast< QskVertex::ColoredLine* >( geometry.vertexData() ); +} + +namespace +{ + template< class Line > + class OrthogonalStroker + { + public: + OrthogonalStroker( const QRectF& rect, qreal thickness, qreal border ) + : m_thickness( thickness ) + , m_border( border ) + , m_rx( 0.5 * ( rect.width() - m_thickness ) ) + , m_ry( 0.5 * ( rect.height() - m_thickness ) ) + , m_offsetToBorder( 0.5 * m_thickness - border ) + , m_aspectRatio( m_rx / m_ry ) + , m_cx( rect.x() + 0.5 * rect.width() ) + , m_cy( rect.y() + 0.5 * rect.height() ) + { + } + + inline void setLinesAt( const qreal radians, + const QskVertex::Color fillColor, const QskVertex::Color borderColor, + Line* fill, Line* outerBorder, Line* innerBorder ) const + { + const auto cos = qFastCos( radians ); + const auto sin = qFastSin( radians ); + + const auto v = normalVector( cos, sin ); + + const QPointF p0( m_cx + m_rx * cos, m_cy - m_ry * sin ); + + const auto v1 = v * m_offsetToBorder; + + const auto p1 = p0 + v1; + const auto p2 = p0 - v1; + + if ( fill ) + fill->setLine( p1, p2, fillColor ); + + if ( outerBorder ) + { + const auto v2 = v * m_border; + + outerBorder->setLine( p1 + v2, p1, borderColor ); + innerBorder->setLine( p2 - v2, p2, borderColor ); + } + } + + inline void setClosingBorderLines( const Line& l, + Line* lines, qreal sign, const QskVertex::Color color ) const + { + const auto& pos = l.p1; + const auto& l0 = lines[0]; + + const auto dx = sign * l0.dy(); + const auto dy = sign * l0.dx(); + + lines[-3].setLine( pos.x, pos.y, pos.x, pos.y, color ); + lines[-2].setLine( pos.x + dx, pos.y - dy, pos.x, pos.y, color ); + lines[-1].setLine( l0.x1() + dx, l0.y1() - dy, l0.x1(), l0.y1(), color ); + } + + private: + inline QPointF normalVector( const qreal cos, const qreal sin ) const + { + /* + The inner/outer points are found by shifting orthogonally along the + ellipse tangent: + + m = w / h * tan( angle ) + y = m * x; + x² + y² = 1.0 + + => x = 1.0 / sqrt( 1.0 + m² ); + + Note: the angle of the orthogonal vector could + also be found ( first quadrant ) by: + + atan2( tan( angle ), h / w ); + + Note: we return the vector mirrored vertically, so that it + matches the coordinate system used by Qt. + */ + + if ( qFuzzyIsNull( cos ) ) + return { 0.0, ( sin < 0.0 ) ? 1.0 : -1.0 }; + + const qreal m = m_aspectRatio * ( sin / cos ); + const qreal t = 1.0 / qSqrt( 1.0 + m * m ); + + const auto dx = ( cos >= 0.0 ) ? t : -t; + return { dx, -m * dx }; + } + + const qreal m_thickness; + const qreal m_border; + + // radii t the middle of the arc + const qreal m_rx, m_ry; + + // distances between the middle and the beginning of the border + const qreal m_offsetToBorder; + + const qreal m_aspectRatio; // m_rx / m_ry + + // center + const qreal m_cx, m_cy; + }; + + template< class Line > + class RadialStroker + { + public: + RadialStroker( const QRectF& rect, qreal thickness, qreal border ) + : m_sx( qMax( rect.width() / rect.height(), 1.0 ) ) + , m_sy( qMax( rect.height() / rect.width(), 1.0 ) ) + , m_rx1( 0.5 * rect.width() ) + , m_ry1( 0.5 * rect.height() ) + , m_rx2( m_rx1 - m_sx * border ) + , m_ry2( m_ry1 - m_sy * border ) + , m_rx3( m_rx1 - m_sx * ( thickness - border ) ) + , m_ry3( m_ry1 - m_sy * ( thickness - border ) ) + , m_rx4( m_rx1 - m_sx * thickness ) + , m_ry4( m_ry1 - m_sy * thickness ) + , m_center( rect.x() + m_rx1, rect.y() + m_ry1 ) + { + } + + inline void setLinesAt( const qreal radians, + const QskVertex::Color fillColor, const QskVertex::Color borderColor, + Line* fill, Line* outer, Line* inner ) const + { + const QPointF v( qFastCos( radians ), -qFastSin( radians ) ); + + const auto x1 = m_center.x() + m_rx2 * v.x(); + const auto y1 = m_center.y() + m_ry2 * v.y(); + + const auto x2 = m_center.x() + m_rx3 * v.x(); + const auto y2 = m_center.y() + m_ry3 * v.y(); + + if ( fill ) + fill->setLine( x1, y1, x2, y2, fillColor ); + + if ( outer ) + { + const auto x3 = m_center.x() + m_rx1 * v.x(); + const auto y3 = m_center.y() + m_ry1 * v.y(); + + const auto x4 = m_center.x() + m_rx4 * v.x(); + const auto y4 = m_center.y() + m_ry4 * v.y(); + + outer->setLine( x3, y3, x1, y1, borderColor ); + inner->setLine( x4, y4, x2, y2, borderColor ); + } + } + + inline void setClosingBorderLines( const Line& l, + Line* lines, qreal sign, const QskVertex::Color color ) const + { + const auto& pos = l.p1; + + // Good enough until it is decided if we want to keep the radial mode. + const auto& l0 = lines[0]; + + const auto s = m_sx / m_sy; + const auto dx = sign * l0.dy() * s; + const auto dy = sign * l0.dx() / s; + + lines[-3].setLine( pos.x, pos.y, pos.x, pos.y, color ); + lines[-2].setLine( pos.x + dx, pos.y - dy, pos.x, pos.y, color ); + lines[-1].setLine( l0.x1() + dx, l0.y1() - dy, l0.x1(), l0.y1(), color ); + } + + private: + // stretch factors of the ellipse + const qreal m_sx, m_sy; + + // radii: out->in + const qreal m_rx1, m_ry1, m_rx2, m_ry2, m_rx3, m_ry3, m_rx4, m_ry4; + + // center point + const QPointF m_center; + }; + + template< class Line > + class CircularStroker + { + public: + CircularStroker( const QRectF& rect, qreal thickness, qreal border ) + : m_center( rect.center() ) + , m_radius( 0.5 * ( rect.width() - thickness ) ) + , m_distOut( 0.5 * thickness ) + , m_distIn( m_distOut - border ) + { + } + + inline void setLinesAt( const qreal radians, + const QskVertex::Color fillColor, const QskVertex::Color borderColor, + Line* fill, Line* outer, Line* inner ) const + { + const QPointF v( qFastCos( radians ), -qFastSin( radians ) ); + + const auto p0 = m_center + m_radius * v; + const auto dv1 = v * m_distIn; + + const auto p1 = p0 + dv1; + const auto p2 = p0 - dv1; + + if ( fill ) + fill->setLine( p1, p2, fillColor ); + + if ( outer ) + { + const auto dv2 = v * m_distOut; + + const auto p3 = p0 + dv2; + const auto p4 = p0 - dv2; + + outer->setLine( p3, p1, borderColor ); + inner->setLine( p4, p2, borderColor ); + } + } + + inline void setClosingBorderLines( const Line& l, + Line* lines, qreal sign, const QskVertex::Color color ) const + { + const auto& pos = l.p1; + const auto& l0 = lines[0]; + + const auto dx = sign * l0.dy(); + const auto dy = sign * l0.dx(); + + lines[-3].setLine( pos.x, pos.y, pos.x, pos.y, color ); + lines[-2].setLine( pos.x + dx, pos.y - dy, pos.x, pos.y, color ); + lines[-1].setLine( l0.x1() + dx, l0.y1() - dy, l0.x1(), l0.y1(), color ); + } + + private: + // center point + const QPointF m_center; + const qreal m_radius; // middle of the arc + + // distances from the middle to the inner/outer side of the border + const qreal m_distOut, m_distIn; + }; +} + +namespace +{ + class Renderer + { + public: + Renderer( const QRectF&, const QskArcMetrics&, + bool radial, const QskGradient&, const QskVertex::Color& ); + + int fillCount() const; + int borderCount() const; + + template< class Line > + void renderArc( const qreal thickness, const qreal border, Line*, Line* ) const; + + private: + int arcLineCount() const; + + template< class LineStroker, class Line > + void renderLines( const LineStroker&, Line*, Line* ) const; + + const QRectF& m_rect; + + const qreal m_radians1; + const qreal m_radians2; + + const bool m_radial; // for circular arcs radial/orthogonal does not differ + const bool m_closed; + + const QskGradient& m_gradient; + const QskVertex::Color m_borderColor; + }; + + Renderer::Renderer( const QRectF& rect, const QskArcMetrics& metrics, + bool radial, const QskGradient& gradient, const QskVertex::Color& borderColor ) + : m_rect( rect ) + , m_radians1( qDegreesToRadians( metrics.startAngle() ) ) + , m_radians2( qDegreesToRadians( metrics.endAngle() ) ) + , m_radial( radial ) + , m_closed( metrics.isClosed() ) + , m_gradient( gradient ) + , m_borderColor( borderColor ) + { + } + + int Renderer::arcLineCount() const + { + // not very sophisticated - TODO ... + + const auto radius = 0.5 * qMax( m_rect.width(), m_rect.height() ); + const auto radians = qAbs( m_radians2 - m_radians1 ); + + const auto count = qCeil( ( radius * radians ) / 3.0 ); + return qBound( 3, count, 160 ); + } + + int Renderer::fillCount() const + { + if ( !m_gradient.isVisible() ) + return 0; + + return arcLineCount() + m_gradient.stepCount() - 1; + } + + template< class Line > + void Renderer::renderArc( const qreal thickness, const qreal border, + Line* fillLines, Line* borderLines ) const + { + if ( qskFuzzyCompare( m_rect.width(), m_rect.height() ) ) + { + const CircularStroker< Line > stroker( m_rect, thickness, border ); + renderLines( stroker, fillLines, borderLines ); + } + else if ( m_radial ) + { + const RadialStroker< Line > stroker( m_rect, thickness, border ); + renderLines( stroker, fillLines, borderLines ); + } + else + { + const OrthogonalStroker< Line > stroker( m_rect, thickness, border ); + renderLines( stroker, fillLines, borderLines ); + } + } + + template< class LineStroker, class Line > + void Renderer::renderLines( const LineStroker& lineStroker, + Line* fillLines, Line* borderLines ) const + { + QskBoxRenderer::GradientIterator it; + + if ( fillLines ) + { + if ( m_gradient.stepCount() <= 1 ) + { + it.reset( m_gradient.rgbStart(), m_gradient.rgbEnd() ); + } + else + { + it.reset( m_gradient.stops() ); + it.advance(); // the first stop is always covered by the contour + } + } + + const auto count = arcLineCount(); + const auto radiansSpan = m_radians2 - m_radians1; + + const qreal stepMax = count - 1; + const auto stepSize = radiansSpan / stepMax; + + auto l = fillLines; + + auto outer = borderLines; + auto inner = borderLines; + + if ( borderLines ) + { + outer = borderLines; + if ( !m_closed ) + outer += 3; + + inner = outer + count; + if ( !m_closed ) + inner += 3; + } + + for ( int i = 0; i < count; i++ ) + { + const auto progress = i / stepMax; + + while( !it.isDone() && ( it.position() < progress ) ) + { + const auto radians = m_radians1 + it.position() * radiansSpan; + lineStroker.setLinesAt( radians, it.color(), m_borderColor, + l++, nullptr, nullptr ); + + it.advance(); + } + + const auto radians = m_radians1 + i * stepSize; + const auto color = it.colorAt( progress ); + + lineStroker.setLinesAt( radians, color, m_borderColor, + l ? l++ : nullptr, + outer ? outer + i : nullptr, + inner ? inner + count - 1 - i : nullptr + ); + } + + if ( borderLines && !m_closed ) + { + const auto sign = ( radiansSpan > 0.0 ) ? 1.0 : -1.0; + + lineStroker.setClosingBorderLines( inner[count - 1], outer, sign, m_borderColor ); + lineStroker.setClosingBorderLines( outer[count - 1], inner, sign, m_borderColor ); + } + } + + int Renderer::borderCount() const + { + if ( m_borderColor.a == 0 ) + return 0; + + auto count = 2 * arcLineCount(); + if ( !m_closed ) + count += 2 * 3; + + return count; + } +} + +bool QskArcRenderer::isGradientSupported( const QRectF& rect, + const QskArcMetrics& metrics, const QskGradient& gradient ) +{ + if ( rect.isEmpty() || metrics.isNull() ) + return true; + + if ( !gradient.isVisible() || gradient.isMonochrome() ) + return true; + + switch( gradient.type() ) + { + case QskGradient::Stops: + { + return true; + } + case QskGradient::Conic: + { + const auto direction = gradient.conicDirection(); + if ( direction.center() == rect.center() ) + { + const auto aspectRatio = rect.width() / rect.height(); + if ( qskFuzzyCompare( direction.aspectRatio(), aspectRatio ) ) + { + /* + we should be able to create a list of stops from + this gradient that works for the renderer. TODO ... + */ + } + } + + return false; + } + default: + { + return false; + } + } + + return false; +} + +void QskArcRenderer::renderArc( const QRectF& rect, const QskArcMetrics& metrics, + bool radial, const QskGradient& gradient, QSGGeometry& geometry ) +{ + renderArc( rect, metrics, radial, 0, gradient, QColor(), geometry ); +} + +void QskArcRenderer::renderArc( const QRectF& rect, const QskArcMetrics& metrics, + bool radial, qreal borderWidth, const QskGradient& gradient, + const QColor& borderColor, QSGGeometry& geometry ) +{ + geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip ); + + const Renderer renderer( rect, metrics, radial, gradient, + borderColor.isValid() ? borderColor : QColor( 0, 0, 0, 0 ) ); + + const auto borderCount = renderer.borderCount(); + const auto fillCount = renderer.fillCount(); + + auto lineCount = borderCount + fillCount; + if ( borderCount && fillCount ) + lineCount++; // connecting line + + const auto lines = qskAllocateColoredLines( geometry, lineCount ); + if ( lines ) + { + const auto fillLines = fillCount ? lines : nullptr; + const auto borderLines = borderCount ? lines + lineCount - borderCount : nullptr; + + renderer.renderArc( metrics.thickness(), borderWidth, fillLines, borderLines ); + + if ( fillCount && borderCount ) + { + const auto idx = fillCount; + + lines[idx].p1 = lines[idx - 1].p2; + lines[idx].p2 = lines[idx + 1].p1; + } + } +} + +void QskArcRenderer::renderBorderGeometry( const QRectF& rect, + const QskArcMetrics& metrics, bool radial, qreal borderWidth, QSGGeometry& geometry ) +{ + geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip ); + + const Renderer renderer( rect, metrics, radial, QskGradient(), 0 ); + + const auto lines = qskAllocateLines( geometry, renderer.borderCount() ); + if ( lines ) + { + QskVertex::Line* fill = nullptr; + renderer.renderArc( metrics.thickness(), borderWidth, fill, lines ); + } +} + +void QskArcRenderer::renderFillGeometry( const QRectF& rect, + const QskArcMetrics& metrics, bool radial, qreal borderWidth, QSGGeometry& geometry ) +{ + geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip ); + + const Renderer renderer( rect, metrics, radial, QskRgb::Black, 0 ); + + const auto lines = qskAllocateLines( geometry, renderer.fillCount() ); + if ( lines ) + { + QskVertex::Line* border = nullptr; + renderer.renderArc( metrics.thickness(), borderWidth, lines, border ); + } +} diff --git a/src/nodes/QskArcRenderer.h b/src/nodes/QskArcRenderer.h new file mode 100644 index 00000000..1b0ce528 --- /dev/null +++ b/src/nodes/QskArcRenderer.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#ifndef QSK_ARC_RENDERER_H +#define QSK_ARC_RENDERER_H + +#include "QskGlobal.h" + +class QskArcMetrics; +class QskGradient; + +class QSGGeometry; +class QRectF; +class QColor; + +namespace QskArcRenderer +{ + /* + Filling the geometry without any color information: + see QSGGeometry::defaultAttributes_Point2D() + + - clip nodes + - using shaders setting the colors + */ + + QSK_EXPORT void renderBorderGeometry( const QRectF&, + const QskArcMetrics&, bool radial, qreal borderWidth, QSGGeometry& ); + + QSK_EXPORT void renderFillGeometry( const QRectF&, + const QskArcMetrics&, bool radial, qreal borderWidth, QSGGeometry& ); + + /* + Filling the geometry with color information: + see QSGGeometry::defaultAttributes_ColoredPoint2D() + */ + QSK_EXPORT bool isGradientSupported( + const QRectF&, const QskArcMetrics&, const QskGradient& ); + + QSK_EXPORT void renderArc( const QRectF&, const QskArcMetrics&, bool radial, + qreal borderWidth, const QskGradient&, const QColor& borderColor, QSGGeometry& ); + + QSK_EXPORT void renderArc( const QRectF&, const QskArcMetrics&, bool radial, + const QskGradient&, QSGGeometry& ); +} + +#endif