Krita Source Code Documentation
Loading...
Searching...
No Matches
PerspectiveAssistant.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: 2017 Scott Petrovic <scottpetrovic@gmail.com>
5 *
6 * SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8
10
11#include <kis_debug.h>
12#include <klocalizedstring.h>
13
14#include <QPainter>
15#include <QPainterPath>
16#include <QLinearGradient>
17#include <QTransform>
18
19#include <kis_algebra_2d.h>
20#include <kis_canvas2.h>
22#include <kis_dom_utils.h>
23
25
26#include <math.h>
27#include <limits>
28
31 , KisPaintingAssistant("perspective", i18n("Perspective assistant"))
32{
33}
34
35PerspectiveAssistant::PerspectiveAssistant(const PerspectiveAssistant &rhs, QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap)
36 : KisAbstractPerspectiveGrid(rhs.parent())
37 , KisPaintingAssistant(rhs, handleMap)
38 , m_subdivisions(rhs.m_subdivisions)
39 , m_snapLine(rhs.m_snapLine)
40 , m_cachedTransform(rhs.m_cachedTransform)
41 , m_cachedPolygon(rhs.m_cachedPolygon)
42 , m_cacheValid(rhs.m_cacheValid)
43 , m_cache(rhs.m_cache)
44{
45 for (int i = 0; i < 4; ++i) {
47 }
48}
49
50KisPaintingAssistantSP PerspectiveAssistant::clone(QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) const
51{
52 return KisPaintingAssistantSP(new PerspectiveAssistant(*this, handleMap));
53}
54
55QPointF PerspectiveAssistant::project(const QPointF& pt, const QPointF& strokeBegin, const bool snapToAnyDirection, qreal moveThresholdPt)
56{
57 const static QPointF nullPoint(std::numeric_limits<qreal>::quiet_NaN(), std::numeric_limits<qreal>::quiet_NaN());
58
59 Q_ASSERT(isAssistantComplete());
60
61 if (snapToAnyDirection || m_snapLine.isNull()) {
62 QPolygonF poly;
63 QTransform transform;
64
65 if (!getTransform(poly, transform)) {
66 return nullPoint;
67 }
68
69 if (!poly.containsPoint(strokeBegin, Qt::OddEvenFill)) {
70 return nullPoint; // avoid problems with multiple assistants: only snap if starting in the grid
71 }
72
73 if (KisAlgebra2D::norm(pt - strokeBegin) < moveThresholdPt) {
74 return strokeBegin; // allow some movement before snapping
75 }
76
77 // construct transformation
78 bool invertible;
79 const QTransform inverse = transform.inverted(&invertible);
80 if (!invertible) {
81 return nullPoint; // shouldn't happen
82 }
83
84
85 // figure out which direction to go
86 const QPointF start = inverse.map(strokeBegin);
87 const QLineF verticalLine = QLineF(strokeBegin, transform.map(start + QPointF(0, 1)));
88 const QLineF horizontalLine = QLineF(strokeBegin, transform.map(start + QPointF(1, 0)));
89
90 // determine whether the horizontal or vertical line is closer to the point
91 m_snapLine = KisAlgebra2D::pointToLineDistSquared(pt, verticalLine) < KisAlgebra2D::pointToLineDistSquared(pt, horizontalLine) ? verticalLine : horizontalLine;
92 }
93
94 // snap to line
95 const qreal
96 dx = m_snapLine.dx(),
97 dy = m_snapLine.dy(),
98 dx2 = dx * dx,
99 dy2 = dy * dy,
100 invsqrlen = 1.0 / (dx2 + dy2);
101 QPointF r(dx2 * pt.x() + dy2 * m_snapLine.x1() + dx * dy * (pt.y() - m_snapLine.y1()),
102 dx2 * m_snapLine.y1() + dy2 * pt.y() + dx * dy * (pt.x() - m_snapLine.x1()));
103
104 r *= invsqrlen;
105 return r;
106}
107
108QPointF PerspectiveAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin, const bool snapToAny, qreal moveThresholdPt)
109{
110 return project(pt, strokeBegin, snapToAny, moveThresholdPt);
111}
112
113void PerspectiveAssistant::adjustLine(QPointF &point, QPointF &strokeBegin)
114{
115 point = project(point, strokeBegin, true, 0.0);
116}
117
123
124bool PerspectiveAssistant::contains(const QPointF& pt) const
125{
126 QPolygonF poly;
128 return poly.containsPoint(pt, Qt::OddEvenFill);
129}
130
136
138{
139 return isSnappingActive();
140}
141
142void PerspectiveAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible)
143{
144 gc.save();
145 gc.resetTransform();
146 QTransform initialTransform = converter->documentToWidgetTransform();
147 //QTransform reverseTransform = converter->widgetToDocument();
148 QPolygonF poly;
149 QTransform transform; // unused, but computed for caching purposes
150 if (getTransform(poly, transform) && assistantVisible==true) {
151 // draw vanishing points
153 drawX(gc, initialTransform.map(m_cache.vanishingPoint1.get()));
154 }
156 drawX(gc, initialTransform.map(m_cache.vanishingPoint2.get()));
157 }
158 }
159
160 if (isSnappingActive() && getTransform(poly, transform) && previewVisible==true){
161 //find vanishing point, find mouse, draw line between both.
162 QPainterPath path2;
163 QPointF intersection(0, 0);//this is the position of the vanishing point.
164 QPointF mousePos = effectiveBrushPosition(converter, canvas);
165 QLineF snapLine;
166 QRect viewport= gc.viewport();
167 QRect bounds;
168
169 //figure out if point is in the perspective grid
170 QPointF intersectTransformed(0, 0); // dummy for holding transformed intersection so the code is more readable.
171
172 if (poly.containsPoint(initialTransform.inverted().map(mousePos), Qt::OddEvenFill)==true){
173 // check if the lines aren't parallel to each other to avoid calculation errors in the intersection calculation (bug 345754)//
174 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) {
175 if (QLineF(poly[0], poly[1]).intersects(QLineF(poly[2], poly[3]), &intersection) != QLineF::NoIntersection) {
176 intersectTransformed = initialTransform.map(intersection);
177 snapLine = QLineF(intersectTransformed, mousePos);
178 KisAlgebra2D::intersectLineRect(snapLine, viewport, true);
179 bounds= QRect(snapLine.p1().toPoint(), snapLine.p2().toPoint());
180
181 if (bounds.contains(intersectTransformed.toPoint())){
182 path2.moveTo(intersectTransformed);
183 path2.lineTo(snapLine.p2());
184 }
185 else {
186 path2.moveTo(snapLine.p1());
187 path2.lineTo(snapLine.p2());
188 }
189 }
190 }
191 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){
192 if (QLineF(poly[1], poly[2]).intersects(QLineF(poly[3], poly[0]), &intersection) != QLineF::NoIntersection) {
193 intersectTransformed = initialTransform.map(intersection);
194 snapLine = QLineF(intersectTransformed, mousePos);
195 KisAlgebra2D::intersectLineRect(snapLine, viewport, true);
196 bounds= QRect(snapLine.p1().toPoint(), snapLine.p2().toPoint());
197 QPainterPath path;
198
199 if (bounds.contains(intersectTransformed.toPoint())){
200 path2.moveTo(intersectTransformed);
201 path2.lineTo(snapLine.p2());
202 }
203 else {
204 path2.moveTo(snapLine.p1());
205 path2.lineTo(snapLine.p2());
206 }
207 }
208 }
209 drawPreview(gc, path2);
210 }
211 }
212
213
214
215 // draw the grid lines themselves
216 gc.setTransform(converter->documentToWidgetTransform());
217
218 if (assistantVisible) {
219 // getTransform was checked before but what if the preview wasn't visible etc., and we need a return value here too
220 if (!getTransform(poly, transform)) {
221 // color red for an invalid transform, but not for an incomplete one
222 if(isAssistantComplete()) {
223 QPainterPath path;
224 // that will create a triangle with a point inside connected to all vertices of the triangle
226 drawError(gc, path);
227 } else {
228 QPainterPath path;
229 path.addPolygon(poly);
230 drawPath(gc, path, isSnappingActive());
231 }
232 } else {
233 gc.setPen(QColor(0, 0, 0, 125));
234 gc.setTransform(transform, true);
235 QPainterPath path;
236 qreal step = 1.0 / subdivisions();
237
238 for (int y = 0; y <= subdivisions(); ++y)
239 {
240 QLineF line = QLineF(QPointF(0.0, y * step), QPointF(1.0, y * step));
241 KisAlgebra2D::cropLineToRect(line, gc.window(), false, false);
242 path.moveTo(line.p1());
243 path.lineTo(line.p2());
244 }
245 for (int x = 0; x <= subdivisions(); ++x)
246 {
247 QLineF line = QLineF(QPointF(x * step, 0.0), QPointF(x * step, 1.0));
248 KisAlgebra2D::cropLineToRect(line, gc.window(), false, false);
249 path.moveTo(line.p1());
250 path.lineTo(line.p2());
251 }
252
253 drawPath(gc, path, isSnappingActive());
254 }
255 }
256 //
257
258
259 gc.restore();
260
261 KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached,canvas, assistantVisible, previewVisible);
262}
263
264void PerspectiveAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible)
265{
266 Q_UNUSED(gc);
267 Q_UNUSED(converter);
268 Q_UNUSED(assistantVisible);
269}
270
272{
273 QPointF centroid(0, 0);
274 for (int i = 0; i < 4; ++i) {
275 centroid += *handles()[i];
276 }
277
278 return centroid * 0.25;
279}
280
281bool PerspectiveAssistant::getTransform(QPolygonF& poly, QTransform& transform) const
282{
283 if (m_cachedPolygon.size() != 0 && isAssistantComplete()) {
284 for (int i = 0; i <= 4; ++i) {
285 if (i == 4) {
286 poly = m_cachedPolygon;
288 return m_cacheValid;
289 }
290 if (m_cachedPoints[i] != *handles()[i]) break;
291 }
292 }
293
294 m_cachedPolygon.clear();
295 m_cacheValid = false;
296
298 m_cachedPolygon = poly;
299 return false;
300 }
301
302 if (!QTransform::squareToQuad(poly, transform)) {
303 qWarning("Failed to create perspective mapping");
304 return false;
305 }
306
307 for (int i = 0; i < 4; ++i) {
308 m_cachedPoints[i] = *handles()[i];
309 }
310
311 m_cachedPolygon = poly;
314 m_cacheValid = true;
315 return true;
316}
317
319{
320 return handles().size() >= 4; // specify 4 corners to make assistant complete
321}
322
326
328 if (subdivisions < 1) m_subdivisions = 1;
330}
331
332void PerspectiveAssistant::saveCustomXml(QXmlStreamWriter *xml) {
333 if (xml) {
334 xml->writeStartElement("subdivisions");
335 xml->writeAttribute("value", KisDomUtils::toString(subdivisions()));
336 xml->writeEndElement();
337 }
338}
339
340bool PerspectiveAssistant::loadCustomXml(QXmlStreamReader *xml) {
341 if (xml && xml->name() == "subdivisions") {
342 setSubdivisions(KisDomUtils::toInt(xml->attributes().value("value").toString()));
343 }
344 return true;
345}
346
347
348
352
356
358{
359 return "perspective";
360}
361
363{
364 return i18n("Perspective");
365}
366
void drawX(QPainter &painter, const QPointF &pt)
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 drawError(QPainter &painter, const QPainterPath &path)
void drawPath(QPainter &painter, const QPainterPath &path, bool drawActive=true)
void drawPreview(QPainter &painter, const QPainterPath &path)
virtual void transform(const QTransform &transform)
virtual void drawAssistant(QPainter &gc, const QRectF &updateRect, const KisCoordinatesConverter *converter, bool cached, KisCanvas2 *canvas=0, bool assistantVisible=true, bool previewVisible=true)
const QList< KisPaintingAssistantHandleSP > & handles() const
KisPaintingAssistant * createPaintingAssistant() const override
QString name() const override
bool getTransform(QPolygonF &polyOut, QTransform &transformOut) const
void drawAssistant(QPainter &gc, const QRectF &updateRect, const KisCoordinatesConverter *converter, bool cached=true, KisCanvas2 *canvas=0, bool assistantVisible=true, bool previewVisible=true) override
PerspectiveAssistant(QObject *parent=0)
bool isAssistantComplete() const override
QPointF project(const QPointF &pt, const QPointF &strokeBegin, const bool snapToAnyDirection, qreal moveThresholdPt)
QPointF getDefaultEditorPosition() const override
void saveCustomXml(QXmlStreamWriter *xml) override
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...
bool contains(const QPointF &point) const override
KisPaintingAssistantSP clone(QMap< KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP > &handleMap) const override
qreal distance(const QPointF &point) const override
PerspectiveBasedAssistantHelper::CacheData m_cache
bool isActive() const override
void adjustLine(QPointF &point, QPointF &strokeBegin) override
bool loadCustomXml(QXmlStreamReader *xml) override
void setSubdivisions(int subdivisions)
QPointF adjustPosition(const QPointF &point, const QPointF &strokeBegin, const bool snapToAny, qreal moveThresholdPt) override
static QPolygonF getAllConnectedTetragon(const QList< KisPaintingAssistantHandleSP > &handles)
static void updateCacheData(CacheData &cache, const QPolygonF &poly)
static qreal distanceInGrid(const QList< KisPaintingAssistantHandleSP > &handles, bool isAssistantComplete, const QPointF &point)
static bool getTetragon(const QList< KisPaintingAssistantHandleSP > &handles, bool isAssistantComplete, QPolygonF &outPolygon)
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define bounds(x, a, b)
QSharedPointer< KisPaintingAssistant > KisPaintingAssistantSP
Definition kis_types.h:189
qreal pointToLineDistSquared(const QPointF &pt, const QLineF &line)
void cropLineToRect(QLineF &line, const QRect rect, bool extendFirst, bool extendSecond)
qreal norm(const T &a)
bool intersectLineRect(QLineF &line, const QRect rect, bool extend)
int toInt(const QString &str, bool *ok=nullptr)
QString toString(const QString &value)