UBGraphicsStroke.cpp 10.6 KB
Newer Older
Claudio Valerio's avatar
Claudio Valerio committed
1
/*
2
 * Copyright (C) 2015-2018 Département de l'Instruction Publique (DIP-SEM)
Craig Watson's avatar
Craig Watson committed
3
 *
Claudio Valerio's avatar
Claudio Valerio committed
4
 * Copyright (C) 2013 Open Education Foundation
Claudio Valerio's avatar
Claudio Valerio committed
5
 *
Claudio Valerio's avatar
Claudio Valerio committed
6 7
 * Copyright (C) 2010-2013 Groupement d'Intérêt Public pour
 * l'Education Numérique en Afrique (GIP ENA)
8
 *
Claudio Valerio's avatar
Claudio Valerio committed
9 10 11
 * This file is part of OpenBoard.
 *
 * OpenBoard is free software: you can redistribute it and/or modify
Claudio Valerio's avatar
Claudio Valerio committed
12 13
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License,
14 15 16 17
 * 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).
 *
Claudio Valerio's avatar
Claudio Valerio committed
18
 * OpenBoard is distributed in the hope that it will be useful,
Claudio Valerio's avatar
Claudio Valerio committed
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
Claudio Valerio's avatar
Claudio Valerio committed
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Claudio Valerio's avatar
Claudio Valerio committed
21
 * GNU General Public License for more details.
Claudio Valerio's avatar
Claudio Valerio committed
22
 *
Claudio Valerio's avatar
Claudio Valerio committed
23
 * You should have received a copy of the GNU General Public License
Claudio Valerio's avatar
Claudio Valerio committed
24
 * along with OpenBoard. If not, see <http://www.gnu.org/licenses/>.
Claudio Valerio's avatar
Claudio Valerio committed
25 26
 */

27

Claudio Valerio's avatar
Claudio Valerio committed
28

Claudio Valerio's avatar
Claudio Valerio committed
29

Claudio Valerio's avatar
Claudio Valerio committed
30 31 32 33
#include "UBGraphicsStroke.h"

#include "UBGraphicsPolygonItem.h"

34 35
#include "board/UBBoardController.h"
#include "core/UBApplication.h"
36
#include "core/memcheck.h"
37
#include "domain/UBGraphicsScene.h"
Claudio Valerio's avatar
Claudio Valerio committed
38

39
#include "frameworks/UBGeometryUtils.h"
40

41 42 43 44 45

typedef QPair<QPointF, qreal> strokePoint;

UBGraphicsStroke::UBGraphicsStroke(UBGraphicsScene *scene)
    :mScene(scene)
Claudio Valerio's avatar
Claudio Valerio committed
46
{
47
    mAntiScaleRatio = 1./(UBApplication::boardController->systemScaleFactor() * UBApplication::boardController->currentZoom());
Claudio Valerio's avatar
Claudio Valerio committed
48 49 50 51 52
}


UBGraphicsStroke::~UBGraphicsStroke()
{
53 54 55 56
    foreach(UBGraphicsPolygonItem* poly, mPolygons)
        poly->setStroke(NULL);

    mPolygons.clear();
Claudio Valerio's avatar
Claudio Valerio committed
57 58 59 60 61
}


void UBGraphicsStroke::addPolygon(UBGraphicsPolygonItem* pol)
{
62
    remove(pol);
Claudio Valerio's avatar
Claudio Valerio committed
63 64 65
    mPolygons << pol;
}

66 67 68 69 70 71
void UBGraphicsStroke::remove(UBGraphicsPolygonItem* polygonItem)
{
    int n = mPolygons.indexOf(polygonItem);
    if (n>=0)
        mPolygons.removeAt(n);
}
Claudio Valerio's avatar
Claudio Valerio committed
72 73 74 75 76 77

QList<UBGraphicsPolygonItem*> UBGraphicsStroke::polygons() const
{
    return mPolygons;
}

78 79
/**
 * @brief Add a point to the curve, interpolating extra points if required
80 81 82 83 84 85 86 87 88
 * @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.
 *
89
 */
