новые иконки в 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/domain/UBGraphicsStroke.cpp

325 lines
11 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 "UBGraphicsStroke.h"
#include "UBGraphicsPolygonItem.h"
#include "board/UBBoardController.h"
#include "core/UBApplication.h"
#include "core/memcheck.h"
#include "domain/UBGraphicsScene.h"
#include "frameworks/UBGeometryUtils.h"
typedef QPair<QPointF, qreal> strokePoint;
UBGraphicsStroke::UBGraphicsStroke(UBGraphicsScene *scene)
:mScene(scene)
{
mAntiScaleRatio = 1./(UBApplication::boardController->systemScaleFactor() * UBApplication::boardController->currentZoom());
}
UBGraphicsStroke::~UBGraphicsStroke()
{
foreach(UBGraphicsPolygonItem* poly, mPolygons)
poly->setStroke(NULL);
mPolygons.clear();
}
void UBGraphicsStroke::addPolygon(UBGraphicsPolygonItem* pol)
{
remove(pol);
mPolygons << pol;
}
void UBGraphicsStroke::remove(UBGraphicsPolygonItem* polygonItem)
{
int n = mPolygons.indexOf(polygonItem);
if (n>=0)
mPolygons.removeAt(n);
}
QList<UBGraphicsPolygonItem*> UBGraphicsStroke::polygons() const
{
return mPolygons;
}
/**
* @brief Add a point to the curve, interpolating extra points if required
* @param point The position of the point to add
* @param width The width of the stroke at that point.
* @param interpolate If true, a Bézier curve will be drawn rather than a straight line
* @return A list containing the last point drawn plus the point(s) that were added
*
* This method should be called when a new point is given by the input method (mouse, pen or other), and the points that are returned
* should be used to draw the actual stroke on-screen. This is because if interpolation (bézier curves) are to be used, the points to draw
* do not correspond to the points that were given by the input method.
*
*/
QList<QPair<QPointF, qreal> > UBGraphicsStroke::addPoint(const QPointF& point, qreal width, bool interpolate)
{
strokePoint newPoint(point, width);
int n = mReceivedPoints.size();
if (n == 0) {
mReceivedPoints << newPoint;
mDrawnPoints << newPoint;
return QList<strokePoint>() << newPoint;
}
if (!interpolate) {
strokePoint lastPoint = mReceivedPoints.last();
mReceivedPoints << newPoint;
mDrawnPoints << newPoint;
return QList<strokePoint>() << lastPoint << newPoint;
}
else {
// 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.
// The first segment is just a straight line to the first midway point
if (n == 1) {
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<strokePoint>() << mReceivedPoints[0] << p;
}
QPointF p0 = mReceivedPoints[mReceivedPoints.size() - 2].first;
QPointF p1 = mReceivedPoints[mReceivedPoints.size() - 1].first;
QPointF p2 = point;
QPointF startPoint = (p1+p0)/2.0;
QPointF endPoint = (p2+p1)/2.0;
QList<QPointF> calculated = UBGeometryUtils::quadraticBezier(startPoint, p1, endPoint, 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
if (newPoints.first().first == mDrawnPoints.last().first)
mDrawnPoints.removeLast();
foreach(strokePoint p, newPoints)
mDrawnPoints << p;
mReceivedPoints << strokePoint(point, width);
return newPoints;
}
return QList<strokePoint>();
}
bool UBGraphicsStroke::hasPressure()
{
if (mPolygons.count() > 2)
{
qreal nominalWidth = mPolygons.at(0)->originalWidth();
foreach(UBGraphicsPolygonItem* pol, mPolygons)
{
if (!pol->isNominalLine() || pol->originalWidth() != nominalWidth)
return true;
}
return false;
}
else
return true;
}
UBGraphicsStroke* UBGraphicsStroke::deepCopy()
{
UBGraphicsStroke* clone = new UBGraphicsStroke();
return clone;
}
bool UBGraphicsStroke::hasAlpha() const
{
if (mPolygons.length() > 0 && mPolygons.at(0)->color().alphaF() != 1.0)
{
return true;
}
else
{
return false;
}
}
void UBGraphicsStroke::clear()
{
if(!mPolygons.empty()){
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.
*
* In case the stroke thickness varies a lot between A and B, then B is not removed even if it
* should be according to the previous criteria.
*
* 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 thresholdAngle = UBSettings::settings()->boardSimplifyPenStrokesThresholdAngle->get().toReal();
// Relative difference in thickness between two consecutive points (A and B) below which they are considered equal
qreal thresholdWidthDifference = UBSettings::settings()->boardSimplifyPenStrokesThresholdWidthDifference->get().toReal();
QList<strokePoint>::iterator it = points.begin();
QList<QList<strokePoint>::iterator> toDelete;
while (it+2 != points.end()) {
// it, b_it and (b_it+1) correspond to A, B and C respectively
QList<strokePoint>::iterator b_it(it+1);
while (b_it+1 != points.end()) {
qreal angle = qFabs(180-(UBGeometryUtils::angle(it->first, b_it->first, (b_it+1)->first)));
qreal widthRatio = qMax(it->second, b_it->second)/qMin(it->second, b_it->second);
if (angle < thresholdAngle && widthRatio < thresholdWidthDifference)
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.
// A new polygon is created every time drawCurve is true.
QList<UBGraphicsPolygonItem*> newPolygons;
QList<strokePoint> newStrokePoints;
int i(0);
while (i < points.size()) {
bool drawCurve = false;
newStrokePoints << points[i];
// Limiting the size of the polygons, and creating new ones when there is a sharp angle between
// consecutive point helps with two issues:
// 1. When a polygon is transparent and it overlaps with itself, it is *sometimes* filled incorrectly.
// 2. This way of simplifying strokes resuls in sharp, rather than rounded, corners when there is a sharp angle
// in the stroke
if (newStrokePoints.size() > 1 && i < points.size() - 1) {
qreal angle = qFabs(UBGeometryUtils::angle(points[i-1].first, points[i].first, points[i+1].first));
if (angle < 150) // arbitrary threshold, change if necessary
drawCurve = true;
}
if (hasAlpha() && 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())); // useful for debugging
// 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;
}