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