From 717a1c2ef2699690908202c8401d09f0039ca38e Mon Sep 17 00:00:00 2001 From: Uwe Rathmann Date: Tue, 28 Nov 2023 13:36:47 +0100 Subject: [PATCH] code from features/plots merged --- playground/CMakeLists.txt | 1 + playground/plots/CMakeLists.txt | 22 + playground/plots/Plot.cpp | 244 +++++++++ playground/plots/Plot.h | 50 ++ playground/plots/PlotCursor.cpp | 81 +++ playground/plots/PlotCursor.h | 37 ++ playground/plots/PlotCursorSkinlet.cpp | 211 ++++++++ playground/plots/PlotCursorSkinlet.h | 42 ++ playground/plots/PlotSkin.cpp | 160 ++++++ playground/plots/PlotSkin.h | 13 + playground/plots/QskPlotCorridor.cpp | 156 ++++++ playground/plots/QskPlotCorridor.h | 67 +++ playground/plots/QskPlotCorridorData.cpp | 138 +++++ playground/plots/QskPlotCorridorData.h | 82 +++ playground/plots/QskPlotCorridorSkinlet.cpp | 261 ++++++++++ playground/plots/QskPlotCorridorSkinlet.h | 39 ++ playground/plots/QskPlotCurve.cpp | 156 ++++++ playground/plots/QskPlotCurve.h | 57 +++ playground/plots/QskPlotCurveData.cpp | 285 +++++++++++ playground/plots/QskPlotCurveData.h | 105 ++++ playground/plots/QskPlotCurveSkinlet.cpp | 163 ++++++ playground/plots/QskPlotCurveSkinlet.h | 28 + playground/plots/QskPlotGrid.cpp | 137 +++++ playground/plots/QskPlotGrid.h | 97 ++++ playground/plots/QskPlotGridSkinlet.cpp | 81 +++ playground/plots/QskPlotGridSkinlet.h | 29 ++ playground/plots/QskPlotItem.cpp | 201 ++++++++ playground/plots/QskPlotItem.h | 122 +++++ playground/plots/QskPlotNamespace.h | 21 + playground/plots/QskPlotView.cpp | 371 ++++++++++++++ playground/plots/QskPlotView.h | 56 ++ playground/plots/QskPlotViewSkinlet.cpp | 283 +++++++++++ playground/plots/QskPlotViewSkinlet.h | 52 ++ playground/plots/main.cpp | 120 +++++ src/CMakeLists.txt | 4 +- src/common/QskGraduation.h | 7 +- src/nodes/QskAxisScaleNode.cpp | 207 ++++++++ src/nodes/QskAxisScaleNode.h | 45 ++ src/nodes/QskScaleRenderer.cpp | 534 ++++++++++++-------- src/nodes/QskScaleRenderer.h | 56 +- src/nodes/QskTickmarksNode.cpp | 126 ----- src/nodes/QskTickmarksNode.h | 30 -- 42 files changed, 4583 insertions(+), 394 deletions(-) create mode 100644 playground/plots/CMakeLists.txt create mode 100644 playground/plots/Plot.cpp create mode 100644 playground/plots/Plot.h create mode 100644 playground/plots/PlotCursor.cpp create mode 100644 playground/plots/PlotCursor.h create mode 100644 playground/plots/PlotCursorSkinlet.cpp create mode 100644 playground/plots/PlotCursorSkinlet.h create mode 100644 playground/plots/PlotSkin.cpp create mode 100644 playground/plots/PlotSkin.h create mode 100644 playground/plots/QskPlotCorridor.cpp create mode 100644 playground/plots/QskPlotCorridor.h create mode 100644 playground/plots/QskPlotCorridorData.cpp create mode 100644 playground/plots/QskPlotCorridorData.h create mode 100644 playground/plots/QskPlotCorridorSkinlet.cpp create mode 100644 playground/plots/QskPlotCorridorSkinlet.h create mode 100644 playground/plots/QskPlotCurve.cpp create mode 100644 playground/plots/QskPlotCurve.h create mode 100644 playground/plots/QskPlotCurveData.cpp create mode 100644 playground/plots/QskPlotCurveData.h create mode 100644 playground/plots/QskPlotCurveSkinlet.cpp create mode 100644 playground/plots/QskPlotCurveSkinlet.h create mode 100644 playground/plots/QskPlotGrid.cpp create mode 100644 playground/plots/QskPlotGrid.h create mode 100644 playground/plots/QskPlotGridSkinlet.cpp create mode 100644 playground/plots/QskPlotGridSkinlet.h create mode 100644 playground/plots/QskPlotItem.cpp create mode 100644 playground/plots/QskPlotItem.h create mode 100644 playground/plots/QskPlotNamespace.h create mode 100644 playground/plots/QskPlotView.cpp create mode 100644 playground/plots/QskPlotView.h create mode 100644 playground/plots/QskPlotViewSkinlet.cpp create mode 100644 playground/plots/QskPlotViewSkinlet.h create mode 100644 playground/plots/main.cpp create mode 100644 src/nodes/QskAxisScaleNode.cpp create mode 100644 src/nodes/QskAxisScaleNode.h delete mode 100644 src/nodes/QskTickmarksNode.cpp delete mode 100644 src/nodes/QskTickmarksNode.h diff --git a/playground/CMakeLists.txt b/playground/CMakeLists.txt index 7fc3449a..2e7b04ca 100644 --- a/playground/CMakeLists.txt +++ b/playground/CMakeLists.txt @@ -6,6 +6,7 @@ add_subdirectory(invoker) add_subdirectory(shadows) add_subdirectory(shapes) add_subdirectory(charts) +add_subdirectory(plots) if (BUILD_INPUTCONTEXT) add_subdirectory(inputpanel) diff --git a/playground/plots/CMakeLists.txt b/playground/plots/CMakeLists.txt new file mode 100644 index 00000000..9cc36159 --- /dev/null +++ b/playground/plots/CMakeLists.txt @@ -0,0 +1,22 @@ +############################################################################ +# QSkinny - Copyright (C) 2016 Uwe Rathmann +# This file may be used under the terms of the 3-clause BSD License +############################################################################ + +list(APPEND HEADERS QskPlotView.h QskPlotItem.h QskPlotViewSkinlet.h QskPlotNamespace.h) +list(APPEND SOURCES QskPlotView.cpp QskPlotItem.cpp QskPlotViewSkinlet.cpp) + +list(APPEND HEADERS QskPlotGrid.h QskPlotGridSkinlet.h) +list(APPEND SOURCES QskPlotGrid.cpp QskPlotGridSkinlet.cpp) + +list(APPEND HEADERS QskPlotCurveData.h QskPlotCurve.h QskPlotCurveSkinlet.h ) +list(APPEND SOURCES QskPlotCurveData.cpp QskPlotCurve.cpp QskPlotCurveSkinlet.cpp) + +list(APPEND HEADERS QskPlotCorridorData.h QskPlotCorridor.h QskPlotCorridorSkinlet.h ) +list(APPEND SOURCES QskPlotCorridorData.cpp QskPlotCorridor.cpp QskPlotCorridorSkinlet.cpp) + +list(APPEND HEADERS PlotCursor.h PlotCursorSkinlet.h ) +list(APPEND SOURCES PlotCursor.cpp PlotCursorSkinlet.cpp) + +qsk_add_example(plots ${SOURCES} ${HEADERS} + PlotSkin.h PlotSkin.cpp Plot.h Plot.cpp main.cpp ) diff --git a/playground/plots/Plot.cpp b/playground/plots/Plot.cpp new file mode 100644 index 00000000..c5723e24 --- /dev/null +++ b/playground/plots/Plot.cpp @@ -0,0 +1,244 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "Plot.h" +#include "PlotSkin.h" +#include "PlotCursor.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace +{ + class CurveData : public QskPlotCurveData + { + public: + CurveData( QObject* parent = nullptr ) + : QskPlotCurveData( parent ) + { + setHint( MonotonicX ); + } + + void setSamples( const QVector< Plot::Sample >& samples ) + { + m_samples = samples; + Q_EMIT changed(); + } + + qsizetype count() const override + { + return m_samples.count(); + } + + QPointF pointAt( qsizetype index ) const override + { + const auto& sample = m_samples.at( index ); + return QPointF( sample.timestamp, sample.value ); + } + + private: + QVector< Plot::Sample > m_samples; + }; + + class CorridorData : public QskPlotCorridorData + { + public: + CorridorData( QObject* parent = nullptr ) + : QskPlotCorridorData( parent ) + { + } + + void setSamples( const QVector< Plot::Sample >& samples ) + { + m_samples = samples; + Q_EMIT changed(); + } + + qsizetype count() const override + { + return m_samples.count(); + } + + QskPlotCorridorSample sampleAt( qsizetype index ) const override + { + const auto& sample = m_samples.at( index ); + return { sample.timestamp, { sample.lowerBound, sample.upperBound } }; + } + + private: + QVector< Plot::Sample > m_samples; + }; +} + +class Plot::PrivateData +{ + public: + QskPlotGrid* grid = nullptr; + QskPlotCorridor* corridor = nullptr; + QskPlotCurve* curve = nullptr; + PlotCursor* cursor = nullptr; + + const QTime startTime = QTime::currentTime(); +}; + +Plot::Plot( QQuickItem* parentItem ) + : QskPlotView( parentItem ) + , m_data( new PrivateData ) +{ + PlotSkin::extendSkin( effectiveSkin() ); + + setAcceptedMouseButtons( Qt::LeftButton ); // cursor + setWheelEnabled( true ); // zooming + + resetAxes(); + + using namespace QskRgb; + + m_data->grid = new QskPlotGrid( this ); + + m_data->corridor = new QskPlotCorridor( this ); + m_data->corridor->setData( new CorridorData() ); + + m_data->curve = new QskPlotCurve( this ); + m_data->curve->setData( new CurveData() ); + + m_data->cursor = new PlotCursor(); + m_data->cursor->setParent( this ); // not attached + m_data->cursor->setPosition( -33 ); + + /* + extra space for the overlapping labels. Actually this should be + the job of the layout code: TODO ... + */ + setPaddingHint( Panel, QskMargins( 10, 15, 40, 10 ) ); +} + +Plot::~Plot() +{ +} + +void Plot::setSamples( const QVector< Sample >& samples ) +{ + auto corridorData = static_cast< CorridorData* >( m_data->corridor->data() ); + corridorData->setSamples( samples ); + + auto curveData = static_cast< CurveData* >( m_data->curve->data() ); + curveData->setSamples( samples ); +} + +void Plot::shiftXAxis( int steps ) +{ + auto range = boundaries( QskPlot::XBottom ); + //range.translate( steps * 0.2 * range.width() ); + range.translate( steps * 1.0 ); + setBoundaries( QskPlot::XBottom, range ); +} + +void Plot::resetAxes() +{ + QskIntervalF rangeX( -50.0, 0.0 ); + + if ( m_data->curve && m_data->curve->data() ) + { + const auto r = m_data->curve->data()->boundingRect(); + if ( !r.isEmpty() ) + rangeX |= QskIntervalF( r.left(), r.right() ); + } + + setBoundaries( QskPlot::XBottom, rangeX ); + setBoundaries( QskPlot::YLeft, 0.0, 100.0 ); +} + +QVariant Plot::labelAt( QskPlot::Axis axis, qreal pos ) const +{ + if ( axis == QskPlot::XBottom ) + { + auto text = QString::number( pos, 'g' ); + text += '\n'; + + const auto time = m_data->startTime.addSecs( qRound( pos ) ); + text += time.toString(); + + return text; + } + + return Inherited::labelAt( axis, pos ); +} + +void Plot::mousePressEvent( QMouseEvent* event ) +{ + auto pos = qskMousePosition( event ); + if ( canvasRect().contains( pos ) ) + { + m_data->cursor->attach( this ); + m_data->cursor->setCanvasPosition( pos.x() ); + + return; + } + + Inherited::mousePressEvent( event ); +} + +void Plot::mouseMoveEvent( QMouseEvent* event ) +{ + if ( m_data->cursor->view() ) + { + auto x = qskMousePosition( event ).x(); + + const auto r = canvasRect(); + x = qBound( r.left(), x, r.right() ); + + m_data->cursor->setCanvasPosition( x ); + + return; + } + + Inherited::mouseMoveEvent( event ); +} + +void Plot::mouseReleaseEvent( QMouseEvent* event ) +{ + if ( m_data->cursor->view() ) + { + m_data->cursor->detach(); + return; + } + + Inherited::mouseReleaseEvent( event ); +} + +void Plot::wheelEvent( QWheelEvent* event ) +{ + const auto steps = qskWheelSteps( event ); + + double f = std::pow( 0.9, qAbs( steps ) ); + if ( steps > 0 ) + f = 1 / f; + + auto range = boundaries( QskPlot::XBottom ); + range.setLowerBound( range.upperBound() - f * range.length() ); + + setBoundaries( QskPlot::XBottom, range ); +} + +void Plot::changeEvent( QEvent* event ) +{ + if ( event->type() == QEvent::StyleChange ) + PlotSkin::extendSkin( effectiveSkin() ); + + Inherited::changeEvent( event ); +} + +#include "moc_Plot.cpp" diff --git a/playground/plots/Plot.h b/playground/plots/Plot.h new file mode 100644 index 00000000..eac16e0b --- /dev/null +++ b/playground/plots/Plot.h @@ -0,0 +1,50 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include "QskPlotView.h" +#include + +class Plot : public QskPlotView +{ + Q_OBJECT + + using Inherited = QskPlotView; + + public: + class Sample + { + public: + qreal timestamp = 0.0; + + qreal lowerBound = 0.0; + qreal value = 0.0; + qreal upperBound = 0.0; + }; + + Plot( QQuickItem* parentItem = nullptr ); + ~Plot() override; + + void setSamples( const QVector< Sample >& ); + + public Q_SLOT: + void resetAxes(); + void shiftXAxis( int steps ); + + protected: + void mousePressEvent( QMouseEvent* ) override; + void mouseMoveEvent( QMouseEvent* ) override; + void mouseReleaseEvent( QMouseEvent* ) override; + + void wheelEvent( QWheelEvent* ) override; + void changeEvent( QEvent* ) override; + + private: + QVariant labelAt( QskPlot::Axis axis, qreal pos ) const final override; + + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; diff --git a/playground/plots/PlotCursor.cpp b/playground/plots/PlotCursor.cpp new file mode 100644 index 00000000..dddb0b54 --- /dev/null +++ b/playground/plots/PlotCursor.cpp @@ -0,0 +1,81 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "PlotCursor.h" +#include + +QSK_SUBCONTROL( PlotCursor, Line ) +QSK_SUBCONTROL( PlotCursor, LabelPanel ) +QSK_SUBCONTROL( PlotCursor, LabelText ) + +class PlotCursor::PrivateData +{ + public: + qreal position = 0.0; + Qt::Orientation orientation = Qt::Horizontal; +}; + +PlotCursor::PlotCursor( QObject* object ) + : Inherited( object ) + , m_data( new PrivateData ) +{ + setCoordinateType( CanvasCoordinates ); +} + +PlotCursor::~PlotCursor() +{ +} + +void PlotCursor::setOrientation( Qt::Orientation orientation ) +{ + if ( m_data->orientation != orientation ) + { + m_data->orientation = orientation; + markDirty(); + } +} + +Qt::Orientation PlotCursor::orientation() const +{ + return m_data->orientation; +} + +void PlotCursor::setCanvasPosition( qreal position ) +{ + const auto t = transformation().inverted(); + + if ( m_data->orientation == Qt::Horizontal ) + position = t.map( QPointF( position, 0.0 ) ).x(); + else + position = t.map( QPointF( 0.0, position ) ).y(); + + setPosition( position ); +} + +void PlotCursor::setPosition( qreal position ) +{ + if ( m_data->position != position ) + { + m_data->position = position; + markDirty(); + } +} + +qreal PlotCursor::position() const +{ + return m_data->position; +} + +void PlotCursor::transformationChanged( ChangeFlags flags ) +{ + Inherited::transformationChanged( flags ); +} + +bool PlotCursor::needsClipping() const +{ + return false; +} + +#include "moc_PlotCursor.cpp" diff --git a/playground/plots/PlotCursor.h b/playground/plots/PlotCursor.h new file mode 100644 index 00000000..1c784693 --- /dev/null +++ b/playground/plots/PlotCursor.h @@ -0,0 +1,37 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include "QskPlotItem.h" +#include + +class PlotCursor : public QskPlotItem +{ + Q_OBJECT + + using Inherited = QskPlotItem; + + public: + QSK_SUBCONTROLS( Line, LabelPanel, LabelText ) + + PlotCursor( QObject* = nullptr ); + ~PlotCursor() override; + + void setOrientation( Qt::Orientation ); + Qt::Orientation orientation() const; + + void setCanvasPosition( qreal ); + + void setPosition( qreal ); + qreal position() const; + + void transformationChanged( ChangeFlags ) override; + bool needsClipping() const override; + + private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; diff --git a/playground/plots/PlotCursorSkinlet.cpp b/playground/plots/PlotCursorSkinlet.cpp new file mode 100644 index 00000000..e0660937 --- /dev/null +++ b/playground/plots/PlotCursorSkinlet.cpp @@ -0,0 +1,211 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "PlotCursorSkinlet.h" +#include "PlotCursor.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +enum { Lower, Upper, Value }; + +static inline QskAspect::Variation variation( int index ) +{ + if ( index == Lower ) + return QskAspect::Lower; + + if ( index == Upper ) + return QskAspect::Upper; + + return QskAspect::NoVariation; +} + +class PlotCursorSkinlet::PrivateData +{ + public: + + struct + { + qreal value; + } labelInfo[ 3 ]; +}; + +PlotCursorSkinlet::PlotCursorSkinlet( QskSkin* skin ) + : Inherited( skin ) + , m_data( new PrivateData ) +{ + setNodeRoles( { CursorLine, TextBox, Text } ); +} + +PlotCursorSkinlet::~PlotCursorSkinlet() +{ +} + +void PlotCursorSkinlet::updateNode( + QskSkinnable* skinnable, QSGNode* parent ) const +{ + const auto cursor = static_cast< const PlotCursor* >( skinnable ); + const auto x = cursor->position(); + + auto info = m_data->labelInfo; + + for ( auto child : cursor->view()->children() ) + { + if ( auto curve = qobject_cast< const QskPlotCurve* >( child ) ) + { + const auto pos = curve->interpolatedPoint( Qt::Horizontal, x ); + info[Value].value = pos.y(); + } + else if ( auto corridor = qobject_cast< const QskPlotCorridor* >( child ) ) + { + const auto boundary = corridor->interpolatedSample( x ).boundary; + + info[ Lower ].value = boundary.lowerBound(); + info[ Upper ].value = boundary.upperBound(); + } + } + + Inherited::updateNode( skinnable, parent ); +} + +QSGNode* PlotCursorSkinlet::updateSubNode( + const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const +{ + using Q = PlotCursor; + + switch( nodeRole ) + { + case CursorLine: + return updateCursorLineNode( skinnable, node ); + + case TextBox: + return updateSeriesNode( skinnable, Q::LabelPanel, node ); + + case Text: + return updateSeriesNode( skinnable, Q::LabelText, node ); + } + + return Inherited::updateSubNode( skinnable, nodeRole, node ); +} + +int PlotCursorSkinlet::sampleCount( + const QskSkinnable*, QskAspect::Subcontrol ) const +{ + return 3; +} + +QRectF PlotCursorSkinlet::sampleRect( const QskSkinnable* skinnable, + const QRectF&, QskAspect::Subcontrol, int index ) const +{ + using Q = PlotCursor; + + const auto cursor = static_cast< const PlotCursor* >( skinnable ); + + auto pos = QPointF( cursor->position(), + m_data->labelInfo[ index ].value ); + + pos = cursor->transformation().map( pos ); + + const QFontMetricsF fm( cursor->effectiveFont( Q::LabelText ) ); + + const qreal w = qskHorizontalAdvance( fm, "100.0" ); + const qreal h = fm.height(); + + QRectF r( 0, 0, w, h ); + r = r.marginsAdded( cursor->paddingHint( Q::LabelPanel ) ); + + r.moveRight( pos.x() - 5 ); + r.moveBottom( pos.y() ); + + return r; +} + +QSGNode* PlotCursorSkinlet::updateSampleNode( const QskSkinnable* skinnable, + QskAspect::Subcontrol subControl, int index, QSGNode* node ) const +{ + using Q = PlotCursor; + + const auto rect = sampleRect( skinnable, QRectF(), subControl, index ); + const auto aspect = subControl | variation( index ); + + if ( subControl == Q::LabelPanel ) + { + const auto gradient = skinnable->gradientHint( aspect ); + return updateBoxNode( skinnable, node, rect, gradient, subControl ); + } + + if ( subControl == Q::LabelText ) + { + const auto info = m_data->labelInfo[ index ]; + + const auto text = QString::number( info.value, 'f', 1 ); + const auto color = skinnable->color( aspect ); + + const auto textOptions = skinnable->textOptionsHint( aspect ); + const auto font = skinnable->effectiveFont( aspect ); + + return updateTextNode( skinnable, node, rect, Qt::AlignCenter, + text, font, textOptions, color, Qsk::Normal ); + } + + return nullptr; +} + +QSGNode* PlotCursorSkinlet::updateCursorLineNode( + const QskSkinnable* skinnable, QSGNode* node ) const +{ + auto cursor = static_cast< const PlotCursor* >( skinnable ); + + const auto r = cursor->scaleRect(); + if ( r.isEmpty() ) + return nullptr; + + QPointF p1 = r.topLeft(); + QPointF p2 = r.bottomRight(); + + if ( cursor->orientation() == Qt::Horizontal ) + { + const auto x = cursor->position(); + if ( x < r.left() || x > r.right() ) + return nullptr; + + p1.rx() = p2.rx() = x; + } + else + { + const auto y = cursor->position(); + if ( y < r.top() || y > r.bottom() ) + return nullptr; + + p1.ry() = p2.ry() = y; + } + + if ( cursor->coordinateType() == QskPlotItem::CanvasCoordinates ) + { + /* + When having a non solid line we might want to use CanvasCoordinates + to avoid the length of dashes/dots being affected from the + plot transformation. + */ + const auto transform = cursor->transformation(); + p1 = transform.map( p1 ); + p2 = transform.map( p2 ); + } + + return updateLineNode( cursor, node, QLineF( p1, p2 ), PlotCursor::Line ); +} + +#include "moc_PlotCursorSkinlet.cpp" diff --git a/playground/plots/PlotCursorSkinlet.h b/playground/plots/PlotCursorSkinlet.h new file mode 100644 index 00000000..b3e03b77 --- /dev/null +++ b/playground/plots/PlotCursorSkinlet.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#pragma once + +#include + +class PlotCursorSkinlet : public QskSkinlet +{ + Q_GADGET + + using Inherited = QskSkinlet; + + public: + enum NodeRole { CursorLine, TextBox, Text }; + + Q_INVOKABLE PlotCursorSkinlet( QskSkin* = nullptr ); + ~PlotCursorSkinlet() override; + + void updateNode( QskSkinnable*, QSGNode* ) const override; + + int sampleCount( const QskSkinnable*, + QskAspect::Subcontrol ) const override final; + + QRectF sampleRect( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol, int index ) const override; + + protected: + QSGNode* updateSubNode( const QskSkinnable*, + quint8 nodeRole, QSGNode* ) const override; + + QSGNode* updateSampleNode( const QskSkinnable*, + QskAspect::Subcontrol, int index, QSGNode* ) const override; + + private: + QSGNode* updateCursorLineNode( const QskSkinnable*, QSGNode* ) const; + + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; diff --git a/playground/plots/PlotSkin.cpp b/playground/plots/PlotSkin.cpp new file mode 100644 index 00000000..81be7ddc --- /dev/null +++ b/playground/plots/PlotSkin.cpp @@ -0,0 +1,160 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "PlotSkin.h" + +#include +#include + +#include +#include + +#include +#include + +#include +#include + +#include +#include + +#include "PlotCursor.h" +#include "PlotCursorSkinlet.h" + +#include +#include +#include +#include + +#include + +#include + +namespace +{ + inline bool isExtended( const QskSkin* skin ) + { + auto metaObject = skin->skinletMetaObject( &QskPlotView::staticMetaObject ); + return metaObject != &QskSkinlet::staticMetaObject; // the fallback for controls + } + + class SkinEditor : private QskSkinHintTableEditor + { + public: + SkinEditor( QskSkinHintTable* table ); + void setupPlotHints(); + }; +} + +void PlotSkin::extendSkin( QskSkin* skin ) +{ + if ( skin == nullptr || isExtended( skin ) ) + return; + + skin->declareSkinlet< QskPlotView, QskPlotViewSkinlet >(); + skin->declareSkinlet< QskPlotGrid, QskPlotGridSkinlet >(); + skin->declareSkinlet< QskPlotCurve, QskPlotCurveSkinlet >(); + skin->declareSkinlet< QskPlotCorridor, QskPlotCorridorSkinlet >(); + skin->declareSkinlet< PlotCursor, PlotCursorSkinlet >(); + + SkinEditor editor( &skin->hintTable() ); + editor.setupPlotHints(); +} + +SkinEditor::SkinEditor( QskSkinHintTable* table ) + : QskSkinHintTableEditor( table ) +{ +} + +void SkinEditor::setupPlotHints() +{ + using A = QskAspect; + using namespace QskRgb; + + const auto rgbLower = DodgerBlue; + const auto rgbUpper = MediumSeaGreen; + const auto rgbValue = Yellow; + { + using Q = QskPlotView; + + // Panel + setBoxShape( Q::Panel, 10 ); + setGradient( Q::Panel, qRgb( 240, 240, 240 ) ); + setBoxBorderMetrics( Q::Panel, 1 ); + setBoxBorderColors( Q::Panel, qRgb( 220, 220, 220 ) ); + + // Canvas + setGradient( Q::Canvas, QGradient::PremiumDark ); + setBoxBorderColors( Q::Canvas, DimGray ); + setBoxBorderMetrics( Q::Canvas, 2 ); + setBoxShape( Q::Canvas, 4 ); + + // AxisScale + const auto padding = 4; // spacing between canvas and axis + setPadding( Q::AxisScale | A::Left, 0, 0, padding, 0 ); + setPadding( Q::AxisScale | A::Bottom, 0, padding, 0, 0 ); + + setColor( Q::AxisScale, qRgb( 20, 20, 20 ) ); + setFontRole( Q::AxisScale, QskSkin::MediumFont ); + setFlag( Q::AxisScale | A::Style, QskScaleRenderer::Backbone ); + + // thickness/length of the major ticks + setStrutSize( Q::AxisScale, 1.0, 8.0 ); + + // spacing between ticks and labels + setSpacing( Q::AxisScale, 5 ); + } + + { + using Q = QskPlotGrid; + + setColor( Q::MajorLine, White ); + setMetric( Q::MajorLine | A::Size, 1 ); + setStippleMetrics( Q::MajorLine, { 2, 6 } ); + + setColor( Q::MinorLine, Gainsboro ); + setMetric( Q::MinorLine | A::Size, 1 ); + setStippleMetrics( Q::MinorLine, { 1, 10 } ); + } + + { + using Q = QskPlotCurve; + + setMetric( Q::Line | A::Size, 2 ); + setColor( Q::Line, rgbValue ); + } + + { + using Q = QskPlotCorridor; + + setMetric( Q::Border | A::Size, 2 ); + setColor( Q::Border | A::Lower, rgbLower ); + setColor( Q::Border | A::Upper, rgbUpper ); + setColor( Q::Corridor, toTransparent( Crimson, 150 ) ); + } + + { + using Q = PlotCursor; + + const int alpha = 200; + + setColor( Q::Line, Qt::yellow ); + setMetric( Q::Line | A::Size, 1 ); + setStippleMetrics( Q::Line, { 4, 8 } ); + + setGradient( Q::LabelPanel | A::Lower, toTransparent( rgbLower, alpha ) ); + setColor( Q::LabelText | A::Lower, Qt::white ); + + setGradient( Q::LabelPanel | A::Upper, toTransparent( rgbUpper, alpha ) ); + setColor( Q::LabelText | A::Upper, Qt::white ); + + setGradient( Q::LabelPanel, toTransparent( rgbValue, alpha ) ); + setColor( Q::LabelText, Qt::black ); + + setBoxShape( Q::LabelPanel, 5 ); + setPadding( Q::LabelPanel, 5, 5, 5, 5 ); + } +} + diff --git a/playground/plots/PlotSkin.h b/playground/plots/PlotSkin.h new file mode 100644 index 00000000..946344c3 --- /dev/null +++ b/playground/plots/PlotSkin.h @@ -0,0 +1,13 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +class QskSkin; + +namespace PlotSkin +{ + void extendSkin( QskSkin* ); +}; diff --git a/playground/plots/QskPlotCorridor.cpp b/playground/plots/QskPlotCorridor.cpp new file mode 100644 index 00000000..a7144e52 --- /dev/null +++ b/playground/plots/QskPlotCorridor.cpp @@ -0,0 +1,156 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "QskPlotCorridor.h" +#include "QskPlotCorridorData.h" + +#include + +#include +#include + +QSK_SUBCONTROL( QskPlotCorridor, Border ) +QSK_SUBCONTROL( QskPlotCorridor, Corridor ) + +class QskPlotCorridor::PrivateData +{ + public: + QPointer< QskPlotCorridorData > corridorData; +}; + +QskPlotCorridor::QskPlotCorridor( QObject* object ) + : Inherited( object ) + , m_data( new PrivateData ) +{ +} + +QskPlotCorridor::~QskPlotCorridor() +{ +} + +void QskPlotCorridor::setBorderWidth( qreal lineWidth ) +{ + const auto aspect = Border | QskAspect::Size; + + resetMetric( aspect | QskAspect::Lower ); + resetMetric( aspect | QskAspect::Upper ); + + lineWidth = qMax( lineWidth, 0.0 ); + + if ( setMetric( aspect, lineWidth ) ) + { + markDirty(); + Q_EMIT borderWidthChanged( lineWidth ); + } +} + +qreal QskPlotCorridor::borderWidth() const +{ + return metric( Border | QskAspect::Size ); +} + +void QskPlotCorridor::setBorderColor( const QColor& color ) +{ + resetColor( Border | QskAspect::Lower ); + resetColor( Border | QskAspect::Upper ); + + if ( setColor( Border, color ) ) + { + markDirty(); + Q_EMIT colorChanged( color ); + } +} + +QColor QskPlotCorridor::borderColor() const +{ + return color( Border ); +} + +void QskPlotCorridor::setColor( const QColor& color ) +{ + if ( setColor( Corridor, color ) ) + { + markDirty(); + Q_EMIT colorChanged( color ); + } +} + +QColor QskPlotCorridor::color() const +{ + return color( Corridor ); +} + +void QskPlotCorridor::setSamples( const QVector< QskPlotCorridorSample >& samples ) +{ + setData( new QskPlotCorridorSamples( samples, this ) ); +} + +void QskPlotCorridor::setData( QskPlotCorridorData* corridorData ) +{ + if ( corridorData == m_data->corridorData ) + return; + + auto oldData = m_data->corridorData.data(); + m_data->corridorData = corridorData; + + if ( oldData ) + { + if ( oldData->parent() == this ) + { + delete oldData; + } + else + { + disconnect( oldData, &QskPlotCorridorData::changed, + this, &QskPlotItem::markDirty ); + } + } + + if ( corridorData ) + { + if ( corridorData->parent() == nullptr ) + corridorData->setParent( this ); + + connect( corridorData, &QskPlotCorridorData::changed, + this, &QskPlotItem::markDirty ); + } + + markDirty(); +} + +QskPlotCorridorData* QskPlotCorridor::data() const +{ + return m_data->corridorData; +} + +QskPlotCorridorSample QskPlotCorridor::interpolatedSample( qreal value ) const +{ + if ( m_data->corridorData ) + return m_data->corridorData->interpolatedSample( value ); + + return QskPlotCorridorSample(); +} + +void QskPlotCorridor::transformationChanged( ChangeFlags flags ) +{ + Inherited::transformationChanged( flags ); +} + +bool QskPlotCorridor::needsClipping() const +{ + auto data = m_data->corridorData.data(); + if ( data == nullptr || data->count() == 0 ) + return false; + + // The skinlet does basic polygon clipping in x direction + + const auto corridorRect = data->boundingRect(); + const auto plotRect = scaleRect(); + + return ( corridorRect.top() < plotRect.top() ) + || ( corridorRect.bottom() > plotRect.bottom() ); +} + +#include "moc_QskPlotCorridor.cpp" diff --git a/playground/plots/QskPlotCorridor.h b/playground/plots/QskPlotCorridor.h new file mode 100644 index 00000000..b375482d --- /dev/null +++ b/playground/plots/QskPlotCorridor.h @@ -0,0 +1,67 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include "QskPlotItem.h" +#include + +class QskPlotCorridorSample; +class QskPlotCorridorData; +class QColor; + +// Only horizontal: TODO +class QskPlotCorridor : public QskPlotItem +{ + Q_OBJECT + + using Inherited = QskPlotItem; + + Q_PROPERTY( qreal borderWidth READ borderWidth + WRITE setBorderWidth NOTIFY borderWidthChanged ) + + Q_PROPERTY( QColor borderColor READ borderColor + WRITE setBorderColor NOTIFY borderColorChanged ) + + Q_PROPERTY( QColor color READ color + WRITE setColor NOTIFY colorChanged ) + + public: + QSK_SUBCONTROLS( Border, Corridor ) + + QskPlotCorridor( QObject* = nullptr ); + ~QskPlotCorridor() override; + + void setBorderWidth( qreal ); + qreal borderWidth() const; + + void setBorderColor( const QColor& ); + QColor borderColor() const; + + void setColor( const QColor& ); + QColor color() const; + + void setSamples( const QVector< QskPlotCorridorSample >& ); + + void setData( QskPlotCorridorData* ); + QskPlotCorridorData* data() const; + + QskPlotCorridorSample interpolatedSample( qreal value ) const; + + void transformationChanged( ChangeFlags ) override; + bool needsClipping() const override; + + using QskSkinnable::setColor; + using QskSkinnable::color; + + Q_SIGNALS: + void borderWidthChanged( qreal ); + void borderColorChanged( const QColor& ); + void colorChanged( const QColor& ); + + private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; diff --git a/playground/plots/QskPlotCorridorData.cpp b/playground/plots/QskPlotCorridorData.cpp new file mode 100644 index 00000000..55b40c57 --- /dev/null +++ b/playground/plots/QskPlotCorridorData.cpp @@ -0,0 +1,138 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "QskPlotCorridorData.h" + +namespace +{ + inline int upperIndex( const QskPlotCorridorData* data, qreal value ) + { + const int indexMax = data->count() - 1; + + if ( indexMax < 0 || data->sampleAt( indexMax ).value < value ) + return -1; + + int indexMin = 0; + int n = indexMax; + + while ( n > 0 ) + { + const int half = n >> 1; + const int indexMid = indexMin + half; + + if ( value < data->sampleAt( indexMid ).value ) + { + n = half; + } + else + { + indexMin = indexMid + 1; + n -= half + 1; + } + } + + return indexMin; + } + + QRectF boundingRect( const QskPlotCorridorData* data ) + { + const auto count = data->count(); + if ( count <= 0 ) + return QRectF(); + + auto boundary = data->sampleAt( 0 ).boundary; + + for ( int i = 1; i < count; i++ ) + boundary.unite( data->sampleAt( i ).boundary ); + + const auto x1 = data->sampleAt( 0 ).value; + const auto x2 = data->sampleAt( count - 1 ).value; + + return QRectF( x1, boundary.lowerBound(), + x2 - x1, boundary.length() ).normalized(); + } +} + +QskPlotCorridorData::QskPlotCorridorData( QObject* parent ) + : QObject( parent ) +{ +} + +QskPlotCorridorData::~QskPlotCorridorData() +{ +} + +QRectF QskPlotCorridorData::boundingRect() const +{ + if ( m_boundingRect.isNull() ) + m_boundingRect = ::boundingRect( this ); + + return m_boundingRect; +} + +int QskPlotCorridorData::upperIndex( qreal value ) const +{ + const int n = count(); + + if ( n == 0 ) + return -1; + + auto index = ::upperIndex( this, value ); + if ( ( index == -1 ) && ( value == sampleAt( n - 1 ).value ) ) + index = n - 1; + + return index; +} + +QskPlotCorridorSample QskPlotCorridorData::interpolatedSample( qreal value ) const +{ + const int n = count(); + if ( n == 0 ) + return QskPlotCorridorSample(); + + if ( n == 1 ) + return { value, { 0.0, 0.0 } }; + + int index = 0; + + if ( n > 2 ) + { + index = upperIndex( value ); + if ( index > 0 ) + index--; + } + + const auto s1 = sampleAt( index ); + const auto s2 = sampleAt( index + 1 ); + + const auto dv = s2.value - s1.value; + if ( dv == 0.0 ) + return s2; + + const auto t = ( value - s1.value ) / dv; + return { value, s1.boundary.interpolated( s2.boundary, t ) }; +} + +QskPlotCorridorSamples::QskPlotCorridorSamples( QObject* parent ) + : QskPlotCorridorData( parent ) +{ +} + +QskPlotCorridorSamples::QskPlotCorridorSamples( + const QVector< QskPlotCorridorSample >& samples, QObject* parent ) + : QskPlotCorridorData( parent ) + , m_samples( samples ) +{ +} + +void QskPlotCorridorSamples::setSamples( const QVector< QskPlotCorridorSample >& samples ) +{ + m_samples = samples; + m_boundingRect = QRectF(); // invalidating + + Q_EMIT changed(); +} + +#include "moc_QskPlotCorridorData.cpp" diff --git a/playground/plots/QskPlotCorridorData.h b/playground/plots/QskPlotCorridorData.h new file mode 100644 index 00000000..3fac0b7a --- /dev/null +++ b/playground/plots/QskPlotCorridorData.h @@ -0,0 +1,82 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include + +#include +#include +#include + +class QskPlotCorridorSample +{ + public: + qreal value = 0.0; + QskIntervalF boundary; +}; + +Q_DECLARE_TYPEINFO( QskPlotCorridorSample, Q_MOVABLE_TYPE ); + +// Hiding the layout of the data behind an abstract API +class QskPlotCorridorData : public QObject +{ + Q_OBJECT + + public: + QskPlotCorridorData( QObject* parent = nullptr ); + virtual ~QskPlotCorridorData(); + + virtual qsizetype count() const = 0; + virtual QskPlotCorridorSample sampleAt( qsizetype index ) const = 0; + + virtual QRectF boundingRect() const; + + int upperIndex( qreal value ) const; + QskPlotCorridorSample interpolatedSample( qreal value ) const; + + Q_SIGNALS: + void changed(); + + protected: + mutable QRectF m_boundingRect; +}; + +// A simple implementation using QVector< CorridorSample > +class QskPlotCorridorSamples : public QskPlotCorridorData +{ + Q_OBJECT + + using Inherited = QskPlotCorridorData; + + public: + QskPlotCorridorSamples( QObject* parent = nullptr ); + QskPlotCorridorSamples( + const QVector< QskPlotCorridorSample >&, QObject* parent = nullptr ); + + void setSamples( const QVector< QskPlotCorridorSample >& ); + QVector< QskPlotCorridorSample > samples() const; + + qsizetype count() const override; + QskPlotCorridorSample sampleAt( qsizetype index ) const override; + + private: + QVector< QskPlotCorridorSample > m_samples; +}; + +inline QVector< QskPlotCorridorSample > QskPlotCorridorSamples::samples() const +{ + return m_samples; +} + +inline qsizetype QskPlotCorridorSamples::count() const +{ + return m_samples.count(); +} + +inline QskPlotCorridorSample QskPlotCorridorSamples::sampleAt( qsizetype index ) const +{ + return m_samples.at( index ); +} diff --git a/playground/plots/QskPlotCorridorSkinlet.cpp b/playground/plots/QskPlotCorridorSkinlet.cpp new file mode 100644 index 00000000..e7ab51bf --- /dev/null +++ b/playground/plots/QskPlotCorridorSkinlet.cpp @@ -0,0 +1,261 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskPlotCorridorSkinlet.h" +#include "QskPlotCorridor.h" +#include "QskPlotCorridorData.h" + +#include +#include + +#include +#include + +namespace +{ + class GeometryNode : public QSGGeometryNode + { + protected: + GeometryNode() + : m_geometry( QSGGeometry::defaultAttributes_ColoredPoint2D(), 0 ) + { + setGeometry( &m_geometry ); + setMaterial( &m_material ); + } + + private: + QSGGeometry m_geometry; + QSGVertexColorMaterial m_material; + }; + + class CorridorNode : public GeometryNode + { + public: + void updateCorridor( const QskPlotCorridorData* data, + const QskPlotCorridorSample& sample1, int index1, + const QskPlotCorridorSample& sample2, int index2, + const QColor& color ) + { + using namespace QskVertex; + + const Color vertexColor( color ); + + geometry()->setDrawingMode( QSGGeometry::DrawTriangleStrip ); + + auto line = allocateLines< ColoredLine >( *geometry(), index2 - index1 + 1 ); + + line++->setLine( sample1.value, sample1.boundary.lowerBound(), + sample1.value, sample1.boundary.upperBound(), vertexColor ); + + for ( int i = index1 + 1; i < index2; i++ ) + { + const auto sample = data->sampleAt( i ); + line++->setLine( sample.value, sample.boundary.lowerBound(), + sample.value, sample.boundary.upperBound(), vertexColor ); + } + + line++->setLine( sample2.value, sample2.boundary.lowerBound(), + sample2.value, sample2.boundary.upperBound(), vertexColor ); + + markDirty( QSGNode::DirtyGeometry ); + } + }; + + class BorderNode : public GeometryNode + { + public: + void updateBorder( const QskPlotCorridorData* data, + const QskPlotCorridorSample& sample1, int index1, + const QskPlotCorridorSample& sample2, int index2, + quint8 nodeRole, const QColor& color, qreal lineWidth ) + { + auto& geometry = *this->geometry(); + + geometry.setDrawingMode( QSGGeometry::DrawLineStrip ); + + const float lineWidthF = lineWidth; + if( lineWidthF != geometry.lineWidth() ) + geometry.setLineWidth( lineWidthF ); + + const QskVertex::Color c( color ); + + geometry.allocate( index2 - index1 + 1 ); + + auto p = geometry.vertexDataAsColoredPoint2D(); + + if( nodeRole == QskPlotCorridorSkinlet::LowerBoundRole ) + { + p++->set( sample1.value, sample1.boundary.lowerBound(), + c.r, c.g, c.b, c.a ); + + for ( int i = index1 + 1; i < index2; i++ ) + { + const auto sample = data->sampleAt( i ); + p++->set( sample.value, sample.boundary.lowerBound(), + c.r, c.g, c.b, c.a ); + } + + p++->set( sample2.value, sample2.boundary.lowerBound(), + c.r, c.g, c.b, c.a ); + } + else + { + p++->set( sample1.value, sample1.boundary.upperBound(), + c.r, c.g, c.b, c.a ); + + for ( int i = index1 + 1; i < index2; i++ ) + { + const auto sample = data->sampleAt( i ); + p++->set( sample.value, sample.boundary.upperBound(), + c.r, c.g, c.b, c.a ); + } + + p++->set( sample2.value, sample2.boundary.upperBound(), + c.r, c.g, c.b, c.a ); + } + + markDirty( QSGNode::DirtyGeometry ); + } + }; +} + +class QskPlotCorridorSkinlet::PrivateData +{ + public: + int index1, index2; + QskPlotCorridorSample sample1, sample2; +}; + +QskPlotCorridorSkinlet::QskPlotCorridorSkinlet( QskSkin* skin ) + : Inherited( skin ) + , m_data( new PrivateData ) +{ + setNodeRoles( { CorridorRole, LowerBoundRole, UpperBoundRole } ); +} + +QskPlotCorridorSkinlet::~QskPlotCorridorSkinlet() +{ +} + +void QskPlotCorridorSkinlet::updateNode( + QskSkinnable* skinnable, QSGNode* parent ) const +{ + /* + As clipping is the same for borders and corridor + we do it only once here + */ + + auto corridor = static_cast< const QskPlotCorridor* >( skinnable ); + const auto data = corridor->data(); + + m_data->index1 = m_data->index2 = -1; + + const auto n = data->count(); + if ( data && n > 0 ) + { + auto& s1 = m_data->sample1; + auto& s2 = m_data->sample2; + + s1 = data->sampleAt( 0 ); + s2 = data->sampleAt( n - 1 ); + + const auto scaleRect = corridor->scaleRect(); + + const qreal x1 = scaleRect.left(); + const qreal x2 = scaleRect.right(); + + if ( !( x1 > s2.value || x2 < s1.value ) ) + { + m_data->index1 = 0; + m_data->index2 = n - 1; + + const int index1 = data->upperIndex( x1 ); + if ( index1 > 0 ) + { + m_data->index1 = index1 - 1; + s1 = data->interpolatedSample( x1 ); + } + + const int index2 = data->upperIndex( x2 ); + if ( index2 > 0 ) + { + m_data->index2 = index2; + s2 = data->interpolatedSample( x2 ); + } + } + } + + Inherited::updateNode( skinnable, parent ); +} + +QSGNode* QskPlotCorridorSkinlet::updateSubNode( + const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const +{ + if ( m_data->index2 < 0 ) + return nullptr; + + if ( nodeRole == CorridorRole ) + return updateCorridorNode( skinnable, node ); + else + return updateBorderNode( skinnable, nodeRole, node ); +} + +QSGNode* QskPlotCorridorSkinlet::updateCorridorNode( + const QskSkinnable* skinnable, QSGNode* node ) const +{ + using Q = QskPlotCorridor; + + auto corridor = static_cast< const QskPlotCorridor* >( skinnable ); + + const auto color = corridor->color( Q::Corridor ); + if ( !color.isValid() || color.alpha() == 0 ) + return nullptr; + + const auto corridorData = corridor->data(); + if ( corridorData->count() == 0 ) + return nullptr; + + auto corridorNode = QskSGNode::ensureNode< CorridorNode >( node ); + + corridorNode->updateCorridor( corridorData, + m_data->sample1, m_data->index1, m_data->sample2, m_data->index2, color ); + + return corridorNode; +} + +QSGNode* QskPlotCorridorSkinlet::updateBorderNode( + const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const +{ + using Q = QskPlotCorridor; + using A = QskAspect; + + auto corridor = static_cast< const QskPlotCorridor* >( skinnable ); + + const auto corridorData = corridor->data(); + if ( corridorData->count() == 0 ) + return nullptr; + + QColor color; + if( nodeRole == QskPlotCorridorSkinlet::LowerBoundRole ) + color = corridor->color( Q::Border | A::Lower ); + else + color = corridor->color( Q::Border | A::Upper ); + + if ( !color.isValid() || color.alpha() == 0 ) + return nullptr; + + auto lineWidth = corridor->metric( Q::Border | A::Size ); + lineWidth = qMax( lineWidth, 0.0 ); + + auto borderNode = QskSGNode::ensureNode< BorderNode >( node ); + + borderNode->updateBorder( corridorData, + m_data->sample1, m_data->index1, m_data->sample2, m_data->index2, + nodeRole, color, lineWidth ); + + return borderNode; +} + +#include "moc_QskPlotCorridorSkinlet.cpp" diff --git a/playground/plots/QskPlotCorridorSkinlet.h b/playground/plots/QskPlotCorridorSkinlet.h new file mode 100644 index 00000000..e79e8b32 --- /dev/null +++ b/playground/plots/QskPlotCorridorSkinlet.h @@ -0,0 +1,39 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#pragma once + +#include + +class QskPlotCorridorSkinlet : public QskSkinlet +{ + Q_GADGET + + using Inherited = QskSkinlet; + + public: + enum NodeRole + { + CorridorRole, + LowerBoundRole, + UpperBoundRole + }; + + Q_INVOKABLE QskPlotCorridorSkinlet( QskSkin* = nullptr ); + ~QskPlotCorridorSkinlet() override; + + void updateNode( QskSkinnable*, QSGNode* ) const override; + + protected: + QSGNode* updateSubNode( const QskSkinnable*, + quint8 nodeRole, QSGNode* ) const override; + + private: + QSGNode* updateCorridorNode( const QskSkinnable*, QSGNode* ) const; + QSGNode* updateBorderNode( const QskSkinnable*, quint8, QSGNode* ) const; + + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; diff --git a/playground/plots/QskPlotCurve.cpp b/playground/plots/QskPlotCurve.cpp new file mode 100644 index 00000000..d39c2b42 --- /dev/null +++ b/playground/plots/QskPlotCurve.cpp @@ -0,0 +1,156 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "QskPlotCurve.h" +#include "QskPlotCurveData.h" + +#include +#include + +QSK_SUBCONTROL( QskPlotCurve, Line ) + +class QskPlotCurve::PrivateData +{ + public: + QPointer< QskPlotCurveData > curveData; +}; + +QskPlotCurve::QskPlotCurve( QObject* object ) + : Inherited( object ) + , m_data( new PrivateData ) +{ +} + +QskPlotCurve::~QskPlotCurve() +{ +} + +void QskPlotCurve::setColor( const QColor& color ) +{ + if ( setColor( Line, color ) ) + { + markDirty(); + Q_EMIT colorChanged( color ); + } +} + +QColor QskPlotCurve::color() const +{ + return color( Line ); +} + +void QskPlotCurve::setLineWidth( qreal lineWidth ) +{ + lineWidth = qMax( lineWidth, 0.0 ); + + if ( setMetric( Line | QskAspect::Size, lineWidth ) ) + { + markDirty(); + Q_EMIT lineWidthChanged( lineWidth ); + } +} + +qreal QskPlotCurve::lineWidth() const +{ + return metric( Line | QskAspect::Size ); +} + +void QskPlotCurve::setPoints( const QVector< QPointF >& points ) +{ + setData( new QskPlotCurvePoints( points, this ) ); +} + +void QskPlotCurve::setData( QskPlotCurveData* curveData ) +{ + if ( curveData == m_data->curveData ) + return; + + auto oldData = m_data->curveData.data(); + m_data->curveData = curveData; + + if ( oldData ) + { + if ( oldData->parent() == this ) + delete oldData; + else + disconnect( oldData, &QskPlotCurveData::changed, this, &QskPlotItem::markDirty ); + } + + if ( curveData ) + { + if ( curveData->parent() == nullptr ) + curveData->setParent( this ); + + connect( curveData, &QskPlotCurveData::changed, this, &QskPlotItem::markDirty ); + } + + markDirty(); +} + +QskPlotCurveData* QskPlotCurve::data() const +{ + return m_data->curveData; +} + +QPointF QskPlotCurve::interpolatedPoint( + Qt::Orientation orientation, qreal value ) const +{ + if ( m_data->curveData ) + return m_data->curveData->interpolatedPoint( orientation, value ); + + return QPointF(); +} + +void QskPlotCurve::transformationChanged( ChangeFlags flags ) +{ + if ( flags & ( XBoundariesChanged | YBoundariesChanged ) ) + { + /* + We could skip updates, when the curve is inside + of old and new boundaries TODO ... + */ + } + + Inherited::transformationChanged( flags ); +} + +bool QskPlotCurve::needsClipping() const +{ + auto data = m_data->curveData.data(); + if ( data == nullptr || data->count() == 0 ) + return false; + + // The skinlet does basic polygon clipping for monotonic data. + + using D = QskPlotCurveData; + + const auto hints = data->hints(); + if ( ( hints & D::MonotonicX ) && ( hints & D::MonotonicY ) ) + return false; + + if ( data->hints() & QskPlotCurveData::BoundingRectangle ) + { + const auto curveRect = data->boundingRect(); + const auto plotRect = scaleRect(); + + if ( hints & D::MonotonicX ) + { + return ( curveRect.top() < plotRect.top() ) + || ( curveRect.bottom() > plotRect.bottom() ); + } + + if ( hints & D::MonotonicY ) + { + return ( curveRect.left() < plotRect.left() ) + && ( curveRect.right() > plotRect.right() ); + } + + return !plotRect.contains( curveRect ); + } + + return true; +} + +#include "moc_QskPlotCurve.cpp" diff --git a/playground/plots/QskPlotCurve.h b/playground/plots/QskPlotCurve.h new file mode 100644 index 00000000..cf5c77e3 --- /dev/null +++ b/playground/plots/QskPlotCurve.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include "QskPlotItem.h" + +#include +#include + +class QskPlotCurveData; +class QColor; + +class QskPlotCurve : public QskPlotItem +{ + Q_OBJECT + + Q_PROPERTY( qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged ) + Q_PROPERTY( QColor color READ color WRITE setColor NOTIFY colorChanged ) + + using Inherited = QskPlotItem; + + public: + QSK_SUBCONTROLS( Line ) + + QskPlotCurve( QObject* = nullptr ); + ~QskPlotCurve() override; + + void setPoints( const QVector< QPointF >& ); + + void setData( QskPlotCurveData* ); + QskPlotCurveData* data() const; + + QPointF interpolatedPoint( Qt::Orientation, qreal ) const; + + void setColor( const QColor& ); + QColor color() const; + + void setLineWidth( qreal ); + qreal lineWidth() const; + + void transformationChanged( ChangeFlags ) override; + bool needsClipping() const override; + + using QskSkinnable::setColor; + using QskSkinnable::color; + + Q_SIGNALS: + void lineWidthChanged( qreal ); + void colorChanged( const QColor& ); + + private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; diff --git a/playground/plots/QskPlotCurveData.cpp b/playground/plots/QskPlotCurveData.cpp new file mode 100644 index 00000000..66b76ac9 --- /dev/null +++ b/playground/plots/QskPlotCurveData.cpp @@ -0,0 +1,285 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "QskPlotCurveData.h" + +#include + +#include +#include + +namespace +{ + struct compareX + { + inline bool operator()( const double x, const QPointF& pos ) const + { + return ( x < pos.x() ); + } + }; + + struct compareY + { + inline bool operator()( const double y, const QPointF& pos ) const + { + return ( y < pos.y() ); + } + }; + + template< typename LessThan > + inline int upperIndex( + const QskPlotCurveData* data, qreal value, LessThan lessThan ) + { + const int indexMax = data->count() - 1; + + if ( indexMax < 0 || !lessThan( value, data->pointAt( indexMax ) ) ) + return -1; + + int indexMin = 0; + int n = indexMax; + + while ( n > 0 ) + { + const int half = n >> 1; + const int indexMid = indexMin + half; + + if ( lessThan( value, data->pointAt( indexMid ) ) ) + { + n = half; + } + else + { + indexMin = indexMid + 1; + n -= half + 1; + } + } + + return indexMin; + } +} + +namespace +{ + QRectF boundingRect( const QskPlotCurveData* data ) + { + const auto count = data->count(); + if ( count <= 0 ) + return QRectF(); + + const auto hints = data->hints(); + + const bool montonicX = hints & QskPlotCurveData::MonotonicX; + const bool montonicY = hints & QskPlotCurveData::MonotonicY; + + if ( montonicX && montonicY ) + { + const auto p1 = data->pointAt( 0 ); + const auto p2 = data->pointAt( count - 1 ); + + return QRectF( p1, p2 ).normalized(); + } + + if ( montonicX ) + { + const auto p1 = data->pointAt( 0 ); + const auto p2 = data->pointAt( count - 1 ); + + qreal yMin, yMax; + yMin = yMax = p1.y(); + + for ( int i = 1; i < count; i++ ) + { + const auto p = data->pointAt( i ); + + if ( p.y() < yMin ) + yMin = p.y(); + else if ( p.y() > yMax ) + yMax = p.y(); + } + + return QRectF( p1.x(), yMin, p2.x() - p1.x(), yMax - yMin ).normalized(); + } + + if ( montonicY ) + { + const auto p1 = data->pointAt( 0 ); + const auto p2 = data->pointAt( count - 1 ); + + qreal xMin, xMax; + xMin = xMax = p1.x(); + + for ( int i = 1; i < count; i++ ) + { + const auto p = data->pointAt( i ); + + if ( p.x() < xMin ) + xMin = p.x(); + else if ( p.x() > xMax ) + xMax = p.x(); + } + + return QRectF( xMin, p1.y(), xMax - xMin, p2.y() - p1.y() ).normalized(); + } + + { + const auto p1 = data->pointAt( 0 ); + + qreal xMin, xMax; + qreal yMin, yMax; + + xMin = xMax = p1.x(); + yMin = yMax = p1.y(); + + for ( int i = 1; i < count; i++ ) + { + const auto p = data->pointAt( i ); + + if ( p.x() < xMin ) + xMin = p.x(); + else if ( p.x() > xMax ) + xMax = p.x(); + + if ( p.y() < yMin ) + yMin = p.y(); + else if ( p.y() > yMax ) + yMax = p.y(); + } + + return QRectF( xMin, yMin, xMax - xMin, yMax - yMin ); + } + } +} + +QskPlotCurveData::QskPlotCurveData( QObject* parent ) + : QObject( parent ) +{ +} + +QskPlotCurveData::~QskPlotCurveData() +{ +} + +void QskPlotCurveData::setHints( Hints hints ) +{ + if ( m_hints != hints ) + { + m_hints = hints; + Q_EMIT changed(); + } +} + +void QskPlotCurveData::setHint( Hint hint, bool on ) +{ + if ( on ) + setHints( m_hints | hint ); + else + setHints( m_hints & ~hint ); +} + +QRectF QskPlotCurveData::boundingRect() const +{ + if ( m_boundingRect.isNull() ) + m_boundingRect = ::boundingRect( this ); + + return m_boundingRect; +} + +int QskPlotCurveData::upperIndex( Qt::Orientation orientation, qreal value ) const +{ + const int n = count(); + + if ( n == 0 ) + return -1; + + int index; + + if ( orientation == Qt::Horizontal ) + { + index = ::upperIndex( this, value, compareX() ); + if ( ( index == -1 ) && ( value == pointAt( n - 1 ).x() ) ) + index = n - 1; + } + else + { + index = ::upperIndex( this, value, compareY() ); + if ( ( index == -1 ) && ( value == pointAt( n - 1 ).y() ) ) + index = n - 1; + } + + return index; +} + +QPointF QskPlotCurveData::interpolatedPoint( + Qt::Orientation orientation, qreal value ) const +{ + const int n = count(); + if ( n == 0 ) + return QPointF(); + + if ( n == 1 ) + { + if ( orientation == Qt::Horizontal ) + return QPointF( value, 0.0 ); + else + return QPointF( 0.0, value ); + } + + int index = 0; + + if ( n > 2 ) + { + index = upperIndex( orientation, value ); + if ( index > 0 ) + index--; + } + + const auto p1 = pointAt( index ); + const auto p2 = pointAt( index + 1 ); + + if ( orientation == Qt::Horizontal ) + { + const auto dx = p2.x() - p1.x(); + if ( dx == 0.0 ) + return p2; + + const auto t = ( value - p1.x() ) / dx; + const auto y = p1.y() + t * ( p2.y() - p1.y() ); + + return QPointF( value, y ); + } + else + { + const auto dy = p2.y() - p1.y(); + if ( dy == 0.0 ) + return p2; + + const auto t = ( value - p1.y() ) / dy; + const auto x = p1.x() + t * ( p2.x() - p1.x() ); + + return QPointF( x, value ); + } +} + +QskPlotCurvePoints::QskPlotCurvePoints( QObject* parent ) + : QskPlotCurveData( parent ) +{ +} + +QskPlotCurvePoints::QskPlotCurvePoints( + const QVector< QPointF >& points, QObject* parent ) + : QskPlotCurveData( parent ) + , m_points( points ) +{ +} + +void QskPlotCurvePoints::setPoints( const QVector< QPointF >& points ) +{ + m_points = points; + m_boundingRect = QRectF(); // invalidating + + Q_EMIT changed(); +} + +#include "moc_QskPlotCurveData.cpp" diff --git a/playground/plots/QskPlotCurveData.h b/playground/plots/QskPlotCurveData.h new file mode 100644 index 00000000..fe802566 --- /dev/null +++ b/playground/plots/QskPlotCurveData.h @@ -0,0 +1,105 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +// Hiding the layout of the data behind an abstract API +class QskPlotCurveData : public QObject +{ + Q_OBJECT + + public: + enum Hint + { + /* + The points are monotonic in/decreasing. F.e a faster algos + can be implemented with this information ( f.e polygon clipping ) + */ + MonotonicX = 1 << 0, + MonotonicY = 1 << 1, + + /* + The data offers a bounding rectangle, that can f.e be used for + clipping or autoscaling purposes + */ + BoundingRectangle = 1 << 2 + }; + Q_ENUM( Hint ); + + Q_DECLARE_FLAGS( Hints, Hint ) + + QskPlotCurveData( QObject* parent = nullptr ); + virtual ~QskPlotCurveData(); + + void setHints( Hints ); + Hints hints() const; + + void setHint( Hint, bool on = true ); + + virtual qsizetype count() const = 0; + virtual QPointF pointAt( qsizetype index ) const = 0; + + virtual QRectF boundingRect() const; + + int upperIndex( Qt::Orientation, qreal value ) const; + QPointF interpolatedPoint( Qt::Orientation, qreal value ) const; + + Q_SIGNALS: + void changed(); + + protected: + mutable QRectF m_boundingRect; + + private: + Hints m_hints = BoundingRectangle; +}; + +inline QskPlotCurveData::Hints QskPlotCurveData::hints() const +{ + return m_hints; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS( QskPlotCurveData::Hints ) + +// A simple implementation using QVector< QPointF > +class QskPlotCurvePoints : public QskPlotCurveData +{ + Q_OBJECT + + using Inherited = QskPlotCurveData; + + public: + QskPlotCurvePoints( QObject* parent = nullptr ); + QskPlotCurvePoints( const QVector< QPointF >&, QObject* parent = nullptr ); + + void setPoints( const QVector< QPointF >& ); + QVector< QPointF > points() const; + + qsizetype count() const override; + QPointF pointAt( qsizetype index ) const override; + + private: + QVector< QPointF > m_points; +}; + +inline QVector< QPointF > QskPlotCurvePoints::points() const +{ + return m_points; +} + +inline qsizetype QskPlotCurvePoints::count() const +{ + return m_points.count(); +} + +inline QPointF QskPlotCurvePoints::pointAt( qsizetype index ) const +{ + return m_points.at( index ); +} diff --git a/playground/plots/QskPlotCurveSkinlet.cpp b/playground/plots/QskPlotCurveSkinlet.cpp new file mode 100644 index 00000000..0731c94f --- /dev/null +++ b/playground/plots/QskPlotCurveSkinlet.cpp @@ -0,0 +1,163 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskPlotCurveSkinlet.h" +#include "QskPlotCurveData.h" +#include "QskPlotCurve.h" + +#include +#include + +#include +#include + +namespace +{ + class CurveNode : public QSGGeometryNode + { + public: + CurveNode() + : m_geometry( QSGGeometry::defaultAttributes_ColoredPoint2D(), 0 ) + { + setGeometry( &m_geometry ); + setMaterial( &m_material ); + } + + void updateCurve( const QRectF& scaleRect, const QskPlotCurveData* data, + const QColor& color, qreal lineWidth ) + { + m_geometry.setDrawingMode( QSGGeometry::DrawLineStrip ); + + const float lineWidthF = lineWidth; + if( lineWidthF != m_geometry.lineWidth() ) + m_geometry.setLineWidth( lineWidthF ); + + const QskVertex::Color c( color ); + + int from = 0; + int to = data->count() - 1; + + auto point1 = data->pointAt( from ); + auto point2 = data->pointAt( to ); + + if ( data->hints() & QskPlotCurveData::MonotonicX ) + { + const qreal x1 = scaleRect.left(); + const qreal x2 = scaleRect.right(); + + if ( x1 > point2.x() || x2 < point1.x() ) + { + QskSGNode::resetGeometry( this ); + return; + } + + const int index1 = data->upperIndex( Qt::Horizontal, x1 ); + if ( index1 > 0 ) + { + from = index1 - 1; + point1 = data->interpolatedPoint( Qt::Horizontal, x1 ); + } + + const int index2 = data->upperIndex( Qt::Horizontal, x2 ); + if ( index2 > 0 ) + { + to = index2; + point2 = data->interpolatedPoint( Qt::Horizontal, x2 ); + } + } + else if ( data->hints() & QskPlotCurveData::MonotonicY ) + { + const qreal y1 = scaleRect.top(); + const qreal y2 = scaleRect.bottom(); + + if ( y1 > point2.y() || y2 < point1.y() ) + { + QskSGNode::resetGeometry( this ); + return; + } + + const int index1 = data->upperIndex( Qt::Vertical, y1 ); + if ( index1 > 0 ) + { + from = index1 - 1; + point1 = data->interpolatedPoint( Qt::Vertical, y1 ); + } + + const int index2 = data->upperIndex( Qt::Vertical, y2 ); + if ( index2 > 0 ) + { + to = index2; + point2 = data->interpolatedPoint( Qt::Vertical, y2 ); + } + } + + m_geometry.allocate( to - from + 1 ); + + auto p = m_geometry.vertexDataAsColoredPoint2D(); + + p++->set( point1.x(), point1.y(), c.r, c.g, c.b, c.a ); + + for ( int i = from + 1; i < to; i++ ) + { + const auto point = data->pointAt( i ); + p++->set( point.x(), point.y(), c.r, c.g, c.b, c.a ); + } + + p++->set( point2.x(), point2.y(), c.r, c.g, c.b, c.a ); + + markDirty( QSGNode::DirtyGeometry ); + } + + private: + QSGGeometry m_geometry; + QSGVertexColorMaterial m_material; + }; +} + +QskPlotCurveSkinlet::QskPlotCurveSkinlet( QskSkin* skin ) + : Inherited( skin ) +{ + setNodeRoles( { Polygon } ); +} + +QskPlotCurveSkinlet::~QskPlotCurveSkinlet() +{ +} + +QSGNode* QskPlotCurveSkinlet::updateSubNode( + const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const +{ + if ( nodeRole == Polygon ) + return updatePolygonNode( skinnable, node ); + + return Inherited::updateSubNode( skinnable, nodeRole, node ); +} + +QSGNode* QskPlotCurveSkinlet::updatePolygonNode( + const QskSkinnable* skinnable, QSGNode* node ) const +{ + using Q = QskPlotCurve; + + auto curve = static_cast< const QskPlotCurve* >( skinnable ); + + const auto curveData = curve->data(); + if ( curveData == nullptr || curveData->count() == 0 ) + return nullptr; + + const auto color = curve->color( Q::Line ); + if ( !color.isValid() || color.alpha() == 0 ) + return nullptr; + + const auto lineWidth = curve->metric( Q::Line | QskAspect::Size ); + if ( lineWidth <= 0.0 ) + return nullptr; + + auto curveNode = QskSGNode::ensureNode< CurveNode >( node ); + curveNode->updateCurve( curve->scaleRect(), curveData, color, lineWidth ); + + return curveNode; +} + +#include "moc_QskPlotCurveSkinlet.cpp" diff --git a/playground/plots/QskPlotCurveSkinlet.h b/playground/plots/QskPlotCurveSkinlet.h new file mode 100644 index 00000000..070487ba --- /dev/null +++ b/playground/plots/QskPlotCurveSkinlet.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#pragma once + +#include + +class QskPlotCurveSkinlet : public QskSkinlet +{ + Q_GADGET + + using Inherited = QskSkinlet; + + public: + enum NodeRole { Polygon }; + + Q_INVOKABLE QskPlotCurveSkinlet( QskSkin* = nullptr ); + ~QskPlotCurveSkinlet() override; + + protected: + QSGNode* updateSubNode( const QskSkinnable*, + quint8 nodeRole, QSGNode* ) const override; + + private: + QSGNode* updatePolygonNode( const QskSkinnable*, QSGNode* ) const; +}; diff --git a/playground/plots/QskPlotGrid.cpp b/playground/plots/QskPlotGrid.cpp new file mode 100644 index 00000000..06d27cd2 --- /dev/null +++ b/playground/plots/QskPlotGrid.cpp @@ -0,0 +1,137 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "QskPlotGrid.h" +#include "QskPlotView.h" +#include "QskStippleMetrics.h" + +#include + +QSK_SUBCONTROL( QskPlotGrid, MajorLine ) +QSK_SUBCONTROL( QskPlotGrid, MinorLine ) + +static inline QskAspect::Subcontrol qskSubcontrol( QskPlotGrid::Type gridType ) +{ + using Q = QskPlotGrid; + return ( gridType == Q::MinorGrid ) ? Q::MinorLine : Q::MajorLine; +} + +class QskPlotGrid::PrivateData +{ +}; + +QskPlotGrid::QskPlotGrid( QObject* object ) + : Inherited( object ) + , m_data( new PrivateData ) +{ + setCoordinateType( CanvasCoordinates ); +} + +QskPlotGrid::~QskPlotGrid() +{ +} + +void QskPlotGrid::setPen( Type gridType, const QPen& pen ) +{ + using A = QskAspect; + + const auto oldPen = this->pen( gridType ); + + const auto subControl = qskSubcontrol( gridType ); + + setColor( subControl, pen.color() ); + setMetric( subControl | A::Size, pen.widthF() ); + setSkinHint( subControl | A::Metric | A::Style, + QVariant::fromValue( QskStippleMetrics( pen ) ) ); + + if ( oldPen != pen ) + { + markDirty(); + + if ( gridType == MinorGrid ) + Q_EMIT minorPenChanged( pen ); + else + Q_EMIT majorPenChanged( pen ); + } +} + +void QskPlotGrid::resetPen( Type gridType ) +{ + using A = QskAspect; + + const auto oldPen = pen( gridType ); + const auto subControl = qskSubcontrol( gridType ); + + resetColor( subControl ); + resetMetric( subControl | A::Size ); + resetMetric( subControl | A::Style ); + + const auto newPen = pen( gridType ); + + if ( oldPen != newPen ) + { + markDirty(); + + if ( gridType == MinorGrid ) + Q_EMIT minorPenChanged( newPen ); + else + Q_EMIT majorPenChanged( newPen ); + } +} + +QPen QskPlotGrid::pen( Type gridType ) const +{ + using A = QskAspect; + + const auto subControl = qskSubcontrol( gridType ); + + const auto stippleMetrics = effectiveSkinHint( + subControl | A::Metric | A::Style ).value< QskStippleMetrics >(); + + QPen pen( Qt::NoPen ); + + if ( stippleMetrics.isValid() ) + { + if ( stippleMetrics.isSolid() ) + { + pen.setStyle( Qt::SolidLine ); + } + else + { + pen.setStyle( Qt::CustomDashLine ); + pen.setDashOffset( stippleMetrics.offset() ); + pen.setDashPattern( stippleMetrics.pattern() ); + } + + pen.setColor( color( subControl ) ); + pen.setWidth( metric( subControl | A::Size ) ); + } + + return pen; +} + +QVector< qreal > QskPlotGrid::lines( Type gridType, Qt::Orientation orientation ) const +{ + if ( auto view = this->view() ) + { + const auto axis = ( orientation == Qt::Horizontal ) ? yAxis() : xAxis(); + + const auto& tickmarks = view->tickmarks( axis ); + + if ( gridType == MajorGrid ) + return tickmarks.majorTicks(); + else + return tickmarks.mediumTicks() + tickmarks.minorTicks(); + } + + return QVector< qreal >(); +} + +bool QskPlotGrid::needsClipping() const +{ + return false; +} + +#include "moc_QskPlotGrid.cpp" diff --git a/playground/plots/QskPlotGrid.h b/playground/plots/QskPlotGrid.h new file mode 100644 index 00000000..4b4ce25b --- /dev/null +++ b/playground/plots/QskPlotGrid.h @@ -0,0 +1,97 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include "QskPlotItem.h" + +#include +#include +#include + +class QPen; + +class QskPlotGrid : public QskPlotItem +{ + Q_OBJECT + + Q_PROPERTY( QPen minorPen READ minorPen + WRITE setMinorPen RESET resetMinorPen NOTIFY minorPenChanged ) + + Q_PROPERTY( QPen majorPen READ majorPen + WRITE setMajorPen RESET resetMajorPen NOTIFY majorPenChanged ) + + using Inherited = QskPlotItem; + + public: + QSK_SUBCONTROLS( MajorLine, MinorLine ) + + enum Type + { + MinorGrid, + MajorGrid + }; + Q_ENUM( Type ) + + QskPlotGrid( QObject* = nullptr ); + ~QskPlotGrid() override; + + void setPen( Type, const QPen& ); + void resetPen( Type ); + QPen pen( Type ) const; + + void setMajorPen( const QPen& ); + void resetMajorPen(); + QPen majorPen() const; + + void setMinorPen( const QPen& ); + void resetMinorPen(); + QPen minorPen() const; + + // positions + virtual QVector< qreal > lines( Type, Qt::Orientation ) const; + + bool needsClipping() const override; + + Q_SIGNALS: + void minorPenChanged( const QPen& ); + void majorPenChanged( const QPen& ); + + private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; + +inline void QskPlotGrid::setMinorPen( const QPen& pen ) +{ + setPen( MinorGrid, pen ); +} + +inline void QskPlotGrid::resetMinorPen() +{ + resetPen( MinorGrid ); +} + +inline QPen QskPlotGrid::minorPen() const +{ + return pen( MinorGrid ); +} + +inline void QskPlotGrid::setMajorPen( const QPen& pen ) +{ + setPen( MajorGrid, pen ); +} + +inline void QskPlotGrid::resetMajorPen() +{ + resetPen( MajorGrid ); +} + +inline QPen QskPlotGrid::majorPen() const +{ + return pen( MajorGrid ); +} + + diff --git a/playground/plots/QskPlotGridSkinlet.cpp b/playground/plots/QskPlotGridSkinlet.cpp new file mode 100644 index 00000000..ebd490c2 --- /dev/null +++ b/playground/plots/QskPlotGridSkinlet.cpp @@ -0,0 +1,81 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskPlotGridSkinlet.h" +#include "QskPlotGrid.h" +#include "QskStippleMetrics.h" + +#include +#include +#include + +#include + +QskPlotGridSkinlet::QskPlotGridSkinlet( QskSkin* skin ) + : Inherited( skin ) +{ + setNodeRoles( { MinorGrid, MajorGrid } ); +} + +QskPlotGridSkinlet::~QskPlotGridSkinlet() +{ +} + +QSGNode* QskPlotGridSkinlet::updateSubNode( + const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const +{ + using Q = QskPlotGrid; + + if ( nodeRole == MinorGrid ) + return updateGridNode( skinnable, Q::MinorLine, node ); + + if ( nodeRole == MajorGrid ) + return updateGridNode( skinnable, Q::MajorLine, node ); + + return Inherited::updateSubNode( skinnable, nodeRole, node ); +} + +QSGNode* QskPlotGridSkinlet::updateGridNode( const QskSkinnable* skinnable, + QskAspect::Subcontrol subControl, QSGNode* node ) const +{ + using Q = QskPlotGrid; + + const auto gridType = + ( subControl == Q::MinorLine ) ? Q::MinorGrid : Q::MajorGrid; + + auto grid = static_cast< const QskPlotGrid* >( skinnable ); + + const auto r = grid->scaleRect(); + if ( r.isEmpty() ) + return nullptr; + + const auto stipple = grid->stippleMetricsHint( subControl ); + if ( !stipple.isValid() ) + return nullptr; + + const auto lineColor = grid->color( subControl ); + if ( !lineColor.isValid() || lineColor.alpha() == 0 ) + return nullptr; + + const auto lineWidth = grid->metric( subControl | QskAspect::Size ); + if ( lineWidth <= 0 ) + return nullptr; + + const auto xValues = grid->lines( gridType, Qt::Vertical ); + const auto yValues = grid->lines( gridType, Qt::Horizontal ); + + if ( xValues.isEmpty() && yValues.isEmpty() ) + return nullptr; + + auto gridNode = QskSGNode::ensureNode< QskLinesNode >( node ); + gridNode->setPixelAlignment( Qt::Horizontal | Qt::Vertical ); + + gridNode->updateGrid( lineColor, lineWidth, + stipple, grid->transformation(), r, xValues, yValues ); + + return gridNode; +} + +#include "moc_QskPlotGridSkinlet.cpp" diff --git a/playground/plots/QskPlotGridSkinlet.h b/playground/plots/QskPlotGridSkinlet.h new file mode 100644 index 00000000..2ab51f80 --- /dev/null +++ b/playground/plots/QskPlotGridSkinlet.h @@ -0,0 +1,29 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#pragma once + +#include + +class QskPlotGridSkinlet : public QskSkinlet +{ + Q_GADGET + + using Inherited = QskSkinlet; + + public: + enum NodeRole { MinorGrid, MajorGrid }; + + Q_INVOKABLE QskPlotGridSkinlet( QskSkin* = nullptr ); + ~QskPlotGridSkinlet() override; + + protected: + QSGNode* updateSubNode( const QskSkinnable*, + quint8 nodeRole, QSGNode* ) const override; + + private: + QSGNode* updateGridNode( + const QskSkinnable*, QskAspect::Subcontrol, QSGNode* ) const; +}; diff --git a/playground/plots/QskPlotItem.cpp b/playground/plots/QskPlotItem.cpp new file mode 100644 index 00000000..de829694 --- /dev/null +++ b/playground/plots/QskPlotItem.cpp @@ -0,0 +1,201 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "QskPlotItem.h" +#include "QskPlotView.h" + +#include +#include +#include + +class QskPlotItem::PrivateData +{ + public: + QskPlot::Axis xAxis = QskPlot::XBottom; + QskPlot::Axis yAxis = QskPlot::YLeft; + qreal z = 0.0; + + QPointer< QskPlotView > view; + + CoordinateType coordinateType = PlotCoordinates; + bool dirty = true; +}; + +QskPlotItem::QskPlotItem( QObject* parent ) + : QObject( parent ) + , m_data( new PrivateData ) +{ + if ( auto view = qobject_cast< QskPlotView* >( parent ) ) + attach( view ); +} + +QskPlotItem::~QskPlotItem() +{ + attach( nullptr ); +} + +void QskPlotItem::attach( QskPlotView* view ) +{ + if ( view == m_data->view ) + return; + + if ( m_data->view ) + m_data->view->detachItem( this ); + + m_data->view = view; + + if ( m_data->view ) + m_data->view->attachItem( this ); +} + +void QskPlotItem::setAxes( QskPlot::Axis xAxis, QskPlot::Axis yAxis ) +{ + setXAxis( xAxis ); + setYAxis( yAxis ); +} + +QskPlot::Axis QskPlotItem::xAxis() const +{ + return m_data->xAxis; +} + +void QskPlotItem::setXAxis( QskPlot::Axis axis ) +{ + if ( m_data->xAxis != axis ) + { + m_data->xAxis = axis; + Q_EMIT axisChanged(); + + markDirty(); + } +} + +QskPlot::Axis QskPlotItem::yAxis() const +{ + return m_data->yAxis; +} + +void QskPlotItem::setYAxis( QskPlot::Axis axis ) +{ + if ( m_data->yAxis != axis ) + { + m_data->yAxis = axis; + Q_EMIT axisChanged(); + + markDirty(); + } +} + +qreal QskPlotItem::z() const +{ + return m_data->z; +} + +bool QskPlotItem::isDirty() const +{ + return m_data->dirty; +} + +void QskPlotItem::setZ( qreal z ) +{ + if ( m_data->z != z ) + { + m_data->z = z; + Q_EMIT zChanged( z ); + + if ( auto view = m_data->view ) + { + view->detachItem( this ); + view->attachItem( this ); + } + } +} + +void QskPlotItem::markDirty() +{ + if ( !m_data->dirty ) + { + m_data->dirty = true; + updatePlot(); + } +} + +void QskPlotItem::resetDirty() +{ + m_data->dirty = false; +} + +void QskPlotItem::updatePlot() +{ + if ( m_data->view ) + m_data->view->update(); +} + +void QskPlotItem::setCoordinateType( CoordinateType type ) +{ + if ( m_data->coordinateType != type ) + { + m_data->coordinateType = type; + m_data->dirty = true; + } +} + +QskPlotItem::CoordinateType QskPlotItem::coordinateType() const +{ + return m_data->coordinateType; +} + +QTransform QskPlotItem::transformation() const +{ + if ( m_data->view ) + return m_data->view->transformation( m_data->xAxis, m_data->yAxis ); + + return QTransform(); +} + +const QskPlotView* QskPlotItem::view() const +{ + return m_data->view; +} + +QQuickItem* QskPlotItem::owningItem() const +{ + return m_data->view; +} + +void QskPlotItem::updateNode( QSGNode* node ) +{ + if ( auto skinlet = effectiveSkinlet() ) + skinlet->updateNode( this, node ); + + resetDirty(); +} + +bool QskPlotItem::needsClipping() const +{ + return false; +} + +void QskPlotItem::transformationChanged( ChangeFlags flags ) +{ + if ( flags == CanvasGeometryChanged && coordinateType() == PlotCoordinates ) + return; + + markDirty(); +} + +QRectF QskPlotItem::scaleRect() const +{ + if ( auto view = m_data->view ) + { + return QskIntervalF::toRect( + view->boundaries( m_data->xAxis ), + view->boundaries( m_data->yAxis ) ); + } + + return QRectF(); +} + +#include "moc_QskPlotItem.cpp" diff --git a/playground/plots/QskPlotItem.h b/playground/plots/QskPlotItem.h new file mode 100644 index 00000000..6ae96c86 --- /dev/null +++ b/playground/plots/QskPlotItem.h @@ -0,0 +1,122 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include "QskPlotNamespace.h" + +#include +#include + +class QskPlotView; +class QskIntervalF; + +class QskPlotItem : public QObject, public QskSkinnable +{ + Q_OBJECT + + Q_PROPERTY( QskPlot::Axis xAxis READ xAxis WRITE setXAxis NOTIFY axisChanged ) + Q_PROPERTY( QskPlot::Axis yAxis READ yAxis WRITE setYAxis NOTIFY axisChanged ) + Q_PROPERTY( qreal z READ z WRITE setZ NOTIFY zChanged ) + + public: + /* + The item can decide if it wants to use plot ( scales ) coordinates or + canvas ( QQuickItem ) coordinates for the vertexes of its scenegraph nodes. + + PlotItems, that represent some sort of data ( f.e curves ) usually + prefer to use plot coordinates, while decorations ( f.e legend ) are + often aligned to the plot canvas geometry. + + Items in plot coordinates often do not need to be updated when + the geometry of the plot canvas or the scales have been changed. + To opt out from these updates the plot item needs to overload + the transformationChange() hook. + */ + + enum CoordinateType + { + CanvasCoordinates, + PlotCoordinates + }; + + Q_ENUM( CoordinateType ); + + enum ChangeFlag + { + XBoundariesChanged = 1 << 0, + XTickmarksChanged = 1 << 1, + + YBoundariesChanged = 1 << 1, + YTickmarksChanged = 1 << 2, + + CanvasGeometryChanged = 1 << 2 + }; + Q_ENUM( ChangeFlag ); + + Q_DECLARE_FLAGS( ChangeFlags, ChangeFlag ); + + QskPlotItem( QObject* = nullptr ); + ~QskPlotItem() override; + + void attach( QskPlotView* ); + void detach(); + + void setXAxis( QskPlot::Axis ); + QskPlot::Axis xAxis() const; + + void setYAxis( QskPlot::Axis ); + QskPlot::Axis yAxis() const; + + void setAxes( QskPlot::Axis xAxis, QskPlot::Axis yAxis ); + + void setZ( qreal ); + qreal z() const; + + void setCoordinateType( CoordinateType ); + CoordinateType coordinateType() const; + + /* + Indicates if the item depends on clipping + + Batching of node updates is one of the main performance features + of the scene graph - however clipping breaks batching. + A plot that has no plot item, that needs clipping, can decide + to skip inserting a clip node. + */ + virtual bool needsClipping() const; + + QTransform transformation() const; + QskIntervalF boundaries( Qt::Orientation ) const; + + QRectF scaleRect() const; + + const QskPlotView* view() const; + + void markDirty(); + void resetDirty(); + bool isDirty() const; + + virtual void updateNode( QSGNode* ); + virtual void transformationChanged( ChangeFlags ); + + Q_SIGNALS: + void axisChanged(); + void zChanged( qreal ); + + private: + QQuickItem* owningItem() const override final; + void updatePlot(); + + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS( QskPlotItem::ChangeFlags ) + +inline void QskPlotItem::detach() +{ + attach( nullptr ); +} diff --git a/playground/plots/QskPlotNamespace.h b/playground/plots/QskPlotNamespace.h new file mode 100644 index 00000000..30644ba7 --- /dev/null +++ b/playground/plots/QskPlotNamespace.h @@ -0,0 +1,21 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#pragma once + +#include + +namespace QskPlot +{ + Q_NAMESPACE + + // for the moment only 2 axes + enum Axis + { + XBottom = 0, + YLeft = 1 + }; + Q_ENUM_NS( Axis ) +} diff --git a/playground/plots/QskPlotView.cpp b/playground/plots/QskPlotView.cpp new file mode 100644 index 00000000..887c6418 --- /dev/null +++ b/playground/plots/QskPlotView.cpp @@ -0,0 +1,371 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "QskPlotView.h" +#include "QskPlotItem.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +QSK_SUBCONTROL( QskPlotView, Panel ) +QSK_SUBCONTROL( QskPlotView, AxisScale ) +QSK_SUBCONTROL( QskPlotView, Canvas ) + +enum { AxisCount = 2 }; + +static inline bool qskIsXAxis( int axis ) +{ + return axis == QskPlot::XBottom; +} + +static inline QTransform qskScaleTransform( Qt::Orientation orientation, + qreal s1, qreal s2, qreal p1, qreal p2 ) +{ + const auto ratio = ( p2 - p1 ) / ( s2 - s1 ); + + if ( orientation == Qt::Horizontal ) + { + auto transform = QTransform::fromTranslate( -s1, 0.0 ); + transform *= QTransform::fromScale( ratio, 1.0 ); + transform *= QTransform::fromTranslate( p1, 0.0 ); + + return transform; + } + else + { + auto transform = QTransform::fromTranslate( 0.0, -s1 ); + transform *= QTransform::fromScale( 1.0, -ratio ); + transform *= QTransform::fromTranslate( 0.0, p2 ); + + return transform; + } +} + +class QskPlotView::PrivateData +{ + public: + + struct + { + QskIntervalF boundaries; + QskTickmarks tickmarks; + + bool boundariesDirty = true; + bool ticksDirty = true; + + } scales[ AxisCount ]; + + class ItemData + { + public: + QskPlotItem* plotItem = nullptr; + QSGTransformNode* node = nullptr; + }; + + inline bool needsClipping() const + { + for ( const auto& data : itemData ) + { + if ( data.plotItem->needsClipping() ) + return true; + } + + return false; + } + + std::vector< ItemData > itemData; // a flat map + QVector< QSGNode* > orphanedNodes; + + QRectF canvasRect; +}; + +QskPlotView::QskPlotView( QQuickItem* parentItem ) + : Inherited( parentItem ) + , m_data( new PrivateData ) +{ + setBoundaries( QskPlot::XBottom, 0.0, 100.0 ); + setBoundaries( QskPlot::YLeft, 0.0, 100.0 ); +} + +QskPlotView::~QskPlotView() +{ +} + +void QskPlotView::setBoundaries( QskPlot::Axis axis, qreal from, qreal to ) +{ + setBoundaries( axis, QskIntervalF( from, to ) ); +} + +void QskPlotView::setBoundaries( QskPlot::Axis axis, const QskIntervalF& boundaries ) +{ + auto& sd = m_data->scales[ axis ]; + + if ( boundaries == sd.boundaries ) + return; + + sd.boundaries = boundaries; + sd.boundariesDirty = true; + + const auto oldTickmarks = sd.tickmarks; + + sd.tickmarks = QskGraduation::divideInterval( + boundaries.lowerBound(), boundaries.upperBound(), 5, 5 ); + + if ( oldTickmarks != sd.tickmarks ) + sd.ticksDirty = true; + + polish(); + update(); +} + +QskIntervalF QskPlotView::boundaries( QskPlot::Axis axis ) const +{ + return m_data->scales[ axis ].boundaries; +} + +QskTickmarks QskPlotView::tickmarks( QskPlot::Axis axis ) const +{ + return m_data->scales[ axis ].tickmarks; +} + +QTransform QskPlotView::transformation( QskPlot::Axis xAxis, QskPlot::Axis yAxis ) const +{ + const auto r = canvasRect(); + + const auto scales = m_data->scales; + + const qreal x1 = scales[xAxis].boundaries.lowerBound(); + const qreal x2 = scales[xAxis].boundaries.upperBound(); + + const qreal y1 = scales[yAxis].boundaries.lowerBound(); + const qreal y2 = scales[yAxis].boundaries.upperBound(); + + auto transform = QTransform::fromTranslate( -x1, -y1 ); + + transform *= QTransform::fromScale( + r.width() / ( x2 - x1 ), r.height() / ( y1 - y2 ) ); + + transform *= QTransform::fromTranslate( r.left(), r.bottom() ); + + return transform; +} + +QRectF QskPlotView::canvasRect() const +{ + const auto r = subControlRect( Canvas ); + + const auto b = boxBorderMetricsHint( Canvas ); + return r.adjusted( b.left(), b.top(), -b.right(), -b.bottom() ); +} + +QVariant QskPlotView::labelAt( QskPlot::Axis, qreal pos ) const +{ + return QString::number( pos, 'g' ); +} + +void QskPlotView::changeEvent( QEvent* event ) +{ + if ( event->type() == QEvent::StyleChange ) + { + for ( auto& itemData : m_data->itemData ) + { + auto plotItem = itemData.plotItem; + + if ( plotItem->skinlet() == nullptr ) + plotItem->setSkinlet( nullptr ); // clearing the cached skinlet + + plotItem->markDirty(); + } + + polish(); + } +} + +void QskPlotView::geometryChange( const QRectF& newGeometry, const QRectF& oldGeometry ) +{ + Inherited::geometryChange( newGeometry, oldGeometry ); + polish(); +} + +void QskPlotView::attachItem( QskPlotItem* plotItem ) +{ + const auto cmp = []( const qreal& z, const PrivateData::ItemData& data ) + { return z < data.plotItem->z(); }; + + auto& itemData = m_data->itemData; + + auto it = std::upper_bound( itemData.begin(), itemData.end(), plotItem->z(), cmp ); + itemData.insert( it, { plotItem, nullptr } ); + + plotItem->markDirty(); + + update(); +} + +void QskPlotView::detachItem( QskPlotItem* plotItem ) +{ + auto& itemData = m_data->itemData; + + for ( auto it = itemData.begin(); it != itemData.end(); ++it ) + { + if ( it->plotItem == plotItem ) + { + if ( it->node ) + m_data->orphanedNodes += it->node; + + itemData.erase( it ); + update(); + + return; + } + } +} + +void QskPlotView::updateResources() +{ + using namespace QskPlot; + using I = QskPlotItem; + + /* + updateResources is called from updatePolish. + We should find a better name: TODO ... + */ + + I::ChangeFlags flags[ AxisCount ]; + + { + bool canvasChanged = false; + + { + const auto canvasRect = this->canvasRect(); + if ( canvasRect != m_data->canvasRect ) + { + m_data->canvasRect = canvasRect; + canvasChanged = true; + } + } + + for ( int axis = 0; axis < AxisCount; axis++ ) + { + I::ChangeFlags flag; + + auto& scaleData = m_data->scales[ axis ]; + if ( canvasChanged ) + flag |= I::CanvasGeometryChanged; + + if ( scaleData.boundariesDirty ) + flag |= qskIsXAxis( axis ) ? I::XBoundariesChanged : I::YBoundariesChanged; + + if ( scaleData.ticksDirty ) + flag |= qskIsXAxis( axis ) ? I::XTickmarksChanged : I::YTickmarksChanged; + + flags[axis] = flag; + scaleData.boundariesDirty = scaleData.ticksDirty = false; + } + } + + if ( auto itemFlags = ( flags[0] | flags[1] ) ) + { + for ( auto& itemData : m_data->itemData ) + itemData.plotItem->transformationChanged( itemFlags ); + } +} + +void QskPlotView::updateNode( QSGNode* node ) +{ + if ( !m_data->orphanedNodes.empty() ) + { + for ( auto node : m_data->orphanedNodes ) + { + if ( auto parentNode = node->parent() ) + parentNode->removeChildNode( node ); + + delete node; + } + + m_data->orphanedNodes.clear(); + } + + // sorting items according to z TODO ... + + auto oldItemsNode = node->lastChild(); + if ( oldItemsNode ) + node->removeChildNode( oldItemsNode ); + + /* + the scene graph subtree might have been removed for situations + like skin changes and the item node pointers are dangling pointers + */ + const bool danglingNodes = ( oldItemsNode == nullptr ); + + Inherited::updateNode( node ); + + auto itemsNode = oldItemsNode; + + if ( m_data->needsClipping() ) + { + if ( itemsNode == nullptr || itemsNode->type() != QSGNode::ClipNodeType ) + itemsNode = new QskBoxClipNode(); + } + else + { + if ( itemsNode == nullptr || itemsNode->type() == QSGNode::ClipNodeType ) + itemsNode = new QSGNode(); + } + + if ( oldItemsNode && itemsNode != oldItemsNode ) + { + oldItemsNode->reparentChildNodesTo( itemsNode ); + delete oldItemsNode; + } + + node->appendChildNode( itemsNode ); + + if ( itemsNode->type() == QSGNode::ClipNodeType ) + { + /* + updating the clip node: + + By setting the clipNode above the geometry nodes of the plot items + we only need on clip node for all plot items. However we exclude all + nodes from scene graph batching. It might be better that each plot + item has its own clip node. But we could share their clipping vertexes. + TODO ... + */ + + itemsNode = QskSkinlet::updateBoxClipNode( this, + itemsNode, subControlRect( Canvas ), Canvas ); + } + + for ( auto& itemData : m_data->itemData ) + { + if ( danglingNodes || itemData.node == nullptr ) + { + itemData.node = new QSGTransformNode(); + itemsNode->appendChildNode( itemData.node ); + } + + QMatrix4x4 matrix; + + if ( itemData.plotItem->coordinateType() == QskPlotItem::PlotCoordinates ) + matrix = itemData.plotItem->transformation(); + + if ( matrix != itemData.node->matrix() ) + itemData.node->setMatrix( matrix ); + + if ( itemData.plotItem->isDirty() ) + itemData.plotItem->updateNode( itemData.node ); + } +} + +#include "moc_QskPlotView.cpp" diff --git a/playground/plots/QskPlotView.h b/playground/plots/QskPlotView.h new file mode 100644 index 00000000..37a7ae9d --- /dev/null +++ b/playground/plots/QskPlotView.h @@ -0,0 +1,56 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include "QskControl.h" +#include "QskPlotNamespace.h" + +class QskPlotItem; + +class QskTickmarks; +class QskIntervalF; +class QTransform; + +class QskPlotView : public QskControl +{ + Q_OBJECT + + using Inherited = QskControl; + + public: + QSK_SUBCONTROLS( Panel, AxisScale, Canvas ) + + QskPlotView( QQuickItem* parent = nullptr ); + ~QskPlotView() override; + + void setBoundaries( QskPlot::Axis, qreal, qreal ); + void setBoundaries( QskPlot::Axis, const QskIntervalF& ); + QskIntervalF boundaries( QskPlot::Axis axis ) const; + + QskTickmarks tickmarks( QskPlot::Axis axis ) const; + + // scales -> item coordinates + QTransform transformation( QskPlot::Axis xAxis, QskPlot::Axis yAxis ) const; + QRectF canvasRect() const; + + virtual QVariant labelAt( QskPlot::Axis, qreal pos ) const; + + protected: + void geometryChange( const QRectF&, const QRectF& ) override; + void updateNode( QSGNode* ) override; + void updateResources() override; + + void changeEvent( QEvent* ) override; + + private: + friend class QskPlotItem; + + void attachItem( QskPlotItem* ); + void detachItem( QskPlotItem* ); + + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; diff --git a/playground/plots/QskPlotViewSkinlet.cpp b/playground/plots/QskPlotViewSkinlet.cpp new file mode 100644 index 00000000..6b6bd117 --- /dev/null +++ b/playground/plots/QskPlotViewSkinlet.cpp @@ -0,0 +1,283 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskPlotViewSkinlet.h" +#include "QskPlotView.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static inline QskTextColors qskTextColors( + const QskSkinnable* skinnable, QskAspect aspect ) +{ + using A = QskAspect; + + QskSkinHintStatus status; + + QskTextColors c; + c.textColor = skinnable->color( aspect | A::TextColor, &status ); + + if ( status.aspect.subControl() != aspect.subControl() ) + { + // using the same color as the one for the ticks + c.textColor = skinnable->color( aspect ); + } + + c.styleColor = skinnable->color( aspect | A::StyleColor ); + c.linkColor = skinnable->color( aspect | A::LinkColor ); + + return c; +} + +static inline QskAspect qskAxisAspect( QskPlot::Axis axis ) +{ + using Q = QskPlotView; + using A = QskAspect; + + return Q::AxisScale | ( ( axis == QskPlot::XBottom ) ? A::Bottom : A::Left ); +} + +namespace +{ + class ScaleRenderer : public QskScaleRenderer + { + using Inherited = QskScaleRenderer; + + public: + ScaleRenderer( const QskPlotView* view, QskPlot::Axis axis ) + : m_view( view ) + , m_axis( axis ) + { + using Q = QskPlotView; + using A = QskAspect; + + setEdge( axis == QskPlot::XBottom ? Qt::BottomEdge : Qt::LeftEdge ); + + setBoundaries( view->boundaries( axis ) ); + setTickmarks( view->tickmarks( axis ) ); + + const auto aspect = Q::AxisScale + | ( ( axis == QskPlot::XBottom ) ? A::Bottom : A::Left ); + + const auto flags = view->flagHint< QskScaleRenderer::Flags >( + aspect | QskAspect::Style, QskScaleRenderer::Backbone ); + + setFlags( flags ); + + setTickColor( view->color( aspect ) ); + + const auto tickSize = view->strutSizeHint( aspect ); + setTickWidth( tickSize.width() ); + setTickLength( tickSize.height() ); + + setSpacing( view->spacingHint( aspect ) ); + + const auto fontRole = view->fontRoleHint( aspect ); + setFont( view->effectiveSkin()->font( fontRole ) ); + + setTextColors( qskTextColors( view, aspect ) ); + } + + qreal labelOffset() const + { + qreal off = tickLength() + spacing(); + if ( flags() & CenteredTickmarks ) + off -= 0.5 * tickLength(); + + return off; + } + + QVariant labelAt( qreal pos ) const override + { + return m_view->labelAt( m_axis, pos ); + } + + private: + const QskPlotView* m_view; + const QskPlot::Axis m_axis; + }; +} + +QskPlotViewSkinlet::QskPlotViewSkinlet( QskSkin* skin ) + : Inherited( skin ) +{ + setNodeRoles( { PanelRole, CanvasRole, AxisRole } ); +} + +QskPlotViewSkinlet::~QskPlotViewSkinlet() = default; + +QRectF QskPlotViewSkinlet::subControlRect( const QskSkinnable* skinnable, + const QRectF& contentsRect, QskAspect::Subcontrol subControl ) const +{ + using Q = QskPlotView; + + if ( subControl == Q::Panel ) + return contentsRect; + + if ( subControl == Q::Canvas ) + { + const auto view = static_cast< const QskPlotView* >( skinnable ); + + const auto rectX = sampleRect( + skinnable, contentsRect, Q::AxisScale, QskPlot::XBottom ); + + const auto rectY = sampleRect( + skinnable, contentsRect, Q::AxisScale, QskPlot::YLeft ); + + auto rect = view->subControlContentsRect( Q::Panel ); + + rect.setBottom( rectX.top() ); + rect.setLeft( rectY.right() ); + + return rect; + } + + return Inherited::subControlRect( skinnable, contentsRect, subControl ); +} + +QSGNode* QskPlotViewSkinlet::updateSubNode( + const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const +{ + using Q = QskPlotView; + + switch ( nodeRole ) + { + case PanelRole: + return updateBoxNode( skinnable, node, Q::Panel ); + + case CanvasRole: + return updateBoxNode( skinnable, node, Q::Canvas ); + + case AxisRole: + return updateSeriesNode( skinnable, Q::AxisScale, node ); + } + + return Inherited::updateSubNode( skinnable, nodeRole, node ); +} + +int QskPlotViewSkinlet::sampleCount( + const QskSkinnable* skinnable, QskAspect::Subcontrol subControl ) const +{ + using Q = QskPlotView; + + if ( subControl == Q::AxisScale ) + return 2; // for the moment only 2 axes + + return Inherited::sampleCount( skinnable, subControl ); +} + +QRectF QskPlotViewSkinlet::sampleRect( const QskSkinnable* skinnable, + const QRectF& contentsRect, QskAspect::Subcontrol subControl, int index ) const +{ + if ( subControl == QskPlotView::AxisScale ) + { + const auto axis = static_cast< QskPlot::Axis >( index ); + return axisRect( skinnable, axis ); + } + + return Inherited::sampleRect( skinnable, contentsRect, subControl, index ); +} + +QSGNode* QskPlotViewSkinlet::updateSampleNode( const QskSkinnable* skinnable, + QskAspect::Subcontrol subControl, int index, QSGNode* node ) const +{ + using Q = QskPlotView; + + if ( subControl == Q::AxisScale ) + { + const auto axis = static_cast< QskPlot::Axis >( index ); + return updateAxisNode( skinnable, axis, node ); + } + + return Inherited::updateSampleNode( skinnable, subControl, index, node ); +} + +QSizeF QskPlotViewSkinlet::sizeHint( const QskSkinnable* skinnable, + Qt::SizeHint which, const QSizeF& constraint ) const +{ + return Inherited::sizeHint( skinnable, which, constraint ); +} + +QRectF QskPlotViewSkinlet::axisRect( + const QskSkinnable* skinnable, QskPlot::Axis axis ) const +{ + using Q = QskPlotView; + using A = QskAspect; + + const auto view = static_cast< const QskPlotView* >( skinnable ); + auto rect = view->subControlContentsRect( Q::Panel ); + + const QskMargins paddingLeft = view->paddingHint( Q::AxisScale | A::Left ); + const QskMargins paddingBottom = view->paddingHint( Q::AxisScale | A::Bottom ); + + qreal x0, y0; + { + const ScaleRenderer renderer( view, QskPlot::XBottom ); + const auto sz = renderer.boundingLabelSize(); + + y0 = rect.bottom() - sz.height() + - renderer.labelOffset() - paddingBottom.height(); + } + { + const ScaleRenderer renderer( view, QskPlot::YLeft ); + const auto sz = renderer.boundingLabelSize(); + + x0 = rect.left() + sz.width() + + renderer.labelOffset() + paddingLeft.width(); + } + + const auto canvasBorder = view->boxBorderMetricsHint( Q::Canvas ); + + if ( axis == QskPlot::XBottom ) + { + rect.setTop( y0 ); + rect.setLeft( x0 + canvasBorder.left() ); + rect.setRight( rect.right() - canvasBorder.right() ); + } + else + { + rect.setRight( x0 ); + rect.setTop( rect.top() + canvasBorder.top() ); + rect.setBottom( y0 - canvasBorder.bottom() ); + } + + return rect; +} + +QSGNode* QskPlotViewSkinlet::updateAxisNode( + const QskSkinnable* skinnable, QskPlot::Axis axis, QSGNode* node ) const +{ + using Q = QskPlotView; + + const auto view = static_cast< const QskPlotView* >( skinnable ); + + const auto axisRect = sampleRect( view, view->contentsRect(), Q::AxisScale, axis ); + const auto padding = view->paddingHint( qskAxisAspect( axis ) ); + + ScaleRenderer renderer( view, axis ); + if ( axis == QskPlot::XBottom ) + { + renderer.setPosition( axisRect.top() + padding.top() ); + renderer.setRange( axisRect.left(), axisRect.right() ); + } + else + { + renderer.setPosition( axisRect.right() - padding.right() ); + renderer.setRange( axisRect.top(), axisRect.bottom() ); + } + + return renderer.updateNode( view, node ); +} + +#include "moc_QskPlotViewSkinlet.cpp" diff --git a/playground/plots/QskPlotViewSkinlet.h b/playground/plots/QskPlotViewSkinlet.h new file mode 100644 index 00000000..f861b6e1 --- /dev/null +++ b/playground/plots/QskPlotViewSkinlet.h @@ -0,0 +1,52 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#pragma once + +#include +#include + +class QskPlotViewSkinlet : public QskSkinlet +{ + Q_GADGET + + using Inherited = QskSkinlet; + + public: + enum NodeRole + { + PanelRole, + CanvasRole, + AxisRole + }; + + Q_INVOKABLE QskPlotViewSkinlet( QskSkin* = nullptr ); + ~QskPlotViewSkinlet() override; + + QRectF subControlRect( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol ) const override; + + QSizeF sizeHint( const QskSkinnable*, + Qt::SizeHint, const QSizeF& ) const override; + + int sampleCount( const QskSkinnable*, + QskAspect::Subcontrol ) const override final; + + QRectF sampleRect( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol, int index ) const override; + + protected: + QSGNode* updateSubNode( const QskSkinnable*, + quint8 nodeRole, QSGNode* ) const override; + + QSGNode* updateSampleNode( const QskSkinnable*, + QskAspect::Subcontrol, int index, QSGNode* ) const override; + + private: + QRectF axisRect( const QskSkinnable*, QskPlot::Axis ) const; + + QSGNode* updateAxisNode( const QskSkinnable*, + QskPlot::Axis, QSGNode* ) const; +}; diff --git a/playground/plots/main.cpp b/playground/plots/main.cpp new file mode 100644 index 00000000..f2612e44 --- /dev/null +++ b/playground/plots/main.cpp @@ -0,0 +1,120 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "Plot.h" + +#include +#include +#include +#include +#include + +#include + +#include + +namespace +{ + class TestPlot : public Plot + { + public: + TestPlot( QQuickItem* parentItem = nullptr ) + : Plot( parentItem ) + { + setMargins( 5 ); + + QVector< Sample > samples; + + samples += { -50, 30, 60, 80 }; + samples += { -45, 35, 55, 85 }; + samples += { -40, 32, 60, 77 }; + samples += { -35, 38, 75, 66 }; + samples += { -30, 40, 65, 75 }; + samples += { -25, 48, 58, 82 }; + samples += { -20, 27, 62, 85 }; + samples += { -15, 32, 22, 70 }; + samples += { -10, 30, 24, 77 }; + samples += { -5, 20, 33, 70 }; + samples += { 0, 40, 60, 80 }; + + setSamples( samples ); + } + }; + + class Header : public QskLinearBox + { + Q_OBJECT + + public: + Header( QQuickItem* parent = nullptr ) + : QskLinearBox( Qt::Horizontal, parent ) + { + initSizePolicy( QskSizePolicy::Ignored, QskSizePolicy::Fixed ); + + setPanel( true ); + setPaddingHint( QskBox::Panel, 5 ); + + addStretch( 1 ); + + auto buttonLeft = new QskPushButton( "<", this ); + buttonLeft->setAutoRepeat( true ); + connect( buttonLeft, &QskPushButton::clicked, + this, [this] { Q_EMIT shiftClicked( +1 ); } ); + + auto buttonRight = new QskPushButton( ">", this ); + buttonRight->setAutoRepeat( true ); + connect( buttonRight, &QskPushButton::clicked, + this, [this] { Q_EMIT shiftClicked( -1 ); } ); + + auto buttonReset = new QskPushButton( "Reset", this ); + connect( buttonReset, &QskPushButton::clicked, + this, &Header::resetClicked ); + } + + Q_SIGNALS: + void shiftClicked( int steps ); + void resetClicked(); + }; + + class MainView : public QskMainView + { + public: + MainView( QQuickItem* parent = nullptr ) + : QskMainView( parent ) + { + auto header = new Header( this ); + auto plot = new TestPlot(); + + connect( header, &Header::resetClicked, + plot, &Plot::resetAxes ); + + connect( header, &Header::shiftClicked, + plot, &Plot::shiftXAxis ); + + setHeader( header ); + setBody( plot ); + } + }; +} + +int main( int argc, char* argv[] ) +{ +#ifdef ITEM_STATISTICS + QskObjectCounter counter( true ); +#endif + + QGuiApplication app( argc, argv ); + + SkinnyShortcut::enable( SkinnyShortcut::AllShortcuts ); + + QskWindow window; + window.addItem( new MainView() ); + window.resize( 800, 600 ); + window.show(); + + return app.exec(); +} + +#include "main.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b5f305a7..4e016fd1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -99,6 +99,7 @@ list(APPEND SOURCES list(APPEND HEADERS nodes/QskArcNode.h + nodes/QskAxisScaleNode.h nodes/QskBasicLinesNode.h nodes/QskBoxNode.h nodes/QskBoxClipNode.h @@ -126,7 +127,6 @@ list(APPEND HEADERS nodes/QskTextNode.h nodes/QskTextRenderer.h nodes/QskTextureRenderer.h - nodes/QskTickmarksNode.h nodes/QskVertex.h ) @@ -136,6 +136,7 @@ list(APPEND PRIVATE_HEADERS list(APPEND SOURCES nodes/QskArcNode.cpp + nodes/QskAxisScaleNode.cpp nodes/QskBasicLinesNode.cpp nodes/QskBoxNode.cpp nodes/QskBoxClipNode.cpp @@ -163,7 +164,6 @@ list(APPEND SOURCES nodes/QskTextNode.cpp nodes/QskTextRenderer.cpp nodes/QskTextureRenderer.cpp - nodes/QskTickmarksNode.cpp nodes/QskVertex.cpp ) diff --git a/src/common/QskGraduation.h b/src/common/QskGraduation.h index 3fa3731a..08d24449 100644 --- a/src/common/QskGraduation.h +++ b/src/common/QskGraduation.h @@ -7,18 +7,15 @@ #define QSK_GRADUATION_H #include -#include class QskTickmarks; namespace QskGraduation { - Q_NAMESPACE_EXPORT( QSK_EXPORT ) - - QskTickmarks divideInterval( qreal x1, qreal x2, + QSK_EXPORT QskTickmarks divideInterval( qreal x1, qreal x2, int maxMajorSteps, int maxMinorSteps, qreal stepSize = 0.0 ); - qreal stepSize( double length, int numSteps ); + QSK_EXPORT qreal stepSize( double length, int numSteps ); } #endif diff --git a/src/nodes/QskAxisScaleNode.cpp b/src/nodes/QskAxisScaleNode.cpp new file mode 100644 index 00000000..266a822c --- /dev/null +++ b/src/nodes/QskAxisScaleNode.cpp @@ -0,0 +1,207 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskAxisScaleNode.h" +#include "QskTickmarks.h" +#include "QskIntervalF.h" + +#include + +namespace +{ + using Points = QSGGeometry::Point2D; + + class Renderer + { + public: + inline Renderer( bool isHorizontal ) + : m_isHorizontal( isHorizontal ) + { + } + + inline Points* addBackbone( Points* points, + qreal pos, qreal v1, qreal v2 ) const + { + if ( m_isHorizontal ) + setLine( points, v1, pos, v2, pos ); + else + setLine( points, pos, v1, pos, v2 ); + + return points + 2; + } + + inline Points* addTickLine( Points* points, + qreal pos, qreal tick, qreal tickLength ) const + { + if ( m_isHorizontal ) + setLine( points, tick, pos, tick, pos + tickLength ); + else + setLine( points, pos, tick, pos + tickLength, tick ); + + return points + 2; + } + + private: + + inline void setLine( Points* points, + qreal x1, qreal y1, qreal x2, qreal y2 ) const + { + points[ 0 ].set( x1, y1 ); + points[ 1 ].set( x2, y2 ); + } + + const bool m_isHorizontal; + }; +} + +class QskAxisScaleNode::PrivateData +{ + public: + inline qreal map( qreal v ) const + { + if ( isHorizontal ) + return transform.dx() + transform.m11() * v; + else + return transform.dy() + transform.m22() * v; + } + + inline qreal length( QskTickmarks::TickType type ) const + { + switch( type ) + { + case QskTickmarks::MinorTick: + return 0.7 * tickLength; + + case QskTickmarks::MediumTick: + return 0.85 * tickLength; + + default: + return tickLength; + } + } + + inline qreal origin( qreal length ) const + { + switch( alignment ) + { + case QskAxisScaleNode::Leading: + return pos - length; + + case QskAxisScaleNode::Centered: + return pos - 0.5 * length; + + default: + return pos; + } + } + + bool isHorizontal = true; + qreal pos; + + QskIntervalF backbone; + QTransform transform; + + QskAxisScaleNode::Alignment alignment = QskAxisScaleNode::Centered; + qreal tickLength = 0.0; + + QskHashValue hash = 0; + + bool dirty = true; +}; + +QskAxisScaleNode::QskAxisScaleNode() + : m_data( new PrivateData() ) +{ +} + +QskAxisScaleNode::~QskAxisScaleNode() +{ +} + +void QskAxisScaleNode::setAxis( Qt::Orientation orientation, + qreal pos, const QTransform& transform ) +{ + const bool isHorizontal = ( orientation == Qt::Horizontal ); + + if( isHorizontal != m_data->isHorizontal + || pos != m_data->pos || transform != m_data->transform ) + { + m_data->isHorizontal = isHorizontal; + m_data->pos = pos; + m_data->transform = transform; + + m_data->dirty = true; + } +} + +void QskAxisScaleNode::setTickGeometry( + Alignment alignment, qreal tickLength, qreal tickWidth ) +{ + setLineWidth( tickWidth ); + + if( tickLength != m_data->tickLength || alignment != m_data->alignment ) + { + m_data->tickLength = tickLength; + m_data->alignment = alignment; + + m_data->dirty = true; + } +} + +void QskAxisScaleNode::update( const QskTickmarks& tickmarks, + const QskIntervalF& backbone ) +{ + const auto hash = tickmarks.hash( 17435 ); + if ( m_data->hash != hash || m_data->backbone != backbone ) + { + m_data->hash = hash; + m_data->backbone = backbone; + m_data->dirty = true; + } + + if( !m_data->dirty ) + return; + + QSGGeometry::Point2D* points; + + { + auto lineCount = tickmarks.tickCount(); + if ( !backbone.isEmpty() ) + lineCount++; + + geometry()->allocate( lineCount * 2 ); + points = geometry()->vertexDataAsPoint2D(); + } + + const Renderer renderer( m_data->isHorizontal ); + + if ( !m_data->backbone.isEmpty() ) + { + const auto v1 = m_data->map( backbone.lowerBound() ); + const auto v2 = m_data->map( backbone.upperBound() ); + + points = renderer.addBackbone( points, m_data->pos, v1, v2 ); + } + + for( int i = QskTickmarks::MinorTick; + i <= QskTickmarks::MajorTick; i++ ) + { + const auto tickType = static_cast< QskTickmarks::TickType >( i ); + + const auto len = m_data->length( tickType ); + const auto origin = m_data->origin( len ); + + const auto ticks = tickmarks.ticks( tickType ); + for( auto tick : ticks ) + { + tick = m_data->map( tick ); + points = renderer.addTickLine( points, origin, tick, len ); + } + } + + geometry()->markVertexDataDirty(); + markDirty( QSGNode::DirtyGeometry ); + m_data->dirty = false; +} diff --git a/src/nodes/QskAxisScaleNode.h b/src/nodes/QskAxisScaleNode.h new file mode 100644 index 00000000..a8579149 --- /dev/null +++ b/src/nodes/QskAxisScaleNode.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#ifndef QSK_AXIS_SCALE_NODE_H +#define QSK_AXIS_SCALE_NODE_H + +#include "QskGlobal.h" +#include "QskBasicLinesNode.h" + +#include + +class QRectF; +class QskIntervalF; +class QskTickmarks; + +class QskAxisScaleNodePrivate; + +class QSK_EXPORT QskAxisScaleNode : public QskBasicLinesNode +{ + using Inherited = QskBasicLinesNode; + + public: + enum Alignment + { + Leading, + Centered, + Trailing + }; + + QskAxisScaleNode(); + ~QskAxisScaleNode() override; + + void setAxis( Qt::Orientation, qreal pos, const QTransform& ); + void setTickGeometry( Alignment, qreal tickLength, qreal tickWidth ); + + void update( const QskTickmarks&, const QskIntervalF& ); + + private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; + +#endif diff --git a/src/nodes/QskScaleRenderer.cpp b/src/nodes/QskScaleRenderer.cpp index 1fe8c140..88b37d83 100644 --- a/src/nodes/QskScaleRenderer.cpp +++ b/src/nodes/QskScaleRenderer.cpp @@ -7,8 +7,7 @@ #include "QskTickmarks.h" #include "QskSkinlet.h" #include "QskSGNode.h" -#include "QskGraduationMetrics.h" -#include "QskTickmarksNode.h" +#include "QskAxisScaleNode.h" #include "QskTextOptions.h" #include "QskTextColors.h" #include "QskGraphic.h" @@ -19,6 +18,31 @@ #include #include +#include + +namespace +{ + class ScaleMap + { + public: + inline ScaleMap( bool isHorizontal, const QTransform& transform ) + : t( isHorizontal ? transform.dx() : transform.dy() ) + , f( isHorizontal ? transform.m11() : transform.m22() ) + { + } + + inline qreal map( qreal v ) const { return t + f * v; }; + + private: + const qreal t; + const qreal f; + }; +} + +static inline bool qskIsHorizontal( Qt::Edge edge ) +{ + return edge & ( Qt::TopEdge | Qt::BottomEdge ); +} static QSGNode* qskRemoveTraillingNodes( QSGNode* node, QSGNode* childNode ) { @@ -26,44 +50,74 @@ static QSGNode* qskRemoveTraillingNodes( QSGNode* node, QSGNode* childNode ) return nullptr; } -static inline void qskInsertRemoveChild( QSGNode* parentNode, - QSGNode* oldNode, QSGNode* newNode, bool append ) +static inline QTransform qskScaleTransform( Qt::Edge edge, + const QskIntervalF& boundaries, const QskIntervalF& range ) { - if ( newNode == oldNode ) - return; + using T = QTransform; - if ( oldNode ) + if ( qskIsHorizontal( edge ) ) { - parentNode->removeChildNode( oldNode ); - if ( oldNode->flags() & QSGNode::OwnedByParent ) - delete oldNode; + auto transform = T::fromTranslate( -boundaries.lowerBound(), 0.0 ); + transform *= T::fromScale( range.length() / boundaries.length(), 1.0 ); + transform *= T::fromTranslate( range.lowerBound(), 0.0 ); + + return transform; + } + else + { + auto transform = T::fromTranslate( 0.0, -boundaries.lowerBound() ); + transform *= T::fromScale( 1.0, -range.length() / boundaries.length() ); + transform *= T::fromTranslate( 0.0, range.upperBound() ); + + return transform; + } +} + +static inline quint8 qskLabelNodeRole( const QVariant& label ) +{ + if ( !label.isNull() ) + { + if ( label.canConvert< QString >() ) + return 1; + + if ( label.canConvert< QskGraphic >() ) + return 2; } - if ( newNode ) - { - if ( append ) - parentNode->appendChildNode( newNode ); - else - parentNode->prependChildNode( newNode ); - } + return QskSGNode::NoRole; } class QskScaleRenderer::PrivateData { public: + + // Coordinates related to the scales QskIntervalF boundaries; QskTickmarks tickmarks; - QColor tickColor = Qt::black; + /* + Item cooordinates. In case of an horizontal scale + position is an y coordinate, while range corresponds to x coordinates + ( vertical: v.v ) + */ + qreal position = 0.0; + QskIntervalF range; + +#if 1 + QColor tickColor = Qt::black; // rgb value ??? +#endif + qreal tickWidth = 1.0; + qreal tickLength = 10.0; + qreal spacing = 5.0; QFont font; QskTextColors textColors; QskColorFilter colorFilter; - Qt::Orientation orientation = Qt::Horizontal; - Qt::Alignment alignment = Qt::AlignBottom | Qt::AlignRight; + Qt::Edge edge = Qt::BottomEdge; + QskScaleRenderer::Flags flags = ClampedLabels; }; QskScaleRenderer::QskScaleRenderer() @@ -75,24 +129,32 @@ QskScaleRenderer::~QskScaleRenderer() { } -void QskScaleRenderer::setOrientation( Qt::Orientation orientation ) +void QskScaleRenderer::setEdge( Qt::Edge edge ) { - m_data->orientation = orientation; + m_data->edge = edge; } -Qt::Orientation QskScaleRenderer::orientation() const +Qt::Edge QskScaleRenderer::edge() const { - return m_data->orientation; + return m_data->edge; } -void QskScaleRenderer::setAlignment( Qt::Alignment alignment ) +void QskScaleRenderer::setFlag( Flag flag, bool on ) { - m_data->alignment = alignment; + if ( on ) + m_data->flags |= flag; + else + m_data->flags &= ~flag; } -Qt::Alignment QskScaleRenderer::aligment() const +void QskScaleRenderer::setFlags( Flags flags ) { - return m_data->alignment; + m_data->flags = flags; +} + +QskScaleRenderer::Flags QskScaleRenderer::flags() const +{ + return m_data->flags; } void QskScaleRenderer::setBoundaries( qreal lowerBound, qreal upperBound ) @@ -110,6 +172,31 @@ QskIntervalF QskScaleRenderer::boundaries() const return m_data->boundaries; } +qreal QskScaleRenderer::position() const +{ + return m_data->position; +} + +void QskScaleRenderer::setPosition( qreal pos ) +{ + m_data->position = pos; +} + +void QskScaleRenderer::setRange( qreal from, qreal to ) +{ + setRange( QskIntervalF( from, to ) ); +} + +void QskScaleRenderer::setRange( const QskIntervalF& range ) +{ + m_data->range = range; +} + +QskIntervalF QskScaleRenderer::range() const +{ + return m_data->range; +} + void QskScaleRenderer::setTickmarks( const QskTickmarks& tickmarks ) { m_data->tickmarks = tickmarks; @@ -120,6 +207,16 @@ const QskTickmarks& QskScaleRenderer::tickmarks() const return m_data->tickmarks; } +void QskScaleRenderer::setSpacing( qreal spacing ) +{ + m_data->spacing = qMax( spacing, 0.0 ); +} + +qreal QskScaleRenderer::spacing() const +{ + return m_data->spacing; +} + void QskScaleRenderer::setTickColor( const QColor& color ) { m_data->tickColor = color; @@ -130,9 +227,19 @@ QColor QskScaleRenderer::tickColor() const return m_data->tickColor; } +void QskScaleRenderer::setTickLength( qreal length ) +{ + m_data->tickLength = qMax( length, 0.0 ); +} + +qreal QskScaleRenderer::tickLength() const +{ + return m_data->tickLength; +} + void QskScaleRenderer::setTickWidth( qreal width ) { - m_data->tickWidth = width; + m_data->tickWidth = qMax( width, 0.0 ); } qreal QskScaleRenderer::tickWidth() const @@ -170,79 +277,74 @@ const QskColorFilter& QskScaleRenderer::colorFilter() const return m_data->colorFilter; } -QSGNode* QskScaleRenderer::updateScaleNode( - const QskSkinnable* skinnable, const QRectF& tickmarksRect, - const QRectF& labelsRect, QSGNode* node ) +QSGNode* QskScaleRenderer::updateNode( + const QskSkinnable* skinnable, QSGNode* node ) { - enum Role - { - Ticks = 1, - Labels = 2 - }; + enum Role : quint8 { Ticks = 1, Labels = 2 }; + static const QVector< quint8 > roles = { Ticks, Labels }; + + const auto transform = qskScaleTransform( + m_data->edge, m_data->boundaries, m_data->range ); if ( node == nullptr ) node = new QSGNode(); + for ( auto role : roles ) { - QSGNode* oldNode = QskSGNode::findChildNode( node, Ticks ); - QSGNode* newNode = nullptr; + auto oldNode = QskSGNode::findChildNode( node, role ); - if ( !tickmarksRect.isEmpty() ) - { - newNode = updateTicksNode( skinnable, tickmarksRect, oldNode ); - if ( newNode ) - QskSGNode::setNodeRole( newNode, Ticks ); - } + auto newNode = ( role == Ticks ) + ? updateTicksNode( transform, oldNode ) + : updateLabelsNode( skinnable, transform, oldNode ); - qskInsertRemoveChild( node, oldNode, newNode, false ); - } - - { - QSGNode* oldNode = QskSGNode::findChildNode( node, Labels ); - QSGNode* newNode = nullptr; - - if ( !labelsRect.isEmpty() ) - { - newNode = updateLabelsNode( skinnable, tickmarksRect, labelsRect, oldNode ); - if ( newNode ) - QskSGNode::setNodeRole( newNode, Labels ); - } - - qskInsertRemoveChild( node, oldNode, newNode, true ); + QskSGNode::replaceChildNode( roles, role, node, oldNode, newNode ); } return node; } QSGNode* QskScaleRenderer::updateTicksNode( - const QskSkinnable*, const QRectF& rect, QSGNode* node ) const + const QTransform& transform, QSGNode* node ) const { - if ( rect.isEmpty() ) - return nullptr; + QskIntervalF backbone; + if ( m_data->flags & Backbone ) + backbone = m_data->boundaries; - auto ticksNode = static_cast< QskTickmarksNode* >( node ); + const auto orientation = qskIsHorizontal( m_data->edge ) + ? Qt::Horizontal : Qt::Vertical; - if( ticksNode == nullptr ) - ticksNode = new QskTickmarksNode; + auto alignment = QskAxisScaleNode::Centered; -#if 1 - const int tickWidth = qRound( m_data->tickWidth ); -#endif + if ( !( m_data->flags & CenteredTickmarks ) ) + { + switch( m_data->edge ) + { + case Qt::LeftEdge: + case Qt::TopEdge: + alignment = QskAxisScaleNode::Leading; + break; + case Qt::BottomEdge: + case Qt::RightEdge: + alignment = QskAxisScaleNode::Trailing; + break; + } + } - ticksNode->update( m_data->tickColor, rect, m_data->boundaries, - m_data->tickmarks, tickWidth, m_data->orientation, - m_data->alignment, {}); + auto axisNode = QskSGNode::ensureNode< QskAxisScaleNode >( node ); - return ticksNode; + axisNode->setColor( m_data->tickColor ); + axisNode->setAxis( orientation, m_data->position, transform ); + axisNode->setTickGeometry( alignment, m_data->tickLength, m_data->tickWidth ); + axisNode->setPixelAlignment( Qt::Horizontal | Qt::Vertical ); + + axisNode->update( m_data->tickmarks, backbone ); + + return axisNode; } -QSGNode* QskScaleRenderer::updateLabelsNode( - const QskSkinnable* skinnable, const QRectF& tickmarksRect, - const QRectF& labelsRect, QSGNode* node ) const +QSGNode* QskScaleRenderer::updateLabelsNode( const QskSkinnable* skinnable, + const QTransform& transform, QSGNode* node ) const { - if ( labelsRect.isEmpty() || tickmarksRect.isEmpty() ) - return nullptr; - const auto ticks = m_data->tickmarks.majorTicks(); if ( ticks.isEmpty() ) return nullptr; @@ -252,158 +354,72 @@ QSGNode* QskScaleRenderer::updateLabelsNode( const QFontMetricsF fm( m_data->font ); - const qreal length = ( m_data->orientation == Qt::Horizontal ) - ? tickmarksRect.width() : tickmarksRect.height(); - const qreal ratio = length / m_data->boundaries.length(); - auto nextNode = node->firstChild(); - QRectF labelRect; + QRectF lastRect; // to skip overlapping label for ( auto tick : ticks ) { - enum LabelNodeRole - { - TextNode = 1, - GraphicNode = 2 - }; - const auto label = labelAt( tick ); - if ( label.isNull() ) - continue; - const qreal tickPos = ratio * ( tick - m_data->boundaries.lowerBound() ); + const auto role = qskLabelNodeRole( label ); + + if ( nextNode && QskSGNode::nodeRole( nextNode ) != role ) + nextNode = qskRemoveTraillingNodes( node, nextNode ); + + QSizeF size; if ( label.canConvert< QString >() ) { - auto text = label.toString(); - if ( text.isEmpty() ) - continue; - - QRectF r; - Qt::Alignment alignment; - - if( m_data->orientation == Qt::Horizontal ) - { - const auto w = qskHorizontalAdvance( fm, text ); - - auto pos = tickmarksRect.x() + tickPos - 0.5 * w; - pos = qBound( labelsRect.left(), pos, labelsRect.right() - w ); - - r = QRectF( pos, labelsRect.y(), w, labelsRect.height() ); - - alignment = Qt::AlignLeft; - } - else - { - const auto h = fm.height(); - - auto pos = tickmarksRect.bottom() - ( tickPos + 0.5 * h ); - - /* - when clipping the label we can expand the clip rectangle - by the ascent/descent margins, as nothing gets painted there - anyway. - */ - const qreal min = labelsRect.top() - ( h - fm.ascent() ); - const qreal max = labelsRect.bottom() + fm.descent(); - pos = qBound( min, pos, max ); - - r = QRectF( labelsRect.x(), pos, labelsRect.width(), h ); - - alignment = Qt::AlignRight; - } - - if ( nextNode && QskSGNode::nodeRole( nextNode ) != TextNode ) - { - nextNode = qskRemoveTraillingNodes( node, nextNode ); - } - - if ( !labelRect.isEmpty() && labelRect.intersects( r ) ) - { - if ( tick != ticks.last() ) - { - text = QString(); - } - else - { - if ( auto obsoleteNode = nextNode - ? nextNode->previousSibling() : node->lastChild() ) - { - node->removeChildNode( obsoleteNode ); - if ( obsoleteNode->flags() & QSGNode::OwnedByParent ) - delete obsoleteNode; - } - - labelRect = r; - } - } - else - { - labelRect = r; - } - - nextNode = QskSkinlet::updateTextNode( skinnable, nextNode, - r, alignment, text, m_data->font, QskTextOptions(), - m_data->textColors, Qsk::Normal ); - - if ( nextNode ) - { - if ( nextNode->parent() != node ) - { - QskSGNode::setNodeRole( nextNode, TextNode ); - node->appendChildNode( nextNode ); - } - - nextNode = nextNode->nextSibling(); - } + size = qskTextRenderSize( fm, label.toString() ); } else if ( label.canConvert< QskGraphic >() ) { const auto graphic = label.value< QskGraphic >(); - if ( graphic.isNull() ) - continue; - - const auto h = fm.height(); - const auto w = graphic.widthForHeight( h ); - - Qt::Alignment alignment; - - if( m_data->orientation == Qt::Horizontal ) + if ( !graphic.isNull() ) { - auto pos = tickmarksRect.x() + tickPos - 0.5 * w; - pos = qBound( labelsRect.left(), pos, labelsRect.right() - w ); - - labelRect = QRectF( pos, labelsRect.y(), w, h ); - alignment = Qt::AlignHCenter | Qt::AlignBottom; + size.rheight() = fm.height(); + size.rwidth() = graphic.widthForHeight( size.height() ); } - else - { - auto pos = tickmarksRect.bottom() - ( tickPos + 0.5 * h ); - pos = qBound( labelsRect.top(), pos, labelsRect.bottom() - h ); + } - labelRect = QRectF( labelsRect.right() - w, pos, w, h ); - alignment = Qt::AlignRight | Qt::AlignVCenter; + if ( size.isEmpty() ) + continue; + + const auto rect = labelRect( transform, tick, size ); + + if ( !lastRect.isEmpty() && lastRect.intersects( rect ) ) + { + /* + Label do overlap: in case it is the last tick we remove + the precessor - otherwise we simply skip this one + */ + + if ( tick != ticks.last() ) + continue; // skip this label + + if ( auto obsoleteNode = nextNode + ? nextNode->previousSibling() : node->lastChild() ) + { + node->removeChildNode( obsoleteNode ); + if ( obsoleteNode->flags() & QSGNode::OwnedByParent ) + delete obsoleteNode; + } + } + + nextNode = updateTickLabelNode( skinnable, nextNode, label, rect ); + + if ( nextNode) + { + lastRect = rect; + + if ( nextNode->parent() != node ) + { + QskSGNode::setNodeRole( nextNode, role ); + node->appendChildNode( nextNode ); } - if ( nextNode && QskSGNode::nodeRole( nextNode ) != GraphicNode ) - { - nextNode = qskRemoveTraillingNodes( node, nextNode ); - } - - nextNode = QskSkinlet::updateGraphicNode( - skinnable, nextNode, graphic, m_data->colorFilter, labelRect, alignment ); - - if ( nextNode ) - { - if ( nextNode->parent() != node ) - { - QskSGNode::setNodeRole( nextNode, GraphicNode ); - node->appendChildNode( nextNode ); - } - - nextNode = nextNode->nextSibling(); - } + nextNode = nextNode->nextSibling(); } } @@ -417,40 +433,116 @@ QVariant QskScaleRenderer::labelAt( qreal pos ) const return QString::number( pos, 'g' ); } +// should be cached QSizeF QskScaleRenderer::boundingLabelSize() const { + QSizeF boundingSize( 0.0, 0.0 ); + const auto ticks = m_data->tickmarks.majorTicks(); if ( ticks.isEmpty() ) - return QSizeF( 0.0, 0.0 ); + return boundingSize; const QFontMetricsF fm( m_data->font ); - qreal maxWidth = 0.0; const qreal h = fm.height(); for ( auto tick : ticks ) { - qreal w = 0.0; - const auto label = labelAt( tick ); if ( label.isNull() ) continue; if ( label.canConvert< QString >() ) { - w = qskHorizontalAdvance( fm, label.toString() ); + boundingSize = boundingSize.expandedTo( + qskTextRenderSize( fm, label.toString() ) ); } else if ( label.canConvert< QskGraphic >() ) { const auto graphic = label.value< QskGraphic >(); if ( !graphic.isNull() ) { - w = graphic.widthForHeight( h ); + const auto w = graphic.widthForHeight( h ); + boundingSize.setWidth( qMax( boundingSize.width(), w ) ); } } - - maxWidth = qMax( w, maxWidth ); } - return QSizeF( maxWidth, h ); + return boundingSize; } + +QRectF QskScaleRenderer::labelRect( + const QTransform& transform, qreal tick, const QSizeF& labelSize ) const +{ + const auto isHorizontal = qskIsHorizontal( m_data->edge ); + + auto offset = m_data->tickLength + m_data->spacing; + if ( m_data->flags & CenteredTickmarks ) + offset -= 0.5 * m_data->tickLength; + + const bool clampLabels = m_data->flags & ClampedLabels; + + const qreal w = labelSize.width(); + const qreal h = labelSize.height(); + + qreal x, y; + + const ScaleMap map( isHorizontal, transform ); + + const auto tickPos = map.map( tick ); + + qreal min, max; + if ( clampLabels ) + { + min = map.map( m_data->boundaries.lowerBound() ); + max = map.map( m_data->boundaries.upperBound() ); + } + + if( isHorizontal ) + { + x = tickPos - 0.5 * w; + + if ( clampLabels ) + x = qBound( min, x, max - w ); + + y = m_data->position + offset; + } + else + { + const auto tickPos = map.map( tick ); + y = tickPos - 0.5 * h; + + if ( clampLabels ) + y = qBound( max, y, min - h ); + + x = m_data->position - offset - w; + } + + return QRectF( x, y, w, h ); +} + +QSGNode* QskScaleRenderer::updateTickLabelNode( const QskSkinnable* skinnable, + QSGNode* node, const QVariant& label, const QRectF& rect ) const +{ + if ( label.canConvert< QString >() ) + { + return QskSkinlet::updateTextNode( skinnable, node, + rect, Qt::AlignCenter, label.toString(), m_data->font, + QskTextOptions(), m_data->textColors, Qsk::Normal ); + } + + if ( label.canConvert< QskGraphic >() ) + { + const auto alignment = qskIsHorizontal( m_data->edge ) + ? ( Qt::AlignHCenter | Qt::AlignBottom ) + : ( Qt::AlignRight | Qt::AlignVCenter ); + + return QskSkinlet::updateGraphicNode( + skinnable, node, label.value< QskGraphic >(), + m_data->colorFilter, rect, alignment ); + } + + return nullptr; +} + +#include "moc_QskScaleRenderer.cpp" diff --git a/src/nodes/QskScaleRenderer.h b/src/nodes/QskScaleRenderer.h index 596b5513..017c29ff 100644 --- a/src/nodes/QskScaleRenderer.h +++ b/src/nodes/QskScaleRenderer.h @@ -23,30 +23,61 @@ class QskColorFilter; class QSGNode; class QVariant; class QRectF; +class QPointF; class QSizeF; +class QTransform; class QSK_EXPORT QskScaleRenderer { + Q_GADGET + public: + enum Flag + { + Backbone = 1 << 0, + CenteredTickmarks = 1 << 1, + ClampedLabels = 1 << 2 + }; + + Q_ENUM( Flag ) + Q_DECLARE_FLAGS( Flags, Flag ) + QskScaleRenderer(); virtual ~QskScaleRenderer(); - void setOrientation( Qt::Orientation ); - Qt::Orientation orientation() const; + void setEdge( Qt::Edge ); + Qt::Edge edge() const; - void setAlignment( Qt::Alignment ); - Qt::Alignment aligment() const; + void setFlags( Flags ); + Flags flags() const; + void setFlag( Flag, bool ); + + // scale coordinates void setBoundaries( qreal lowerBound, qreal upperBound ); void setBoundaries( const QskIntervalF& ); QskIntervalF boundaries() const; + // item coordiates + qreal position() const; + void setPosition( qreal ); + + void setRange( qreal from, qreal to ); + void setRange( const QskIntervalF& ); + QskIntervalF range() const; + void setTickmarks( const QskTickmarks& ); const QskTickmarks& tickmarks() const; + void setSpacing( qreal ); + qreal spacing() const; + void setTickColor( const QColor& ); QColor tickColor() const; + void setTickLength( qreal ); + qreal tickLength() const; + void setTickWidth( qreal ); qreal tickWidth() const; @@ -59,24 +90,29 @@ class QSK_EXPORT QskScaleRenderer void setColorFilter( const QskColorFilter& ); const QskColorFilter& colorFilter() const; - QSGNode* updateScaleNode( const QskSkinnable*, - const QRectF& tickmarksRect, const QRectF& labelsRect, QSGNode* ); + QSGNode* updateNode( const QskSkinnable*, QSGNode* ); virtual QVariant labelAt( qreal pos ) const; QSizeF boundingLabelSize() const; - virtual QSGNode* updateTicksNode( - const QskSkinnable*, const QRectF&, QSGNode* ) const; + protected: + virtual QSGNode* updateTicksNode( const QTransform&, QSGNode* ) const; virtual QSGNode* updateLabelsNode( - const QskSkinnable*, const QRectF& ticksRect, - const QRectF& labelsRect, QSGNode* node ) const; + const QskSkinnable*, const QTransform&, QSGNode* ) const; private: Q_DISABLE_COPY( QskScaleRenderer ) + QRectF labelRect( const QTransform&, qreal, const QSizeF& ) const; + + QSGNode* updateTickLabelNode( const QskSkinnable*, + QSGNode*, const QVariant&, const QRectF& ) const; + class PrivateData; std::unique_ptr< PrivateData > m_data; }; +Q_DECLARE_OPERATORS_FOR_FLAGS( QskScaleRenderer::Flags ) + #endif diff --git a/src/nodes/QskTickmarksNode.cpp b/src/nodes/QskTickmarksNode.cpp deleted file mode 100644 index 3757c94b..00000000 --- a/src/nodes/QskTickmarksNode.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/****************************************************************************** - * QSkinny - Copyright (C) 2016 Uwe Rathmann - * SPDX-License-Identifier: BSD-3-Clause - *****************************************************************************/ - -#include "QskTickmarksNode.h" -#include "QskTickmarks.h" -#include "QskGraduationMetrics.h" - -#include -#include - -QskTickmarksNode::QskTickmarksNode() -{ -} - -QskTickmarksNode::~QskTickmarksNode() -{ -} - -void QskTickmarksNode::update( - const QColor& color, const QRectF& rect, - const QskIntervalF& boundaries, const QskTickmarks& tickmarks, - int lineWidth, Qt::Orientation orientation, Qt::Alignment alignment, - const QskGraduationMetrics& graduationMetrics ) -{ - setLineWidth( lineWidth ); - - auto hash = tickmarks.hash( 17435 ); - hash = graduationMetrics.hash( hash ); - hash = qHashBits( &boundaries, sizeof( boundaries ), hash ); - hash = qHashBits( &rect, sizeof( rect ), hash ); - hash = qHash( orientation, hash ); - hash = qHash( alignment, hash ); - - if ( hash != m_hash ) - { - m_hash = hash; - - geometry()->allocate( tickmarks.tickCount() * 2 ); - auto vertexData = geometry()->vertexDataAsPoint2D(); - - const qreal min = boundaries.lowerBound(); - const qreal range = boundaries.length(); - - using TM = QskTickmarks; - - for( int i = TM::MinorTick; i <= TM::MajorTick; i++ ) - { - const auto tickType = static_cast< TM::TickType >( i ); - - const auto ticks = tickmarks.ticks( tickType ); - const float len = graduationMetrics.tickLength( tickType ); - - if ( orientation == Qt::Horizontal ) - { - const qreal ratio = rect.width() / range; - - for( const auto tick : ticks ) - { - const auto x = rect.x() + ( tick - min ) * ratio; - - qreal y1, y2; - - if( alignment & Qt::AlignTop ) - { - y1 = rect.top() + len; - y2 = rect.top(); - } - else if( alignment & Qt::AlignVCenter ) - { - const auto offset = ( rect.height() - len ) / 2; - y1 = rect.bottom() - offset; - y2 = rect.top() + offset; - } - else // Bottom (default) - { - y1 = rect.bottom(); - y2 = rect.bottom() - len; - } - - vertexData[ 0 ].set( x, y1 ); - vertexData[ 1 ].set( x, y2 ); - vertexData += 2; - } - } - else - { - const qreal ratio = rect.height() / range; - - for( const auto tick : ticks ) - { - const auto y = rect.bottom() - ( tick - min ) * ratio; - - qreal x1, x2; - - if( alignment & Qt::AlignLeft ) - { - x1 = rect.left() + len; - x2 = rect.left(); - } - else if( alignment & Qt::AlignHCenter ) - { - const auto offset = ( rect.width() - len ) / 2; - x1 = rect.right() - offset; - x2 = rect.left() + offset; - } - else // Right (default) - { - x1 = rect.right(); - x2 = rect.right() - len; - } - - vertexData[ 0 ].set( x1, y ); - vertexData[ 1 ].set( x2, y ); - vertexData += 2; - } - } - } - - geometry()->markVertexDataDirty(); - markDirty( QSGNode::DirtyGeometry ); - } - - setColor( color ); -} diff --git a/src/nodes/QskTickmarksNode.h b/src/nodes/QskTickmarksNode.h deleted file mode 100644 index dadf88c6..00000000 --- a/src/nodes/QskTickmarksNode.h +++ /dev/null @@ -1,30 +0,0 @@ -/****************************************************************************** - * QSkinny - Copyright (C) 2016 Uwe Rathmann - * SPDX-License-Identifier: BSD-3-Clause - *****************************************************************************/ - -#ifndef QSK_TICKMARKS_NODE_H -#define QSK_TICKMARKS_NODE_H - -#include "QskBasicLinesNode.h" - -class QskIntervalF; -class QskTickmarks; -class QskGraduationMetrics; -class QRectF; - -class QSK_EXPORT QskTickmarksNode : public QskBasicLinesNode -{ - public: - QskTickmarksNode(); - ~QskTickmarksNode() override; - - void update(const QColor&, const QRectF&, const QskIntervalF&, - const QskTickmarks&, int tickLineWidth, Qt::Orientation, - Qt::Alignment, const QskGraduationMetrics& ); - - private: - QskHashValue m_hash = 0; -}; - -#endif