qskinny/src/nodes/QskBoxRendererDEllipse.cpp
2019-01-04 13:42:16 +01:00

629 lines
19 KiB
C++

/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the QSkinny License, Version 1.0
*****************************************************************************/
#include "QskBoxRenderer.h"
#include "QskBoxRendererColorMap.h"
#include "QskGradient.h"
#include "QskVertex.h"
#include <qmath.h>
namespace
{
using namespace QskVertex;
class ContourLine
{
public:
inline void setLine( qreal x1, qreal y1, qreal value1,
qreal x2, qreal y2, qreal value2 )
{
p1.x = x1;
p1.y = y1;
p1.v = value1;
p2.x = x2;
p2.y = y2;
p2.v = value2;
}
inline QPointF pointAt( qreal value ) const
{
const qreal r = ( value - p1.v ) / ( p2.v - p1.v );
return QPointF( p1.x + r * ( p2.x - p1.x ), p1.y + r * ( p2.y - p1.y ) );
}
struct
{
qreal x, y, v;
} p1, p2;
};
class ValueCurve
{
public:
ValueCurve( const QskBoxRenderer::Metrics& m )
{
/*
The slopes of the value line and those for the fill lines.
*/
const qreal mv = -( m.innerQuad.width / m.innerQuad.height );
const qreal md = m.innerQuad.height / m.innerQuad.width;
/*
first we find the point, where the tangent line
with the slope of md touches the top left border
of the ellipse around the top left corner point
*/
qreal xt, yt;
{
const auto& c = m.corner[ Qt::TopLeftCorner ];
const qreal k = c.radiusInnerY / c.radiusInnerX * md;
const qreal u = ::sqrt( 1.0 + k * k );
const qreal dx = c.radiusInnerX / u;
const qreal dy = ::sqrt( 1.0 - 1.0 / ( u * u ) ) * c.radiusInnerY;
xt = c.centerX - dx;
yt = c.centerY - dy;
}
/*
the cutting point between the tangent and the diagonal of the
fill rectangle is the origin of our line, where we iterate
the values along.
*/
qreal left, top, right;
{
const auto& r = m.innerQuad;
left = ( yt - r.top - mv * xt + md * r.left ) / ( md - mv );
const qreal dx = left - r.left;
top = r.top + md * dx;
right = r.right - dx;
}
/*
Finally we can precaclculate the coefficients needed for
evaluating points in valueAt.
*/
{
const qreal k = mv + 1.0 / mv;
const qreal r = top + left / mv;
const qreal w = right - left;
m_coeff_0 = ( r / k - left ) / w;
m_coeff_y = -1.0 / ( k * w );
m_coeff_x = -mv * m_coeff_y;
}
}
inline qreal valueAt( qreal x, qreal y ) const
{
// The value, where the perpendicular runs through x,y
return m_coeff_0 + x * m_coeff_x + y * m_coeff_y;
}
private:
qreal m_coeff_0, m_coeff_x, m_coeff_y;
};
class ContourIterator
{
public:
ContourIterator()
: m_clockwise( true )
, m_isLeading( true )
, m_isDone( false )
{
}
void setup( const QskBoxRenderer::Metrics& metrics,
bool isLeading, bool clockwise,
qreal cos, qreal cosStep, qreal sin, qreal sinStep,
qreal x1, qreal y1, qreal v1, qreal x2, qreal y2, qreal v2 )
{
m_cos = cos;
m_sin = sin;
m_cosStep = cosStep;
m_sinStep = sinStep;
m_stepInv1 = m_sinStep / m_cosStep;
m_stepInv2 = m_cosStep + m_sinStep * m_stepInv1;
m_contourLine.setLine( x1, y1, v1, x2, y2, v2 );
m_clockwise = clockwise;
m_isLeading = isLeading;
m_isDone = false;
m_corner = Qt::TopLeftCorner;
if ( clockwise )
{
if ( x2 >= metrics.corner[ Qt::TopRightCorner ].centerX )
setCorner( Qt::TopRightCorner, metrics );
}
else
{
if ( y2 >= metrics.corner[ Qt::BottomLeftCorner ].centerY )
setCorner( Qt::BottomLeftCorner, metrics );
}
}
inline bool isClockwise() const { return m_clockwise; }
inline bool isDone() const { return m_isDone; }
inline qreal value() const { return m_contourLine.p2.v; }
inline const ContourLine& contourLine() const { return m_contourLine; }
inline void advance( const QskBoxRenderer::Metrics& metrics, const ValueCurve& curve )
{
if ( m_isDone )
return;
using namespace Qt;
const auto& corners = metrics.corner;
const auto& c = corners[ m_corner ];
m_contourLine.p1 = m_contourLine.p2;
auto& p = m_contourLine.p2;
if ( m_clockwise )
{
switch ( m_corner )
{
case TopLeftCorner:
{
if ( p.x >= c.centerX )
{
p.x = corners[ TopRightCorner ].centerX;
p.y = metrics.innerQuad.top;
setCorner( TopRightCorner, metrics );
}
else
{
decrement();
p.x = c.centerX - m_cos * c.radiusInnerX;
p.y = c.centerY - m_sin * c.radiusInnerY;
if ( p.x >= corners[ TopRightCorner ].centerX )
setCorner( TopRightCorner, metrics );
}
break;
}
case TopRightCorner:
{
if ( p.y >= c.centerY )
{
p.x = metrics.innerQuad.right;
p.y = corners[ BottomRightCorner ].centerY;
setCorner( BottomRightCorner, metrics );
}
else
{
increment();
p.x = c.centerX + m_cos * c.radiusInnerX;
p.y = c.centerY - m_sin * c.radiusInnerY;
if ( p.y >= corners[ BottomRightCorner ].centerY )
setCorner( BottomRightCorner, metrics );
}
break;
}
case BottomRightCorner:
{
// we are bottom/right
increment();
p.x = c.centerX + m_cos * c.radiusInnerX;
p.y = c.centerY + m_sin * c.radiusInnerY;
break;
}
default:
{
Q_ASSERT( false );
}
}
}
else
{
switch ( m_corner )
{
case TopLeftCorner:
{
if ( p.y >= c.centerY )
{
p.x = metrics.innerQuad.left;
p.y = corners[ BottomLeftCorner ].centerY;
setCorner( BottomLeftCorner, metrics );
}
else
{
increment();
p.x = c.centerX - m_cos * c.radiusInnerX;
p.y = c.centerY - m_sin * c.radiusInnerY;
if ( p.y >= corners[ BottomLeftCorner ].centerY )
setCorner( BottomLeftCorner, metrics );
}
break;
}
case BottomLeftCorner:
{
if ( p.x >= c.centerX )
{
p.x = corners[ BottomRightCorner ].centerX;
p.y = metrics.innerQuad.bottom;
setCorner( BottomRightCorner, metrics );
}
else
{
increment();
p.x = c.centerX - m_cos * c.radiusInnerX;
p.y = c.centerY + m_sin * c.radiusInnerY;
if ( p.x >= corners[ BottomRightCorner ].centerX )
setCorner( BottomRightCorner, metrics );
}
break;
}
case BottomRightCorner:
{
increment();
p.x = c.centerX + m_cos * c.radiusInnerX;
p.y = c.centerY + m_sin * c.radiusInnerY;
break;
}
default:
{
Q_ASSERT( false );
}
}
}
p.v = curve.valueAt( p.x, p.y );
if ( ( p.v > 0.5 ) && ( p.v < m_contourLine.p1.v ) )
{
p = m_contourLine.p1;
m_isDone = true;
}
}
private:
static constexpr qreal m_eps = 1e-4;
inline void setCorner(
Qt::Corner corner, const QskBoxRenderer::Metrics& metrics )
{
m_corner = corner;
const auto& c = metrics.corner[ corner ];
const double angleStep = M_PI_2 / c.stepCount;
m_cosStep = qFastCos( angleStep );
m_sinStep = qFastSin( angleStep );
m_stepInv1 = m_sinStep / m_cosStep;
m_stepInv2 = m_cosStep + m_sinStep * m_stepInv1;
bool horizontal;
if ( corner == Qt::TopRightCorner || corner == Qt::BottomLeftCorner )
horizontal = !m_clockwise;
else
horizontal = m_clockwise;
if ( horizontal )
{
m_cos = 1.0;
m_sin = 0.0;
m_sinStep = -m_sinStep;
}
else
{
m_cos = 0.0;
m_sin = 1.0;
}
}
inline void increment()
{
#if 0
/*
We are running into this assertions when closing the filling
at the bottom right corner for rectangles with small width/height
ratio. Seems to be no problem, but has to be understood. TODO ...
*/
Q_ASSERT( m_sinStep < 0.0 || m_cos < 1.0 );
Q_ASSERT( m_sinStep > 0.0 || m_cos > 0.0 );
#endif
const double cos0 = m_cos;
m_cos = m_cos * m_cosStep + m_sin * m_sinStep;
m_sin = m_sin * m_cosStep - cos0 * m_sinStep;
if ( m_sinStep < 0.0 )
{
if ( m_cos < m_eps )
m_cos = 0.0;
if ( m_sin > 1.0 - m_eps )
m_sin = 1.0;
}
else
{
if ( m_cos > 1.0 - m_eps )
m_cos = 1.0;
if ( m_sin < m_eps )
m_sin = 0.0;
}
}
inline void decrement()
{
m_cos = ( m_cos - m_stepInv1 * m_sin ) / m_stepInv2;
if ( std::abs( m_cos ) < m_eps )
m_cos = 0.0;
m_sin = m_sin / m_cosStep + m_stepInv1 * m_cos;
if ( std::abs( m_sin ) < m_eps )
m_sin = 0.0;
}
bool m_clockwise;
bool m_isLeading;
bool m_isDone;
qreal m_cos, m_cosStep;
qreal m_sin, m_sinStep;
qreal m_stepInv1, m_stepInv2;
ContourLine m_contourLine;
Qt::Corner m_corner;
};
class OutlineIterator
{
public:
OutlineIterator( const QskBoxRenderer::Metrics& metrics,
const ValueCurve& curve, bool clockwise )
: m_metrics( metrics )
, m_curve( curve )
{
const auto& c = metrics.corner[ Qt::TopLeftCorner ];
#if 1
// This does not need to be done twice !!!
#endif
const double angleStep = M_PI_2 / c.stepCount;
const qreal cosStep = qFastCos( angleStep );
const qreal sinStep = qFastSin( angleStep );
/*
Initialize the iterators to start with the
minimal value, what is somewhere around the top left corner
when having a gradient going from top/left to bottom/right.
*/
qreal cos1 = 0.0;
qreal sin1 = 1.0;
qreal x1 = c.centerX;
qreal y1 = metrics.innerQuad.top;
qreal v1 = m_curve.valueAt( x1, y1 );
for ( int step = 1;; step++ )
{
const qreal cos2 = cos1 * cosStep + sin1 * sinStep;
const qreal sin2 = sin1 * cosStep - cos1 * sinStep;
const qreal x2 = c.centerX - c.radiusInnerX * cos2;
const qreal y2 = c.centerY - c.radiusInnerY * sin2;
const qreal v2 = m_curve.valueAt( x2, y2 );
if ( v2 >= v1 || step >= c.stepCount )
{
if ( clockwise )
{
m_iterator[ 0 ].setup( metrics, true, true,
cos1, cosStep, sin1, sinStep, x2, y2, v2, x1, y1, v1 );
m_iterator[ 1 ].setup( metrics, false, false,
cos2, cosStep, sin2, sinStep, x1, y1, v1, x2, y2, v2 );
}
else
{
m_iterator[ 0 ].setup( metrics, true, false,
cos2, cosStep, sin2, sinStep, x1, y1, v1, x2, y2, v2 );
m_iterator[ 1 ].setup( metrics, false, true,
cos1, cosStep, sin1, sinStep, x2, y2, v2, x1, y1, v1 );
}
while ( !m_iterator[ 1 ].isDone() &&
( m_iterator[ 0 ].value() > m_iterator[ 1 ].value() ) )
{
m_iterator[ 1 ].advance( metrics, m_curve );
}
return;
}
cos1 = cos2;
sin1 = sin2;
x1 = x2;
y1 = y2;
v1 = v2;
}
}
inline void advance()
{
m_iterator[ 0 ].advance( m_metrics, m_curve );
if ( !m_iterator[ 0 ].isDone() )
{
/*
adjusting the counter vertex until its top value is above
the value of the border. Then our value is between
the values of the counter vertex and we find out counter
border point by cutting the counter vertex.
*/
while ( !m_iterator[ 1 ].isDone() &&
( m_iterator[ 0 ].value() > m_iterator[ 1 ].value() ) )
{
m_iterator[ 1 ].advance( m_metrics, m_curve );
}
}
}
inline bool isDone() const
{
return m_iterator[ 0 ].isDone();
}
inline qreal value() const
{
return m_iterator[ 0 ].value();
}
inline void setLineAt( qreal value, Color color, ColoredLine* line )
{
const auto& contourLine1 = m_iterator[ 0 ].contourLine();
const auto& contourLine2 = m_iterator[ 1 ].contourLine();
const QPointF pos = contourLine1.pointAt( value );
const QPointF cPos = contourLine2.pointAt( value );
if ( m_iterator[ 0 ].isClockwise() )
setLine( cPos.x(), cPos.y(), pos.x(), pos.y(), color, line );
else
setLine( pos.x(), pos.y(), cPos.x(), cPos.y(), color, line );
}
inline void setLine( Color color, ColoredLine* line )
{
const auto& contourLine1 = m_iterator[ 0 ].contourLine();
const auto& contourLine2 = m_iterator[ 1 ].contourLine();
const QPointF cPos = contourLine2.pointAt( contourLine1.p2.v );
if ( m_iterator[ 0 ].isClockwise() )
setLine( cPos.x(), cPos.y(), contourLine1.p2.x, contourLine1.p2.y, color, line );
else
setLine( contourLine1.p2.x, contourLine1.p2.y, cPos.x(), cPos.y(), color, line );
}
private:
inline void setLine( qreal x1, qreal y1, qreal x2, qreal y2,
Color color, ColoredLine* line )
{
line->setLine( x1, y1, x2, y2, color );
}
const QskBoxRenderer::Metrics& m_metrics;
const ValueCurve& m_curve;
/*
The first iterator for running along the left or right
half of the ellipse. The other one is for finding the
corresponing point at the other side. */
ContourIterator m_iterator[ 2 ];
};
class DRectellipseIterator
{
public:
DRectellipseIterator(
const QskBoxRenderer::Metrics& metrics, const ValueCurve& curve )
: m_left( metrics, curve, false )
, m_right( metrics, curve, true )
{
m_next = ( m_left.value() < m_right.value() ) ? &m_left : &m_right;
}
template< class ColorIterator >
inline void setGradientLine( const ColorIterator& it, ColoredLine* line )
{
m_next->setLineAt( it.value(), it.color(), line );
}
template< class ColorIterator >
inline void setContourLine( const ColorIterator& it, ColoredLine* line )
{
m_next->setLine( it.colorAt( m_next->value() ), line );
}
inline qreal value() const
{
return m_next->value();
}
inline bool advance()
{
m_next->advance();
m_next = ( m_left.value() < m_right.value() ) ? &m_left : &m_right;
return !m_next->isDone();
}
private:
OutlineIterator m_left, m_right;
OutlineIterator* m_next;
};
}
void QskBoxRenderer::renderDiagonalFill( const QskBoxRenderer::Metrics& metrics,
const QskGradient& gradient, int fillLineCount, QskVertex::ColoredLine* lines )
{
const ValueCurve curve( metrics );
DRectellipseIterator it( metrics, curve );
auto line = QskVertex::fillOrdered( it, 0.0, 1.0, gradient, lines );
/*
There are a couple of reasons, why less points have been rendered
than expected: f.e the positions of the gradient lines are
calculated from a diagonal, where the start/endpoints
might be a little outside of the contour.
As effects like this one are hard to precalculate we simply
insert dummy lines to match the allocated memory.
*/
while ( line - lines < fillLineCount )
{
line[ 0 ] = line[ -1 ];
line++;
}
}