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