новые иконки в OpenBoard
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
OpenBoard/src/frameworks/UBGeometryUtils.cpp

469 lines
15 KiB

/*
* Copyright (C) 2015-2022 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;
painterPath.setFillRule(Qt::WindingFill);
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;
}