Krita Source Code Documentation
Loading...
Searching...
No Matches
TwoPointAssistant.cc
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2008 Cyrille Berger <cberger@cberger.net>
3 * SPDX-FileCopyrightText: 2010 Geoffry Song <goffrie@gmail.com>
4 * SPDX-FileCopyrightText: 2021 Nabil Maghfur Usman <nmaghfurusman@gmail.com>
5 *
6 * SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8
9#include "TwoPointAssistant.h"
10#include "kis_debug.h"
11#include <klocalizedstring.h>
12
13#include <QPainter>
14#include <QPainterPath>
15#include <QLinearGradient>
16#include <QTransform>
17
18#include <kis_canvas2.h>
20#include <kis_algebra_2d.h>
21#include <kis_dom_utils.h>
22#include <math.h>
23#include <QtCore/qmath.h>
24#include <kis_assert.h>
25
27 : KisPaintingAssistant("two point", i18n("Two point assistant"))
28{
29}
30
31TwoPointAssistant::TwoPointAssistant(const TwoPointAssistant &rhs, QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap)
32 : KisPaintingAssistant(rhs, handleMap)
33 , m_canvas(rhs.m_canvas)
34 , m_snapLine(rhs.m_snapLine)
35 , m_gridDensity(rhs.m_gridDensity)
36 , m_useVertical(rhs.m_useVertical)
37 , m_lastUsedPoint(rhs.m_lastUsedPoint)
38{
39}
40
41KisPaintingAssistantSP TwoPointAssistant::clone(QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) const
42{
43 return KisPaintingAssistantSP(new TwoPointAssistant(*this, handleMap));
44}
45
46QPointF TwoPointAssistant::project(const QPointF& point, const QPointF& strokeBegin, const bool snapToAny, qreal moveThreshold)
47{
48 Q_ASSERT(isAssistantComplete());
49
50 QPointF best_pt = point;
51 double best_dist = DBL_MAX;
52 QList<int> possibleHandles;
53
54 // must be above or equal to 0;
55 // if useVertical, then last used point must be below 3, because 2 means vertical
56 // and it's the last possible point here (sanity check)
57 // if !useVertical, then it must be below 2, because 2 means vertical
58 bool isLastUsedPointCorrectNow = m_lastUsedPoint >= 0 && (m_useVertical ? m_lastUsedPoint < 3 : m_lastUsedPoint < 2);
59
60 if (isLocal() && handles().size() == 5) {
61 // here we can just return since we don't want to do anything
62 // so we're returning a NaN
63 // but only if we don't have a point/axes it was already using
64
65 QRectF rect = getLocalRect();
66 bool insideLocalRect = rect.contains(point);
67 if (!insideLocalRect && (!isLastUsedPointCorrectNow || !m_hasBeenInsideLocalRect)) {
68 return QPointF(qQNaN(), qQNaN());
69 } else if (insideLocalRect) {
71 }
72 }
73
74 if (!isLastUsedPointCorrectNow && KisAlgebra2D::norm(point - strokeBegin) < moveThreshold) {
75 return strokeBegin;
76 }
77
78 if (!snapToAny && isLastUsedPointCorrectNow) {
79 possibleHandles = QList<int>({m_lastUsedPoint});
80 } else {
81 if (m_useVertical) {
82 possibleHandles = QList<int>({0, 1, 2});
83 } else {
84 possibleHandles = QList<int>({0, 1});
85 }
86 }
87
88 Q_FOREACH (int vpIndex, possibleHandles) {
89 QPointF vp = *handles()[vpIndex];
90 double dist = 0;
91 QPointF pt = QPointF();
92 QLineF snapLine = QLineF();
93
94 // TODO: Would be a good idea to generalize this whole routine
95 // in KisAlgebra2d, as it's all lifted from the vanishing
96 // point assistant and parallel ruler assistant, and by
97 // extension the perspective assistant...
98 qreal dx = point.x() - strokeBegin.x();
99 qreal dy = point.y() - strokeBegin.y();
100
101 if (vp != *handles()[2]) {
102 snapLine = QLineF(vp, strokeBegin);
103 } else {
104 QLineF vertical = QLineF(*handles()[0],*handles()[1]).normalVector();
105 snapLine = QLineF(vertical.p1(), vertical.p2());
106 QPointF translation = (vertical.p1()-strokeBegin)*-1.0;
107 snapLine = snapLine.translated(translation);
108 }
109
110 dx = snapLine.dx();
111 dy = snapLine.dy();
112
113 const qreal dx2 = dx * dx;
114 const qreal dy2 = dy * dy;
115 const qreal invsqrlen = 1.0 / (dx2 + dy2);
116
117 pt = QPointF(dx2 * point.x() + dy2 * snapLine.x1() + dx * dy * (point.y() - snapLine.y1()),
118 dx2 * snapLine.y1() + dy2 * point.y() + dx * dy * (point.x() - snapLine.x1()));
119
120 pt *= invsqrlen;
121 dist = qAbs(pt.x() - point.x()) + qAbs(pt.y() - point.y());
122
123 if (dist < best_dist) {
124 best_pt = pt;
125 best_dist = dist;
126 m_lastUsedPoint = vpIndex;
127 }
128 }
129
130 return best_pt;
131}
132
139
140QPointF TwoPointAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin, const bool snapToAny, qreal moveThresholdPt)
141{
142 return project(pt, strokeBegin, snapToAny, moveThresholdPt);
143}
144
145void TwoPointAssistant::adjustLine(QPointF &point, QPointF &strokeBegin)
146{
147 QPointF p = project(point, strokeBegin, true, 0.0);
148 point = p;
149}
150
151void TwoPointAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible)
152{
153 Q_UNUSED(updateRect);
154 Q_UNUSED(cached);
155 gc.save();
156 gc.resetTransform();
157
158 const QTransform initialTransform = converter->documentToWidgetTransform();
159 bool isEditing = false;
160 bool showLocal = isLocal() && handles().size() == 5;
161
162 if (canvas) {
163 isEditing = canvas->paintingAssistantsDecoration()->isEditingAssistants();
164 }
165
166 if (isEditing) {
167 Q_FOREACH (const QPointF* handle, handles()) {
168 QPointF h = initialTransform.map(*handle);
169 QRectF ellipse = QRectF(QPointF(h.x() -15, h.y() -15), QSizeF(30, 30));
170
171 QPainterPath pathCenter;
172 pathCenter.addEllipse(ellipse);
173 drawPath(gc, pathCenter, isSnappingActive());
174
175 // Draw circle to represent center of vision
176 if (handles().length() == 3 && handle == handles()[2]) {
177 const QLineF horizon = QLineF(*handles()[0],*handles()[1]);
178 QLineF normal = horizon.normalVector();
179 normal.translate(*handles()[2]-normal.p1());
180 QPointF cov = horizon.center();
181 normal.intersects(horizon,&cov);
182 const QPointF center = initialTransform.map(cov);
183 QRectF center_ellipse = QRectF(QPointF(center.x() -15, center.y() -15), QSizeF(30, 30));
184 QPainterPath pathCenter;
185 pathCenter.addEllipse(center_ellipse);
186 drawPath(gc, pathCenter, isSnappingActive());
187 }
188 }
189
190 if (handles().size() <= 2) {
191 QPainterPath path;
192 int tempDensity = m_gridDensity * 10; // the vanishing point density seems visibly more dense, hence let's make it less dense
193 QRect viewport = gc.viewport();
194
195 for (int i = 0; i < handles().size(); i++) {
196 const QPointF p = initialTransform.map(*handles()[i]);
197 for (int currentAngle=0; currentAngle <= 180; currentAngle = currentAngle + tempDensity) {
198
199 // determine the correct angle based on the iteration
200 float xPos = cos(currentAngle * M_PI / 180);
201 float yPos = sin(currentAngle * M_PI / 180);
202 float length = 100;
203 QPointF unit = QPointF(length*xPos, length*yPos);
204
205 // find point
206 QLineF snapLine = QLineF(p, p + unit);
207 if (KisAlgebra2D::intersectLineRect(snapLine, viewport, false)) {
208 // make a line from VP center to edge of canvas with that angle
209
210 path.moveTo(snapLine.p1());
211 path.lineTo(snapLine.p2());
212 }
213
214 QLineF snapLine2 = QLineF(p, p - unit);
215 if (KisAlgebra2D::intersectLineRect(snapLine2, viewport, false)) {
216 // make a line from VP center to edge of canvas with that angle
217
218 path.moveTo(snapLine2.p1());
219 path.lineTo(snapLine2.p2());
220 }
221
222
223 }
224
225 drawPreview(gc, path);//and we draw the preview.
226
227 }
228 }
229
230 }
231
232 if (handles().size() >= 2) {
233 QPointF mousePos = effectiveBrushPosition(converter, canvas);
234 const QPointF p1 = *handles()[0];
235 const QPointF p2 = *handles()[1];
236 const QRect viewport= gc.viewport();
237
238 const QPolygonF localPoly = (isLocal() && handles().size() == 5) ? initialTransform.map(QPolygonF(getLocalRect())) : QPolygonF();
239 const QPolygonF viewportAndLocalPoly = !localPoly.isEmpty() ? QPolygonF(QRectF(viewport)).intersected(localPoly) : QRectF(viewport);
240
241
242 QPainterPath path;
243 QPainterPath previewPath; // part of the preview, instead of the assistant itself
244
245 // draw the horizon
246 if (assistantVisible == true || isEditing == true) {
247 QLineF horizonLine = initialTransform.map(QLineF(p1,p2));
248 KisAlgebra2D::cropLineToConvexPolygon(horizonLine, viewportAndLocalPoly, true, true);
249 path.moveTo(horizonLine.p1());
250 path.lineTo(horizonLine.p2());
251 }
252
253 // draw the VP-->mousePos lines
254 if (isEditing == false && previewVisible == true && isSnappingActive() == true) {
255 // draw the line vp <-> mouse even outside of the local rectangle
256 // but only if the mouse pos is inside the rectangle
257 QLineF snapMouse1 = QLineF(initialTransform.map(p1), mousePos);
258 QLineF snapMouse2 = QLineF(initialTransform.map(p2), mousePos);
259 KisAlgebra2D::cropLineToConvexPolygon(snapMouse1, viewportAndLocalPoly, false, true);
260 KisAlgebra2D::cropLineToConvexPolygon(snapMouse2, viewportAndLocalPoly, false, true);
261 previewPath.moveTo(snapMouse1.p1());
262 previewPath.lineTo(snapMouse1.p2());
263 previewPath.moveTo(snapMouse2.p1());
264 previewPath.lineTo(snapMouse2.p2());
265 }
266
267 // draw the side handle bars
268 if (isEditing == true && !sideHandles().isEmpty()) {
269 path.moveTo(initialTransform.map(p1));
270 path.lineTo(initialTransform.map(*sideHandles()[0]));
271 path.lineTo(initialTransform.map(*sideHandles()[1]));
272 path.moveTo(initialTransform.map(p2));
273 path.lineTo(initialTransform.map(*sideHandles()[2]));
274 path.lineTo(initialTransform.map(*sideHandles()[3]));
275 path.moveTo(initialTransform.map(p1));
276 path.lineTo(initialTransform.map(*sideHandles()[4]));
277 path.lineTo(initialTransform.map(*sideHandles()[5]));
278 path.moveTo(initialTransform.map(p2));
279 path.lineTo(initialTransform.map(*sideHandles()[6]));
280 path.lineTo(initialTransform.map(*sideHandles()[7]));
281 }
282
283 // draw the local rectangle
284 if (showLocal && assistantVisible) {
285 QPointF p1 = *handles()[(int)LocalFirstHandle];
286 QPointF p3 = *handles()[(int)LocalSecondHandle];
287 QPointF p2 = QPointF(p1.x(), p3.y());
288 QPointF p4 = QPointF(p3.x(), p1.y());
289
290 path.moveTo(initialTransform.map(p1));
291
292 path.lineTo(initialTransform.map(p2));
293 path.lineTo(initialTransform.map(p3));
294 path.lineTo(initialTransform.map(p4));
295 path.lineTo(initialTransform.map(p1));
296 }
297
298
299 drawPreview(gc,previewPath);
300 drawPath(gc, path, isSnappingActive());
301
302 if (handles().size() >= 3 && isSnappingActive()) {
303 path = QPainterPath(); // clear
304 const QPointF p3 = *handles()[2];
305
306 qreal size = 0;
307 const QTransform t = localTransform(p1,p2,p3,&size);
308 const QTransform inv = t.inverted();
309 const QPointF vp_a = t.map(p1);
310 const QPointF vp_b = t.map(p2);
311
312 if ((vp_a.x() < 0 && vp_b.x() > 0) ||
313 (vp_a.x() > 0 && vp_b.x() < 0)) {
314 if (m_useVertical) {
315 // Draw vertical line, but only if the center is between both VPs
316 QLineF vertical = initialTransform.map(inv.map(QLineF::fromPolar(1,90)));
317 if (!isEditing) vertical.translate(mousePos - vertical.p1());
318 KisAlgebra2D::cropLineToConvexPolygon(vertical, viewportAndLocalPoly, true, true);
319 if (previewVisible) {
320 path.moveTo(vertical.p1());
321 path.lineTo(vertical.p2());
322 }
323
324 if (assistantVisible) {
325 // Display a notch to represent the center of vision
326 path.moveTo(initialTransform.map(inv.map(QPointF(0,vp_a.y()-10))));
327 path.lineTo(initialTransform.map(inv.map(QPointF(0,vp_a.y()+10))));
328 }
329 drawPreview(gc,path);
330 path = QPainterPath(); // clear
331 }
332 }
333
334 const QPointF upper = QPointF(0,vp_a.y() + size);
335 const QPointF lower = QPointF(0,vp_a.y() - size);
336
337 // Set up the fading effect for the grid lines
338 // Needed so the grid density doesn't look distracting
339 QColor color = effectiveAssistantColor();
340 QGradient fade = QLinearGradient(initialTransform.map(inv.map(upper)),
341 initialTransform.map(inv.map(lower)));
342 color.setAlphaF(0);
343 fade.setColorAt(0.4, effectiveAssistantColor());
344 fade.setColorAt(0.5, color);
345 fade.setColorAt(0.6, effectiveAssistantColor());
346 const QPen pen = gc.pen();
347 const QBrush new_brush = QBrush(fade);
348 int width = 1;
349 const QPen new_pen = QPen(new_brush, width, pen.style());
350 gc.setPen(new_pen);
351
352 const QList<QPointF> station_points = {upper, lower};
353 const QList<QPointF> vanishing_points = {vp_a, vp_b};
354
355 // Draw grid lines above and below the horizon
356 Q_FOREACH (const QPointF sp, station_points) {
357
358 // Draw grid lines towards each vanishing point
359 Q_FOREACH (const QPointF vp, vanishing_points) {
360
361 // Interval between each grid line, uses grid density specified by user
362 const qreal initial_angle = QLineF(sp, vp).angle();
363 const qreal interval = size*m_gridDensity / cos((initial_angle - 90) * M_PI/180);
364 const QPointF translation = QPointF(interval, 0);
365
366 // Draw grid lines originating from both the left and right of the central vertical line
367 Q_FOREACH (const int dir, QList<int>({-1, 1})) {
368
369 // Limit at 300 grid lines per direction, reasonable even for m_gridDensity=0.1;
370 for (int i = 0; i <= 300; i++) {
371 const QLineF gridline = QLineF(sp + translation * i * dir, vp);
372
373 // Don't bother drawing lines that are nearly parallel to horizon
374 const qreal angle = gridline.angle();
375 if (angle < 0.25 || angle > 359.75 || (angle < 180.25 && angle > 179.75)) {
376 break;
377 }
378
379 QLineF drawn_gridline = initialTransform.map(inv.map(gridline));
380 KisAlgebra2D::cropLineToConvexPolygon(drawn_gridline, viewportAndLocalPoly, true, false);
381
382 if (assistantVisible || isEditing == true) {
383 path.moveTo(drawn_gridline.p2());
384 path.lineTo(drawn_gridline.p1());
385 }
386 }
387 }
388 }
389 }
390 gc.drawPath(path);
391 }
392 }
393
394 gc.restore();
395 //KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible);
396}
397
398void TwoPointAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible)
399{
400 Q_UNUSED(gc);
401 Q_UNUSED(converter);
402 Q_UNUSED(assistantVisible);
403 if (!m_canvas || !isAssistantComplete()) {
404 return;
405 }
406
407 if (assistantVisible == false || m_canvas->paintingAssistantsDecoration()->isEditingAssistants()) {
408 return;
409 }
410}
411
413{
414 if (handles().size() > LocalFirstHandle) {
415 return handles().at(LocalFirstHandle);
416 } else {
417 return nullptr;
418 }
419}
420
422{
423 if (handles().size() > LocalSecondHandle) {
424 return handles().at(LocalSecondHandle);
425 } else {
426 return nullptr;
427 }
428}
429
431{
432 int centerOfVisionHandle = 2;
433 if (handles().size() > centerOfVisionHandle) {
434 return *handles().at(centerOfVisionHandle);
435 } else if (handles().size() > 0) {
437 return *handles().at(0);
438 } else {
439 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(false, QPointF(0, 0));
440 return QPointF(0, 0);
441 }
442}
443
445{
446 m_gridDensity = density;
447}
448
453
458
460{
461 return m_gridDensity;
462}
463
464QTransform TwoPointAssistant::localTransform(QPointF vp_a, QPointF vp_b, QPointF pt_c, qreal* size)
465{
466 QTransform t = QTransform();
467 t.rotate(QLineF(vp_a, vp_b).angle());
468 t.translate(-pt_c.x(),-pt_c.y());
469 const QLineF horizon = QLineF(t.map(vp_a), QPointF(t.map(vp_b).x(),t.map(vp_a).y()));
470 *size = sqrt(pow(horizon.length()/2.0,2) - pow(abs(horizon.center().x()),2));
471
472 return t;
473}
474
476{
477 return handles().size() >= numHandles();
478}
479
481{
482 return true;
483}
484
485void TwoPointAssistant::saveCustomXml(QXmlStreamWriter* xml)
486{
487 xml->writeStartElement("gridDensity");
488 xml->writeAttribute("value", KisDomUtils::toString( this->gridDensity()));
489 xml->writeEndElement();
490 xml->writeStartElement("useVertical");
491 xml->writeAttribute("value", KisDomUtils::toString( (int)this->useVertical()));
492 xml->writeEndElement();
493 xml->writeStartElement("isLocal");
494 xml->writeAttribute("value", KisDomUtils::toString( (int)this->isLocal()));
495 xml->writeEndElement();
496
497}
498
499bool TwoPointAssistant::loadCustomXml(QXmlStreamReader* xml)
500{
501 if (xml && xml->name() == "gridDensity") {
502 this->setGridDensity((float)KisDomUtils::toDouble(xml->attributes().value("value").toString()));
503 }
504 if (xml && xml->name() == "useVertical") {
505 this->setUseVertical((bool)KisDomUtils::toInt(xml->attributes().value("value").toString()));
506 }
507 if (xml && xml->name() == "isLocal") {
508 this->setLocal((bool)KisDomUtils::toInt(xml->attributes().value("value").toString()));
509 }
510 return true;
511}
512
516
520
522{
523 return "two point";
524}
525
527{
528 return i18n("2 Point Perspective");
529}
530
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
float value(const T *src, size_t ch)
const Params2D p
QPointF p2
QPointF p3
QPointF p1
KisPaintingAssistantsDecorationSP paintingAssistantsDecoration() const
QPointF effectiveBrushPosition(const KisCoordinatesConverter *converter, KisCanvas2 *canvas) const
Query the effective brush position to be used for preview lines. This is intended to be used for pain...
void drawPath(QPainter &painter, const QPainterPath &path, bool drawActive=true)
void drawPreview(QPainter &painter, const QPainterPath &path)
QRectF getLocalRect() const
getLocalRect The function deals with local handles not being topLeft and bottomRight gracefully and r...
const QList< KisPaintingAssistantHandleSP > & sideHandles() const
void setLocal(bool value)
setLocal
const QList< KisPaintingAssistantHandleSP > & handles() const
QString name() const override
QString id() const override
KisPaintingAssistant * createPaintingAssistant() const override
KisPaintingAssistantSP clone(QMap< KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP > &handleMap) const override
void drawAssistant(QPainter &gc, const QRectF &updateRect, const KisCoordinatesConverter *converter, bool cached=true, KisCanvas2 *canvas=nullptr, bool assistantVisible=true, bool previewVisible=true) override
void setGridDensity(double density)
void setUseVertical(bool value)
KisPaintingAssistantHandleSP secondLocalHandle() const override
secondLocalHandle Note: this doesn't guarantee it will be the bottomRight corner! For that,...
void drawCache(QPainter &gc, const KisCoordinatesConverter *converter, bool assistantVisible=true) override
performance layer where the graphics can be drawn from a cache instead of generated every render upda...
QTransform localTransform(QPointF vp_a, QPointF vp_b, QPointF pt_c, qreal *size)
void endStroke() override
void saveCustomXml(QXmlStreamWriter *xml) override
QPointF adjustPosition(const QPointF &point, const QPointF &strokeBegin, const bool snapToAny, qreal moveThresholdPt) override
bool loadCustomXml(QXmlStreamReader *xml) override
QPointF getDefaultEditorPosition() const override
QPointF project(const QPointF &pt, const QPointF &strokeBegin, const bool snapToAny, qreal moveThreshold)
void adjustLine(QPointF &point, QPointF &strokeBegin) override
bool canBeLocal() const override
canBeLocal
int numHandles() const override
bool isAssistantComplete() const override
KisPaintingAssistantHandleSP firstLocalHandle() const override
firstLocalHandle Note: this doesn't guarantee it will be the topleft corner! For that,...
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define M_PI
Definition kis_global.h:111
QSharedPointer< KisPaintingAssistant > KisPaintingAssistantSP
Definition kis_types.h:189
void cropLineToConvexPolygon(QLineF &line, const QPolygonF polygon, bool extendFirst, bool extendSecond)
qreal norm(const T &a)
bool intersectLineRect(QLineF &line, const QRect rect, bool extend)
double toDouble(const QString &str, bool *ok=nullptr)
int toInt(const QString &str, bool *ok=nullptr)
QString toString(const QString &value)