Added stroke simplification algorithm

If enabled in the preferences menu, pen and marker strokes will be
replaced by a simplified stroke after they are drawn.

The algorithm is very basic (for now): if three points are almost lined
up (the threshold angle can be specified in the config file), then the
middle one is removed. This is repeated over the whole stroke; new
polygons are then generated based on the simplified stroke points.

This typically cuts down on number of points and polygons by a factor of
about 10, while having minimal visual impact.
preferencesAboutTextFull
Craig Watson 8 years ago
parent 02205e861b
commit df71f158c3
  1. 24
      resources/forms/brushProperties.ui
  2. 6
      src/core/UBPreferencesController.cpp
  3. 5
      src/core/UBSettings.cpp
  4. 4
      src/core/UBSettings.h
  5. 2
      src/domain/UBGraphicsPolygonItem.cpp
  6. 77
      src/domain/UBGraphicsScene.cpp
  7. 3
      src/domain/UBGraphicsScene.h
  8. 195
      src/domain/UBGraphicsStroke.cpp
  9. 15
      src/domain/UBGraphicsStroke.h
  10. 76
      src/frameworks/UBGeometryUtils.cpp
  11. 3
      src/frameworks/UBGeometryUtils.h
  12. 2
      src/frameworks/UBInterpolator.cpp

@ -521,6 +521,30 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="8" column="0" colspan="2">
<layout class="QHBoxLayout" name="strokeSimplificationFrame">
<item>
<widget class="QCheckBox" name="simplifyStrokesCheckBox">
<property name="text">
<string>Simplify strokes after drawing (experimental)</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
<customwidgets> <customwidgets>

@ -166,6 +166,7 @@ void UBPreferencesController::wire()
connect(mPenProperties->strongSlider, SIGNAL(valueChanged(int)), this, SLOT(widthSliderChanged(int))); connect(mPenProperties->strongSlider, SIGNAL(valueChanged(int)), this, SLOT(widthSliderChanged(int)));
connect(mPenProperties->pressureSensitiveCheckBox, SIGNAL(clicked(bool)), settings, SLOT(setPenPressureSensitive(bool))); connect(mPenProperties->pressureSensitiveCheckBox, SIGNAL(clicked(bool)), settings, SLOT(setPenPressureSensitive(bool)));
connect(mPenProperties->interpolateStrokesCheckBox, SIGNAL(clicked(bool)), settings->boardInterpolatePenStrokes, SLOT(setBool(bool))); connect(mPenProperties->interpolateStrokesCheckBox, SIGNAL(clicked(bool)), settings->boardInterpolatePenStrokes, SLOT(setBool(bool)));
connect(mPenProperties->simplifyStrokesCheckBox, SIGNAL(clicked(bool)), settings->boardSimplifyPenStrokes, SLOT(setBool(bool)));
// marker // marker
QList<QColor> markerLightBackgroundColors = settings->boardMarkerLightBackgroundColors->colors(); QList<QColor> markerLightBackgroundColors = settings->boardMarkerLightBackgroundColors->colors();
@ -183,6 +184,8 @@ void UBPreferencesController::wire()
connect(mMarkerProperties->mediumSlider, SIGNAL(valueChanged(int)), this, SLOT(widthSliderChanged(int))); connect(mMarkerProperties->mediumSlider, SIGNAL(valueChanged(int)), this, SLOT(widthSliderChanged(int)));
connect(mMarkerProperties->strongSlider, 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->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))); connect(mMarkerProperties->opacitySlider, SIGNAL(valueChanged(int)), this, SLOT(opacitySliderChanged(int)));
// about tab // about tab
@ -233,12 +236,15 @@ void UBPreferencesController::init()
mPenProperties->strongSlider->setValue(settings->boardPenStrongWidth->get().toDouble() * sSliderRatio); mPenProperties->strongSlider->setValue(settings->boardPenStrongWidth->get().toDouble() * sSliderRatio);
mPenProperties->pressureSensitiveCheckBox->setChecked(settings->boardPenPressureSensitive->get().toBool()); mPenProperties->pressureSensitiveCheckBox->setChecked(settings->boardPenPressureSensitive->get().toBool());
mPenProperties->interpolateStrokesCheckBox->setChecked(settings->boardInterpolatePenStrokes->get().toBool()); mPenProperties->interpolateStrokesCheckBox->setChecked(settings->boardInterpolatePenStrokes->get().toBool());
mPenProperties->simplifyStrokesCheckBox->setChecked(settings->boardSimplifyPenStrokes->get().toBool());
// marker tab // marker tab
mMarkerProperties->fineSlider->setValue(settings->boardMarkerFineWidth->get().toDouble() * sSliderRatio); mMarkerProperties->fineSlider->setValue(settings->boardMarkerFineWidth->get().toDouble() * sSliderRatio);
mMarkerProperties->mediumSlider->setValue(settings->boardMarkerMediumWidth->get().toDouble() * sSliderRatio); mMarkerProperties->mediumSlider->setValue(settings->boardMarkerMediumWidth->get().toDouble() * sSliderRatio);
mMarkerProperties->strongSlider->setValue(settings->boardMarkerStrongWidth->get().toDouble() * sSliderRatio); mMarkerProperties->strongSlider->setValue(settings->boardMarkerStrongWidth->get().toDouble() * sSliderRatio);
mMarkerProperties->pressureSensitiveCheckBox->setChecked(settings->boardMarkerPressureSensitive->get().toBool()); 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); mMarkerProperties->opacitySlider->setValue(settings->boardMarkerAlpha->get().toDouble() * 100);

