QskArcRenderer using the normal vector of the tangents for expanding

the arc to the desired thickness. This matches the result of what
QPainter::drawArc does. However our implementation is much
simpler as we do not convert the arc into a sequence of
bezier curves finally running into code that has to deal with
random QPainterPath element lists.
This commit is contained in:
Uwe Rathmann 2024-05-30 19:06:02 +02:00
parent a327084c3f
commit bc066c8103
8 changed files with 172 additions and 221 deletions

View File

@ -27,7 +27,7 @@ CircularChart::CircularChart( QQuickItem* parentItem )
setGradientHint( Panel, QskGradient() );
setBoxBorderMetricsHint( Panel, 0 );
setArcMetricsHint( Arc, { 90.0, -360.0, 100.0, Qt::RelativeSize, true } );
setArcMetricsHint( Arc, { 90.0, -360.0, 100.0, Qt::RelativeSize } );
setGradientHint( Arc, QskRgb::toTransparent( QskRgb::LightGray, 100 ) );
setColor( Arc | QskAspect::Border, QskRgb::LightGray );

View File

@ -106,7 +106,7 @@ ShadowedArc::ShadowedArc( QQuickItem* parent )
setBorderWidth( 0 );
setBorderColor( Qt::gray );
setShadowColor( Qt::black );
setShadowColor( QColor() );
setShadowMetrics( { 0, 0, QPointF( 0, 0 ), Qt::AbsoluteSize } );
}

View File

@ -51,11 +51,6 @@ void QskArcMetrics::setSizeMode( Qt::SizeMode sizeMode ) noexcept
m_relativeSize = ( sizeMode == Qt::RelativeSize );
}
void QskArcMetrics::setProportional( bool on ) noexcept
{
m_proportional = on;
}
bool QskArcMetrics::isClosed() const
{
return qAbs( m_spanAngle ) >= 360.0;
@ -92,7 +87,7 @@ QskArcMetrics QskArcMetrics::interpolated(
const qreal s1 = qskInterpolated( m_startAngle, to.m_startAngle, ratio );
const qreal s2 = qskInterpolated( endAngle(), to.endAngle(), ratio );
return QskArcMetrics( s1, s2 - s1, thickness, sizeMode(), to.isProportional() );
return QskArcMetrics( s1, s2 - s1, thickness, sizeMode() );
}
QVariant QskArcMetrics::interpolate(
@ -119,8 +114,7 @@ QskArcMetrics QskArcMetrics::toAbsolute( qreal radius ) const noexcept
return *this;
const qreal t = qskEffectiveThickness( radius, m_thickness );
return QskArcMetrics( m_startAngle, m_spanAngle, t,
Qt::AbsoluteSize, m_proportional );
return QskArcMetrics( m_startAngle, m_spanAngle, t, Qt::AbsoluteSize );
}
QPainterPath QskArcMetrics::painterPath( const QRectF& ellipseRect ) const
@ -136,18 +130,7 @@ QPainterPath QskArcMetrics::painterPath( const QRectF& ellipseRect ) const
if ( t <= 0.0 || qFuzzyIsNull( m_spanAngle ) )
return QPainterPath();
auto tx = t;
auto ty = t;
if ( m_proportional )
{
const auto sz = qMin( ellipseRect.width(), ellipseRect.height() );
tx *= ellipseRect.width() / sz;
ty *= ellipseRect.height() / sz;
}
const auto innerRect = ellipseRect.adjusted( tx, ty, -tx, -ty );
const auto innerRect = ellipseRect.adjusted( t, t, -t, -t );
QPainterPath path;
@ -168,32 +151,18 @@ QPainterPath QskArcMetrics::painterPath( const QRectF& ellipseRect ) const
}
else
{
if ( qAbs( m_spanAngle ) >= 360.0 )
{
path.addEllipse( ellipseRect );
const auto t2 = 0.5 * t;
const auto r = ellipseRect.adjusted( t2, t2, -t2, -t2 );
QPainterPath innerPath;
innerPath.addEllipse( innerRect );
path -= innerPath;
}
else
{
/*
We need the end point of the inner arc to add the line that connects
the inner/outer arcs. As QPainterPath does not offer such a method
we insert a dummy arcMoveTo and grab the calculated position.
*/
path.arcMoveTo( innerRect, m_startAngle + m_spanAngle );
const auto pos = path.currentPosition();
QPainterPath arcPath;
arcPath.arcMoveTo( r, m_startAngle ); // replaces the dummy arcMoveTo above
arcPath.arcTo( r, m_startAngle, m_spanAngle );
path.arcMoveTo( ellipseRect, m_startAngle ); // replaces the dummy arcMoveTo above
path.arcTo( ellipseRect, m_startAngle, m_spanAngle );
QPainterPathStroker stroker;
stroker.setCapStyle( Qt::FlatCap );
stroker.setWidth( t );
path.lineTo( pos );
path.arcTo( innerRect, m_startAngle + m_spanAngle, -m_spanAngle );
path.closeSubpath();
}
path = stroker.createStroke( arcPath );
}
return path;
@ -227,9 +196,8 @@ QskHashValue QskArcMetrics::hash( QskHashValue seed ) const noexcept
auto hash = qHash( m_thickness, seed );
hash = qHash( m_startAngle, hash );
hash = qHash( m_spanAngle, hash );
hash = qHash( m_relativeSize, hash );
return qHash( m_proportional, hash );
return qHash( m_relativeSize, hash );
}
#ifndef QT_NO_DEBUG_STREAM
@ -242,8 +210,7 @@ QDebug operator<<( QDebug debug, const QskArcMetrics& metrics )
debug.nospace();
debug << "QskArcMetrics" << '(';
debug << metrics.thickness() << ',' << metrics.sizeMode() << ','
<< metrics.isProportional();
debug << metrics.thickness() << ',' << metrics.sizeMode();
debug << ",[" << metrics.startAngle() << ',' << metrics.spanAngle() << ']';
debug << ')';

