diff --git a/src/common/QskScaleEngine.cpp b/src/common/QskScaleEngine.cpp new file mode 100644 index 00000000..7a5d16bf --- /dev/null +++ b/src/common/QskScaleEngine.cpp @@ -0,0 +1,359 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +// code cpoied from Qwt - with permission from the author ( = myself ) + +#include "QskScaleEngine.h" +#include "QskFunctions.h" +#include "QskIntervalF.h" +#include "QskScaleTickmarks.h" + +#include +#include + +#include + +namespace +{ + // What about using qskFuzzyCompare and friends ??? + + const double _eps = 1.0e-6; + + inline int fuzzyCompare( double value1, double value2, double intervalSize ) + { + const double eps = qAbs( 1.0e-6 * intervalSize ); + + if ( value2 - value1 > eps ) + return -1; + + if ( value1 - value2 > eps ) + return 1; + + return 0; + } + + inline bool fuzzyContains( const QskIntervalF& interval, double value ) + { + if ( !interval.isValid() ) + return false; + + if ( fuzzyCompare( value, interval.lowerBound(), interval.width() ) < 0 ) + return false; + + if ( fuzzyCompare( value, interval.upperBound(), interval.width() ) > 0 ) + return false; + + return true; + } + + double ceilEps( double value, double intervalSize ) + { + const double eps = _eps * intervalSize; + + value = ( value - eps ) / intervalSize; + return std::ceil( value ) * intervalSize; + } + + double floorEps( double value, double intervalSize ) + { + const double eps = _eps * intervalSize; + + value = ( value + eps ) / intervalSize; + return std::floor( value ) * intervalSize; + } + + double divideEps( double intervalSize, double numSteps ) + { + if ( numSteps == 0.0 || intervalSize == 0.0 ) + return 0.0; + + return ( intervalSize - ( _eps * intervalSize ) ) / numSteps; + } + + double divideInterval( double intervalSize, int numSteps ) + { + if ( numSteps <= 0 ) + return 0.0; + + const auto v = divideEps( intervalSize, numSteps ); + if ( v == 0.0 ) + return 0.0; + + constexpr double base = 10.0; + + // the same as std::log10( std::fabs( v ) ); + const double lx = std::log( std::fabs( v ) ) / std::log( base ); + const double p = std::floor( lx ); + + const double fraction = std::pow( base, lx - p ); + + uint n = base; + while ( ( n > 1 ) && ( fraction <= n / 2 ) ) + n /= 2; + + double stepSize = n * std::pow( base, p ); + if ( v < 0 ) + stepSize = -stepSize; + + return stepSize; + } +} + +namespace +{ + double minorStepSize( double intervalSize, int maxSteps ) + { + const double minStep = divideInterval( intervalSize, maxSteps ); + + if ( minStep != 0.0 ) + { + // # ticks per interval + const int numTicks = qCeil( qAbs( intervalSize / minStep ) ) - 1; + + // Do the minor steps fit into the interval? + if ( fuzzyCompare( ( numTicks + 1 ) * qAbs( minStep ), + qAbs( intervalSize ), intervalSize ) > 0 ) + { + // The minor steps doesn't fit into the interval + return 0.5 * intervalSize; + } + } + + return minStep; + } +} + +QskScaleEngine::QskScaleEngine() +{ +} + +QskScaleEngine::~QskScaleEngine() +{ +} + +void QskScaleEngine::setAttribute( Attribute attribute, bool on ) +{ + if ( on ) + m_attributes |= attribute; + else + m_attributes &= ~attribute; +} + +bool QskScaleEngine::testAttribute( Attribute attribute ) const +{ + return m_attributes & attribute; +} + +void QskScaleEngine::setAttributes( Attributes attributes ) +{ + m_attributes = attributes; +} + +QskScaleEngine::Attributes QskScaleEngine::attributes() const +{ + return m_attributes; +} + +QskScaleTickmarks QskScaleEngine::divideScale( + qreal x1, qreal x2, int maxMajorSteps, int maxMinorSteps, qreal stepSize) const +{ + QskScaleTickmarks tickmarks; + + const auto interval = QskIntervalF::normalized( x1, x2 ); + + if ( interval.width() > std::numeric_limits::max() ) + { + qWarning() << "QskScaleEngine::divideScale: overflow"; + return tickmarks; + } + + if ( interval.width() <= 0 ) + return tickmarks; + + stepSize = qAbs( stepSize ); + if ( stepSize == 0.0 ) + { + if ( maxMajorSteps < 1 ) + maxMajorSteps = 1; + + stepSize = divideInterval( interval.width(), maxMajorSteps ); + } + + + if ( stepSize != 0.0 ) + { + tickmarks = buildTicks( interval, stepSize, maxMinorSteps ); + } + + if ( x1 > x2 ) + tickmarks.invert(); + + return tickmarks; +} + +void QskScaleEngine::autoScale(int maxNumSteps, qreal& x1, qreal& x2, qreal& stepSize) const +{ + auto interval = QskIntervalF::normalized( x1, x2 ); + + interval.setLowerBound( interval.lowerBound() ); + interval.setUpperBound( interval.upperBound() ); + + stepSize = divideInterval( interval.width(), qMax( maxNumSteps, 1 ) ); + + if ( !testAttribute( QskScaleEngine::Floating ) ) + interval = align( interval, stepSize ); + + x1 = interval.lowerBound(); + x2 = interval.upperBound(); + + if ( testAttribute( QskScaleEngine::Inverted ) ) + { + qSwap( x1, x2 ); + stepSize = -stepSize; + } +} + +QskIntervalF QskScaleEngine::align( const QskIntervalF& interval, qreal stepSize ) const +{ + auto x1 = interval.lowerBound(); + auto x2 = interval.upperBound(); + + // when there is no rounding beside some effect, when + // calculating with doubles, we keep the original value + + const auto max = std::numeric_limits::max(); + + if ( -max + stepSize <= x1 ) + { + const auto x = floorEps( x1, stepSize ); + if ( qFuzzyIsNull( x ) || !qFuzzyCompare( x1, x ) ) + x1 = x; + } + + if ( max - stepSize >= x2 ) + { + const auto x = ceilEps( x2, stepSize ); + if ( qFuzzyIsNull( x ) || !qFuzzyCompare( x2, x ) ) + x2 = x; + } + + return QskIntervalF( x1, x2 ); +} + +QVector QskScaleEngine::strip( + const QVector& ticks, const QskIntervalF& interval ) const +{ + if ( !interval.isValid() || ticks.count() == 0 ) + return QVector(); + + if ( fuzzyContains( interval, ticks.first() ) + && fuzzyContains( interval, ticks.last() ) ) + { + return ticks; + } + + QVector strippedTicks; + for ( int i = 0; i < ticks.count(); i++ ) + { + if ( fuzzyContains( interval, ticks[i] ) ) + strippedTicks += ticks[i]; + } + + return strippedTicks; +} + +QskScaleTickmarks QskScaleEngine::buildTicks( + const QskIntervalF &interval, qreal stepSize, int maxMinorSteps ) const +{ + using T = QskScaleTickmarks; + + const auto boundingInterval = align( interval, stepSize ); + + QVector ticks[3]; + ticks[T::MajorTick] = buildMajorTicks( boundingInterval, stepSize ); + + if ( maxMinorSteps > 0 ) + { + buildMinorTicks( ticks[T::MajorTick], maxMinorSteps, stepSize, + ticks[T::MinorTick], ticks[T::MediumTick] ); + } + + for ( auto& t : ticks ) + { + t = strip( t, interval ); + + // ticks very close to 0.0 are + // explicitely set to 0.0 + + for ( int i = 0; i < t.count(); i++ ) + { + if ( fuzzyCompare( t[i], 0.0, stepSize ) == 0 ) + t[i] = 0.0; + } + } + + QskScaleTickmarks tickmarks; + tickmarks.setMinorTicks( ticks[T::MinorTick] ); + tickmarks.setMediumTicks( ticks[T::MediumTick] ); + tickmarks.setMajorTicks( ticks[T::MajorTick] ); + + return tickmarks; +} + +QVector QskScaleEngine::buildMajorTicks( + const QskIntervalF& interval, qreal stepSize ) const +{ + int numTicks = qRound( interval.width() / stepSize ) + 1; + if ( numTicks > 10000 ) + numTicks = 10000; + + QVector ticks; + ticks.reserve( numTicks ); + + ticks += interval.lowerBound(); + for ( int i = 1; i < numTicks - 1; i++ ) + ticks += interval.lowerBound() + i * stepSize; + ticks += interval.upperBound(); + + return ticks; +} + +void QskScaleEngine::buildMinorTicks( + const QVector& majorTicks, int maxMinorSteps, qreal stepSize, + QVector &minorTicks, QVector &mediumTicks ) const +{ + auto minStep = minorStepSize( stepSize, maxMinorSteps ); + if ( minStep == 0.0 ) + return; + + // # ticks per interval + const int numTicks = qCeil( qAbs( stepSize / minStep ) ) - 1; + + int medIndex = -1; + if ( numTicks % 2 ) + medIndex = numTicks / 2; + + // calculate minor ticks + + for ( int i = 0; i < majorTicks.count(); i++ ) + { + auto val = majorTicks[i]; + for ( int k = 0; k < numTicks; k++ ) + { + val += minStep; + + double alignedValue = val; + if ( fuzzyCompare( val, 0.0, stepSize ) == 0 ) + alignedValue = 0.0; + + if ( k == medIndex ) + mediumTicks += alignedValue; + else + minorTicks += alignedValue; + } + } +} + +#include "moc_QskScaleEngine.cpp" diff --git a/src/common/QskScaleEngine.h b/src/common/QskScaleEngine.h new file mode 100644 index 00000000..1584bb78 --- /dev/null +++ b/src/common/QskScaleEngine.h @@ -0,0 +1,60 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#ifndef QSK_SCALE_ENGINE_H +#define QSK_SCALE_ENGINE_H + +#include +#include + +class QskScaleTickmarks; +class QskIntervalF; + +class QSK_EXPORT QskScaleEngine +{ + Q_GADGET + + public: + enum Attribute + { + Inverted = 1 << 0, + Floating = 1 << 1 + }; + + Q_ENUM( Attribute ) + Q_DECLARE_FLAGS( Attributes, Attribute ) + + QskScaleEngine(); + ~QskScaleEngine(); + + void setAttribute( Attribute, bool on = true ); + bool testAttribute( Attribute ) const; + + void setAttributes( Attributes ); + Attributes attributes() const; + + QskScaleTickmarks divideScale(qreal x1, qreal x2, + int maxMajorSteps, int maxMinorSteps, qreal stepSize = 0.0) const; + + void autoScale( int maxNumSteps, qreal &x1, qreal &x2, qreal &stepSize ) const; + + private: + QskIntervalF align( const QskIntervalF&, qreal stepSize ) const; + + QVector strip( const QVector&, const QskIntervalF& ) const; + + QskScaleTickmarks buildTicks( + const QskIntervalF&, qreal stepSize, int maxMinorSteps ) const; + + QVector buildMajorTicks( const QskIntervalF&, qreal stepSize ) const; + + void buildMinorTicks( const QVector& majorTicks, + int maxMinorSteps, qreal stepSize, QVector& minorTicks, + QVector& mediumTicks ) const; + + Attributes m_attributes; +}; + +#endif diff --git a/src/common/QskScaleTickmarks.cpp b/src/common/QskScaleTickmarks.cpp new file mode 100644 index 00000000..eafa916b --- /dev/null +++ b/src/common/QskScaleTickmarks.cpp @@ -0,0 +1,69 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#include "QskScaleTickmarks.h" +#include + +int QskScaleTickmarks::tickCount() const noexcept +{ + return m_ticks[ MajorTick ].count() + + m_ticks[ MediumTick ].count() + + m_ticks[ MinorTick ].count(); +} + +QskScaleTickmarks::QskScaleTickmarks() +{ +} + +QskScaleTickmarks::~QskScaleTickmarks() +{ +} + +int QskScaleTickmarks::tickCount( TickType type ) const noexcept +{ + return m_ticks[ type ].count(); +} + +QVector QskScaleTickmarks::ticks( TickType type ) const noexcept +{ + return m_ticks[ type ]; +} + +void QskScaleTickmarks::setTicks(TickType type, const QVector& ticks ) +{ + m_ticks[ type ] = ticks; +} + +void QskScaleTickmarks::reset() +{ + m_ticks[ 0 ].clear(); + m_ticks[ 1 ].clear(); + m_ticks[ 2 ].clear(); +} + +void QskScaleTickmarks::invert() +{ + std::reverse( m_ticks[ 0 ].begin(), m_ticks[ 0 ].end() ); + std::reverse( m_ticks[ 1 ].begin(), m_ticks[ 1 ].end() ); + std::reverse( m_ticks[ 2 ].begin(), m_ticks[ 2 ].end() ); +} + +uint QskScaleTickmarks::hash( uint seed ) const +{ + seed = qHash( m_ticks[0], seed ); + seed = qHash( m_ticks[1], seed ); + seed = qHash( m_ticks[2], seed ); + + return seed; +} + +bool QskScaleTickmarks::operator==( const QskScaleTickmarks &other ) const noexcept +{ + return ( m_ticks[ 0 ] == other.m_ticks[ 0 ] ) + && ( m_ticks[ 1 ] == other.m_ticks[ 1 ] ) + && ( m_ticks[ 2 ] == other.m_ticks[ 2 ] ); +} + +#include "moc_QskScaleTickmarks.cpp" diff --git a/src/common/QskScaleTickmarks.h b/src/common/QskScaleTickmarks.h new file mode 100644 index 00000000..de6db6f7 --- /dev/null +++ b/src/common/QskScaleTickmarks.h @@ -0,0 +1,96 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#ifndef QSK_SCALE_TICKMARKS_H +#define QSK_SCALE_TICKMARKS_H + +#include +#include + +class QSK_EXPORT QskScaleTickmarks +{ + Q_GADGET + + Q_PROPERTY( QVector majorTicks READ majorTicks WRITE setMajorTicks ) + Q_PROPERTY( QVector mediumTicks READ mediumTicks WRITE setMediumTicks ) + Q_PROPERTY( QVector minorTicks READ minorTicks WRITE setMinorTicks ) + + public: + enum TickType + { + MinorTick, + MediumTick, + MajorTick, + }; + + Q_ENUM( TickType ) + + QskScaleTickmarks(); + ~QskScaleTickmarks(); + + bool operator==( const QskScaleTickmarks& ) const noexcept; + bool operator!=( const QskScaleTickmarks& ) const noexcept; + + int tickCount() const noexcept; + int tickCount( TickType ) const noexcept; + + QVector ticks( TickType ) const noexcept; + void setTicks( TickType, const QVector & ); + + void setMinorTicks( const QVector& ); + QVector minorTicks() const noexcept; + + void setMediumTicks( const QVector& ); + QVector mediumTicks() const noexcept; + + void setMajorTicks( const QVector& ); + QVector majorTicks() const noexcept; + + void invert(); + void reset(); + + uint hash( uint seed = 0 ) const; + + private: + QVector< qreal > m_ticks[ 3 ]; +}; + +inline void QskScaleTickmarks::setMinorTicks( const QVector& ticks ) +{ + setTicks( MinorTick, ticks ); +} + +inline QVector QskScaleTickmarks::minorTicks() const noexcept +{ + return ticks( MinorTick ); +} + +inline void QskScaleTickmarks::setMediumTicks( const QVector& ticks ) +{ + setTicks( MediumTick, ticks ); +} + +inline QVector QskScaleTickmarks::mediumTicks() const noexcept +{ + return ticks( MediumTick ); +} + +inline void QskScaleTickmarks::setMajorTicks( const QVector& ticks ) +{ + setTicks( MajorTick, ticks ); +} + +inline QVector QskScaleTickmarks::majorTicks() const noexcept +{ + return ticks( MajorTick ); +} + +inline bool QskScaleTickmarks::operator!=( + const QskScaleTickmarks& other ) const noexcept +{ + return !( *this == other ); +} + +#endif diff --git a/src/nodes/QskTickmarksNode.cpp b/src/nodes/QskTickmarksNode.cpp new file mode 100644 index 00000000..d2da1f8f --- /dev/null +++ b/src/nodes/QskTickmarksNode.cpp @@ -0,0 +1,127 @@ +#include "QskTickmarksNode.h" +#include "QskScaleTickmarks.h" + +#include +#include +#include + +QSK_QT_PRIVATE_BEGIN +#include +QSK_QT_PRIVATE_END + +static constexpr inline qreal qskTickFactor( QskScaleTickmarks::TickType type ) +{ + using TM = QskScaleTickmarks; + return type == TM::MinorTick ? 0.7 : ( type == TM::MinorTick ? 0.85 : 1.0 ); +} + +class QskTickmarksNodePrivate final : public QSGGeometryNodePrivate +{ + public: + QskTickmarksNodePrivate() + : geometry( QSGGeometry::defaultAttributes_Point2D(), 0 ) + { + geometry.setDrawingMode( GL_LINES ); + geometry.setVertexDataPattern( QSGGeometry::StaticPattern ); + } + + QSGGeometry geometry; + QSGFlatColorMaterial material; + + QskIntervalF boundaries; + QskScaleTickmarks tickmarks; + + QRectF rect; + int lineWidth = 0; + + uint hash = 0; +}; + +QskTickmarksNode::QskTickmarksNode() + : QSGGeometryNode( *new QskTickmarksNodePrivate ) +{ + Q_D( QskTickmarksNode ); + + setGeometry( &d->geometry ); + setMaterial( &d->material ); +} + +QskTickmarksNode::~QskTickmarksNode() +{ +} + +void QskTickmarksNode::update( + const QColor& color, const QRectF& rect, + const QskIntervalF& boundaries, const QskScaleTickmarks& tickmarks, + int lineWidth, Qt::Orientation orientation ) +{ + Q_D( QskTickmarksNode ); + + if( lineWidth != d->lineWidth ) + { + d->lineWidth = lineWidth; + d->geometry.setLineWidth( lineWidth ); + + markDirty( QSGNode::DirtyGeometry ); + } + + const uint hash = tickmarks.hash( 17435 ); + + if( ( hash != d->hash ) || ( rect != d->rect ) ) + { + d->hash = hash; + d->rect = rect; + + d->geometry.allocate( tickmarks.tickCount() * 2 ); + auto vertexData = d->geometry.vertexDataAsPoint2D(); + + const qreal min = boundaries.lowerBound(); + const qreal range = boundaries.width(); + + using TM = QskScaleTickmarks; + + for( int i = TM::MinorTick; i <= TM::MajorTick; i++ ) + { + const auto tickType = static_cast< TM::TickType >(i); + const auto ticks = tickmarks.ticks( tickType ); + + if ( orientation == Qt::Horizontal ) + { + const qreal ratio = rect.width() / range; + const float len = rect.height() * qskTickFactor( tickType ); + + for( const auto tick : ticks ) + { + const auto x = rect.x() + ( tick - min ) * ratio; + + vertexData[ 0 ].set( x, rect.bottom() ); + vertexData[ 1 ].set( x, rect.bottom() - len ); + vertexData += 2; + } + } + else + { + const qreal ratio = rect.height() / range; + const float len = rect.width() * qskTickFactor( tickType ); + + for( const auto tick : ticks ) + { + const auto y = rect.bottom() - ( tick - min ) * ratio; + + vertexData[ 0 ].set( rect.right(), y ); + vertexData[ 1 ].set( rect.right() - len, y ); + vertexData += 2; + } + } + } + + d->geometry.markVertexDataDirty(); + markDirty( QSGNode::DirtyGeometry ); + } + + if ( color != d->material.color() ) + { + d->material.setColor( color ); + markDirty( QSGNode::DirtyMaterial ); + } +} diff --git a/src/nodes/QskTickmarksNode.h b/src/nodes/QskTickmarksNode.h new file mode 100644 index 00000000..ee4f0691 --- /dev/null +++ b/src/nodes/QskTickmarksNode.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#ifndef QSK_TICKMARKS_NODE_H +#define QSK_TICKMARKS_NODE_H + +#include "QskGlobal.h" + +#include +#include + +class QColor; +class QRectF; +class QskIntervalF; +class QskScaleTickmarks; + +class QskTickmarksNodePrivate; + +class QSK_EXPORT QskTickmarksNode : public QSGGeometryNode +{ + public: + QskTickmarksNode(); + ~QskTickmarksNode() override; + + void update( const QColor&, const QRectF&, const QskIntervalF&, + const QskScaleTickmarks&, int tickLineWidth, Qt::Orientation ); + + private: + Q_DECLARE_PRIVATE( QskTickmarksNode ) +}; + +#endif diff --git a/src/src.pro b/src/src.pro index d19b034f..13d63a12 100644 --- a/src/src.pro +++ b/src/src.pro @@ -30,6 +30,8 @@ HEADERS += \ common/QskObjectCounter.h \ common/QskRgbValue.h \ common/QskRgbPalette.h \ + common/QskScaleEngine.h \ + common/QskScaleTickmarks.h \ common/QskShadowMetrics.h \ common/QskSizePolicy.h \ common/QskTextColors.h \ @@ -50,6 +52,8 @@ SOURCES += \ common/QskObjectCounter.cpp \ common/QskRgbValue.cpp \ common/QskRgbPalette.cpp \ + common/QskScaleEngine.cpp \ + common/QskScaleTickmarks.cpp \ common/QskShadowMetrics.cpp \ common/QskSizePolicy.cpp \ common/QskTextColors.cpp \ @@ -92,6 +96,7 @@ HEADERS += \ nodes/QskTextRenderer.h \ nodes/QskTextureNode.h \ nodes/QskTextureRenderer.h \ + nodes/QskTickmarksNode.h \ nodes/QskVertex.h SOURCES += \ @@ -108,6 +113,7 @@ SOURCES += \ nodes/QskTextRenderer.cpp \ nodes/QskTextureNode.cpp \ nodes/QskTextureRenderer.cpp \ + nodes/QskTickmarksNode.cpp \ nodes/QskVertex.cpp HEADERS += \