/*
* Copyright ( C ) 2015 - 2016 Département de l ' Instruction Publique ( DIP - SEM )
*
* Copyright ( C ) 2013 Open Education Foundation
*
* Copyright ( C ) 2010 - 2013 Groupement d ' Intérêt Public pour
* l ' Education Numérique en Afrique ( GIP ENA )
*
* This file is part of OpenBoard .
*
* OpenBoard is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , version 3 of the License ,
* with a specific linking exception for the OpenSSL project ' s
* " OpenSSL " library ( or with modified versions of it that use the
* same license as the " OpenSSL " library ) .
*
* OpenBoard is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with OpenBoard . If not , see < http : //www.gnu.org/licenses/>.
*/
# include "UBGeometryUtils.h"
# include "core/memcheck.h"
const double PI = 4.0 * atan ( 1.0 ) ;
const int UBGeometryUtils : : centimeterGraduationHeight = 15 ;
const int UBGeometryUtils : : halfCentimeterGraduationHeight = 10 ;
const int UBGeometryUtils : : millimeterGraduationHeight = 5 ;
const int UBGeometryUtils : : millimetersPerCentimeter = 10 ;
const int UBGeometryUtils : : millimetersPerHalfCentimeter = 5 ;
const float UBGeometryUtils : : inchSize = 25.4f ;
UBGeometryUtils : : UBGeometryUtils ( )
{
// NOOP
}
UBGeometryUtils : : ~ UBGeometryUtils ( )
{
// NOOP
}
QPolygonF UBGeometryUtils : : lineToPolygon ( const QLineF & pLine , const qreal & pWidth )
{
qreal x1 = pLine . x1 ( ) ;
qreal y1 = pLine . y1 ( ) ;
qreal x2 = pLine . x2 ( ) ;
qreal y2 = pLine . y2 ( ) ;
qreal alpha = ( 90.0 - pLine . angle ( ) ) * PI / 180.0 ;
qreal hypothenuse = pWidth / 2 ;
// TODO UB 4.x PERF cache sin/cos table
qreal opposite = sin ( alpha ) * hypothenuse ;
qreal adjacent = cos ( alpha ) * hypothenuse ;
QPointF p1a ( x1 - adjacent , y1 - opposite ) ;
QPointF p1b ( x1 + adjacent , y1 + opposite ) ;
QPointF p2a ( x2 - adjacent , y2 - opposite ) ;
QPointF p2b ( x2 + adjacent , y2 + opposite ) ;
QPainterPath painterPath ;
painterPath . moveTo ( p1a ) ;
painterPath . lineTo ( p2a ) ;
painterPath . arcTo ( x2 - hypothenuse , y2 - hypothenuse , pWidth , pWidth , ( 90.0 + pLine . angle ( ) ) , - 180.0 ) ;
//painterPath.lineTo(p2b);
painterPath . lineTo ( p1b ) ;
painterPath . arcTo ( x1 - hypothenuse , y1 - hypothenuse , pWidth , pWidth , - 1 * ( 90.0 - pLine . angle ( ) ) , - 180.0 ) ;
painterPath . closeSubpath ( ) ;
return painterPath . toFillPolygon ( ) ;
}
QPolygonF UBGeometryUtils : : lineToPolygon ( const QLineF & pLine , const qreal & pStartWidth , const qreal & pEndWidth )
{
qreal x1 = pLine . x1 ( ) ;
qreal y1 = pLine . y1 ( ) ;
qreal x2 = pLine . x2 ( ) ;
qreal y2 = pLine . y2 ( ) ;
qreal alpha = ( 90.0 - pLine . angle ( ) ) * PI / 180.0 ;
qreal startHypothenuse = pStartWidth / 2 ;
qreal endHypothenuse = pEndWidth / 2 ;
// TODO UB 4.x PERF cache sin/cos table
qreal startOpposite = sin ( alpha ) * startHypothenuse ;
qreal startAdjacent = cos ( alpha ) * startHypothenuse ;
qreal endOpposite = sin ( alpha ) * endHypothenuse ;
qreal endAdjacent = cos ( alpha ) * endHypothenuse ;
QPointF p1a ( x1 - startAdjacent , y1 - startOpposite ) ;
QPointF p1b ( x1 + startAdjacent , y1 + startOpposite ) ;
QPointF p2a ( x2 - endAdjacent , y2 - endOpposite ) ;
QPointF p2b ( x2 + endAdjacent , y2 + endOpposite ) ;
QPainterPath painterPath ;
painterPath . moveTo ( p1a ) ;
painterPath . lineTo ( p2a ) ;
painterPath . arcTo ( x2 - endHypothenuse , y2 - endHypothenuse , pEndWidth , pEndWidth , ( 90.0 + pLine . angle ( ) ) , - 180.0 ) ;
//painterPath.lineTo(p2b);
painterPath . lineTo ( p1b ) ;
painterPath . arcTo ( x1 - startHypothenuse , y1 - startHypothenuse , pStartWidth , pStartWidth , - 1 * ( 90.0 - pLine . angle ( ) ) , - 180.0 ) ;
painterPath . closeSubpath ( ) ;
return painterPath . toFillPolygon ( ) ;
}
QPolygonF UBGeometryUtils : : lineToPolygon ( const QPointF & pStart , const QPointF & pEnd ,
const qreal & pStartWidth , const qreal & pEndWidth )
{
qreal x1 = pStart . x ( ) ;
qreal y1 = pStart . y ( ) ;
qreal x2 = pEnd . x ( ) ;
qreal y2 = pEnd . y ( ) ;
QLineF line ( pStart , pEnd ) ;
qreal alpha = ( 90.0 - line . angle ( ) ) * PI / 180.0 ;
qreal hypothenuseStart = pStartWidth / 2 ;
qreal hypothenuseEnd = pEndWidth / 2 ;
qreal sinAlpha = sin ( alpha ) ;
qreal cosAlpha = cos ( alpha ) ;
// TODO UB 4.x PERF cache sin/cos table
qreal oppositeStart = sinAlpha * hypothenuseStart ;
qreal adjacentStart = cosAlpha * hypothenuseStart ;
QPointF p1a ( x1 - adjacentStart , y1 - oppositeStart ) ;
QPointF p1b ( x1 + adjacentStart , y1 + oppositeStart ) ;
qreal oppositeEnd = sinAlpha * hypothenuseEnd ;
qreal adjacentEnd = cosAlpha * hypothenuseEnd ;
QPointF p2a ( x2 - adjacentEnd , y2 - oppositeEnd ) ;
QPainterPath painterPath ;
painterPath . moveTo ( p1a ) ;
painterPath . lineTo ( p2a ) ;
painterPath . arcTo ( x2 - hypothenuseEnd , y2 - hypothenuseEnd , pEndWidth , pEndWidth , ( 90.0 + line . angle ( ) ) , - 180.0 ) ;
painterPath . lineTo ( p1b ) ;
painterPath . arcTo ( x1 - hypothenuseStart , y1 - hypothenuseStart , pStartWidth , pStartWidth , - 1 * ( 90.0 - line . angle ( ) ) , - 180.0 ) ;
painterPath . closeSubpath ( ) ;
return painterPath . toFillPolygon ( ) ;
}
QPolygonF UBGeometryUtils : : arcToPolygon ( const QLineF & startRadius , qreal spanAngleInDegrees , qreal width )
{
qreal startAngleInDegrees = - startRadius . angle ( ) ;
if ( startAngleInDegrees > 180 )
startAngleInDegrees - = 360 ;
else if ( startAngleInDegrees < - 180 )
startAngleInDegrees + = 360 ;
qreal radiusLength = startRadius . length ( ) ;
qreal angle = 2 * asin ( width / ( 2 * radiusLength ) ) * 180 / PI ;
bool overlap = qAbs ( spanAngleInDegrees ) > 360 - angle ;
if ( overlap )
spanAngleInDegrees = spanAngleInDegrees < 0 ? - 360 : 360 ;
qreal endAngleInDegrees = startAngleInDegrees + spanAngleInDegrees ;
qreal innerRadius = radiusLength - width / 2 ;
QRectF innerSquare (
startRadius . p1 ( ) . x ( ) - innerRadius ,
startRadius . p1 ( ) . y ( ) - innerRadius ,
2 * innerRadius ,
2 * innerRadius ) ;
qreal outerRadius = radiusLength + width / 2 ;
QRectF outerSquare (
startRadius . p1 ( ) . x ( ) - outerRadius ,
startRadius . p1 ( ) . y ( ) - outerRadius ,
2 * outerRadius ,
2 * outerRadius ) ;
QRectF startSquare (
startRadius . p2 ( ) . x ( ) - width / 2 ,
startRadius . p2 ( ) . y ( ) - width / 2 ,
width ,
width ) ;
QRectF endSquare (
startRadius . p1 ( ) . x ( ) + radiusLength * cos ( endAngleInDegrees * PI / 180.0 ) - width / 2 ,
startRadius . p1 ( ) . y ( ) + radiusLength * sin ( endAngleInDegrees * PI / 180.0 ) - width / 2 ,
width ,
width ) ;
QPainterPath painterPath (
QPointF (
startRadius . p1 ( ) . x ( ) + innerRadius * cos ( startAngleInDegrees * PI / 180.0 ) ,
startRadius . p1 ( ) . y ( ) + innerRadius * sin ( startAngleInDegrees * PI / 180.0 ) ) ) ;
startAngleInDegrees = - startAngleInDegrees ;
endAngleInDegrees = - endAngleInDegrees ;
spanAngleInDegrees = - spanAngleInDegrees ;
if ( overlap )
{
painterPath . addEllipse ( outerSquare ) ;
QPainterPath innerPainterPath ;
innerPainterPath . addEllipse ( innerSquare ) ;
painterPath = painterPath . subtracted ( innerPainterPath ) ;
}
else
{
painterPath . arcTo ( innerSquare , startAngleInDegrees , spanAngleInDegrees ) ;
painterPath . arcTo ( endSquare , 180.0 + endAngleInDegrees , spanAngleInDegrees > 0 ? - 180.0 : 180.0 ) ;
painterPath . arcTo ( outerSquare , endAngleInDegrees , - spanAngleInDegrees ) ;
painterPath . arcTo ( startSquare , startAngleInDegrees , spanAngleInDegrees > 0 ? - 180.0 : 180.0 ) ;
painterPath . closeSubpath ( ) ;
}
return painterPath . toFillPolygon ( ) ;
}
/**
* @ brief Build and return a polygon from a list of points ( at least 2 ) , and start and end widths .
*
* The resulting polygon will pass by all points in the curve ; its thickness is calculated at each point
* of the curve ( linearly interpolated between start and end widths ) and the segments are joined by
* ( approximately ) curved joints .
*
* Like with lineToPolygon , the ends are semi - circular .
*/
QPolygonF UBGeometryUtils : : curveToPolygon ( const QList < QPointF > & points , qreal startWidth , qreal endWidth )
{
int n_points = points . size ( ) ;
if ( n_points < 2 )
return QPolygonF ( ) ;
if ( n_points = = 2 )
return lineToPolygon ( points [ 0 ] , points [ 1 ] , startWidth , endWidth ) ;
QList < QPair < QPointF , qreal > > pointsAndWidths ;
for ( int i ( 0 ) ; i < n_points ; + + i ) {
qreal width = startWidth + ( qreal ( i ) / qreal ( n_points - 1 ) ) * ( endWidth - startWidth ) ;
pointsAndWidths < < QPair < QPointF , qreal > ( points [ i ] , width ) ;
}
return curveToPolygon ( pointsAndWidths , true , true ) ;
}
/**
* @ brief Build and return a polygon from a list of points and thicknesses ( at least 2 )
*
* The resulting polygon will pass by all points in the curve ; the segments are joined by
* ( approximately ) curved joints . The ends of the polygon can be terminated by arcs by passing
* ` true ` as the ` roundStart ` and / or ` roundEnd ` parameters .
*
*/
QPolygonF UBGeometryUtils : : curveToPolygon ( const QList < QPair < QPointF , qreal > > & points , bool roundStart , bool roundEnd )
{
int n_points = points . size ( ) ;
if ( n_points = = 0 )
return QPolygonF ( ) ;
if ( n_points = = 1 )
return lineToPolygon ( points . first ( ) . first , points . first ( ) . first , points . first ( ) . second , points . first ( ) . second ) ;
qreal startWidth = points . first ( ) . second ;
qreal endWidth = points . last ( ) . second ;
/* The vertices (x's) are calculated based on the stroke's width and angle, and the position of the
supplied points ( o ' s ) :
x - - - - - - - - - - x - - - - - - - - x
o o o
x - - - - - - - - - - x - - - - - - - x
The vertices above and below each ' o ' point are temporarily stored together ,
as a pair of points .
*/
typedef QPair < QPointF , QPointF > pointPair ;
QList < pointPair > newPoints ;
QLineF firstSegment = QLineF ( points [ 0 ] . first , points [ 1 ] . first ) ;
QLineF normal = firstSegment . normalVector ( ) ;
normal . setLength ( startWidth / 2.0 ) ;
newPoints < < pointPair ( normal . p2 ( ) , points [ 0 ] . first - QPointF ( normal . dx ( ) , normal . dy ( ) ) ) ;
/*
Calculating the vertices ( d1 and d2 , below ) is a little less trivial for the
next points : their positions depend on the angle between one segment and the next .
d1
- - - - - - - - - - - - x
\
. a b . \
\
- - - - - - - - x \
d2 \ \
\ . c \
Here , points a , b and c are supplied in the ` points ` list .
N . B : The drawing isn ' t quite accurate ; we don ' t do a miter joint but a kind
of rounded - off joint ( the distance between b and d1 is half the width of the stroke )
*/
for ( int i ( 1 ) ; i < n_points - 1 ; + + i ) {
//qreal width = startWidth + (qreal(i)/qreal(n_points-1)) * (endWidth - startWidth);
QLineF normal = ( QLineF ( points [ i - 1 ] . first , points [ i + 1 ] . first ) ) . normalVector ( ) ;
normal . setLength ( points [ i ] . second / 2.0 ) ;
QPointF d1 = points [ i ] . first + QPointF ( normal . dx ( ) , normal . dy ( ) ) ;
QPointF d2 = points [ i ] . first - QPointF ( normal . dx ( ) , normal . dy ( ) ) ;
newPoints < < pointPair ( d1 , d2 ) ;
}
// The last point is similar to the first
QLineF lastSegment = QLineF ( points [ n_points - 2 ] . first , points [ n_points - 1 ] . first ) ;
normal = lastSegment . normalVector ( ) ;
normal . setLength ( endWidth / 2.0 ) ;
QPointF d1 = points . last ( ) . first + QPointF ( normal . dx ( ) , normal . dy ( ) ) ;
QPointF d2 = points . last ( ) . first - QPointF ( normal . dx ( ) , normal . dy ( ) ) ;
newPoints < < pointPair ( d1 , d2 ) ;
QPainterPath path ;
path . setFillRule ( Qt : : WindingFill ) ;
path . moveTo ( newPoints [ 0 ] . first ) ;
for ( int i ( 1 ) ; i < n_points ; + + i ) {
path . lineTo ( newPoints [ i ] . first ) ;
}
if ( roundEnd )
path . arcTo ( points . last ( ) . first . x ( ) - endWidth / 2.0 , points . last ( ) . first . y ( ) - endWidth / 2.0 , endWidth , endWidth , ( 90.0 + lastSegment . angle ( ) ) , - 180.0 ) ;
else
path . lineTo ( newPoints . last ( ) . second ) ;
for ( int i ( n_points - 1 ) ; i > = 0 ; - - i ) {
path . lineTo ( newPoints [ i ] . second ) ;
}
if ( roundStart )
path . arcTo ( points [ 0 ] . first . x ( ) - startWidth / 2.0 , points [ 0 ] . first . y ( ) - startWidth / 2.0 , startWidth , startWidth , ( firstSegment . angle ( ) - 90.0 ) , - 180.0 ) ;
else
path . lineTo ( newPoints [ 0 ] . first ) ;
//path.closeSubpath();
return path . toFillPolygon ( ) ;
}
QPointF UBGeometryUtils : : pointConstrainedInRect ( QPointF point , QRectF rect )
{
return QPointF ( qMax ( rect . x ( ) , qMin ( rect . x ( ) + rect . width ( ) , point . x ( ) ) ) , qMax ( rect . y ( ) , qMin ( rect . y ( ) + rect . height ( ) , point . y ( ) ) ) ) ;
}
QPoint UBGeometryUtils : : pointConstrainedInRect ( QPoint point , QRect rect )
{
return QPoint ( qMax ( rect . x ( ) , qMin ( rect . x ( ) + rect . width ( ) , point . x ( ) ) ) , qMax ( rect . y ( ) , qMin ( rect . y ( ) + rect . height ( ) , point . y ( ) ) ) ) ;
}
QRectF UBGeometryUtils : : lineToInnerRect ( const QLineF & pLine , const qreal & pWidth )
{
qreal centerX = ( pLine . x1 ( ) + pLine . x2 ( ) ) / 2 ;
qreal centerY = ( pLine . y1 ( ) + pLine . y2 ( ) ) / 2 ;
// Please put a fucking comment here
qreal side = sqrt ( ( pWidth * pWidth ) / 2 ) ;
qreal halfSide = side / 2 ;
return QRectF ( centerX - halfSide , centerY - halfSide , side , side ) ;
}
void UBGeometryUtils : : crashPointList ( QVector < QPointF > & points )
{
// QVector<QPointF> result(points);
int position = 1 ;
while ( position < points . size ( ) )
{
if ( points . at ( position ) = = points . at ( position - 1 ) )
{
points . remove ( position ) ;
}
else
{
+ + position ;
}
}
}
/**
* @ brief Return the angle in degrees between three points
*/
qreal UBGeometryUtils : : angle ( const QPointF & p1 , const QPointF & p2 , const QPointF & p3 )
{
// Angle between three points, using the law of cosines:
// The angle at B equals arccos((a^2-b^2+c^2)/(2*a*c)), where a, b and c are the sides of the triangle
// opposite A, B and C, respectively
qreal a , b , c , beta ;
a = qSqrt ( qPow ( p2 . x ( ) - p3 . x ( ) , 2 ) + qPow ( p2 . y ( ) - p3 . y ( ) , 2 ) ) ;
b = qSqrt ( qPow ( p1 . x ( ) - p3 . x ( ) , 2 ) + qPow ( p1 . y ( ) - p3 . y ( ) , 2 ) ) ;
c = qSqrt ( qPow ( p1 . x ( ) - p2 . x ( ) , 2 ) + qPow ( p1 . y ( ) - p2 . y ( ) , 2 ) ) ;
if ( a = = 0 | | c = = 0 )
beta = 3.14159 ;
else
beta = qAcos ( std : : max ( - 1.0 , std : : min ( 1.0 , ( a * a - b * b + c * c ) / ( 2 * a * c ) ) ) ) ;
return 180. * beta / 3.14159 ;
}
/**
* @ brief Calculate a quadratic Bézier curve and return it in the form of a list of points
* @ param p0 The start point of the curve
* @ param p1 The control point of the curve
* @ param p2 The end point of the curve
* @ param nPoints The number of points by which to approximate the curve , i . e . the length of the returned list
* @ return A list of points that can be used to draw the curve .
*/
QList < QPointF > UBGeometryUtils : : quadraticBezier ( const QPointF & p0 , const QPointF & p1 , const QPointF & p2 , unsigned int nPoints )
{
QPainterPath path ( p0 ) ;
path . quadTo ( p1 , p2 ) ;
QList < QPointF > points ;
if ( nPoints < = 1 )
return points ;
for ( unsigned int i ( 0 ) ; i < = nPoints ; + + i ) {
qreal percent = qreal ( i ) / qreal ( nPoints ) ;
points < < path . pointAtPercent ( percent ) ;
}
return points ;
}