diff --git a/resources/forms/brushProperties.ui b/resources/forms/brushProperties.ui index eea50fba..2866714e 100644 --- a/resources/forms/brushProperties.ui +++ b/resources/forms/brushProperties.ui @@ -521,6 +521,30 @@ + + + + + + Simplify strokes after drawing (experimental) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/src/core/UBPreferencesController.cpp b/src/core/UBPreferencesController.cpp index 7f87cdda..df58462c 100644 --- a/src/core/UBPreferencesController.cpp +++ b/src/core/UBPreferencesController.cpp @@ -166,6 +166,7 @@ void UBPreferencesController::wire() connect(mPenProperties->strongSlider, SIGNAL(valueChanged(int)), this, SLOT(widthSliderChanged(int))); connect(mPenProperties->pressureSensitiveCheckBox, SIGNAL(clicked(bool)), settings, SLOT(setPenPressureSensitive(bool))); connect(mPenProperties->interpolateStrokesCheckBox, SIGNAL(clicked(bool)), settings->boardInterpolatePenStrokes, SLOT(setBool(bool))); + connect(mPenProperties->simplifyStrokesCheckBox, SIGNAL(clicked(bool)), settings->boardSimplifyPenStrokes, SLOT(setBool(bool))); // marker QList markerLightBackgroundColors = settings->boardMarkerLightBackgroundColors->colors(); @@ -183,6 +184,8 @@ void UBPreferencesController::wire() connect(mMarkerProperties->mediumSlider, SIGNAL(valueChanged(int)), this, SLOT(widthSliderChanged(int))); connect(mMarkerProperties->strongSlider, SIGNAL(valueChanged(int)), this, SLOT(widthSliderChanged(int))); connect(mMarkerProperties->pressureSensitiveCheckBox, SIGNAL(clicked(bool)), settings, SLOT(setMarkerPressureSensitive(bool))); + connect(mMarkerProperties->interpolateStrokesCheckBox, SIGNAL(clicked(bool)), settings->boardInterpolateMarkerStrokes, SLOT(setBool(bool))); + connect(mMarkerProperties->simplifyStrokesCheckBox, SIGNAL(clicked(bool)), settings->boardSimplifyMarkerStrokes, SLOT(setBool(bool))); connect(mMarkerProperties->opacitySlider, SIGNAL(valueChanged(int)), this, SLOT(opacitySliderChanged(int))); // about tab @@ -233,12 +236,15 @@ void UBPreferencesController::init() mPenProperties->strongSlider->setValue(settings->boardPenStrongWidth->get().toDouble() * sSliderRatio); mPenProperties->pressureSensitiveCheckBox->setChecked(settings->boardPenPressureSensitive->get().toBool()); mPenProperties->interpolateStrokesCheckBox->setChecked(settings->boardInterpolatePenStrokes->get().toBool()); + mPenProperties->simplifyStrokesCheckBox->setChecked(settings->boardSimplifyPenStrokes->get().toBool()); // marker tab mMarkerProperties->fineSlider->setValue(settings->boardMarkerFineWidth->get().toDouble() * sSliderRatio); mMarkerProperties->mediumSlider->setValue(settings->boardMarkerMediumWidth->get().toDouble() * sSliderRatio); mMarkerProperties->strongSlider->setValue(settings->boardMarkerStrongWidth->get().toDouble() * sSliderRatio); mMarkerProperties->pressureSensitiveCheckBox->setChecked(settings->boardMarkerPressureSensitive->get().toBool()); + mMarkerProperties->interpolateStrokesCheckBox->setChecked(settings->boardInterpolateMarkerStrokes->get().toBool()); + mMarkerProperties->simplifyStrokesCheckBox->setChecked(settings->boardSimplifyMarkerStrokes->get().toBool()); mMarkerProperties->opacitySlider->setValue(settings->boardMarkerAlpha->get().toDouble() * 100); diff --git a/src/core/UBSettings.cpp b/src/core/UBSettings.cpp index fc9e2b55..cfde5119 100644 --- a/src/core/UBSettings.cpp +++ b/src/core/UBSettings.cpp @@ -270,6 +270,11 @@ void UBSettings::init() boardUseHighResTabletEvent = new UBSetting(this, "Board", "UseHighResTabletEvent", true); boardInterpolatePenStrokes = new UBSetting(this, "Board", "InterpolatePenStrokes", true); + boardSimplifyPenStrokes = new UBSetting(this, "Board", "SimplifyPenStrokes", true); + boardSimplifyPenStrokesThresholdAngle = new UBSetting(this, "Board", "SimplifyPenStrokesThresholdAngle", 2); + + boardInterpolateMarkerStrokes = new UBSetting(this, "Board", "InterpolateMarkerStrokes", true); + boardSimplifyMarkerStrokes = new UBSetting(this, "Board", "SimplifyMarkerStrokes", true); boardKeyboardPaletteKeyBtnSize = new UBSetting(this, "Board", "KeyboardPaletteKeyBtnSize", "16x16"); ValidateKeyboardPaletteKeyBtnSize(); diff --git a/src/core/UBSettings.h b/src/core/UBSettings.h index 6be12962..0fb8bf68 100644 --- a/src/core/UBSettings.h +++ b/src/core/UBSettings.h @@ -274,6 +274,10 @@ class UBSettings : public QObject UBSetting* boardUseHighResTabletEvent; UBSetting* boardInterpolatePenStrokes; + UBSetting* boardSimplifyPenStrokes; + UBSetting* boardSimplifyPenStrokesThresholdAngle; + UBSetting* boardInterpolateMarkerStrokes; + UBSetting* boardSimplifyMarkerStrokes; UBSetting* boardKeyboardPaletteKeyBtnSize; diff --git a/src/domain/UBGraphicsPolygonItem.cpp b/src/domain/UBGraphicsPolygonItem.cpp index 39fb5151..320da3f3 100644 --- a/src/domain/UBGraphicsPolygonItem.cpp +++ b/src/domain/UBGraphicsPolygonItem.cpp @@ -121,10 +121,12 @@ void UBGraphicsPolygonItem::setStrokesGroup(UBGraphicsStrokesGroup *group) void UBGraphicsPolygonItem::setStroke(UBGraphicsStroke* stroke) { - clearStroke(); + if (stroke) { + clearStroke(); - mStroke = stroke; - mStroke->addPolygon(this); + mStroke = stroke; + mStroke->addPolygon(this); + } } UBGraphicsStroke* UBGraphicsPolygonItem::stroke() const diff --git a/src/domain/UBGraphicsScene.cpp b/src/domain/UBGraphicsScene.cpp index 23db81d5..b63ee57f 100644 --- a/src/domain/UBGraphicsScene.cpp +++ b/src/domain/UBGraphicsScene.cpp @@ -415,7 +415,7 @@ bool UBGraphicsScene::inputDevicePress(const QPointF& scenePos, const qreal& pre // --------------------------------------------------------------- // Create a new Stroke. A Stroke is a collection of QGraphicsLines // --------------------------------------------------------------- - mCurrentStroke = new UBGraphicsStroke(); + mCurrentStroke = new UBGraphicsStroke(this); if (currentTool != UBStylusTool::Line){ // Handle the pressure @@ -438,7 +438,7 @@ bool UBGraphicsScene::inputDevicePress(const QPointF& scenePos, const qreal& pre moveTo(scenePos); drawLineTo(scenePos, width, UBDrawingController::drawingController()->stylusTool() == UBStylusTool::Line); - mCurrentStroke->addPoint(scenePos); + mCurrentStroke->addPoint(scenePos, width); } accepted = true; } @@ -543,7 +543,7 @@ bool UBGraphicsScene::inputDeviceMove(const QPointF& scenePos, const qreal& pres } if (!mCurrentStroke) - mCurrentStroke = new UBGraphicsStroke(); + mCurrentStroke = new UBGraphicsStroke(this); if(dc->mActiveRuler){ dc->mActiveRuler->DrawLine(position, width); @@ -551,20 +551,15 @@ bool UBGraphicsScene::inputDeviceMove(const QPointF& scenePos, const qreal& pres else{ 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; - } - */ - - if (UBSettings::settings()->boardInterpolatePenStrokes->get().toBool()) { + if ((currentTool == UBStylusTool::Pen && UBSettings::settings()->boardInterpolatePenStrokes->get().toBool()) + || (currentTool == UBStylusTool::Marker && UBSettings::settings()->boardInterpolateMarkerStrokes->get().toBool())) + { interpolator = UBInterpolator::Bezier; } - QList newPoints = mCurrentStroke->addPoint(scenePos, interpolator); + QList > newPoints = mCurrentStroke->addPoint(scenePos, width, interpolator); if (newPoints.length() > 1) { - drawCurve(newPoints, mPreviousWidth, width); + drawCurve(newPoints); } if (interpolator == UBInterpolator::Bezier) { @@ -578,7 +573,7 @@ bool UBGraphicsScene::inputDeviceMove(const QPointF& scenePos, const qreal& pres mTempPolygon = NULL; } - QPointF lastDrawnPoint = newPoints.last(); + QPointF lastDrawnPoint = newPoints.last().first; mTempPolygon = lineToPolygonItem(QLineF(lastDrawnPoint, scenePos), mPreviousWidth, width); addItem(mTempPolygon); @@ -658,6 +653,14 @@ bool UBGraphicsScene::inputDeviceRelease() addPolygonItemToCurrentStroke(poly); } + // replace the stroke by a simplified version of it + if ((currentTool == UBStylusTool::Pen && UBSettings::settings()->boardSimplifyPenStrokes->get().toBool()) + || (currentTool == UBStylusTool::Marker && UBSettings::settings()->boardSimplifyMarkerStrokes->get().toBool())) + { + simplifyCurrentStroke(); + } + + UBGraphicsStrokesGroup* pStrokes = new UBGraphicsStrokesGroup(); // Remove the strokes that were just drawn here and replace them by a stroke item @@ -857,6 +860,15 @@ void UBGraphicsScene::drawLineTo(const QPointF &pEndPoint, const qreal &startWid } } +void UBGraphicsScene::drawCurve(const QList >& points) +{ + UBGraphicsPolygonItem* polygonItem = curveToPolygonItem(points); + addPolygonItemToCurrentStroke(polygonItem); + + mPreviousPoint = points.last().first; + mPreviousWidth = points.last().second; +} + void UBGraphicsScene::drawCurve(const QList& points, qreal startWidth, qreal endWidth) { UBGraphicsPolygonItem* polygonItem = curveToPolygonItem(points, startWidth, endWidth); @@ -886,7 +898,7 @@ void UBGraphicsScene::addPolygonItemToCurrentStroke(UBGraphicsPolygonItem* polyg // Here we add the item to the scene addItem(polygonItem); if (!mCurrentStroke) - mCurrentStroke = new UBGraphicsStroke(); + mCurrentStroke = new UBGraphicsStroke(this); polygonItem->setStroke(mCurrentStroke); @@ -1151,12 +1163,19 @@ UBGraphicsPolygonItem* UBGraphicsScene::arcToPolygonItem(const QLineF& pStartRad return polygonToPolygonItem(polygon); } +UBGraphicsPolygonItem* UBGraphicsScene::curveToPolygonItem(const QList >& points) +{ + QPolygonF polygon = UBGeometryUtils::curveToPolygon(points, false, true); + + 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() @@ -2625,6 +2644,30 @@ bool UBGraphicsScene::hasTextItemWithFocus(UBGraphicsGroupContainerItem *item){ return bHasFocus; } + +void UBGraphicsScene::simplifyCurrentStroke() +{ + if (!mCurrentStroke) + return; + + UBGraphicsStroke* simplerStroke = mCurrentStroke->simplify(); + if (!simplerStroke) + return; + + foreach(UBGraphicsPolygonItem* poly, mCurrentStroke->polygons()){ + mPreviousPolygonItems.removeAll(poly); + removeItem(poly); + } + + mCurrentStroke = simplerStroke; + + foreach(UBGraphicsPolygonItem* poly, mCurrentStroke->polygons()) { + addItem(poly); + mPreviousPolygonItems.append(poly); + } + +} + void UBGraphicsScene::setDocumentUpdated() { if (document()) @@ -2738,5 +2781,5 @@ void UBGraphicsScene::setToolCursor(int tool) void UBGraphicsScene::initStroke() { - mCurrentStroke = new UBGraphicsStroke(); + mCurrentStroke = new UBGraphicsStroke(this); } diff --git a/src/domain/UBGraphicsScene.h b/src/domain/UBGraphicsScene.h index 37f1a470..74577250 100644 --- a/src/domain/UBGraphicsScene.h +++ b/src/domain/UBGraphicsScene.h @@ -196,6 +196,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); void drawCurve(const QList& points, qreal startWidth, qreal endWidth); bool isEmpty() const; @@ -368,7 +369,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); + UBGraphicsPolygonItem* curveToPolygonItem(const QList > &points); + UBGraphicsPolygonItem* curveToPolygonItem(const QList &points, qreal startWidth, qreal endWidth); void addPolygonItemToCurrentStroke(UBGraphicsPolygonItem* polygonItem); void initPolygonItem(UBGraphicsPolygonItem*); @@ -402,6 +404,7 @@ public slots: void updateEraserColor(); void updateMarkerCircleColor(); bool hasTextItemWithFocus(UBGraphicsGroupContainerItem* item); + void simplifyCurrentStroke(); QGraphicsEllipseItem* mEraser; QGraphicsEllipseItem* mPointer; // "laser" pointer diff --git a/src/domain/UBGraphicsStroke.cpp b/src/domain/UBGraphicsStroke.cpp index f20b680b..a640d0a0 100644 --- a/src/domain/UBGraphicsStroke.cpp +++ b/src/domain/UBGraphicsStroke.cpp @@ -34,9 +34,15 @@ #include "board/UBBoardController.h" #include "core/UBApplication.h" #include "core/memcheck.h" +#include "domain/UBGraphicsScene.h" +#include "frameworks/UBGeometryUtils.h" -UBGraphicsStroke::UBGraphicsStroke() + +typedef QPair strokePoint; + +UBGraphicsStroke::UBGraphicsStroke(UBGraphicsScene *scene) + :mScene(scene) { mAntiScaleRatio = 1./(UBApplication::boardController->systemScaleFactor() * UBApplication::boardController->currentZoom()); } @@ -44,7 +50,10 @@ UBGraphicsStroke::UBGraphicsStroke() UBGraphicsStroke::~UBGraphicsStroke() { - // NOOP + foreach(UBGraphicsPolygonItem* poly, mPolygons) + poly->setStroke(NULL); + + mPolygons.clear(); } @@ -70,47 +79,51 @@ QList UBGraphicsStroke::polygons() const * @brief Add a point to the curve, interpolating extra points if required * @return The points (or point, if none were interpolated) that were added */ -QList UBGraphicsStroke::addPoint(const QPointF& point, UBInterpolator::InterpolationMethod interpolationMethod) +QList > UBGraphicsStroke::addPoint(const QPointF& point, qreal width, UBInterpolator::InterpolationMethod interpolationMethod) { + strokePoint newPoint(point, width); + int n = mReceivedPoints.size(); if (n == 0) { - mReceivedPoints << point; - mDrawnPoints << point; - return QList(); + mReceivedPoints << newPoint; + mDrawnPoints << newPoint; + return QList(); } if (interpolationMethod == UBInterpolator::NoInterpolation) { - QPointF lastPoint = mReceivedPoints.last(); - mReceivedPoints << point; - mDrawnPoints << point; - return QList() << lastPoint << point; + strokePoint lastPoint = mReceivedPoints.last(); + mReceivedPoints << newPoint; + mDrawnPoints << newPoint; + return QList() << lastPoint << newPoint; } else if (interpolationMethod == UBInterpolator::Bezier) { - // This is a bit special, as the curve we are interpolating is not between two drawn points; + // 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. // Don't draw segments smaller than a certain length. This can help with performance - // (less polygons in a stroke) but mostly with keeping the curve smooth. - qreal MIN_DISTANCE = 5*mAntiScaleRatio; - qreal distance = QLineF(mReceivedPoints.last(), point).length(); + // (less polygons to draw) but mostly with keeping the curve smooth. + qreal MIN_DISTANCE = 3*mAntiScaleRatio; + qreal distance = QLineF(mReceivedPoints.last().first, newPoint.first).length(); if (distance < MIN_DISTANCE) { - return QList() << mDrawnPoints.last(); + return QList() << mDrawnPoints.last(); } // The first segment is just a straight line to the first midway point if (n == 1) { - QPointF lastPoint = mReceivedPoints[0]; - mReceivedPoints << point; - mDrawnPoints << QPointF((lastPoint + point)/2.0); + QPointF lastPoint = mReceivedPoints[0].first; + qreal lastWidth = mReceivedPoints[0].second; + strokePoint p(((lastPoint+point)/2.0), (lastWidth+width)/2.0); + mReceivedPoints << newPoint; + mDrawnPoints << p; - return QList() << lastPoint << ((lastPoint + point)/2.0); + return QList() << mReceivedPoints[0] << p; } - QPointF p0 = mReceivedPoints[mReceivedPoints.size() - 2]; - QPointF p1 = mReceivedPoints[mReceivedPoints.size() - 1]; + QPointF p0 = mReceivedPoints[mReceivedPoints.size() - 2].first; + QPointF p1 = mReceivedPoints[mReceivedPoints.size() - 1].first; QPointF p2 = point; UBQuadraticBezier bz; @@ -120,21 +133,28 @@ QList UBGraphicsStroke::addPoint(const QPointF& point, UBInterpolator:: bz.setPoints(startPoint, p1, endPoint); - QList newPoints = bz.getPoints(10); + QList calculated = bz.getPoints(10); + QList newPoints; + + qreal startWidth = mDrawnPoints.last().second; + + for (int i(0); i < calculated.size(); ++i) { + qreal w = startWidth + (qreal(i)/qreal(calculated.size()-1)) * (width - startWidth); + newPoints << strokePoint(calculated[i], w); + } // avoid adding duplicates - if (newPoints.first() == mDrawnPoints.last()) + if (newPoints.first().first == mDrawnPoints.last().first) mDrawnPoints.removeLast(); - foreach(QPointF p, newPoints) { + foreach(strokePoint p, newPoints) mDrawnPoints << p; - } - mReceivedPoints << point; + mReceivedPoints << strokePoint(point, width); return newPoints; } - return QList(); + return QList(); } bool UBGraphicsStroke::hasPressure() @@ -179,3 +199,124 @@ void UBGraphicsStroke::clear() mPolygons.clear(); } } + +/** + * @brief Return a simplified version of the stroke, with less points and polygons. + * + */ +UBGraphicsStroke* UBGraphicsStroke::simplify() +{ + if (mDrawnPoints.size() < 3) + return NULL; + + UBGraphicsStroke* newStroke = new UBGraphicsStroke(); + newStroke->mDrawnPoints = QList(mDrawnPoints); + + QList& points = newStroke->mDrawnPoints; + qDebug() << "Simplifying. Before: " << points.size() << " points and " << polygons().size() << " polygons"; + + /* Basic simplifying algorithm: consider A, B and C the current point and the two following ones. + * If the angle between (AB) and (BC) is lower than a certain threshold, + * the three points are considered to be aligned and the middle one (B) is removed. + * + * We then consider the two following points as the new B and C while keeping the same A, and + * test these three points. As long as they are aligned, B is erased and we start over. + * If not, the current B becomes the new A, and so on. + * + * + * TODO: more advanced algorithm that could also simplify curved sections of the stroke + */ + + // angle difference in degrees between AB and BC below which the segments are considered colinear + qreal threshold = UBSettings::settings()->boardSimplifyPenStrokesThresholdAngle->get().toReal(); + + QList::iterator it = points.begin(); + QList::iterator> toDelete; + + while (it+2 != points.end()) { + QList::iterator b_it(it+1); + + while (b_it+1 != points.end()) { + qreal angle = qFabs(QLineF(it->first, b_it->first).angle() - QLineF(b_it->first, (b_it+1)->first).angle()); + + if (angle < threshold) + b_it = points.erase(b_it); + else + break; + } + + if (b_it+1 == points.end()) + break; + else + it = b_it; + } + + // Next, we iterate over the new points to build the polygons that make up the stroke + + QList newPolygons; + QList newStrokePoints; + int i(0); + + while (i < points.size()) { + bool drawCurve = false; + + newStrokePoints << points[i]; + + // When a polygon is transparent and it overlaps with itself, it is *sometimes* filled incorrectly. + // Limiting the size of the polygons, and creating new ones when the angle between consecutive points is above a + // certain threshold helps mitigate this issue. + // TODO: fix fill issue + + if (newStrokePoints.size() > 1 && i < points.size() - 1) { + qreal angle = qFabs(UBGeometryUtils::angle(points[i-1].first, points[i].first, points[i+1].first)); + qDebug() << "Angle: " << angle; + if (angle > 40 && angle < 320) + drawCurve = true; + } + + if (newStrokePoints.size() % 20 == 0) + drawCurve = true; + + + if (drawCurve) { + UBGraphicsPolygonItem* poly = mScene->polygonToPolygonItem(UBGeometryUtils::curveToPolygon(newStrokePoints, true, true)); + //poly->setColor(QColor(rand()%256, rand()%256, rand()%256, poly->brush().color().alpha())); + + // Subtract overlapping polygons if the stroke is translucent + + if (!poly->brush().isOpaque()) { + foreach(UBGraphicsPolygonItem* prev, newPolygons) + poly->subtract(prev); + } + + newPolygons << poly; + newStrokePoints.clear(); + --i; + } + + ++i; + } + + if (newStrokePoints.size() > 0) { + UBGraphicsPolygonItem* poly = mScene->polygonToPolygonItem(UBGeometryUtils::curveToPolygon(newStrokePoints, true, true)); + + if (!poly->brush().isOpaque()) { + foreach(UBGraphicsPolygonItem* prev, newPolygons) + poly->subtract(prev); + } + + newPolygons << poly; + } + + + newStroke->mPolygons = QList(newPolygons); + + foreach(UBGraphicsPolygonItem* poly, newStroke->mPolygons) { + poly->setFillRule(Qt::WindingFill); + poly->setStroke(newStroke); + } + + qDebug() << "After: " << points.size() << " points and " << newStroke->polygons().size() << " polygons"; + + return newStroke; +} diff --git a/src/domain/UBGraphicsStroke.h b/src/domain/UBGraphicsStroke.h index 8c332498..918ec894 100644 --- a/src/domain/UBGraphicsStroke.h +++ b/src/domain/UBGraphicsStroke.h @@ -38,13 +38,14 @@ class UBGraphicsPolygonItem; +class UBGraphicsScene; class UBGraphicsStroke { friend class UBGraphicsPolygonItem; public: - UBGraphicsStroke(); + UBGraphicsStroke(UBGraphicsScene* scene = NULL); virtual ~UBGraphicsStroke(); bool hasPressure(); @@ -59,22 +60,26 @@ class UBGraphicsStroke void clear(); - QList addPoint(const QPointF& point, UBInterpolator::InterpolationMethod interpolationMethod = UBInterpolator::NoInterpolation); + QList > addPoint(const QPointF& point, qreal width, UBInterpolator::InterpolationMethod interpolationMethod = UBInterpolator::NoInterpolation); - const QList& points() { return mDrawnPoints; } + const QList >& points() { return mDrawnPoints; } + + UBGraphicsStroke* simplify(); protected: void addPolygon(UBGraphicsPolygonItem* pol); private: + UBGraphicsScene * mScene; + QList mPolygons; /// Points that were drawn by the user (i.e, actually received through input device) - QList mReceivedPoints; + QList > mReceivedPoints; /// All the points (including interpolated) that are used to draw the stroke - QList mDrawnPoints; + QList > mDrawnPoints; qreal mAntiScaleRatio; }; diff --git a/src/frameworks/UBGeometryUtils.cpp b/src/frameworks/UBGeometryUtils.cpp index f42f6684..eb36a27e 100644 --- a/src/frameworks/UBGeometryUtils.cpp +++ b/src/frameworks/UBGeometryUtils.cpp @@ -265,6 +265,36 @@ QPolygonF UBGeometryUtils::curveToPolygon(const QList& points, qreal st 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): @@ -276,15 +306,16 @@ QPolygonF UBGeometryUtils::curveToPolygon(const QList& points, qreal st 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], points[1]); + QLineF firstSegment = QLineF(points[0].first, points[1].first); QLineF normal = firstSegment.normalVector(); normal.setLength(startWidth/2.0); - newPoints << pointPair(normal.p2(), points[0] - QPointF(normal.dx(), normal.dy())); + 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 @@ -306,45 +337,50 @@ QPolygonF UBGeometryUtils::curveToPolygon(const QList& points, qreal st */ for (int i(1); i < n_points-1; ++i) { - qreal width = startWidth + (qreal(i)/qreal(n_points-1)) * (endWidth - startWidth); + //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()); + 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], points[n_points-1]); + 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() + QPointF(normal.dx(), normal.dy()); - QPointF d2 = points.last() - QPointF(normal.dx(), normal.dy()); + 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); } - 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); + 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); } - 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); + 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(); + //path.closeSubpath(); return path.toFillPolygon(); } @@ -391,3 +427,11 @@ void UBGeometryUtils::crashPointList(QVector &points) } } } + +/** + * @brief Return the angle between three points + */ +qreal UBGeometryUtils::angle(const QPointF& a, const QPointF& b, const QPointF& c) +{ + return (QLineF(a, b).angle() - QLineF(b, c).angle()); +} diff --git a/src/frameworks/UBGeometryUtils.h b/src/frameworks/UBGeometryUtils.h index 6d0aeff1..988b4a20 100644 --- a/src/frameworks/UBGeometryUtils.h +++ b/src/frameworks/UBGeometryUtils.h @@ -48,12 +48,15 @@ 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 QPolygonF curveToPolygon(const QList >& points, bool roundStart, bool roundEnd); static QPointF pointConstrainedInRect(QPointF point, QRectF rect); static QPoint pointConstrainedInRect(QPoint point, QRect rect); static void crashPointList(QVector &points); + static qreal angle(const QPointF& a, const QPointF& b, const QPointF& c); + const static int centimeterGraduationHeight; const static int halfCentimeterGraduationHeight; const static int millimeterGraduationHeight; diff --git a/src/frameworks/UBInterpolator.cpp b/src/frameworks/UBInterpolator.cpp index b5777bcd..3b75da12 100644 --- a/src/frameworks/UBInterpolator.cpp +++ b/src/frameworks/UBInterpolator.cpp @@ -34,7 +34,7 @@ void UBQuadraticBezier::setPoints(QPointF start, QPointF control, QPointF end) /** * @brief Return n points along the curve, including start and end points (thus n should be larger than or equal to 2). * - * The higher n, the more accurate the result + * The higher n, the more accurate the resulting curve will be. */ QList UBQuadraticBezier::getPoints(int n) {