From bf19d6464c68f02d1abb8cca418e6a8f8f5b8e02 Mon Sep 17 00:00:00 2001 From: Uwe Rathmann Date: Wed, 17 May 2023 14:21:40 +0200 Subject: [PATCH] QskLinesNode introduced --- src/CMakeLists.txt | 4 + src/common/QskStippleMetrics.cpp | 83 ++++++++ src/common/QskStippleMetrics.h | 97 +++++++++ src/nodes/QskLinesNode.cpp | 327 +++++++++++++++++++++++++++++++ src/nodes/QskLinesNode.h | 52 +++++ src/nodes/QskStrokeNode.cpp | 2 + src/nodes/QskStrokeNode.h | 1 + 7 files changed, 566 insertions(+) create mode 100644 src/common/QskStippleMetrics.cpp create mode 100644 src/common/QskStippleMetrics.h create mode 100644 src/nodes/QskLinesNode.cpp create mode 100644 src/nodes/QskLinesNode.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8d8cf4e6..e9e18947 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,6 +32,7 @@ list(APPEND HEADERS common/QskShadowMetrics.h common/QskSizePolicy.h common/QskStateCombination.h + common/QskStippleMetrics.h common/QskTextColors.h common/QskTextOptions.h ) @@ -61,6 +62,7 @@ list(APPEND SOURCES common/QskScaleTickmarks.cpp common/QskShadowMetrics.cpp common/QskSizePolicy.cpp + common/QskStippleMetrics.cpp common/QskTextColors.cpp common/QskTextOptions.cpp ) @@ -107,6 +109,7 @@ list(APPEND HEADERS nodes/QskBoxShadowNode.h nodes/QskColorRamp.h nodes/QskGraphicNode.h + nodes/QskLinesNode.h nodes/QskPaintedNode.h nodes/QskPlainTextRenderer.h nodes/QskRichTextRenderer.h @@ -135,6 +138,7 @@ list(APPEND SOURCES nodes/QskBoxShadowNode.cpp nodes/QskColorRamp.cpp nodes/QskGraphicNode.cpp + nodes/QskLinesNode.cpp nodes/QskPaintedNode.cpp nodes/QskPlainTextRenderer.cpp nodes/QskRectangleNode.cpp diff --git a/src/common/QskStippleMetrics.cpp b/src/common/QskStippleMetrics.cpp new file mode 100644 index 00000000..7bee7e54 --- /dev/null +++ b/src/common/QskStippleMetrics.cpp @@ -0,0 +1,83 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskStippleMetrics.h" + +#include +#include +#include + +static void qskRegisterStippleMetrics() +{ + qRegisterMetaType< QskStippleMetrics >(); + +#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) + QMetaType::registerEqualsComparator< QskStippleMetrics >(); +#endif +} + +static inline QVector< qreal > qskDashPattern( const Qt::PenStyle& style ) +{ + static QVector< qreal > pattern[] = + { + {}, { 1 }, { 4, 2 }, { 1, 2 }, + { 4, 2, 1, 2 }, { 4, 2, 1, 2, 1, 2 }, {} + }; + + return pattern[ style ]; +} + +Q_CONSTRUCTOR_FUNCTION( qskRegisterStippleMetrics ) + +QskStippleMetrics::QskStippleMetrics( Qt::PenStyle penStyle ) + : m_pattern( qskDashPattern( penStyle ) ) +{ +} + +QskStippleMetrics::QskStippleMetrics( const QPen& pen ) + : QskStippleMetrics( pen.style() ) +{ + if ( pen.style() == Qt::CustomDashLine ) + { + m_offset = pen.dashOffset(); + m_pattern = pen.dashPattern(); + } +} + +void QskStippleMetrics::setPattern( const QVector< qreal >& pattern ) +{ + m_pattern = pattern; +} + +void QskStippleMetrics::setOffset( qreal offset ) noexcept +{ + m_offset = offset; +} + +QskHashValue QskStippleMetrics::hash( QskHashValue seed ) const noexcept +{ + auto hash = qHash( m_offset, seed ); + return qHash( m_pattern, hash ); +} + +#ifndef QT_NO_DEBUG_STREAM + +#include + +QDebug operator<<( QDebug debug, const QskStippleMetrics& metrics ) +{ + QDebugStateSaver saver( debug ); + debug.nospace(); + + debug << "QskStippleMetrics" << '('; + debug << metrics.offset() << ',' << metrics.pattern(); + debug << ')'; + + return debug; +} + +#endif + +#include "moc_QskStippleMetrics.cpp" diff --git a/src/common/QskStippleMetrics.h b/src/common/QskStippleMetrics.h new file mode 100644 index 00000000..5029b0d9 --- /dev/null +++ b/src/common/QskStippleMetrics.h @@ -0,0 +1,97 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#ifndef QSK_STIPPLE_METRICS_H +#define QSK_STIPPLE_METRICS_H + +#include "QskGlobal.h" + +#include +#include +#include + +class QPen; + +class QSK_EXPORT QskStippleMetrics +{ + Q_GADGET + + Q_PROPERTY( qreal offset READ offset WRITE setOffset ) + Q_PROPERTY( QVector< qreal > pattern READ pattern WRITE setPattern ) + + public: + QskStippleMetrics( Qt::PenStyle = Qt::SolidLine ); + QskStippleMetrics( const QPen& ); + QskStippleMetrics( const QVector< qreal >&, qreal offset = 0.0 ); + + bool operator==( const QskStippleMetrics& ) const noexcept; + bool operator!=( const QskStippleMetrics& ) const noexcept; + + bool isValid() const noexcept; + bool isSolid() const noexcept; + + void setOffset( qreal offset ) noexcept; + qreal offset() const noexcept; + + void setPattern( const QVector< qreal >& ); + QVector< qreal > pattern() const; + + QskHashValue hash( QskHashValue seed = 0 ) const noexcept; + + private: + qreal m_offset = 0.0; + QVector< qreal > m_pattern; +}; + +inline QskStippleMetrics::QskStippleMetrics( + const QVector< qreal >& pattern, qreal offset ) + : m_offset( offset ) + , m_pattern( pattern ) +{ +} + +inline qreal QskStippleMetrics::offset() const noexcept +{ + return m_offset; +} + +inline QVector< qreal > QskStippleMetrics::pattern() const +{ + return m_pattern; +} + +inline bool QskStippleMetrics::operator==( + const QskStippleMetrics& other ) const noexcept +{ + return ( m_offset == other.m_offset ) + && ( m_pattern == other.m_pattern ); +} + +inline bool QskStippleMetrics::operator!=( + const QskStippleMetrics& other ) const noexcept +{ + return !( *this == other ); +} + +inline bool QskStippleMetrics::isValid() const noexcept +{ + return !m_pattern.isEmpty(); +} + +inline bool QskStippleMetrics::isSolid() const noexcept +{ + return m_pattern.count() == 1; +} + +#ifndef QT_NO_DEBUG_STREAM + +class QDebug; +QSK_EXPORT QDebug operator<<( QDebug, const QskStippleMetrics& ); + +#endif + +Q_DECLARE_METATYPE( QskStippleMetrics ) + +#endif diff --git a/src/nodes/QskLinesNode.cpp b/src/nodes/QskLinesNode.cpp new file mode 100644 index 00000000..29c0000f --- /dev/null +++ b/src/nodes/QskLinesNode.cpp @@ -0,0 +1,327 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskLinesNode.h" +#include "QskIntervalF.h" +#include "QskVertex.h" +#include "QskStippleMetrics.h" +#include "QskSGNode.h" + +#include +#include +#include +#include +#include + +QSK_QT_PRIVATE_BEGIN +#include +#include +QSK_QT_PRIVATE_END + +namespace +{ + inline qreal mapX( const QTransform& t, qreal x ) + { + return t.dx() + t.m11() * x; + } + + inline qreal mapY( const QTransform& t, qreal y ) + { + return t.dy() + t.m22() * y; + } + + /* + Thanks to the hooks of the stroker classes we can make use + of QDashStroker without having to deal with the overhead of + QPainterPaths. But it might be worth to check if this could + be done in a shader. TODO ... + */ + class DashStroker : public QDashStroker + { + public: + DashStroker( const QskStippleMetrics& metrics ) + : QDashStroker( nullptr ) + { + setDashOffset( metrics.offset() ); + setDashPattern( metrics.pattern() ); + + m_elements.reserve( 2 ); + } + + QSGGeometry::Point2D* addDashes( QSGGeometry::Point2D* points, + qreal x1, qreal y1, qreal x2, qreal y2 ) + { + setMoveToHook( addPoint ); + setLineToHook( addPoint ); + + m_points = points; + + begin( this ); + + m_elements.add( { QPainterPath::MoveToElement, x1, y1 } ); + m_elements.add( { QPainterPath::LineToElement, x2, y2 } ); + + processCurrentSubpath(); + + end(); + + return m_points; + } + + int pointCount( qreal x1, qreal y1, qreal x2, qreal y2 ) + { + /* + There should be a faster way to calculate the + number of points. TODO ... + */ + setMoveToHook( countPoint ); + setLineToHook( countPoint ); + + m_count = 0; + + begin( this ); + + m_elements.add( { QPainterPath::MoveToElement, x1, y1 } ); + m_elements.add( { QPainterPath::LineToElement, x2, y2 } ); + + processCurrentSubpath(); + + end(); + + return m_count; + } + + private: + static void addPoint( qfixed x, qfixed y, void* data ) + { + auto stroker = reinterpret_cast< DashStroker* >( data ); + ( stroker->m_points++ )->set( x, y ); + } + + static void countPoint( qfixed, qfixed, void* data ) + { + auto stroker = reinterpret_cast< DashStroker* >( data ); + stroker->m_count++; + } + + int m_count = 0; + QSGGeometry::Point2D* m_points; + }; +} + +class QskLinesNodePrivate final : public QSGGeometryNodePrivate +{ + public: + QskLinesNodePrivate() + : geometry( QSGGeometry::defaultAttributes_Point2D(), 0 ) + { + geometry.setDrawingMode( QSGGeometry::DrawLines ); + geometry.setVertexDataPattern( QSGGeometry::StaticPattern ); + } + + inline qreal round( bool isHorizontal, qreal v ) const + { + const auto r2 = 2.0 * devicePixelRatio; + const qreal v0 = isHorizontal ? p0.x() : p0.y(); + + const int d = qRound( r2 * ( v + v0 ) ); + const auto f = ( d % 2 ? d : d - 1 ) / r2; + + return f / devicePixelRatio - v0; + } + + QSGGeometry geometry; + QSGFlatColorMaterial material; + + // position of [0,0] in device coordinates + QPointF p0; + qreal devicePixelRatio = 1.0; + + QskHashValue hash = 0.0; + bool dirty = true; +}; + +QskLinesNode::QskLinesNode() + : QSGGeometryNode( *new QskLinesNodePrivate ) +{ + Q_D( QskLinesNode ); + + setGeometry( &d->geometry ); + setMaterial( &d->material ); +} + +QskLinesNode::~QskLinesNode() +{ +} + +void QskLinesNode::setGlobalPosition( const QQuickItem* item ) +{ + QPointF p0; + qreal devicePixelRatio = 1.0; + + if ( item ) + { + p0 = item->mapToGlobal( QPointF() ); + + if ( auto w = item->window() ) + devicePixelRatio = w->devicePixelRatio(); + } + + setGlobalPosition( p0, devicePixelRatio ); +} + +void QskLinesNode::setGlobalPosition( + const QPointF& pos, qreal devicePixelRatio ) +{ + Q_D( QskLinesNode ); + + if ( pos != d->p0 || devicePixelRatio != d->devicePixelRatio ) + { + d->p0 = pos; + d->devicePixelRatio = devicePixelRatio; + + d->dirty = true; + } +} + +void QskLinesNode::updateGrid( const QColor& color, qreal lineWidth, + const QskStippleMetrics& stippleMetrics, const QTransform& transform, + const QskIntervalF& xBoundaries, const QVector< qreal >& xValues, + const QskIntervalF& yBoundaries, const QVector< qreal >& yValues ) +{ + Q_D( QskLinesNode ); + + if ( color != d->material.color() ) + { + d->material.setColor( color ); + markDirty( QSGNode::DirtyMaterial ); + } + + QskHashValue hash = 9784; + + hash = stippleMetrics.hash( hash ); + hash = qHash( transform, hash ); + hash = qHashBits( &xBoundaries, sizeof( xBoundaries ), hash ); + hash = qHashBits( &yBoundaries, sizeof( yBoundaries ), hash ); + hash = qHash( xValues, hash ); + hash = qHash( yValues, hash ); + + if ( hash != d->hash ) + { + d->dirty = true; + d->hash = hash; + } + + if( d->dirty ) + { + if ( !( stippleMetrics.isValid() + && color.isValid() && color.alpha() > 0 ) ) + { + QskSGNode::resetGeometry( this ); + } + else + { + updateGeometry( stippleMetrics, transform, + xBoundaries, xValues, yBoundaries, yValues ); + } + + markDirty( QSGNode::DirtyGeometry ); + d->dirty = false; + } + + const float lineWidthF = lineWidth; + if( lineWidthF != d->geometry.lineWidth() ) + d->geometry.setLineWidth( lineWidthF ); +} + +void QskLinesNode::updateGeometry( + const QskStippleMetrics& stippleMetrics, const QTransform& transform, + const QskIntervalF& xBoundaries, const QVector< qreal >& xValues, + const QskIntervalF& yBoundaries, const QVector< qreal >& yValues ) +{ + Q_D( QskLinesNode ); + + const auto x1 = mapX( transform, xBoundaries.lowerBound() ); + const auto x2 = mapX( transform, xBoundaries.upperBound() ); + + const auto y1 = mapY( transform, yBoundaries.lowerBound() ); + const auto y2 = mapY( transform, yBoundaries.upperBound() ); + + if ( stippleMetrics.isSolid() ) + { + using namespace QskVertex; + + auto lines = allocateLines< Line >( + d->geometry, xValues.count() + yValues.count() ); + + for ( auto x : xValues ) + { + x = mapX( transform, x ); + x = d->round( true, x ); + + lines++->setVLine( x, y1, y2 ); + } + + for ( auto y : yValues ) + { + y = mapY( transform, y ); + y = d->round( false, y ); + + lines++->setHLine( x1, x2, y ); + } + } + else + { + DashStroker stroker( stippleMetrics ); + + const int countX = stroker.pointCount( 0.0, y1, 0.0, y2 ); + const int countY = stroker.pointCount( x1, 0.0, x2, 0.0 ); + + auto count = xValues.count() * countX + yValues.count() * countY; + + d->geometry.allocate( count ); + auto points = d->geometry.vertexDataAsPoint2D(); + + /* + We have to calculate the first line only. All others are + translation without changing the positions of the dashes. + */ + auto p0 = points; + + for ( int i = 0; i < xValues.count(); i++ ) + { + auto x = mapX( transform, xValues[i] ); + x = d->round( true, x ); + + if ( i == 0 ) + { + points = stroker.addDashes( points, x, y1, x, y2 ); + } + else + { + for ( int j = 0; j < countX; j++ ) + points++->set( x, p0[j].y ); + } + } + + p0 = points; + + for ( int i = 0; i < yValues.count(); i++ ) + { + auto y = mapY( transform, yValues[i] ); + y = d->round( false, y ); + + if ( i == 0 ) + { + points = stroker.addDashes( points, x1, y, x2, y ); + } + else + { + for ( int j = 0; j < countY; j++ ) + points++->set( p0[j].x, y ); + } + } + } +} diff --git a/src/nodes/QskLinesNode.h b/src/nodes/QskLinesNode.h new file mode 100644 index 00000000..d52c4ba1 --- /dev/null +++ b/src/nodes/QskLinesNode.h @@ -0,0 +1,52 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#ifndef QSK_LINES_NODE_H +#define QSK_LINES_NODE_H + +#include "QskGlobal.h" + +#include +#include + +class QskIntervalF; +class QskStippleMetrics; +class QTransform; +class QPointF; +class QQuickItem; + +class QskLinesNodePrivate; + +/* + A node for stippled or solid lines. + For the moment limited to horizontal/vertical lines: TODO + */ +class QSK_EXPORT QskLinesNode : public QSGGeometryNode +{ + public: + QskLinesNode(); + ~QskLinesNode() override; + + void setGlobalPosition( const QPointF&, qreal devicePixelRatio ); + void setGlobalPosition( const QQuickItem* ); + + void setLineColor( const QColor& ); + void setLineWidth( qreal ); + + void updateGrid( const QColor&, qreal lineWidth, + const QskStippleMetrics&, const QTransform&, + const QskIntervalF&, const QVector< qreal >&, + const QskIntervalF&, const QVector< qreal >& ); + + private: + void updateGeometry( + const QskStippleMetrics&, const QTransform&, + const QskIntervalF&, const QVector< qreal >&, + const QskIntervalF&, const QVector< qreal >& ); + + Q_DECLARE_PRIVATE( QskLinesNode ) +}; + +#endif diff --git a/src/nodes/QskStrokeNode.cpp b/src/nodes/QskStrokeNode.cpp index 588ce62a..b58d5208 100644 --- a/src/nodes/QskStrokeNode.cpp +++ b/src/nodes/QskStrokeNode.cpp @@ -86,6 +86,8 @@ QskStrokeNode::QskStrokeNode() setMaterial( qskMaterialColorVertex ); } +QskStrokeNode::~QskStrokeNode() = default; + void QskStrokeNode::setRenderHint( RenderHint renderHint ) { Q_D( QskStrokeNode ); diff --git a/src/nodes/QskStrokeNode.h b/src/nodes/QskStrokeNode.h index 9f212e75..31e8a446 100644 --- a/src/nodes/QskStrokeNode.h +++ b/src/nodes/QskStrokeNode.h @@ -19,6 +19,7 @@ class QSK_EXPORT QskStrokeNode : public QSGGeometryNode { public: QskStrokeNode(); + ~QskStrokeNode() override; /* We only support monochrome pens ( QPen::color() ) and using a