90
QList<QPair<QPointF, qreal> > UBGraphicsStroke::addPoint(const QPointF& point, qreal width, bool interpolate)
91
{
92 93
    strokePoint newPoint(point, width);

94
    int n = mReceivedPoints.size();
95

96
    if (n == 0) {
97 98
        mReceivedPoints << newPoint;
        mDrawnPoints << newPoint;
99
        return QList<strokePoint>() << newPoint;
100 101
    }

102
    if (!interpolate) {
103 104 105 106
        strokePoint lastPoint = mReceivedPoints.last();
        mReceivedPoints << newPoint;
        mDrawnPoints << newPoint;
        return QList<strokePoint>() << lastPoint << newPoint;
107 108
    }

109
    else {
110
        // The curve we are interpolating is not between two drawn points;
111 112
        // it is between the midway points of the second-to-last and last point, and last and current point.

113

114
        // The first segment is just a straight line to the first midway point
115
        if (n == 1) {
116 117 118 119 120
            QPointF lastPoint = mReceivedPoints[0].first;
            qreal lastWidth = mReceivedPoints[0].second;
            strokePoint p(((lastPoint+point)/2.0), (lastWidth+width)/2.0);
            mReceivedPoints << newPoint;
            mDrawnPoints << p;
121

122
            return QList<strokePoint>() << mReceivedPoints[0] << p;
123 124
        }

125 126
        QPointF p0 = mReceivedPoints[mReceivedPoints.size() - 2].first;
        QPointF p1 = mReceivedPoints[mReceivedPoints.size() - 1].first;
127 128 129 130 131
        QPointF p2 = point;

        QPointF startPoint = (p1+p0)/2.0;
        QPointF endPoint = (p2+p1)/2.0;

132
        QList<QPointF> calculated = UBGeometryUtils::quadraticBezier(startPoint, p1, endPoint, 10);
133 134 135 136 137 138 139 140
        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);
        }
141

142
        // avoid adding duplicates
143
        if (newPoints.first().first == mDrawnPoints.last().first)
144 145
            mDrawnPoints.removeLast();

146
        foreach(strokePoint p, newPoints)
147
            mDrawnPoints << p;
148

149
        mReceivedPoints << strokePoint(point, width);
150 151 152
        return newPoints;
    }

153
    return QList<strokePoint>();
154
}
Claudio Valerio's avatar
Claudio Valerio committed
155 156 157 158 159 160 161 162 163 164 165 166

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;
        }
167
        return false;
Claudio Valerio's avatar
Claudio Valerio committed
168
    }
169 170
    else
        return true;
Claudio Valerio's avatar
Claudio Valerio committed
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
}


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;
    }
}

193 194 195 196 197 198
void UBGraphicsStroke::clear()
{
    if(!mPolygons.empty()){
        mPolygons.clear();
    }
}
199 200 201 202 203 204 205 206 207 208 209 210 211 212

/**
 * @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;
213
    //qDebug() << "Simplifying. Before: " << points.size() << " points and " << polygons().size() << " polygons";
214 215 216 217 218 219 220 221 222

    /* 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.
     *
223 224
     * 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.
225 226 227 228 229
     *
     * 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
230 231 232 233
    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();
234 235 236 237 238

    QList<strokePoint>::iterator it = points.begin();
    QList<QList<strokePoint>::iterator> toDelete;

    while (it+2 != points.end()) {
239
        // it, b_it and (b_it+1) correspond to A, B and C respectively
240 241 242
        QList<strokePoint>::iterator b_it(it+1);

        while (b_it+1 != points.end()) {
243
            qreal angle = qFabs(180-(UBGeometryUtils::angle(it->first, b_it->first, (b_it+1)->first)));
244
            qreal widthRatio = qMax(it->second, b_it->second)/qMin(it->second, b_it->second);
245

246
            if (angle < thresholdAngle && widthRatio < thresholdWidthDifference)
247 248 249 250 251 252 253 254 255 256 257
                b_it = points.erase(b_it);
            else
                break;
        }

        if (b_it+1 == points.end())
            break;
        else
            it = b_it;
    }

258 259
    // 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.
260 261 262 263 264 265 266 267 268 269

    QList<UBGraphicsPolygonItem*> newPolygons;
    QList<strokePoint> newStrokePoints;
    int i(0);

    while (i < points.size()) {
        bool drawCurve = false;

        newStrokePoints << points[i];

270 271 272 273 274
        // 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
275

276 277 278
        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
279 280 281
                drawCurve = true;
        }

282 283
        if (hasAlpha() && newStrokePoints.size() % 20 == 0)
            drawCurve = true;
284 285 286

        if (drawCurve) {
            UBGraphicsPolygonItem* poly = mScene->polygonToPolygonItem(UBGeometryUtils::curveToPolygon(newStrokePoints, true, true));
287
            //poly->setColor(QColor(rand()%256, rand()%256, rand()%256, poly->brush().color().alpha())); // useful for debugging
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321

            // 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);
    }

322
    //qDebug() << "After: " << points.size() << " points and " << newStroke->polygons().size() << " polygons";
323 324 325

    return newStroke;
}