1 /*
2  * Copyright (c) 2008 Cyrille Berger <cberger@cberger.net>
3  * Copyright (c) 2010 Geoffry Song <goffrie@gmail.com>
4  * Copyright (c) 2017 Scott Petrovic <scottpetrovic@gmail.com>
5  *
6  *  This library is free software; you can redistribute it and/or modify
7  *  it under the terms of the GNU Lesser General Public License as published by
8  *  the Free Software Foundation; version 2 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This library is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU Lesser General Public License for more details.
15  *
16  *  You should have received a copy of the GNU Lesser General Public License
17  *  along with this program; if not, write to the Free Software
18  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19  */
20 
21 #include "PerspectiveAssistant.h"
22 
23 #include "kis_debug.h"
24 #include <klocalizedstring.h>
25 
26 #include <QPainter>
27 #include <QPainterPath>
28 #include <QLinearGradient>
29 #include <QTransform>
30 
31 #include <kis_canvas2.h>
32 #include <kis_coordinates_converter.h>
33 #include <kis_algebra_2d.h>
34 
35 #include <math.h>
36 #include <limits>
37 
PerspectiveAssistant(QObject * parent)38 PerspectiveAssistant::PerspectiveAssistant(QObject *parent)
39     : KisAbstractPerspectiveGrid(parent)
40     , KisPaintingAssistant("perspective", i18n("Perspective assistant"))
41     , m_followBrushPosition(false)
42     , m_adjustedPositionValid(false)
43 {
44 }
45 
PerspectiveAssistant(const PerspectiveAssistant & rhs,QMap<KisPaintingAssistantHandleSP,KisPaintingAssistantHandleSP> & handleMap)46 PerspectiveAssistant::PerspectiveAssistant(const PerspectiveAssistant &rhs, QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap)
47     : KisAbstractPerspectiveGrid(rhs.parent())
48     , KisPaintingAssistant(rhs, handleMap)
49     , m_snapLine(rhs.m_snapLine)
50     , m_cachedTransform(rhs.m_cachedTransform)
51     , m_cachedPolygon(rhs.m_cachedPolygon)
52     , m_cacheValid(rhs.m_cacheValid)
53     , m_followBrushPosition(rhs.m_followBrushPosition)
54     , m_adjustedPositionValid(rhs.m_adjustedPositionValid)
55     , m_adjustedBrushPosition(rhs.m_adjustedBrushPosition)
56 {
57     for (int i = 0; i < 4; ++i) {
58         m_cachedPoints[i] = rhs.m_cachedPoints[i];
59     }
60 }
61 
clone(QMap<KisPaintingAssistantHandleSP,KisPaintingAssistantHandleSP> & handleMap) const62 KisPaintingAssistantSP PerspectiveAssistant::clone(QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) const
63 {
64     return KisPaintingAssistantSP(new PerspectiveAssistant(*this, handleMap));
65 }
66 // squared distance from a point to a line
distsqr(const QPointF & pt,const QLineF & line)67 inline qreal distsqr(const QPointF& pt, const QLineF& line)
68 {
69     // distance = |(p2 - p1) x (p1 - pt)| / |p2 - p1|
70 
71     // magnitude of (p2 - p1) x (p1 - pt)
72     const qreal cross = (line.dx() * (line.y1() - pt.y()) - line.dy() * (line.x1() - pt.x()));
73 
74     return cross * cross / (line.dx() * line.dx() + line.dy() * line.dy());
75 }
76 
setAdjustedBrushPosition(const QPointF position)77 void PerspectiveAssistant::setAdjustedBrushPosition(const QPointF position)
78 {
79     m_adjustedBrushPosition = position;
80     m_adjustedPositionValid = true;
81 }
82 
setFollowBrushPosition(bool follow)83 void PerspectiveAssistant::setFollowBrushPosition(bool follow)
84 {
85     m_followBrushPosition = follow;
86 }
87 
project(const QPointF & pt,const QPointF & strokeBegin)88 QPointF PerspectiveAssistant::project(const QPointF& pt, const QPointF& strokeBegin)
89 {
90     const static QPointF nullPoint(std::numeric_limits<qreal>::quiet_NaN(), std::numeric_limits<qreal>::quiet_NaN());
91 
92     Q_ASSERT(isAssistantComplete());
93 
94     if (m_snapLine.isNull()) {
95         QPolygonF poly;
96         QTransform transform;
97 
98         if (!getTransform(poly, transform)) {
99             return nullPoint;
100         }
101 
102         if (!poly.containsPoint(strokeBegin, Qt::OddEvenFill)) {
103             return nullPoint; // avoid problems with multiple assistants: only snap if starting in the grid
104         }
105 
106 
107         const qreal dx = pt.x() - strokeBegin.x();
108         const qreal dy = pt.y() - strokeBegin.y();
109 
110         if (dx * dx + dy * dy < 4.0) {
111             return strokeBegin; // allow some movement before snapping
112         }
113 
114         // construct transformation
115         bool invertible;
116         const QTransform inverse = transform.inverted(&invertible);
117         if (!invertible) {
118             return nullPoint; // shouldn't happen
119         }
120 
121 
122         // figure out which direction to go
123         const QPointF start = inverse.map(strokeBegin);
124         const QLineF verticalLine = QLineF(strokeBegin, transform.map(start + QPointF(0, 1)));
125         const QLineF horizontalLine = QLineF(strokeBegin, transform.map(start + QPointF(1, 0)));
126 
127         // determine whether the horizontal or vertical line is closer to the point
128         m_snapLine = distsqr(pt, verticalLine) < distsqr(pt, horizontalLine) ? verticalLine : horizontalLine;
129     }
130 
131     // snap to line
132     const qreal
133             dx = m_snapLine.dx(),
134             dy = m_snapLine.dy(),
135             dx2 = dx * dx,
136             dy2 = dy * dy,
137             invsqrlen = 1.0 / (dx2 + dy2);
138     QPointF r(dx2 * pt.x() + dy2 * m_snapLine.x1() + dx * dy * (pt.y() - m_snapLine.y1()),
139               dx2 * m_snapLine.y1() + dy2 * pt.y() + dx * dy * (pt.x() - m_snapLine.x1()));
140 
141     r *= invsqrlen;
142     return r;
143 }
144 
adjustPosition(const QPointF & pt,const QPointF & strokeBegin)145 QPointF PerspectiveAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin)
146 {
147     return project(pt, strokeBegin);
148 }
149 
endStroke()150 void PerspectiveAssistant::endStroke()
151 {
152     // Brush stroke ended, guides should follow the brush position again.
153     m_followBrushPosition = false;
154     m_adjustedPositionValid = false;
155 
156     m_snapLine = QLineF();
157 }
158 
contains(const QPointF & pt) const159 bool PerspectiveAssistant::contains(const QPointF& pt) const
160 {
161     QPolygonF poly;
162     if (!quad(poly)) return false;
163     return poly.containsPoint(pt, Qt::OddEvenFill);
164 }
165 
lengthSquared(const QPointF & vector)166 inline qreal lengthSquared(const QPointF& vector)
167 {
168     return vector.x() * vector.x() + vector.y() * vector.y();
169 }
170 
localScale(const QTransform & transform,QPointF pt)171 inline qreal localScale(const QTransform& transform, QPointF pt)
172 {
173     //    const qreal epsilon = 1e-5, epsilonSquared = epsilon * epsilon;
174     //    qreal xSizeSquared = lengthSquared(transform.map(pt + QPointF(epsilon, 0.0)) - orig) / epsilonSquared;
175     //    qreal ySizeSquared = lengthSquared(transform.map(pt + QPointF(0.0, epsilon)) - orig) / epsilonSquared;
176     //    xSizeSquared /= lengthSquared(transform.map(QPointF(0.0, pt.y())) - transform.map(QPointF(1.0, pt.y())));
177     //    ySizeSquared /= lengthSquared(transform.map(QPointF(pt.x(), 0.0)) - transform.map(QPointF(pt.x(), 1.0)));
178     //  when taking the limit epsilon->0:
179     //  xSizeSquared=((m23*y+m33)^2*(m23*y+m33+m13)^2)/(m23*y+m13*x+m33)^4
180     //  ySizeSquared=((m23*y+m33)^2*(m23*y+m33+m13)^2)/(m23*y+m13*x+m33)^4
181     //  xSize*ySize=(abs(m13*x+m33)*abs(m13*x+m33+m23)*abs(m23*y+m33)*abs(m23*y+m33+m13))/(m23*y+m13*x+m33)^4
182     const qreal x = transform.m13() * pt.x(),
183             y = transform.m23() * pt.y(),
184             a = x + transform.m33(),
185             b = y + transform.m33(),
186             c = x + y + transform.m33(),
187             d = c * c;
188     return fabs(a*(a + transform.m23())*b*(b + transform.m13()))/(d * d);
189 }
190 
191 // returns the reciprocal of the maximum local scale at the points (0,0),(0,1),(1,0),(1,1)
inverseMaxLocalScale(const QTransform & transform)192 inline qreal inverseMaxLocalScale(const QTransform& transform)
193 {
194     const qreal a = fabs((transform.m33() + transform.m13()) * (transform.m33() + transform.m23())),
195             b = fabs((transform.m33()) * (transform.m13() + transform.m33() + transform.m23())),
196             d00 = transform.m33() * transform.m33(),
197             d11 = (transform.m33() + transform.m23() + transform.m13())*(transform.m33() + transform.m23() + transform.m13()),
198             s0011 = qMin(d00, d11) / a,
199             d10 = (transform.m33() + transform.m13()) * (transform.m33() + transform.m13()),
200             d01 = (transform.m33() + transform.m23()) * (transform.m33() + transform.m23()),
201             s1001 = qMin(d10, d01) / b;
202     return qMin(s0011, s1001);
203 }
204 
distance(const QPointF & pt) const205 qreal PerspectiveAssistant::distance(const QPointF& pt) const
206 {
207     QPolygonF poly;
208     QTransform transform;
209 
210     if (!getTransform(poly, transform)) {
211         return 1.0;
212     }
213 
214     bool invertible;
215     QTransform inverse = transform.inverted(&invertible);
216 
217     if (!invertible) {
218         return 1.0;
219     }
220 
221     if (inverse.m13() * pt.x() + inverse.m23() * pt.y() + inverse.m33() == 0.0) {
222         return 0.0; // point at infinity
223     }
224 
225     return localScale(transform, inverse.map(pt)) * inverseMaxLocalScale(transform);
226 }
227 
228 // draw a vanishing point marker
drawX(const QPointF & pt)229 inline QPainterPath drawX(const QPointF& pt)
230 {
231     QPainterPath path;
232     path.moveTo(QPointF(pt.x() - 5.0, pt.y() - 5.0)); path.lineTo(QPointF(pt.x() + 5.0, pt.y() + 5.0));
233     path.moveTo(QPointF(pt.x() - 5.0, pt.y() + 5.0)); path.lineTo(QPointF(pt.x() + 5.0, pt.y() - 5.0));
234     return path;
235 }
236 
drawAssistant(QPainter & gc,const QRectF & updateRect,const KisCoordinatesConverter * converter,bool cached,KisCanvas2 * canvas,bool assistantVisible,bool previewVisible)237 void PerspectiveAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible)
238 {
239     gc.save();
240     gc.resetTransform();
241     QTransform initialTransform = converter->documentToWidgetTransform();
242     //QTransform reverseTransform = converter->widgetToDocument();
243     QPolygonF poly;
244     QTransform transform; // unused, but computed for caching purposes
245     if (getTransform(poly, transform) && assistantVisible==true) {
246         // draw vanishing points
247         QPointF intersection(0, 0);
248         if (fmod(QLineF(poly[0], poly[1]).angle(), 180.0)>=fmod(QLineF(poly[2], poly[3]).angle(), 180.0)+2.0 || fmod(QLineF(poly[0], poly[1]).angle(), 180.0)<=fmod(QLineF(poly[2], poly[3]).angle(), 180.0)-2.0) {
249             if (QLineF(poly[0], poly[1]).intersect(QLineF(poly[2], poly[3]), &intersection) != QLineF::NoIntersection) {
250                 drawPath(gc, drawX(initialTransform.map(intersection)));
251             }
252         }
253         if (fmod(QLineF(poly[1], poly[2]).angle(), 180.0)>=fmod(QLineF(poly[3], poly[0]).angle(), 180.0)+2.0 || fmod(QLineF(poly[1], poly[2]).angle(), 180.0)<=fmod(QLineF(poly[3], poly[0]).angle(), 180.0)-2.0){
254             if (QLineF(poly[1], poly[2]).intersect(QLineF(poly[3], poly[0]), &intersection) != QLineF::NoIntersection) {
255                 drawPath(gc, drawX(initialTransform.map(intersection)));
256             }
257         }
258     }
259 
260     if (isSnappingActive() && getTransform(poly, transform) && previewVisible==true){
261         //find vanishing point, find mouse, draw line between both.
262         QPainterPath path2;
263         QPointF intersection(0, 0);//this is the position of the vanishing point.
264         QPointF mousePos(0,0);
265         QLineF snapLine;
266         QRect viewport= gc.viewport();
267         QRect bounds;
268 
269         if (canvas){
270             //simplest, cheapest way to get the mouse-position
271             mousePos= canvas->canvasWidget()->mapFromGlobal(QCursor::pos());
272         }
273         else {
274             //...of course, you need to have access to a canvas-widget for that.
275             mousePos = QCursor::pos(); // this'll give an offset
276             dbgFile<<"canvas does not exist, you may have passed arguments incorrectly:"<<canvas;
277         }
278 
279         if (m_followBrushPosition && m_adjustedPositionValid) {
280             mousePos = initialTransform.map(m_adjustedBrushPosition);
281         }
282 
283         //figure out if point is in the perspective grid
284         QPointF intersectTransformed(0, 0); // dummy for holding transformed intersection so the code is more readable.
285 
286         if (poly.containsPoint(initialTransform.inverted().map(mousePos), Qt::OddEvenFill)==true){
287             // check if the lines aren't parallel to each other to avoid calculation errors in the intersection calculation (bug 345754)//
288             if (fmod(QLineF(poly[0], poly[1]).angle(), 180.0)>=fmod(QLineF(poly[2], poly[3]).angle(), 180.0)+2.0 || fmod(QLineF(poly[0], poly[1]).angle(), 180.0)<=fmod(QLineF(poly[2], poly[3]).angle(), 180.0)-2.0) {
289                 if (QLineF(poly[0], poly[1]).intersect(QLineF(poly[2], poly[3]), &intersection) != QLineF::NoIntersection) {
290                     intersectTransformed = initialTransform.map(intersection);
291                     snapLine = QLineF(intersectTransformed, mousePos);
292                     KisAlgebra2D::intersectLineRect(snapLine, viewport);
293                     bounds= QRect(snapLine.p1().toPoint(), snapLine.p2().toPoint());
294                     QPainterPath path;
295 
296                     if (bounds.contains(intersectTransformed.toPoint())){
297                         path2.moveTo(intersectTransformed);
298                         path2.lineTo(snapLine.p1());
299                     }
300                     else {
301                         path2.moveTo(snapLine.p1());
302                         path2.lineTo(snapLine.p2());
303                     }
304                 }
305             }
306             if (fmod(QLineF(poly[1], poly[2]).angle(), 180.0)>=fmod(QLineF(poly[3], poly[0]).angle(), 180.0)+2.0 || fmod(QLineF(poly[1], poly[2]).angle(), 180.0)<=fmod(QLineF(poly[3], poly[0]).angle(), 180.0)-2.0){
307                 if (QLineF(poly[1], poly[2]).intersect(QLineF(poly[3], poly[0]), &intersection) != QLineF::NoIntersection) {
308                     intersectTransformed = initialTransform.map(intersection);
309                     snapLine = QLineF(intersectTransformed, mousePos);
310                     KisAlgebra2D::intersectLineRect(snapLine, viewport);
311                     bounds= QRect(snapLine.p1().toPoint(), snapLine.p2().toPoint());
312                     QPainterPath path;
313 
314                     if (bounds.contains(intersectTransformed.toPoint())){
315                         path2.moveTo(intersectTransformed);
316                         path2.lineTo(snapLine.p1());
317                     }
318                     else {
319                         path2.moveTo(snapLine.p1());
320                         path2.lineTo(snapLine.p2());
321                     }
322                 }
323             }
324             drawPreview(gc, path2);
325         }
326     }
327 
328     gc.restore();
329 
330     KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached,canvas, assistantVisible, previewVisible);
331 }
332 
drawCache(QPainter & gc,const KisCoordinatesConverter * converter,bool assistantVisible)333 void PerspectiveAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible)
334 {
335     if (assistantVisible == false) {
336         return;
337     }
338 
339     gc.setTransform(converter->documentToWidgetTransform());
340     QPolygonF poly;
341     QTransform transform;
342 
343     if (!getTransform(poly, transform)) {
344         // color red for an invalid transform, but not for an incomplete one
345         if(isAssistantComplete()) {
346             gc.setPen(QColor(255, 0, 0, 125));
347             gc.drawPolygon(poly);
348         } else {
349             QPainterPath path;
350             path.addPolygon(poly);
351             drawPath(gc, path, isSnappingActive());
352         }
353     } else {
354         gc.setPen(QColor(0, 0, 0, 125));
355         gc.setTransform(transform, true);
356         QPainterPath path;
357         for (int y = 0; y <= 8; ++y)
358         {
359             path.moveTo(QPointF(0.0, y * 0.125));
360             path.lineTo(QPointF(1.0, y * 0.125));
361         }
362         for (int x = 0; x <= 8; ++x)
363         {
364             path.moveTo(QPointF(x * 0.125, 0.0));
365             path.lineTo(QPointF(x * 0.125, 1.0));
366         }
367         drawPath(gc, path, isSnappingActive());
368     }
369 
370 }
371 
getEditorPosition() const372 QPointF PerspectiveAssistant::getEditorPosition() const
373 {
374     QPointF centroid(0, 0);
375     for (int i = 0; i < 4; ++i) {
376         centroid += *handles()[i];
377     }
378 
379     return centroid * 0.25;
380 }
381 
sign(T a)382 template <typename T> int sign(T a)
383 {
384     return (a > 0) - (a < 0);
385 }
386 // perpendicular dot product
pdot(const QPointF & a,const QPointF & b)387 inline qreal pdot(const QPointF& a, const QPointF& b)
388 {
389     return a.x() * b.y() - a.y() * b.x();
390 }
391 
quad(QPolygonF & poly) const392 bool PerspectiveAssistant::quad(QPolygonF& poly) const
393 {
394     for (int i = 0; i < handles().size(); ++i) {
395         poly.push_back(*handles()[i]);
396     }
397 
398     if (!isAssistantComplete()) {
399         return false;
400     }
401 
402     int sum = 0;
403     int signs[4];
404 
405     for (int i = 0; i < 4; ++i) {
406         int j = (i == 3) ? 0 : (i + 1);
407         int k = (j == 3) ? 0 : (j + 1);
408         signs[i] = sign(pdot(poly[j] - poly[i], poly[k] - poly[j]));
409         sum += signs[i];
410     }
411 
412     if (sum == 0) {
413         // complex (crossed)
414         for (int i = 0; i < 4; ++i) {
415             int j = (i == 3) ? 0 : (i + 1);
416             if (signs[i] * signs[j] == -1) {
417                 // opposite signs: uncross
418                 std::swap(poly[i], poly[j]);
419                 return true;
420             }
421         }
422         // okay, maybe it's just a line
423         return false;
424     } else if (sum != 4 && sum != -4) {
425         // concave, or a triangle
426         if (sum == 2 || sum == -2) {
427             // concave, let's return a triangle instead
428             for (int i = 0; i < 4; ++i) {
429                 int j = (i == 3) ? 0 : (i + 1);
430                 if (signs[i] != sign(sum)) {
431                     // wrong sign: drop the inside node
432                     poly.remove(j);
433                     return false;
434                 }
435             }
436         }
437         return false;
438     }
439     // convex
440     return true;
441 }
442 
getTransform(QPolygonF & poly,QTransform & transform) const443 bool PerspectiveAssistant::getTransform(QPolygonF& poly, QTransform& transform) const
444 {
445     if (m_cachedPolygon.size() != 0 && isAssistantComplete()) {
446         for (int i = 0; i <= 4; ++i) {
447             if (i == 4) {
448                 poly = m_cachedPolygon;
449                 transform = m_cachedTransform;
450                 return m_cacheValid;
451             }
452             if (m_cachedPoints[i] != *handles()[i]) break;
453         }
454     }
455 
456     m_cachedPolygon.clear();
457     m_cacheValid = false;
458 
459     if (!quad(poly)) {
460         m_cachedPolygon = poly;
461         return false;
462     }
463 
464     if (!QTransform::squareToQuad(poly, transform)) {
465         qWarning("Failed to create perspective mapping");
466         return false;
467     }
468 
469     for (int i = 0; i < 4; ++i) {
470         m_cachedPoints[i] = *handles()[i];
471     }
472 
473     m_cachedPolygon = poly;
474     m_cachedTransform = transform;
475     m_cacheValid = true;
476     return true;
477 }
478 
isAssistantComplete() const479 bool PerspectiveAssistant::isAssistantComplete() const
480 {
481     return handles().size() >= 4; // specify 4 corners to make assistant complete
482 }
483 
484 
485 
PerspectiveAssistantFactory()486 PerspectiveAssistantFactory::PerspectiveAssistantFactory()
487 {
488 }
489 
~PerspectiveAssistantFactory()490 PerspectiveAssistantFactory::~PerspectiveAssistantFactory()
491 {
492 }
493 
id() const494 QString PerspectiveAssistantFactory::id() const
495 {
496     return "perspective";
497 }
498 
name() const499 QString PerspectiveAssistantFactory::name() const
500 {
501     return i18n("Perspective");
502 }
503 
createPaintingAssistant() const504 KisPaintingAssistant* PerspectiveAssistantFactory::createPaintingAssistant() const
505 {
506     return new PerspectiveAssistant;
507 }
508