/* * Copyright (C) 2015-2018 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 . */ #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& 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 > pointsAndWidths; for (int i(0); i < n_points; ++i) { qreal width = startWidth + (qreal(i)/qreal(n_points-1)) * (endWidth - startWidth); pointsAndWidths << QPair(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 >& 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 pointPair; QList 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 &points) { // QVector 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 UBGeometryUtils::quadraticBezier(const QPointF& p0, const QPointF& p1, const QPointF& p2, unsigned int nPoints) { QPainterPath path(p0); path.quadTo(p1, p2); QList 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; }