@ -270,6 +270,11 @@ void UBSettings::init()
boardUseHighResTabletEvent = new UBSetting(this, "Board", "UseHighResTabletEvent", true); boardUseHighResTabletEvent = new UBSetting(this, "Board", "UseHighResTabletEvent", true);
boardInterpolatePenStrokes = new UBSetting(this, "Board", "InterpolatePenStrokes", 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"); boardKeyboardPaletteKeyBtnSize = new UBSetting(this, "Board", "KeyboardPaletteKeyBtnSize", "16x16");
ValidateKeyboardPaletteKeyBtnSize(); ValidateKeyboardPaletteKeyBtnSize();

@ -274,6 +274,10 @@ class UBSettings : public QObject
UBSetting* boardUseHighResTabletEvent; UBSetting* boardUseHighResTabletEvent;
UBSetting* boardInterpolatePenStrokes; UBSetting* boardInterpolatePenStrokes;
UBSetting* boardSimplifyPenStrokes;
UBSetting* boardSimplifyPenStrokesThresholdAngle;
UBSetting* boardInterpolateMarkerStrokes;
UBSetting* boardSimplifyMarkerStrokes;
UBSetting* boardKeyboardPaletteKeyBtnSize; UBSetting* boardKeyboardPaletteKeyBtnSize;

@ -121,11 +121,13 @@ void UBGraphicsPolygonItem::setStrokesGroup(UBGraphicsStrokesGroup *group)
void UBGraphicsPolygonItem::setStroke(UBGraphicsStroke* stroke) void UBGraphicsPolygonItem::setStroke(UBGraphicsStroke* stroke)
{ {
if (stroke) {
clearStroke(); clearStroke();
mStroke = stroke; mStroke = stroke;
mStroke->addPolygon(this); mStroke->addPolygon(this);
} }
}
UBGraphicsStroke* UBGraphicsPolygonItem::stroke() const UBGraphicsStroke* UBGraphicsPolygonItem::stroke() const
{ {

@ -415,7 +415,7 @@ bool UBGraphicsScene::inputDevicePress(const QPointF& scenePos, const qreal& pre
// --------------------------------------------------------------- // ---------------------------------------------------------------
// Create a new Stroke. A Stroke is a collection of QGraphicsLines // Create a new Stroke. A Stroke is a collection of QGraphicsLines
// --------------------------------------------------------------- // ---------------------------------------------------------------
mCurrentStroke = new UBGraphicsStroke(); mCurrentStroke = new UBGraphicsStroke(this);
if (currentTool != UBStylusTool::Line){ if (currentTool != UBStylusTool::Line){
// Handle the pressure // Handle the pressure
@ -438,7 +438,7 @@ bool UBGraphicsScene::inputDevicePress(const QPointF& scenePos, const qreal& pre
moveTo(scenePos); moveTo(scenePos);
drawLineTo(scenePos, width, UBDrawingController::drawingController()->stylusTool() == UBStylusTool::Line); drawLineTo(scenePos, width, UBDrawingController::drawingController()->stylusTool() == UBStylusTool::Line);
mCurrentStroke->addPoint(scenePos); mCurrentStroke->addPoint(scenePos, width);
} }
accepted = true; accepted = true;
} }
@ -543,7 +543,7 @@ bool UBGraphicsScene::inputDeviceMove(const QPointF& scenePos, const qreal& pres
} }
if (!mCurrentStroke) if (!mCurrentStroke)
mCurrentStroke = new UBGraphicsStroke(); mCurrentStroke = new UBGraphicsStroke(this);
if(dc->mActiveRuler){ if(dc->mActiveRuler){
dc->mActiveRuler->DrawLine(position, width); dc->mActiveRuler->DrawLine(position, width);
@ -551,20 +551,15 @@ bool UBGraphicsScene::inputDeviceMove(const QPointF& scenePos, const qreal& pres
else{ else{
UBInterpolator::InterpolationMethod interpolator = UBInterpolator::NoInterpolation; UBInterpolator::InterpolationMethod interpolator = UBInterpolator::NoInterpolation;
/* if ((currentTool == UBStylusTool::Pen && UBSettings::settings()->boardInterpolatePenStrokes->get().toBool())
if (currentTool == UBStylusTool::Marker) { || (currentTool == UBStylusTool::Marker && UBSettings::settings()->boardInterpolateMarkerStrokes->get().toBool()))
// 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()) {
interpolator = UBInterpolator::Bezier; interpolator = UBInterpolator::Bezier;
} }
QList<QPointF> newPoints = mCurrentStroke->addPoint(scenePos, interpolator); QList<QPair<QPointF, qreal> > newPoints = mCurrentStroke->addPoint(scenePos, width, interpolator);
if (newPoints.length() > 1) { if (newPoints.length() > 1) {
drawCurve(newPoints, mPreviousWidth, width); drawCurve(newPoints);
} }
if (interpolator == UBInterpolator::Bezier) { if (interpolator == UBInterpolator::Bezier) {
@ -578,7 +573,7 @@ bool UBGraphicsScene::inputDeviceMove(const QPointF& scenePos, const qreal& pres
mTempPolygon = NULL; mTempPolygon = NULL;
} }
QPointF lastDrawnPoint = newPoints.last(); QPointF lastDrawnPoint = newPoints.last().first;
mTempPolygon = lineToPolygonItem(QLineF(lastDrawnPoint, scenePos), mPreviousWidth, width); mTempPolygon = lineToPolygonItem(QLineF(lastDrawnPoint, scenePos), mPreviousWidth, width);
addItem(mTempPolygon); addItem(mTempPolygon);
@ -658,6 +653,14 @@ bool UBGraphicsScene::inputDeviceRelease()
addPolygonItemToCurrentStroke(poly); 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(); UBGraphicsStrokesGroup* pStrokes = new UBGraphicsStrokesGroup();
// Remove the strokes that were just drawn here and replace them by a stroke item // 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<QPair<QPointF, qreal> >& points)
{
UBGraphicsPolygonItem* polygonItem = curveToPolygonItem(points);
addPolygonItemToCurrentStroke(polygonItem);
mPreviousPoint = points.last().first;
mPreviousWidth = points.last().second;
}
void UBGraphicsScene::drawCurve(const QList<QPointF>& points, qreal startWidth, qreal endWidth) void UBGraphicsScene::drawCurve(const QList<QPointF>& points, qreal startWidth, qreal endWidth)
{ {
UBGraphicsPolygonItem* polygonItem = curveToPolygonItem(points, startWidth, endWidth); UBGraphicsPolygonItem* polygonItem = curveToPolygonItem(points, startWidth, endWidth);
@ -886,7 +898,7 @@ void UBGraphicsScene::addPolygonItemToCurrentStroke(UBGraphicsPolygonItem* polyg
// Here we add the item to the scene // Here we add the item to the scene
addItem(polygonItem); addItem(polygonItem);
if (!mCurrentStroke) if (!mCurrentStroke)
mCurrentStroke = new UBGraphicsStroke(); mCurrentStroke = new UBGraphicsStroke(this);
polygonItem->setStroke(mCurrentStroke); polygonItem->setStroke(mCurrentStroke);
@ -1151,12 +1163,19 @@ UBGraphicsPolygonItem* UBGraphicsScene::arcToPolygonItem(const QLineF& pStartRad
return polygonToPolygonItem(polygon); return polygonToPolygonItem(polygon);
} }
UBGraphicsPolygonItem* UBGraphicsScene::curveToPolygonItem(const QList<QPair<QPointF, qreal> >& points)
{
QPolygonF polygon = UBGeometryUtils::curveToPolygon(points, false, true);
return polygonToPolygonItem(polygon);
}
UBGraphicsPolygonItem* UBGraphicsScene::curveToPolygonItem(const QList<QPointF>& points, qreal startWidth, qreal endWidth) UBGraphicsPolygonItem* UBGraphicsScene::curveToPolygonItem(const QList<QPointF>& points, qreal startWidth, qreal endWidth)
{ {
QPolygonF polygon = UBGeometryUtils::curveToPolygon(points, startWidth, endWidth); QPolygonF polygon = UBGeometryUtils::curveToPolygon(points, startWidth, endWidth);
return polygonToPolygonItem(polygon); return polygonToPolygonItem(polygon);
} }
void UBGraphicsScene::clearSelectionFrame() void UBGraphicsScene::clearSelectionFrame()
@ -2625,6 +2644,30 @@ bool UBGraphicsScene::hasTextItemWithFocus(UBGraphicsGroupContainerItem *item){
return bHasFocus; 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() void UBGraphicsScene::setDocumentUpdated()
{ {
if (document()) if (document())
@ -2738,5 +2781,5 @@ void UBGraphicsScene::setToolCursor(int tool)
void UBGraphicsScene::initStroke() void UBGraphicsScene::initStroke()
{ {
mCurrentStroke = new UBGraphicsStroke(); mCurrentStroke = new UBGraphicsStroke(this);
} }

@ -196,6 +196,7 @@ class UBGraphicsScene: public UBCoreGraphicsScene, public UBItem
void drawLineTo(const QPointF& pEndPoint, const qreal& pStartWidth, const qreal& endWidth, bool bLineStyle); void drawLineTo(const QPointF& pEndPoint, const qreal& pStartWidth, const qreal& endWidth, bool bLineStyle);
void eraseLineTo(const QPointF& pEndPoint, const qreal& pWidth); void eraseLineTo(const QPointF& pEndPoint, const qreal& pWidth);
void drawArcTo(const QPointF& pCenterPoint, qreal pSpanAngle); void drawArcTo(const QPointF& pCenterPoint, qreal pSpanAngle);
void drawCurve(const QList<QPair<QPointF, qreal> > &points);
void drawCurve(const QList<QPointF>& points, qreal startWidth, qreal endWidth); void drawCurve(const QList<QPointF>& points, qreal startWidth, qreal endWidth);
bool isEmpty() const; bool isEmpty() const;
@ -368,6 +369,7 @@ public slots:
UBGraphicsPolygonItem* lineToPolygonItem(const QLineF &pLine, const qreal &pStartWidth, const qreal &pEndWidth); UBGraphicsPolygonItem* lineToPolygonItem(const QLineF &pLine, const qreal &pStartWidth, const qreal &pEndWidth);
UBGraphicsPolygonItem* arcToPolygonItem(const QLineF& pStartRadius, qreal pSpanAngle, qreal pWidth); UBGraphicsPolygonItem* arcToPolygonItem(const QLineF& pStartRadius, qreal pSpanAngle, qreal pWidth);
UBGraphicsPolygonItem* curveToPolygonItem(const QList<QPair<QPointF, qreal> > &points);
UBGraphicsPolygonItem* curveToPolygonItem(const QList<QPointF> &points, qreal startWidth, qreal endWidth); UBGraphicsPolygonItem* curveToPolygonItem(const QList<QPointF> &points, qreal startWidth, qreal endWidth);
void addPolygonItemToCurrentStroke(UBGraphicsPolygonItem* polygonItem); void addPolygonItemToCurrentStroke(UBGraphicsPolygonItem* polygonItem);
@ -402,6 +404,7 @@ public slots:
void updateEraserColor(); void updateEraserColor();
void updateMarkerCircleColor(); void updateMarkerCircleColor();
bool hasTextItemWithFocus(UBGraphicsGroupContainerItem* item); bool hasTextItemWithFocus(UBGraphicsGroupContainerItem* item);
void simplifyCurrentStroke();
QGraphicsEllipseItem* mEraser; QGraphicsEllipseItem* mEraser;
QGraphicsEllipseItem* mPointer; // "laser" pointer QGraphicsEllipseItem* mPointer; // "laser" pointer

@ -34,9 +34,15 @@
#include "board/UBBoardController.h" #include "board/UBBoardController.h"
#include "core/UBApplication.h" #include "core/UBApplication.h"
#include "core/memcheck.h" #include "core/memcheck.h"
#include "domain/UBGraphicsScene.h"
#include "frameworks/UBGeometryUtils.h"
UBGraphicsStroke::UBGraphicsStroke()
typedef QPair<QPointF, qreal> strokePoint;
UBGraphicsStroke::UBGraphicsStroke(UBGraphicsScene *scene)
:mScene(scene)
{ {
mAntiScaleRatio = 1./(UBApplication::boardController->systemScaleFactor() * UBApplication::boardController->currentZoom()); mAntiScaleRatio = 1./(UBApplication::boardController->systemScaleFactor() * UBApplication::boardController->currentZoom());
} }
@ -44,7 +50,10 @@ UBGraphicsStroke::UBGraphicsStroke()
UBGraphicsStroke::~UBGraphicsStroke() UBGraphicsStroke::~UBGraphicsStroke()
{ {
// NOOP foreach(UBGraphicsPolygonItem* poly, mPolygons)
poly->setStroke(NULL);
mPolygons.clear();
} }
@ -70,47 +79,51 @@ QList<UBGraphicsPolygonItem*> UBGraphicsStroke::polygons() const
* @brief Add a point to the curve, interpolating extra points if required * @brief Add a point to the curve, interpolating extra points if required
* @return The points (or point, if none were interpolated) that were added * @return The points (or point, if none were interpolated) that were added
*/ */
QList<QPointF> UBGraphicsStroke::addPoint(const QPointF& point, UBInterpolator::InterpolationMethod interpolationMethod) QList<QPair<QPointF, qreal> > UBGraphicsStroke::addPoint(const QPointF& point, qreal width, UBInterpolator::InterpolationMethod interpolationMethod)
{ {
strokePoint newPoint(point, width);
int n = mReceivedPoints.size(); int n = mReceivedPoints.size();
if (n == 0) { if (n == 0) {
mReceivedPoints << point; mReceivedPoints << newPoint;
mDrawnPoints << point; mDrawnPoints << newPoint;
return QList<QPointF>(); return QList<strokePoint>();
} }
if (interpolationMethod == UBInterpolator::NoInterpolation) { if (interpolationMethod == UBInterpolator::NoInterpolation) {
QPointF lastPoint = mReceivedPoints.last(); strokePoint lastPoint = mReceivedPoints.last();
mReceivedPoints << point; mReceivedPoints << newPoint;
mDrawnPoints << point; mDrawnPoints << newPoint;
return QList<QPointF>() << lastPoint << point; return QList<strokePoint>() << lastPoint << newPoint;
} }
else if (interpolationMethod == UBInterpolator::Bezier) { 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. // 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 // 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. // (less polygons to draw) but mostly with keeping the curve smooth.
qreal MIN_DISTANCE = 5*mAntiScaleRatio; qreal MIN_DISTANCE = 3*mAntiScaleRatio;
qreal distance = QLineF(mReceivedPoints.last(), point).length(); qreal distance = QLineF(mReceivedPoints.last().first, newPoint.first).length();
if (distance < MIN_DISTANCE) { if (distance < MIN_DISTANCE) {
return QList<QPointF>() << mDrawnPoints.last(); return QList<strokePoint>() << mDrawnPoints.last();
} }
// The first segment is just a straight line to the first midway point // The first segment is just a straight line to the first midway point
if (n == 1) { if (n == 1) {
QPointF lastPoint = mReceivedPoints[0]; QPointF lastPoint = mReceivedPoints[0].first;
mReceivedPoints << point; qreal lastWidth = mReceivedPoints[0].second;
mDrawnPoints << QPointF((lastPoint + point)/2.0); strokePoint p(((lastPoint+point)/2.0), (lastWidth+width)/2.0);
mReceivedPoints << newPoint;
mDrawnPoints << p;
return QList<QPointF>() << lastPoint << ((lastPoint + point)/2.0); return QList<strokePoint>() << mReceivedPoints[0] << p;
} }
QPointF p0 = mReceivedPoints[mReceivedPoints.size() - 2]; QPointF p0 = mReceivedPoints[mReceivedPoints.size() - 2].first;
QPointF p1 = mReceivedPoints[mReceivedPoints.size() - 1]; QPointF p1 = mReceivedPoints[mReceivedPoints.size() - 1].first;
QPointF p2 = point; QPointF p2 = point;
UBQuadraticBezier bz; UBQuadraticBezier bz;
@ -120,21 +133,28 @@ QList<QPointF> UBGraphicsStroke::addPoint(const QPointF& point, UBInterpolator::
bz.setPoints(startPoint, p1, endPoint); bz.setPoints(startPoint, p1, endPoint);
QList<QPointF> newPoints = bz.getPoints(10); QList<QPointF> calculated = bz.getPoints(10);
QList<strokePoint> 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 // avoid adding duplicates
if (newPoints.first() == mDrawnPoints.last()) if (newPoints.first().first == mDrawnPoints.last().first)
mDrawnPoints.removeLast(); mDrawnPoints.removeLast();
foreach(QPointF p, newPoints) { foreach(strokePoint p, newPoints)
mDrawnPoints << p; mDrawnPoints << p;
}
mReceivedPoints << point; mReceivedPoints << strokePoint(point, width);
return newPoints; return newPoints;
} }
return QList<QPointF>(); return QList<strokePoint>();
} }
bool UBGraphicsStroke::hasPressure() bool UBGraphicsStroke::hasPressure()
@ -179,3 +199,124 @@ void UBGraphicsStroke::clear()
mPolygons.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<strokePoint>(mDrawnPoints);
QList<strokePoint>& 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<strokePoint>::iterator it = points.begin();
QList<QList<strokePoint>::iterator> toDelete;
while (it+2 != points.end()) {
QList<strokePoint>::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<UBGraphicsPolygonItem*> newPolygons;
QList<strokePoint> 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<UBGraphicsPolygonItem*>(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;
}

@ -38,13 +38,14 @@
class UBGraphicsPolygonItem; class UBGraphicsPolygonItem;
class UBGraphicsScene;
class UBGraphicsStroke class UBGraphicsStroke
{ {
friend class UBGraphicsPolygonItem; friend class UBGraphicsPolygonItem;
public: public:
UBGraphicsStroke(); UBGraphicsStroke(UBGraphicsScene* scene = NULL);
virtual ~UBGraphicsStroke(); virtual ~UBGraphicsStroke();
bool hasPressure(); bool hasPressure();
@ -59,22 +60,26 @@ class UBGraphicsStroke
void clear(); void clear();
QList<QPointF> addPoint(const QPointF& point, UBInterpolator::InterpolationMethod interpolationMethod = UBInterpolator::NoInterpolation); QList<QPair<QPointF, qreal> > addPoint(const QPointF& point, qreal width, UBInterpolator::InterpolationMethod interpolationMethod = UBInterpolator::NoInterpolation);
const QList<QPointF>& points() { return mDrawnPoints; } const QList<QPair<QPointF, qreal> >& points() { return mDrawnPoints; }
UBGraphicsStroke* simplify();
protected: protected:
void addPolygon(UBGraphicsPolygonItem* pol); void addPolygon(UBGraphicsPolygonItem* pol);
private: private:
UBGraphicsScene * mScene;
QList<UBGraphicsPolygonItem*> mPolygons; QList<UBGraphicsPolygonItem*> mPolygons;
/// Points that were drawn by the user (i.e, actually received through input device) /// Points that were drawn by the user (i.e, actually received through input device)
QList<QPointF> mReceivedPoints; QList<QPair<QPointF, qreal> > mReceivedPoints;
/// All the points (including interpolated) that are used to draw the stroke /// All the points (including interpolated) that are used to draw the stroke
QList<QPointF> mDrawnPoints; QList<QPair<QPointF, qreal> > mDrawnPoints;
qreal mAntiScaleRatio; qreal mAntiScaleRatio;
}; };

@ -265,6 +265,36 @@ QPolygonF UBGeometryUtils::curveToPolygon(const QList<QPointF>& points, qreal st
if (n_points == 2) if (n_points == 2)
return lineToPolygon(points[0], points[1], startWidth, endWidth); 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 /* The vertices (x's) are calculated based on the stroke's width and angle, and the position of the
supplied points (o's): supplied points (o's):
@ -276,15 +306,16 @@ QPolygonF UBGeometryUtils::curveToPolygon(const QList<QPointF>& points, qreal st
The vertices above and below each 'o' point are temporarily stored together, The vertices above and below each 'o' point are temporarily stored together,
as a pair of points. as a pair of points.
*/ */
typedef QPair<QPointF, QPointF> pointPair;
QList<pointPair> newPoints; QList<pointPair> newPoints;
QLineF firstSegment = QLineF(points[0], points[1]); QLineF firstSegment = QLineF(points[0].first, points[1].first);
QLineF normal = firstSegment.normalVector(); QLineF normal = firstSegment.normalVector();
normal.setLength(startWidth/2.0); 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 Calculating the vertices (d1 and d2, below) is a little less trivial for the
@ -306,45 +337,50 @@ QPolygonF UBGeometryUtils::curveToPolygon(const QList<QPointF>& points, qreal st
*/ */
for (int i(1); i < n_points-1; ++i) { 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(); QLineF normal = (QLineF(points[i-1].first, points[i+1].first)).normalVector();
normal.setLength(width/2.0); normal.setLength(points[i].second/2.0);
QPointF d1 = points[i] + QPointF(normal.dx(), normal.dy()); QPointF d1 = points[i].first + QPointF(normal.dx(), normal.dy());
QPointF d2 = points[i] - QPointF(normal.dx(), normal.dy()); QPointF d2 = points[i].first - QPointF(normal.dx(), normal.dy());
newPoints << pointPair(d1, d2); newPoints << pointPair(d1, d2);
} }
// The last point is similar to the first // 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 = lastSegment.normalVector();
normal.setLength(endWidth/2.0); normal.setLength(endWidth/2.0);
QPointF d1 = points.last() + QPointF(normal.dx(), normal.dy()); QPointF d1 = points.last().first + QPointF(normal.dx(), normal.dy());
QPointF d2 = points.last() - QPointF(normal.dx(), normal.dy()); QPointF d2 = points.last().first - QPointF(normal.dx(), normal.dy());
newPoints << pointPair(d1, d2); newPoints << pointPair(d1, d2);
QPainterPath path; QPainterPath path;
path.setFillRule(Qt::WindingFill);
path.moveTo(newPoints[0].first); path.moveTo(newPoints[0].first);
for (int i(1); i < n_points; ++i) { for (int i(1); i < n_points; ++i) {
path.lineTo(newPoints[i].first); 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); if (roundEnd)
//path.lineTo(newPoints.last().second); 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) { for (int i(n_points-1); i >= 0; --i) {
path.lineTo(newPoints[i].second); 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); if (roundStart)
//path.lineTo(newPoints[0].second); 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(); return path.toFillPolygon();
} }
@ -391,3 +427,11 @@ void UBGeometryUtils::crashPointList(QVector<QPointF> &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());
}

@ -48,12 +48,15 @@ class UBGeometryUtils
static QPolygonF lineToPolygon(const QPointF& pStart, const QPointF& pEnd, static QPolygonF lineToPolygon(const QPointF& pStart, const QPointF& pEnd,
const qreal& pStartWidth, const qreal& pEndWidth); const qreal& pStartWidth, const qreal& pEndWidth);
static QPolygonF curveToPolygon(const QList<QPointF>& points, qreal startWidth, qreal endWidth); static QPolygonF curveToPolygon(const QList<QPointF>& points, qreal startWidth, qreal endWidth);
static QPolygonF curveToPolygon(const QList<QPair<QPointF, qreal> >& points, bool roundStart, bool roundEnd);
static QPointF pointConstrainedInRect(QPointF point, QRectF rect); static QPointF pointConstrainedInRect(QPointF point, QRectF rect);
static QPoint pointConstrainedInRect(QPoint point, QRect rect); static QPoint pointConstrainedInRect(QPoint point, QRect rect);
static void crashPointList(QVector<QPointF> &points); static void crashPointList(QVector<QPointF> &points);
static qreal angle(const QPointF& a, const QPointF& b, const QPointF& c);
const static int centimeterGraduationHeight; const static int centimeterGraduationHeight;
const static int halfCentimeterGraduationHeight; const static int halfCentimeterGraduationHeight;
const static int millimeterGraduationHeight; const static int millimeterGraduationHeight;

@ -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). * @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<QPointF> UBQuadraticBezier::getPoints(int n) QList<QPointF> UBQuadraticBezier::getPoints(int n)
{ {

Loading…
Cancel
Save