From 0df95fcb3708c1391f1de31019a1884b0b90fda4 Mon Sep 17 00:00:00 2001 From: Peter Hartmann Date: Fri, 16 Apr 2021 09:48:43 +0200 Subject: [PATCH] Add Diagram drawn with OpenGL --- examples/iot-dashboard/Diagram.cpp | 94 +++++++++++++ examples/iot-dashboard/Diagram.h | 49 +++++++ examples/iot-dashboard/DiagramSkinlet.cpp | 125 ++++++++++++++++++ examples/iot-dashboard/DiagramSkinlet.h | 34 +++++ examples/iot-dashboard/Skin.cpp | 2 +- examples/iot-dashboard/iot-dashboard.pro | 20 ++- .../iot-dashboard/nodes/DiagramDataNode.cpp | 85 ++++++++++++ .../iot-dashboard/nodes/DiagramDataNode.h | 33 +++++ .../nodes/DiagramSegmentsNode.cpp | 51 +++++++ .../iot-dashboard/nodes/DiagramSegmentsNode.h | 22 +++ 10 files changed, 510 insertions(+), 5 deletions(-) create mode 100644 examples/iot-dashboard/Diagram.cpp create mode 100644 examples/iot-dashboard/Diagram.h create mode 100644 examples/iot-dashboard/DiagramSkinlet.cpp create mode 100644 examples/iot-dashboard/DiagramSkinlet.h create mode 100644 examples/iot-dashboard/nodes/DiagramDataNode.cpp create mode 100644 examples/iot-dashboard/nodes/DiagramDataNode.h create mode 100644 examples/iot-dashboard/nodes/DiagramSegmentsNode.cpp create mode 100644 examples/iot-dashboard/nodes/DiagramSegmentsNode.h diff --git a/examples/iot-dashboard/Diagram.cpp b/examples/iot-dashboard/Diagram.cpp new file mode 100644 index 00000000..4161165f --- /dev/null +++ b/examples/iot-dashboard/Diagram.cpp @@ -0,0 +1,94 @@ +#include "Diagram.h" + +#include + +class Diagram::PrivateData +{ + public: + PrivateData() + { + } + + QVector dataPoints; + int xGridLines = -1; + qreal yMax = -1; + Qsk::Position position = Qsk::Bottom; + Types types = Area; +}; + +QSK_SUBCONTROL( Diagram, Chart ) +QSK_SUBCONTROL( Diagram, Segments ) +QSK_SUBCONTROL( Diagram, ChartLine ) +QSK_SUBCONTROL( Diagram, ChartArea ) + +Diagram::Diagram( QQuickItem* parent ) + : Inherited( parent ) + , m_data( new PrivateData() ) +{ +} + +Diagram::~Diagram() +{ +} + +QVector Diagram::dataPoints() const +{ + return m_data->dataPoints; +} + +void Diagram::setDataPoints( const QVector& dataPoints ) +{ + m_data->dataPoints = dataPoints; +} + +Diagram::Types Diagram::types() const +{ + return m_data->types; +} + +void Diagram::setTypes( Types types ) +{ + m_data->types = types; +} + +qreal Diagram::yMax() const +{ + return m_data->yMax; +} + +void Diagram::setYMax( qreal yMax ) +{ + m_data->yMax = yMax; +} + +int Diagram::xGridLines() const +{ + return m_data->xGridLines; +} + +void Diagram::setXGridLines( int lines ) +{ + m_data->xGridLines = lines; +} + +Qsk::Position Diagram::chartPosition() const +{ + return m_data->position; +} + +void Diagram::setChartPosition( Qsk::Position position ) +{ + m_data->position = position; +} + +QSizeF Diagram::contentsSizeHint( Qt::SizeHint which, const QSizeF& ) const +{ + if( which != Qt::PreferredSize ) + { + return QSizeF(); + } + + return {}; +} + +#include "moc_Diagram.cpp" diff --git a/examples/iot-dashboard/Diagram.h b/examples/iot-dashboard/Diagram.h new file mode 100644 index 00000000..40bf95cf --- /dev/null +++ b/examples/iot-dashboard/Diagram.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +class Diagram : public QskControl +{ + Q_OBJECT + + using Inherited = QskControl; + + public: + QSK_SUBCONTROLS( Chart, Segments, ChartLine, ChartArea ) + + enum Type + { + Line = 0x01, + Area = 0x02, + }; + + Q_DECLARE_FLAGS( Types, Type ) + + Diagram( QQuickItem* parent = nullptr ); + ~Diagram() override; + + QVector dataPoints() const; + void setDataPoints( const QVector& dataPoints ); + + Types types() const; + void setTypes( Types types ); + + qreal yMax() const; + void setYMax( qreal yMax ); + + int xGridLines() const; + void setXGridLines( int lines ); + + Qsk::Position chartPosition() const; + void setChartPosition( Qsk::Position position ); + + protected: + QSizeF contentsSizeHint( Qt::SizeHint, const QSizeF& ) const override; + + private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS( Diagram::Types ) diff --git a/examples/iot-dashboard/DiagramSkinlet.cpp b/examples/iot-dashboard/DiagramSkinlet.cpp new file mode 100644 index 00000000..0b604b97 --- /dev/null +++ b/examples/iot-dashboard/DiagramSkinlet.cpp @@ -0,0 +1,125 @@ +#include "DiagramSkinlet.h" +#include "Diagram.h" +#include "nodes/DiagramDataNode.h" +#include "nodes/DiagramSegmentsNode.h" + +DiagramSkinlet::DiagramSkinlet( QskSkin* skin ) + : QskSkinlet( skin ) +{ + setNodeRoles( { ChartRole, SeparatorRole } ); +} + +DiagramSkinlet::~DiagramSkinlet() +{ +} + +QRectF DiagramSkinlet::subControlRect( + const QskSkinnable* skinnable, const QRectF& contentsRect, + QskAspect::Subcontrol subControl ) const +{ + if( subControl == Diagram::Chart ) + { + return contentsRect; + } + + return Inherited::subControlRect( skinnable, contentsRect, subControl ); +} + +QSGNode* DiagramSkinlet::updateSubNode( + const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const +{ + const auto discharge = static_cast< const Diagram* >( skinnable ); + + switch( nodeRole ) + { + case ChartRole: + { + return updateChartNode( discharge, node ); + } + + case SeparatorRole: + { + return updateSeparatorNode( discharge, node ); + } + } + + return nullptr; +} + +QSGNode* DiagramSkinlet::updateChartNode( const Diagram* distribution, QSGNode* node ) const +{ + if( node == nullptr ) + { + node = new IdlChartNode; + } + + using Q = Diagram; + const QRectF rect = distribution->subControlRect( Q::Chart ); + const QVector dataPoints = distribution->dataPoints(); + const qreal yMax = distribution->yMax(); + const Qsk::Position position = distribution->chartPosition(); + int lineWidth = distribution->metric( Diagram::ChartLine | QskAspect::Size ); + QVector types = {Diagram::Line, Diagram::Area}; + + int nodeIndex = 0; + + for( int i = 0; i < types.size(); ++i ) + { + if( distribution->types() & types.at( i ) ) + { + IdlChartNode* lineNode; + + if( node->childCount() > nodeIndex ) + { + lineNode = static_cast( node->childAtIndex( nodeIndex ) ); + } + else + { + lineNode = new IdlChartNode; + node->appendChildNode( lineNode ); + } + + const IdlChartNode::Type nodeType = ( types.at( i ) == Diagram::Line ) ? IdlChartNode::Line : IdlChartNode::Area; + const QColor color = ( types.at( i ) == Diagram::Line ) ? distribution->color( Q::ChartLine ) + : distribution->color( Q::ChartArea ); + + lineNode->update( rect, nodeType, color, dataPoints, yMax, position, lineWidth ); + nodeIndex++; + } + } + + while( nodeIndex < node->childCount() ) + { + node->removeChildNode( node->childAtIndex( nodeIndex++ ) ); + } + + return node; +} + +QSGNode* DiagramSkinlet::updateSeparatorNode( const Diagram* discharge, QSGNode* node ) const +{ + const int xGridLines = discharge->xGridLines(); + + if( xGridLines <= 0 ) + { + return nullptr; + } + + auto* separatorNode = static_cast( node ); + + if( separatorNode == nullptr ) + { + separatorNode = new IdlChartSegmentsNode; + } + + using Q = Diagram; + const QRectF rect = discharge->subControlRect( Q::Chart ); + const QColor color = discharge->color( Q::Segments ); + const QVector dataPoints = discharge->dataPoints(); + + separatorNode->update( rect, color, dataPoints, xGridLines ); + + return separatorNode; +} + +#include "moc_DiagramSkinlet.cpp" diff --git a/examples/iot-dashboard/DiagramSkinlet.h b/examples/iot-dashboard/DiagramSkinlet.h new file mode 100644 index 00000000..4534a13a --- /dev/null +++ b/examples/iot-dashboard/DiagramSkinlet.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +class Diagram; + +class DiagramSkinlet : public QskSkinlet +{ + Q_GADGET + + using Inherited = QskSkinlet; + + public: + enum NodeRole + { + ChartRole, + SeparatorRole, + }; + + Q_INVOKABLE DiagramSkinlet( QskSkin* = nullptr ); + ~DiagramSkinlet() override; + + QRectF subControlRect( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol ) const override; + + protected: + + QSGNode* updateSubNode( const QskSkinnable*, + quint8 nodeRole, QSGNode* ) const override; + + private: + QSGNode* updateChartNode( const Diagram*, QSGNode* ) const; + QSGNode* updateSeparatorNode( const Diagram*, QSGNode* ) const; +}; diff --git a/examples/iot-dashboard/Skin.cpp b/examples/iot-dashboard/Skin.cpp index 0e8a24db..c573a4f0 100644 --- a/examples/iot-dashboard/Skin.cpp +++ b/examples/iot-dashboard/Skin.cpp @@ -2,7 +2,6 @@ #include "Box.h" #include "BoxWithButtons.h" -#include "Diagram.h" #include "LightIntensity.h" #include "MainContent.h" #include "MenuBar.h" @@ -10,6 +9,7 @@ #include "TopBar.h" #include "UpAndDownButton.h" #include "Usage.h" +#include "UsageDiagram.h" #include #include diff --git a/examples/iot-dashboard/iot-dashboard.pro b/examples/iot-dashboard/iot-dashboard.pro index 4796d0c9..2ea361d3 100644 --- a/examples/iot-dashboard/iot-dashboard.pro +++ b/examples/iot-dashboard/iot-dashboard.pro @@ -4,7 +4,8 @@ SOURCES += \ Box.cpp \ BoxWithButtons.cpp \ CircularProgressBar.cpp \ - UsageDiagram.cpp \ + Diagram.cpp \ + DiagramSkinlet.cpp \ LightIntensity.cpp \ MainContent.cpp \ MenuBar.cpp \ @@ -18,13 +19,19 @@ SOURCES += \ UpAndDownButton.cpp \ Usage.cpp \ main.cpp \ - MainWindow.cpp + MainWindow.cpp \ + UsageDiagram.cpp + +SOURCES += \ + nodes/DiagramDataNode.cpp \ + nodes/DiagramSegmentsNode.cpp HEADERS += \ Box.h \ BoxWithButtons.h \ CircularProgressBar.h \ - UsageDiagram.h \ + Diagram.h \ + DiagramSkinlet.h \ LightIntensity.h \ MainContent.h \ MainWindow.h \ @@ -37,7 +44,12 @@ HEADERS += \ Skin.h \ TopBar.h \ UpAndDownButton.h \ - Usage.h + Usage.h \ + UsageDiagram.h + +HEADERS += \ + nodes/DiagramDataNode.h \ + nodes/DiagramSegmentsNode.h HEADERS += \ src/shadowedrectangle.h \ diff --git a/examples/iot-dashboard/nodes/DiagramDataNode.cpp b/examples/iot-dashboard/nodes/DiagramDataNode.cpp new file mode 100644 index 00000000..a4abc7bc --- /dev/null +++ b/examples/iot-dashboard/nodes/DiagramDataNode.cpp @@ -0,0 +1,85 @@ +#include "DiagramDataNode.h" +#include +#include + +IdlChartNode::IdlChartNode() + : m_geometry( QSGGeometry::defaultAttributes_Point2D(), 0 ) +{ + setGeometry( &m_geometry ); + setMaterial( &m_material ); +} + +void IdlChartNode::update( const QRectF& rect, Type type, const QColor& color, const QVector& dataPoints, + const qreal yMax, Qsk::Position position, int lineWidth ) +{ + Q_UNUSED( rect ); + + if( color != m_color ) + { + m_material.setColor( color ); + m_color = color; + markDirty( QSGNode::DirtyMaterial ); + } + + if( m_rect == rect && m_dataPoints == dataPoints && m_yMax == yMax && m_position == position + && m_type == type && m_lineWidth == lineWidth ) + { + return; + } + + if( lineWidth != m_lineWidth ) + { + m_lineWidth = lineWidth; + m_geometry.setLineWidth( lineWidth ); + + markDirty( QSGNode::DirtyGeometry ); + } + + m_rect = rect; + m_dataPoints = dataPoints; + m_yMax = yMax; + m_position = position; + m_type = type; + + GLenum drawingMode = ( m_type == Line ) ? GL_LINES : GL_TRIANGLE_STRIP; + m_geometry.setDrawingMode( drawingMode ); + + const int vertexCount = ( m_type == Line ) ? m_dataPoints.size() * 2 - 1 : m_dataPoints.size() * 2; + + m_geometry.allocate( vertexCount ); + auto vertexData = m_geometry.vertexDataAsPoint2D(); + + const qreal xMin = m_dataPoints.at( 0 ).x(); + const qreal xMax = m_dataPoints.at( m_dataPoints.count() - 1 ).x(); + + // ### we should have a different function for each chart type + for( int i = 0; i < m_dataPoints.size(); ++i ) + { + const qreal x = ( ( m_dataPoints.at( i ).x() - xMin ) / ( xMax - xMin ) ) * rect.width(); + const qreal fraction = ( m_dataPoints.at( i ).y() / yMax ) * rect.height(); + const qreal y = ( position == Qsk::Top ) ? fraction : rect.height() - fraction; + + if( m_type == Line && i < m_dataPoints.size() - 1 ) + { + const qreal x2 = ( ( m_dataPoints.at( i + 1 ).x() - xMin ) / ( xMax - xMin ) ) * rect.width(); + const qreal fraction2 = ( m_dataPoints.at( i + 1 ).y() / yMax ) * rect.height(); + const qreal y2 = ( position == Qsk::Top ) ? fraction2 : rect.height() - fraction2; + vertexData[2 * i].x = x; + vertexData[2 * i].y = y; + + vertexData[2 * i + 1].x = x2; + vertexData[2 * i + 1].y = y2; + } + else if( m_type == Area ) + { + const qreal y0 = ( position == Qsk::Top ) ? 0 : rect.height(); + vertexData[2 * i].x = x; + vertexData[2 * i].y = y; + + vertexData[2 * i + 1].x = x; + vertexData[2 * i + 1].y = y0; + } + } + + markDirty( QSGNode::DirtyGeometry ); +} diff --git a/examples/iot-dashboard/nodes/DiagramDataNode.h b/examples/iot-dashboard/nodes/DiagramDataNode.h new file mode 100644 index 00000000..766fee0f --- /dev/null +++ b/examples/iot-dashboard/nodes/DiagramDataNode.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include +#include +#include + +class IdlChartNode : public QSGGeometryNode +{ + public: + enum Type + { + Line, + Area, + }; + + IdlChartNode(); + + void update( const QRectF& rect, Type type, const QColor& color, const QVector& dataPoints, const qreal yMax, Qsk::Position position, int lineWidth ); + + private: + QSGFlatColorMaterial m_material; + QSGGeometry m_geometry; + + QRectF m_rect; + Type m_type; + QColor m_color; + QVector m_dataPoints; + qreal m_yMax; + Qsk::Position m_position; + int m_lineWidth; +}; diff --git a/examples/iot-dashboard/nodes/DiagramSegmentsNode.cpp b/examples/iot-dashboard/nodes/DiagramSegmentsNode.cpp new file mode 100644 index 00000000..af5f75d6 --- /dev/null +++ b/examples/iot-dashboard/nodes/DiagramSegmentsNode.cpp @@ -0,0 +1,51 @@ +#include "DiagramSegmentsNode.h" + +#include + +IdlChartSegmentsNode::IdlChartSegmentsNode() + : m_geometry( QSGGeometry::defaultAttributes_Point2D(), 0 ) +{ + m_geometry.setDrawingMode( GL_LINES ); + + setGeometry( &m_geometry ); + setMaterial( &m_material ); +} + +void IdlChartSegmentsNode::update( const QRectF& rect, const QColor& color, const QVector& dataPoints, int xGridLines ) +{ + Q_UNUSED( rect ); + + if( color != m_color ) + { + m_material.setColor( color ); + m_color = color; + markDirty( QSGNode::DirtyMaterial ); + } + + if( m_rect == rect && m_dataPoints == dataPoints && m_xGridLines == xGridLines ) + { + return; + } + + m_rect = rect; + m_dataPoints = dataPoints; + m_xGridLines = xGridLines; + + const qreal step = rect.width() / ( xGridLines - 1 ); + + m_geometry.allocate( m_xGridLines * 2 ); + auto vertexData = m_geometry.vertexDataAsPoint2D(); + + for( int i = 0; i < m_xGridLines; ++i ) + { + const qreal x = i * step; + + vertexData[2 * i].x = x; + vertexData[2 * i].y = rect.height(); + + vertexData[2 * i + 1].x = x; + vertexData[2 * i + 1].y = 0; + } + + markDirty( QSGNode::DirtyGeometry ); +} diff --git a/examples/iot-dashboard/nodes/DiagramSegmentsNode.h b/examples/iot-dashboard/nodes/DiagramSegmentsNode.h new file mode 100644 index 00000000..6faa9d4a --- /dev/null +++ b/examples/iot-dashboard/nodes/DiagramSegmentsNode.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +class IdlChartSegmentsNode : public QSGGeometryNode +{ + public: + IdlChartSegmentsNode(); + + void update( const QRectF& rect, const QColor& color, const QVector& dataPoints, int xGridLines ); + + private: + QSGFlatColorMaterial m_material; + QSGGeometry m_geometry; + + QRectF m_rect; + QColor m_color; + QVector m_dataPoints; + int m_xGridLines; +};