From 4811d3501790a63d52baa39ec20893477110fb07 Mon Sep 17 00:00:00 2001 From: Craig Watson Date: Tue, 5 Apr 2016 14:49:56 +0200 Subject: [PATCH] Improved drawing of interpolated strokes: - Added a curveToPolygon function that creates a curved polygon, eliminating the need to generate lots of small ones to make a curve look smooth. - Cleaned up the rest of the code a bit --- src/domain/UBGraphicsScene.cpp | 96 +++++++++++++------------- src/domain/UBGraphicsScene.h | 3 + src/domain/UBGraphicsStroke.cpp | 29 +++----- src/domain/UBGraphicsStroke.h | 3 + src/frameworks/UBGeometryUtils.cpp | 105 +++++++++++++++++++++++++++++ src/frameworks/UBGeometryUtils.h | 1 + 6 files changed, 167 insertions(+), 70 deletions(-) diff --git a/src/domain/UBGraphicsScene.cpp b/src/domain/UBGraphicsScene.cpp index 72eae6a9..d19a9457 100644 --- a/src/domain/UBGraphicsScene.cpp +++ b/src/domain/UBGraphicsScene.cpp @@ -537,52 +537,31 @@ bool UBGraphicsScene::inputDeviceMove(const QPointF& scenePos, const qreal& pres position = newPosition; } + if (!mCurrentStroke) + mCurrentStroke = new UBGraphicsStroke(); + if(dc->mActiveRuler){ dc->mActiveRuler->DrawLine(position, width); } else{ - bool showDots = false; - - if (showDots) { - UBGraphicsPolygonItem * p = lineToPolygonItem(QLineF(scenePos, scenePos), width, width); - p->setColor(Qt::red); - this->addItem(p); - } - UBInterpolator::InterpolationMethod interpolator = UBInterpolator::NoInterpolation; + /* if (currentTool == UBStylusTool::Marker) { // The marker is already super slow due to the transparency, we can't also do interpolation interpolator = UBInterpolator::NoInterpolation; } + */ - else if (UBSettings::settings()->boardInterpolatePenStrokes->get().toBool()) { + if (UBSettings::settings()->boardInterpolatePenStrokes->get().toBool()) { interpolator = UBInterpolator::Bezier; } QList newPoints = mCurrentStroke->addPoint(scenePos, interpolator); - int n = newPoints.length(); - int i = 1; - qreal startWidth = mPreviousWidth; - foreach(QPointF point, newPoints) { - - if (showDots) { - if (point != scenePos) { - UBGraphicsPolygonItem * p = lineToPolygonItem(QLineF(point, point), width, width); - p->setColor(Qt::yellow); - this->addItem(p); - } - } - - - // linear interpolation of the end width - qreal endWidth = mPreviousWidth + qreal(i)*(width - mPreviousWidth)/qreal(n); - i++; - - drawLineTo(point, startWidth, endWidth, UBDrawingController::drawingController()->stylusTool() == UBStylusTool::Line); - startWidth = endWidth; - } - //drawLineTo(scenePos, width, UBDrawingController::drawingController()->stylusTool() == UBStylusTool::Line); + if (newPoints.length() > 1) + drawCurve(newPoints, mPreviousWidth, width); + else + drawLineTo(scenePos, width, UBDrawingController::drawingController()->stylusTool() == UBStylusTool::Line); } } else if (currentTool == UBStylusTool::Eraser) @@ -855,10 +834,36 @@ void UBGraphicsScene::drawLineTo(const QPointF &pEndPoint, const qreal &startWid if (initialWidth == endWidth) initialWidth = mPreviousWidth; - // UBGraphicsPolygonItem *polygonItem = lineToPolygonItem(QLineF(mPreviousPoint, pEndPoint), pWidth); + if (bLineStyle) { + QSetIterator itItems(mAddedItems); + + while (itItems.hasNext()) { + QGraphicsItem* item = itItems.next(); + removeItem(item); + } + mAddedItems.clear(); + } UBGraphicsPolygonItem *polygonItem = lineToPolygonItem(QLineF(mPreviousPoint, pEndPoint), initialWidth, endWidth); + addPolygonItemToCurrentStroke(polygonItem); + + if (!bLineStyle) { + mPreviousPoint = pEndPoint; + mPreviousWidth = endWidth; + } +} + +void UBGraphicsScene::drawCurve(const QList& points, qreal startWidth, qreal endWidth) +{ + UBGraphicsPolygonItem* polygonItem = curveToPolygonItem(points, startWidth, endWidth); + addPolygonItemToCurrentStroke(polygonItem); + + mPreviousWidth = endWidth; + mPreviousPoint = points.last(); +} +void UBGraphicsScene::addPolygonItemToCurrentStroke(UBGraphicsPolygonItem* polygonItem) +{ if (!polygonItem->brush().isOpaque()) { // ------------------------------------------------------------------------------------- @@ -871,19 +876,6 @@ void UBGraphicsScene::drawLineTo(const QPointF &pEndPoint, const qreal &startWid } } - - if (bLineStyle) - { - QSetIterator itItems(mAddedItems); - - while (itItems.hasNext()) - { - QGraphicsItem* item = itItems.next(); - removeItem(item); - } - mAddedItems.clear(); - } - mpLastPolygon = polygonItem; mAddedItems.insert(polygonItem); @@ -896,12 +888,6 @@ void UBGraphicsScene::drawLineTo(const QPointF &pEndPoint, const qreal &startWid mPreviousPolygonItems.append(polygonItem); - - if (!bLineStyle) - { - mPreviousPoint = pEndPoint; - mPreviousWidth = endWidth; - } } void UBGraphicsScene::eraseLineTo(const QPointF &pEndPoint, const qreal &pWidth) @@ -1149,6 +1135,14 @@ UBGraphicsPolygonItem* UBGraphicsScene::arcToPolygonItem(const QLineF& pStartRad return polygonToPolygonItem(polygon); } +UBGraphicsPolygonItem* UBGraphicsScene::curveToPolygonItem(const QList& points, qreal startWidth, qreal endWidth) +{ + QPolygonF polygon = UBGeometryUtils::curveToPolygon(points, startWidth, endWidth); + + return polygonToPolygonItem(polygon); + +} + void UBGraphicsScene::clearSelectionFrame() { if (mSelectionFrame) { diff --git a/src/domain/UBGraphicsScene.h b/src/domain/UBGraphicsScene.h index 19571d62..a62d66e7 100644 --- a/src/domain/UBGraphicsScene.h +++ b/src/domain/UBGraphicsScene.h @@ -194,6 +194,7 @@ class UBGraphicsScene: public UBCoreGraphicsScene, public UBItem void drawLineTo(const QPointF& pEndPoint, const qreal& pStartWidth, const qreal& endWidth, bool bLineStyle); void eraseLineTo(const QPointF& pEndPoint, const qreal& pWidth); void drawArcTo(const QPointF& pCenterPoint, qreal pSpanAngle); + void drawCurve(const QList& points, qreal startWidth, qreal endWidth); bool isEmpty() const; @@ -357,6 +358,8 @@ public slots: UBGraphicsPolygonItem* lineToPolygonItem(const QLineF &pLine, const qreal &pStartWidth, const qreal &pEndWidth); UBGraphicsPolygonItem* arcToPolygonItem(const QLineF& pStartRadius, qreal pSpanAngle, qreal pWidth); + UBGraphicsPolygonItem* curveToPolygonItem(const QList& points, qreal startWidth, qreal endWidth); + void addPolygonItemToCurrentStroke(UBGraphicsPolygonItem* polygonItem); void initPolygonItem(UBGraphicsPolygonItem*); diff --git a/src/domain/UBGraphicsStroke.cpp b/src/domain/UBGraphicsStroke.cpp index 9208315c..90db6dde 100644 --- a/src/domain/UBGraphicsStroke.cpp +++ b/src/domain/UBGraphicsStroke.cpp @@ -70,19 +70,6 @@ QList UBGraphicsStroke::addPoint(const QPointF& point, UBInterpolator:: { int n = mDrawnPoints.size(); - /* - if (n > 0) { - qreal MIN_DISTANCE = 3; - QPointF lastPoint = mDrawnPoints.last(); - qreal distance = QLineF(lastPoint, point).length(); - - //qDebug() << "distance: " << distance; - - if (distance < MIN_DISTANCE) - return QList(); - } - */ - if (interpolationMethod == UBInterpolator::NoInterpolation || n == 0) { mDrawnPoints << point; mAllPoints << point; @@ -92,14 +79,20 @@ QList UBGraphicsStroke::addPoint(const QPointF& point, UBInterpolator:: else if (interpolationMethod == UBInterpolator::Bezier) { // This is a bit special, as the curve we are interpolating is not between two drawn points; // it is between the midway points of the second-to-last and last point, and last and current point. - qreal MIN_DISTANCE = 3; + qreal MIN_DISTANCE = 3; // TODO: make this dependant on zoom QPointF lastPoint = mDrawnPoints.last(); qreal distance = QLineF(lastPoint, point).length(); //qDebug() << "distance: " << distance; - if (distance < MIN_DISTANCE) + // We don't draw anything below the minimum distance. For performance reasons but also to make the curve + // look smoother (e.g shaking slightly won't affect the curve). + if (distance < MIN_DISTANCE) { + // we still keep track of that point to calculate the distance correctly next time around + mDrawnPoints << point; return QList(); + } + if (n == 1) { // We start with a straight line to the first midway point QPointF lastPoint = mDrawnPoints[0]; @@ -119,7 +112,7 @@ QList UBGraphicsStroke::addPoint(const QPointF& point, UBInterpolator:: bz.setPoints(startPoint, p1, endPoint); - QList newPoints = bz.getPoints(7); + QList newPoints = bz.getPoints(10); foreach(QPointF p, newPoints) { mAllPoints << p; @@ -127,8 +120,6 @@ QList UBGraphicsStroke::addPoint(const QPointF& point, UBInterpolator:: mDrawnPoints << point; return newPoints; - - } else { @@ -149,7 +140,7 @@ QList UBGraphicsStroke::addPoint(const QPointF& point, UBInterpolator:: sp.setPoints(mDrawnPoints[n-2], mDrawnPoints[n-1], point); */ - // todo: better way of avoiding problems with constant x's. in the meantime this'll do. + // todo: better way of avoiding problems with equal x's (such as PCA). in the meantime this'll do. qreal x0 = mDrawnPoints[n-3].x(); qreal x1 = mDrawnPoints[n-2].x(); qreal x2 = mDrawnPoints[n-1].x(); diff --git a/src/domain/UBGraphicsStroke.h b/src/domain/UBGraphicsStroke.h index 2f9b2759..2f9f4eae 100644 --- a/src/domain/UBGraphicsStroke.h +++ b/src/domain/UBGraphicsStroke.h @@ -70,7 +70,10 @@ class UBGraphicsStroke QList mPolygons; + /// Points that were drawn (actually received through input device) QList mDrawnPoints; + + /// All the points (including interpolated) that are used to draw the stroke QList mAllPoints; }; diff --git a/src/frameworks/UBGeometryUtils.cpp b/src/frameworks/UBGeometryUtils.cpp index ea5f22b2..fb3392e7 100644 --- a/src/frameworks/UBGeometryUtils.cpp +++ b/src/frameworks/UBGeometryUtils.cpp @@ -242,6 +242,111 @@ QPolygonF UBGeometryUtils::arcToPolygon(const QLineF& startRadius, qreal spanAng 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) +{ + typedef QPair pointPair; + + int n_points = points.size(); + + if (n_points < 2) + return QPolygonF(); + + if (n_points == 2) + return lineToPolygon(points[0], points[1], startWidth, endWidth); + + /* 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. + + */ + QList newPoints; + + + QLineF firstSegment = QLineF(points[0], points[1]); + QLineF normal = firstSegment.normalVector(); + normal.setLength(startWidth/2.0); + newPoints << pointPair(normal.p2(), points[0] - 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], points[i+1])).normalVector(); + normal.setLength(width/2.0); + QPointF d1 = points[i] + QPointF(normal.dx(), normal.dy()); + QPointF d2 = points[i] - QPointF(normal.dx(), normal.dy()); + + newPoints << pointPair(d1, d2); + } + + // The last point is similar to the first + QLineF lastSegment = QLineF(points[n_points-2], points[n_points-1]); + normal = lastSegment.normalVector(); + normal.setLength(endWidth/2.0); + + QPointF d1 = points.last() + QPointF(normal.dx(), normal.dy()); + QPointF d2 = points.last() - QPointF(normal.dx(), normal.dy()); + + newPoints << pointPair(d1, d2); + + QPainterPath path; + path.moveTo(newPoints[0].first); + + for (int i(1); i < n_points; ++i) { + path.lineTo(newPoints[i].first); + } + + path.arcTo(points.last().x() - endWidth/2.0, points.last().y() - endWidth/2.0, endWidth, endWidth, (90.0 + lastSegment.angle()), -180.0); + //path.lineTo(newPoints.last().second); + + for (int i(n_points-1); i >= 0; --i) { + path.lineTo(newPoints[i].second); + } + + path.arcTo(points[0].x() - startWidth/2.0, points[0].y() - startWidth/2.0, startWidth, startWidth, (firstSegment.angle() - 90.0), -180.0); + //path.lineTo(newPoints[0].second); + + + 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()))); diff --git a/src/frameworks/UBGeometryUtils.h b/src/frameworks/UBGeometryUtils.h index b54cfe97..fef01891 100644 --- a/src/frameworks/UBGeometryUtils.h +++ b/src/frameworks/UBGeometryUtils.h @@ -45,6 +45,7 @@ class UBGeometryUtils static QPolygonF lineToPolygon(const QPointF& pStart, const QPointF& pEnd, const qreal& pStartWidth, const qreal& pEndWidth); + static QPolygonF curveToPolygon(const QList& points, qreal startWidth, qreal endWidth); static QPointF pointConstrainedInRect(QPointF point, QRectF rect); static QPoint pointConstrainedInRect(QPoint point, QRect rect);