File indexing completed on 2024-12-22 04:14:25

0001 /*
0002  * SPDX-FileCopyrightText: 2008 Cyrille Berger <cberger@cberger.net>
0003  * SPDX-FileCopyrightText: 2010 Geoffry Song <goffrie@gmail.com>
0004  * SPDX-FileCopyrightText: 2017 Scott Petrovic <scottpetrovic@gmail.com>
0005  *
0006  *  SPDX-License-Identifier: LGPL-2.0-or-later
0007  */
0008 
0009 #include "PerspectiveAssistant.h"
0010 
0011 #include <kis_debug.h>
0012 #include <klocalizedstring.h>
0013 
0014 #include <QPainter>
0015 #include <QPainterPath>
0016 #include <QLinearGradient>
0017 #include <QTransform>
0018 
0019 #include <kis_algebra_2d.h>
0020 #include <kis_canvas2.h>
0021 #include <kis_coordinates_converter.h>
0022 #include <kis_dom_utils.h>
0023 
0024 #include "PerspectiveBasedAssistantHelper.h"
0025 
0026 #include <math.h>
0027 #include <limits>
0028 
0029 PerspectiveAssistant::PerspectiveAssistant(QObject *parent)
0030     : KisAbstractPerspectiveGrid(parent)
0031     , KisPaintingAssistant("perspective", i18n("Perspective assistant"))
0032 {
0033 }
0034 
0035 PerspectiveAssistant::PerspectiveAssistant(const PerspectiveAssistant &rhs, QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap)
0036     : KisAbstractPerspectiveGrid(rhs.parent())
0037     , KisPaintingAssistant(rhs, handleMap)
0038     , m_subdivisions(rhs.m_subdivisions)
0039     , m_snapLine(rhs.m_snapLine)
0040     , m_cachedTransform(rhs.m_cachedTransform)
0041     , m_cachedPolygon(rhs.m_cachedPolygon)
0042     , m_cacheValid(rhs.m_cacheValid)
0043 {
0044     for (int i = 0; i < 4; ++i) {
0045         m_cachedPoints[i] = rhs.m_cachedPoints[i];
0046     }
0047 }
0048 
0049 KisPaintingAssistantSP PerspectiveAssistant::clone(QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) const
0050 {
0051     return KisPaintingAssistantSP(new PerspectiveAssistant(*this, handleMap));
0052 }
0053 
0054 QPointF PerspectiveAssistant::project(const QPointF& pt, const QPointF& strokeBegin, const bool snapToAnyDirection, qreal moveThresholdPt)
0055 {
0056     const static QPointF nullPoint(std::numeric_limits<qreal>::quiet_NaN(), std::numeric_limits<qreal>::quiet_NaN());
0057 
0058     Q_ASSERT(isAssistantComplete());
0059 
0060     if (snapToAnyDirection || m_snapLine.isNull()) {
0061         QPolygonF poly;
0062         QTransform transform;
0063 
0064         if (!getTransform(poly, transform)) {
0065             return nullPoint;
0066         }
0067 
0068         if (!poly.containsPoint(strokeBegin, Qt::OddEvenFill)) {
0069             return nullPoint; // avoid problems with multiple assistants: only snap if starting in the grid
0070         }
0071 
0072         if (KisAlgebra2D::norm(pt - strokeBegin) < moveThresholdPt) {
0073             return strokeBegin; // allow some movement before snapping
0074         }
0075 
0076         // construct transformation
0077         bool invertible;
0078         const QTransform inverse = transform.inverted(&invertible);
0079         if (!invertible) {
0080             return nullPoint; // shouldn't happen
0081         }
0082 
0083 
0084         // figure out which direction to go
0085         const QPointF start = inverse.map(strokeBegin);
0086         const QLineF verticalLine = QLineF(strokeBegin, transform.map(start + QPointF(0, 1)));
0087         const QLineF horizontalLine = QLineF(strokeBegin, transform.map(start + QPointF(1, 0)));
0088 
0089         // determine whether the horizontal or vertical line is closer to the point
0090         m_snapLine = KisAlgebra2D::pointToLineDistSquared(pt, verticalLine) < KisAlgebra2D::pointToLineDistSquared(pt, horizontalLine) ? verticalLine : horizontalLine;
0091     }
0092 
0093     // snap to line
0094     const qreal
0095             dx = m_snapLine.dx(),
0096             dy = m_snapLine.dy(),
0097             dx2 = dx * dx,
0098             dy2 = dy * dy,
0099             invsqrlen = 1.0 / (dx2 + dy2);
0100     QPointF r(dx2 * pt.x() + dy2 * m_snapLine.x1() + dx * dy * (pt.y() - m_snapLine.y1()),
0101               dx2 * m_snapLine.y1() + dy2 * pt.y() + dx * dy * (pt.x() - m_snapLine.x1()));
0102 
0103     r *= invsqrlen;
0104     return r;
0105 }
0106 
0107 QPointF PerspectiveAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin, const bool snapToAny, qreal moveThresholdPt)
0108 {
0109     return project(pt, strokeBegin, snapToAny, moveThresholdPt);
0110 }
0111 
0112 void PerspectiveAssistant::adjustLine(QPointF &point, QPointF &strokeBegin)
0113 {
0114     point = project(point, strokeBegin, true, 0.0);
0115 }
0116 
0117 void PerspectiveAssistant::endStroke()
0118 {
0119     m_snapLine = QLineF();
0120     KisPaintingAssistant::endStroke();
0121 }
0122 
0123 bool PerspectiveAssistant::contains(const QPointF& pt) const
0124 {
0125     QPolygonF poly;
0126     if (!PerspectiveBasedAssistantHelper::getTetragon(handles(), isAssistantComplete(), poly)) return false;
0127     return poly.containsPoint(pt, Qt::OddEvenFill);
0128 }
0129 
0130 qreal PerspectiveAssistant::distance(const QPointF& pt) const
0131 {
0132     return PerspectiveBasedAssistantHelper::distanceInGrid(m_cache, pt);
0133 }
0134 
0135 bool PerspectiveAssistant::isActive() const
0136 {
0137     return isSnappingActive();
0138 }
0139 
0140 void PerspectiveAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible)
0141 {
0142     gc.save();
0143     gc.resetTransform();
0144     QTransform initialTransform = converter->documentToWidgetTransform();
0145     //QTransform reverseTransform = converter->widgetToDocument();
0146     QPolygonF poly;
0147     QTransform transform; // unused, but computed for caching purposes
0148     if (getTransform(poly, transform) && assistantVisible==true) {
0149         // draw vanishing points
0150         if (m_cache.vanishingPoint1) {
0151             drawX(gc, initialTransform.map(m_cache.vanishingPoint1.get()));
0152         }
0153         if (m_cache.vanishingPoint2) {
0154             drawX(gc, initialTransform.map(m_cache.vanishingPoint2.get()));
0155         }
0156     }
0157 
0158     if (isSnappingActive() && getTransform(poly, transform) && previewVisible==true){
0159         //find vanishing point, find mouse, draw line between both.
0160         QPainterPath path2;
0161         QPointF intersection(0, 0);//this is the position of the vanishing point.
0162         QPointF mousePos = effectiveBrushPosition(converter, canvas);
0163         QLineF snapLine;
0164         QRect viewport= gc.viewport();
0165         QRect bounds;
0166 
0167         //figure out if point is in the perspective grid
0168         QPointF intersectTransformed(0, 0); // dummy for holding transformed intersection so the code is more readable.
0169 
0170         if (poly.containsPoint(initialTransform.inverted().map(mousePos), Qt::OddEvenFill)==true){
0171             // check if the lines aren't parallel to each other to avoid calculation errors in the intersection calculation (bug 345754)//
0172             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) {
0173                 if (QLineF(poly[0], poly[1]).intersect(QLineF(poly[2], poly[3]), &intersection) != QLineF::NoIntersection) {
0174                     intersectTransformed = initialTransform.map(intersection);
0175                     snapLine = QLineF(intersectTransformed, mousePos);
0176                     KisAlgebra2D::intersectLineRect(snapLine, viewport, true);
0177                     bounds= QRect(snapLine.p1().toPoint(), snapLine.p2().toPoint());
0178 
0179                     if (bounds.contains(intersectTransformed.toPoint())){
0180                         path2.moveTo(intersectTransformed);
0181                         path2.lineTo(snapLine.p2());
0182                     }
0183                     else {
0184                         path2.moveTo(snapLine.p1());
0185                         path2.lineTo(snapLine.p2());
0186                     }
0187                 }
0188             }
0189             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){
0190                 if (QLineF(poly[1], poly[2]).intersect(QLineF(poly[3], poly[0]), &intersection) != QLineF::NoIntersection) {
0191                     intersectTransformed = initialTransform.map(intersection);
0192                     snapLine = QLineF(intersectTransformed, mousePos);
0193                     KisAlgebra2D::intersectLineRect(snapLine, viewport, true);
0194                     bounds= QRect(snapLine.p1().toPoint(), snapLine.p2().toPoint());
0195                     QPainterPath path;
0196 
0197                     if (bounds.contains(intersectTransformed.toPoint())){
0198                         path2.moveTo(intersectTransformed);
0199                         path2.lineTo(snapLine.p2());
0200                     }
0201                     else {
0202                         path2.moveTo(snapLine.p1());
0203                         path2.lineTo(snapLine.p2());
0204                     }
0205                 }
0206             }
0207             drawPreview(gc, path2);
0208         }
0209     }
0210 
0211 
0212 
0213     // draw the grid lines themselves
0214     gc.setTransform(converter->documentToWidgetTransform());
0215 
0216     if (assistantVisible) {
0217         // getTransform was checked before but what if the preview wasn't visible etc., and we need a return value here too
0218         if (!getTransform(poly, transform)) {
0219             // color red for an invalid transform, but not for an incomplete one
0220             if(isAssistantComplete()) {
0221                 QPainterPath path;
0222                 // that will create a triangle with a point inside connected to all vertices of the triangle
0223                 path.addPolygon(PerspectiveBasedAssistantHelper::getAllConnectedTetragon(handles()));
0224                 drawError(gc, path);
0225             } else {
0226                 QPainterPath path;
0227                 path.addPolygon(poly);
0228                 drawPath(gc, path, isSnappingActive());
0229             }
0230         } else {
0231             gc.setPen(QColor(0, 0, 0, 125));
0232             gc.setTransform(transform, true);
0233             QPainterPath path;
0234             qreal step = 1.0 / subdivisions();
0235             
0236             for (int y = 0; y <= subdivisions(); ++y)
0237             {
0238                 QLineF line = QLineF(QPointF(0.0, y * step), QPointF(1.0, y * step));
0239                 KisAlgebra2D::cropLineToRect(line, gc.window(), false, false);
0240                 path.moveTo(line.p1());
0241                 path.lineTo(line.p2());
0242             }
0243             for (int x = 0; x <= subdivisions(); ++x)
0244             {
0245                 QLineF line = QLineF(QPointF(x * step, 0.0), QPointF(x * step, 1.0));
0246                 KisAlgebra2D::cropLineToRect(line, gc.window(), false, false);
0247                 path.moveTo(line.p1());
0248                 path.lineTo(line.p2());
0249             }
0250 
0251             drawPath(gc, path, isSnappingActive());
0252         }
0253     }
0254     //
0255 
0256 
0257     gc.restore();
0258 
0259     KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached,canvas, assistantVisible, previewVisible);
0260 }
0261 
0262 void PerspectiveAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible)
0263 {
0264     Q_UNUSED(gc);
0265     Q_UNUSED(converter);
0266     Q_UNUSED(assistantVisible);
0267 }
0268 
0269 QPointF PerspectiveAssistant::getDefaultEditorPosition() const
0270 {
0271     QPointF centroid(0, 0);
0272     for (int i = 0; i < 4; ++i) {
0273         centroid += *handles()[i];
0274     }
0275 
0276     return centroid * 0.25;
0277 }
0278 
0279 bool PerspectiveAssistant::getTransform(QPolygonF& poly, QTransform& transform) const
0280 {
0281     if (m_cachedPolygon.size() != 0 && isAssistantComplete()) {
0282         for (int i = 0; i <= 4; ++i) {
0283             if (i == 4) {
0284                 poly = m_cachedPolygon;
0285                 transform = m_cachedTransform;
0286                 return m_cacheValid;
0287             }
0288             if (m_cachedPoints[i] != *handles()[i]) break;
0289         }
0290     }
0291 
0292     m_cachedPolygon.clear();
0293     m_cacheValid = false;
0294 
0295     if (!PerspectiveBasedAssistantHelper::getTetragon(handles(), isAssistantComplete(), poly)) {
0296         m_cachedPolygon = poly;
0297         return false;
0298     }
0299 
0300     if (!QTransform::squareToQuad(poly, transform)) {
0301         qWarning("Failed to create perspective mapping");
0302         return false;
0303     }
0304 
0305     for (int i = 0; i < 4; ++i) {
0306         m_cachedPoints[i] = *handles()[i];
0307     }
0308 
0309     m_cachedPolygon = poly;
0310     m_cachedTransform = transform;
0311     PerspectiveBasedAssistantHelper::updateCacheData(m_cache, poly);
0312     m_cacheValid = true;
0313     return true;
0314 }
0315 
0316 bool PerspectiveAssistant::isAssistantComplete() const
0317 {
0318     return handles().size() >= 4; // specify 4 corners to make assistant complete
0319 }
0320 
0321 int PerspectiveAssistant::subdivisions() const {
0322     return m_subdivisions;
0323 }
0324 
0325 void PerspectiveAssistant::setSubdivisions(int subdivisions) {
0326     if (subdivisions < 1) m_subdivisions = 1;
0327     else m_subdivisions = subdivisions;
0328 }
0329 
0330 void PerspectiveAssistant::saveCustomXml(QXmlStreamWriter *xml) {
0331     if (xml) {
0332         xml->writeStartElement("subdivisions");
0333         xml->writeAttribute("value", KisDomUtils::toString(subdivisions()));
0334         xml->writeEndElement();
0335     }
0336 }
0337 
0338 bool PerspectiveAssistant::loadCustomXml(QXmlStreamReader *xml) {
0339     if (xml && xml->name() == "subdivisions") {
0340         setSubdivisions(KisDomUtils::toInt(xml->attributes().value("value").toString()));
0341     }
0342     return true;
0343 }
0344 
0345 
0346 
0347 PerspectiveAssistantFactory::PerspectiveAssistantFactory()
0348 {
0349 }
0350 
0351 PerspectiveAssistantFactory::~PerspectiveAssistantFactory()
0352 {
0353 }
0354 
0355 QString PerspectiveAssistantFactory::id() const
0356 {
0357     return "perspective";
0358 }
0359 
0360 QString PerspectiveAssistantFactory::name() const
0361 {
0362     return i18n("Perspective");
0363 }
0364 
0365 KisPaintingAssistant* PerspectiveAssistantFactory::createPaintingAssistant() const
0366 {
0367     return new PerspectiveAssistant;
0368 }