View File

@ -22,16 +22,15 @@ class QSK_EXPORT QskArcMetrics
Q_PROPERTY( qreal thickness READ thickness WRITE setThickness )
Q_PROPERTY( Qt::SizeMode sizeMode READ sizeMode WRITE setSizeMode )
Q_PROPERTY( bool proportional READ isProportional WRITE setProportional )
public:
constexpr QskArcMetrics() noexcept = default;
constexpr QskArcMetrics( qreal thickness,
Qt::SizeMode = Qt::AbsoluteSize, bool proportional = false ) noexcept;
Qt::SizeMode = Qt::AbsoluteSize ) noexcept;
constexpr QskArcMetrics( qreal startAngle, qreal spanAngle, qreal thickness,
Qt::SizeMode = Qt::AbsoluteSize, bool proportional = false ) noexcept;
Qt::SizeMode = Qt::AbsoluteSize ) noexcept;
bool operator==( const QskArcMetrics& ) const noexcept;
bool operator!=( const QskArcMetrics& ) const noexcept;
@ -53,19 +52,6 @@ class QSK_EXPORT QskArcMetrics
void setThickness( qreal ) noexcept;
constexpr qreal thickness() const noexcept;
/*
A proportional arc scales the thickness of the arc according to the
aspect ratio of the target rectangle. F.e when having a 20x10 rectangle
the thickness in west/east direction is doubled, while for a
10x20 rectangle the thickness in north/south direction is doubled.
This matches the lines that result from a filling with a conic gradient.
A non proportional arc will have a fixed thickness regardless of
the aspect ratio.
*/
void setProportional( bool ) noexcept;
constexpr bool isProportional() const noexcept;
void setSizeMode( Qt::SizeMode ) noexcept;
constexpr Qt::SizeMode sizeMode() const noexcept;
@ -92,22 +78,20 @@ class QSK_EXPORT QskArcMetrics
qreal m_thickness = 0.0;
bool m_relativeSize = false;
bool m_proportional = false;
};
inline constexpr QskArcMetrics::QskArcMetrics(
qreal thickness, Qt::SizeMode sizeMode, bool proportional ) noexcept
: QskArcMetrics( 0.0, 360.0, thickness, sizeMode, proportional )
qreal thickness, Qt::SizeMode sizeMode ) noexcept
: QskArcMetrics( 0.0, 360.0, thickness, sizeMode )
{
}
inline constexpr QskArcMetrics::QskArcMetrics( qreal startAngle, qreal spanAngle,
qreal thickness, Qt::SizeMode sizeMode, bool proportional ) noexcept
qreal thickness, Qt::SizeMode sizeMode ) noexcept
: m_startAngle( startAngle )
, m_spanAngle( spanAngle )
, m_thickness( thickness )
, m_relativeSize( sizeMode == Qt::RelativeSize )
, m_proportional( proportional )
{
}
@ -117,8 +101,7 @@ inline bool QskArcMetrics::operator==(
return qskFuzzyCompare( m_thickness, other.m_thickness )
&& qskFuzzyCompare( m_startAngle, other.m_startAngle )
&& qskFuzzyCompare( m_spanAngle, other.m_spanAngle )
&& ( m_relativeSize == other.m_relativeSize )
&& ( m_proportional == other.m_proportional );
&& ( m_relativeSize == other.m_relativeSize );
}
inline bool QskArcMetrics::operator!=(
@ -162,11 +145,6 @@ inline constexpr Qt::SizeMode QskArcMetrics::sizeMode() const noexcept
return m_relativeSize ? Qt::RelativeSize : Qt::AbsoluteSize;
}
inline constexpr bool QskArcMetrics::isProportional() const noexcept
{
return m_proportional;
}
#ifndef QT_NO_DEBUG_STREAM
class QDebug;

View File

@ -16,15 +16,21 @@
#include <qpainterpath.h>
#define ARC_RENDERER
// #define ARC_BORDER_NODE
#define ARC_FILL_NODE
#ifdef ARC_RENDERER
#ifdef ARC_BORDER_NODE
using BorderNode = QskArcRenderNode;
#else
#include <qpen.h>
using BorderNode = QskStrokeNode;
#endif
#ifdef ARC_FILL_NODE
using FillNode = QskArcRenderNode;
#else
using FillNode = QskShapeNode;
#endif
namespace
{
@ -114,7 +120,7 @@ void QskArcNode::setArcData( const QRectF& rect, const QskArcMetrics& arcMetrics
auto shadowNode = static_cast< QskArcShadowNode* >(
QskSGNode::findChildNode( this, ShadowRole ) );
auto fillNode = static_cast< QskShapeNode* >(
auto fillNode = static_cast< FillNode* >(
QskSGNode::findChildNode( this, FillRole ) );
auto borderNode = static_cast< BorderNode* >(
@ -168,11 +174,15 @@ void QskArcNode::setArcData( const QRectF& rect, const QskArcMetrics& arcMetrics
{
if ( fillNode == nullptr )
{
fillNode = new QskShapeNode;
fillNode = new FillNode;
QskSGNode::setNodeRole( fillNode, FillRole );
}
#ifdef ARC_FILL_NODE
fillNode->updateNode( arcRect, metricsArc, gradient );
#else
fillNode->updateNode( path, QTransform(), arcRect, gradient );
#endif
}
else
{
@ -188,9 +198,8 @@ void QskArcNode::setArcData( const QRectF& rect, const QskArcMetrics& arcMetrics
QskSGNode::setNodeRole( borderNode, BorderRole );
}
#ifdef ARC_RENDERER
borderNode->updateNode( arcRect, metricsArc, borderWidth,
borderColor, gradient );
#ifdef ARC_BORDER_NODE
borderNode->updateNode( arcRect, metricsArc, borderWidth, borderColor );
#else
QPen pen( borderColor, borderWidth );
pen.setCapStyle( Qt::FlatCap );

View File

@ -49,6 +49,18 @@ QskArcRenderNode::QskArcRenderNode()
setFlag( QSGNode::OwnsMaterial, false );
}
void QskArcRenderNode::updateNode( const QRectF& rect,
const QskArcMetrics& metrics, const QskGradient& gradient )
{
updateNode( rect, metrics, 0.0, QColor(), gradient );
}
void QskArcRenderNode::updateNode( const QRectF& rect,
const QskArcMetrics& metrics, qreal borderWidth, const QColor& borderColor )
{
updateNode( rect, metrics, borderWidth, borderColor, QskGradient() );
}
void QskArcRenderNode::updateNode(
const QRectF& rect, const QskArcMetrics& metrics, qreal borderWidth,
const QColor& borderColor, const QskGradient& gradient )
@ -86,8 +98,16 @@ void QskArcRenderNode::updateNode(
{
d->hash = hash;
if ( borderWidth > 0.0 )
{
QskArcRenderer::renderBorder(
rect, metrics, borderWidth, borderColor, *geometry() );
}
else
{
QskArcRenderer::renderFillGeometry(
rect, metrics, *geometry() );
}
markDirty( QSGNode::DirtyGeometry );
markDirty( QSGNode::DirtyMaterial );

View File

@ -21,6 +21,10 @@ class QSK_EXPORT QskArcRenderNode : public QSGGeometryNode
public:
QskArcRenderNode();
void updateNode( const QRectF&, const QskArcMetrics&, const QskGradient& );
void updateNode( const QRectF&, const QskArcMetrics&, qreal borderWidth,
const QColor& borderColor );
void updateNode( const QRectF&, const QskArcMetrics&, qreal borderWidth,
const QColor& borderColor, const QskGradient& );

View File

@ -9,10 +9,7 @@
#include "QskVertex.h"
#include <qsggeometry.h>
#if 1
#include <qdebug.h>
#endif
static inline QskVertex::Line* qskAllocateLines(
QSGGeometry& geometry, int lineCount )
@ -28,36 +25,6 @@ static inline QskVertex::ColoredLine* qskAllocateColoredLines(
return reinterpret_cast< QskVertex::ColoredLine* >( geometry.vertexData() );
}
static inline int qskApproximatedCircumference( const QRectF& rect )
{
const qreal a = rect.width();
const qreal b = rect.height();
const auto ratio = a / b;
if ( ratio > 0.9 || ratio < 1.1 )
return std::max( a, b ) * 2.0 * M_PI; // circle
// Srinivasa Ramanujan: https://en.wikipedia.org/wiki/Ellipse#Circumference
const qreal d1 = ( a - b );
const qreal d2 = ( a + b );
const qreal h = ( d1 * d1 ) / ( d2 * d2 );
return M_PI * d2 * ( 1.0 + ( 3 * h / ( 10 + sqrt( 4 - 3 * h ) ) ) );
}
static inline int qskStepCount( const QRectF& rect )
{
#if 0
const auto dist = 3.0;
#else
const auto dist = 20.0;
#endif
const int length = qskApproximatedCircumference( rect );
return std::max( 3, qCeil( length / dist ) );
}
namespace
{
class AngleIterator
@ -73,14 +40,13 @@ namespace
inline int step() const { return m_stepIndex; }
inline int stepCount() const { return m_stepCount; }
inline bool isDone() const { return m_stepIndex > m_stepCount; }
inline bool isDone() const { return m_stepIndex >= m_stepCount; }
private:
double m_cos;
double m_sin;
int m_stepIndex;
int m_stepCount;
const double m_radians1;
@ -93,7 +59,7 @@ namespace
, m_stepCount( stepCount )
, m_radians1( radians1 )
, m_radians2( radians2 )
, m_radiansStep( ( radians2 - radians1 ) / stepCount )
, m_radiansStep( ( radians2 - radians1 ) / ( stepCount - 1 ) )
{
m_cos = qFastCos( radians1 );
m_sin = qFastSin( radians1 );
@ -130,15 +96,11 @@ namespace
int borderCount() const;
int setBorderLines( QskVertex::ColoredLine*, const QskVertex::Color ) const;
int setBorderLines( QskVertex::Line* ) const;
int setFillLines( QskVertex::ColoredLine*, const QskVertex::Color ) const;
private:
int arcLineCount() const;
void setArcLines( QskVertex::ColoredLine*, int lineCount,
const QPointF&, const QSizeF&,
const qreal radians1, const qreal radians2,
qreal arcWidth, const QskVertex::Color ) const;
int arcLineCount( qreal radians = 2.0 * M_PI ) const;
QLineF fillLineAt( qreal x, qreal y, qreal sin, qreal cos ) const;
const QRectF& m_rect;
const QskArcMetrics& m_metrics;
@ -156,17 +118,35 @@ namespace
int Stroker::fillCount() const
{
return 0; // TODO
int n = 0;
qreal radians1 = qDegreesToRadians( m_metrics.startAngle() );
qreal radians2 = qDegreesToRadians( m_metrics.endAngle() );
if ( radians2 < radians1 )
qSwap( radians1, radians2 );
for ( auto r = qFloor( radians1 / M_PI_2 ) * M_PI_2;
r < radians2; r += M_PI_2 )
{
const auto r1 = qMax( r, radians1 );
const auto r2 = qMin( r + M_PI_2, radians2 );
n += arcLineCount( r2 - r1 );
}
int Stroker::arcLineCount() const
{
if ( m_metrics.isNull() )
return 0;
return n;
}
int n = qskStepCount( m_rect );
if ( !m_metrics.isClosed() )
n = qCeil( n * qAbs( m_metrics.spanAngle() ) / 360.0 );
int Stroker::arcLineCount( const qreal radians ) const
{
// not very sophisticated - TODO ...
const auto ratio = qAbs( radians ) / ( 2.0 * M_PI );
int n = ( m_rect.width() + m_rect.height() ) * M_PI_2;
n = qBound( 3, n, 80 );
n = qCeil( n * ratio );
return n;
}
@ -176,122 +156,115 @@ namespace
if ( m_metrics.isNull() )
return 0;
return 2 * arcLineCount() + 1;
}
void Stroker::setArcLines( QskVertex::ColoredLine* lines, int lineCount,
const QPointF& center, const QSizeF& size,
const qreal radians1, const qreal radians2,
qreal arcWidth, const QskVertex::Color color ) const
{
const auto w1 = size.width();
const auto h1 = size.height();
const auto w2 = w1 - arcWidth;
const auto h2 = h1 - arcWidth;
auto l = lines;
for ( AngleIterator it( radians1, radians2, lineCount - 1 ); !it.isDone(); ++it )
{
const auto x1 = center.x() + w1 * it.cos();
const auto x2 = center.x() + w2 * it.cos();
const auto y1 = center.y() + h1 * it.sin();
const auto y2 = center.y() + h2 * it.sin();
l++->setLine( x1, y1, x2, y2, color );
}
if ( l - lines != lineCount )
qWarning() << lineCount << "->" << l - lines;
Q_ASSERT( l - lines == lineCount );
}
int Stroker::setBorderLines( QskVertex::Line* ) const
{
return 0;
}
int Stroker::setBorderLines( QskVertex::ColoredLine* lines,
const QskVertex::Color color ) const
{
Q_UNUSED( lines );
Q_UNUSED( color );
return 0;
}
int Stroker::setFillLines( QskVertex::ColoredLine* lines,
const QskVertex::Color color ) const
{
qreal radians1 = qDegreesToRadians( m_metrics.startAngle() );
qreal radians2 = qDegreesToRadians( m_metrics.endAngle() );
if ( m_metrics.spanAngle() < 0.0 )
std::swap( radians1, radians2 );
const qreal w = 0.5 * ( m_rect.width() - m_metrics.thickness() );
const qreal h = 0.5 * ( m_rect.height() - m_metrics.thickness() );
const auto center = m_rect.center();
const qreal radians1 = qDegreesToRadians( m_metrics.startAngle() );
const qreal radians2 = qDegreesToRadians( m_metrics.endAngle() );
auto l = lines;
const int n = arcLineCount();
auto size = 0.5 * m_rect.size();
setArcLines( lines, n, center, size,
radians1, radians2, m_borderWidth, color );
const bool stretched = true;
if ( !stretched )
for ( auto r = qFloor( radians1 / M_PI_2 ) * M_PI_2;
r < radians2; r += M_PI_2 )
{
size.rwidth() -= m_metrics.thickness() - m_borderWidth;
size.rheight() -= m_metrics.thickness() - m_borderWidth;
const auto r1 = qMax( r, radians1 );
const auto r2 = qMin( r + M_PI_2, radians2 );
const auto lineCount = arcLineCount( r2 - r1 );
for ( AngleIterator it( r1, r2, lineCount ); !it.isDone(); ++it )
{
const auto line = fillLineAt( w, h, it.cos(), it.sin() );
l++->setLine( center.x() + line.x1(), center.y() - line.y1(),
center.x() + line.x2(), center.y() - line.y2(), color );
}
else
{
qreal tx = m_metrics.thickness();
qreal ty = m_metrics.thickness();
const qreal ratio = m_rect.width() / m_rect.height();
if ( ratio >= 1.0 )
tx *= ratio;
else
ty /= ratio;
size.rwidth() -= tx;
size.rheight() -= ty;
}
setArcLines( lines + n + 1, n, center, size,
radians2, radians1, m_borderWidth, color );
lines[n] = { lines[n - 1].p2, lines[n + 1].p1 };
return 2 * n + 1;
return l - lines;
}
}
void QskArcRenderer::renderBorderGeometry( const QRectF& rect,
const QskArcMetrics& metrics, qreal borderWidth, QSGGeometry& geometry )
{
geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip );
Stroker stroker( rect, metrics, borderWidth );
const auto lineCount = stroker.borderCount();
const auto lines = qskAllocateLines( geometry, lineCount );
if ( lines )
inline qreal sqr( qreal x )
{
const auto effectiveCount = stroker.setBorderLines( lines );
if ( lineCount != effectiveCount )
{
qWarning() << lineCount << effectiveCount;
return x * x;
}
QLineF Stroker::fillLineAt( qreal w, qreal h, qreal cos, qreal sin ) const
{
const auto x = w * cos;
const auto y = h * sin;
/*
The inner/outer points are found by shifting along the
normal vector of the tangent at the ellipse point.
*/
const auto t = 0.5 * m_metrics.thickness();
if ( qFuzzyIsNull( sin ) )
{
const qreal dx = cos * t;
return QLineF( x + dx, y, x - dx, y );
}
else if ( qFuzzyIsNull( cos ) )
{
const qreal dy = sin * t;
return QLineF( x, y + dy, x, y - dy );
}
const qreal m = qSqrt( w * w - x * x ) * ( w / h ) / x;
const auto dt = t * qSqrt( ( 1.0 / ( 1.0 + m * m ) ) );
const qreal dx = ( x >= 0 ) ? dt : -dt;
const qreal dy = m * ( ( y >= 0 ) ? dx : -dx );
const auto x1 = x + dx;
const auto y1 = y + dy;
const auto x2 = x - dx;
const auto y2 = y - dy;
return QLineF( x1, y1, x2, y2 );
}
}
void QskArcRenderer::renderFillGeometry( const QRectF& rect,
const QskArcMetrics& metrics, qreal borderWidth, QSGGeometry& geometry )
{
Q_UNUSED( rect );
Q_UNUSED( geometry );
geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip );
Stroker stroker( rect, metrics, borderWidth );
const auto lines = qskAllocateColoredLines( geometry, stroker.fillCount() );
const auto lineCount = stroker.fillCount();
const auto lines = qskAllocateColoredLines( geometry, lineCount );
if ( lines )
{
// TODO
const auto effectiveCount = stroker.setFillLines( lines, QColor( Qt::darkRed ) );
if ( lineCount != effectiveCount )
{
qWarning() << lineCount << effectiveCount;
}
}
}