File indexing completed on 2024-06-16 04:15:52

0001 /*
0002  * SPDX-FileCopyrightText: 2008 Cyrille Berger <cberger@cberger.net>
0003  * SPDX-FileCopyrightText: 2010 Geoffry Song <goffrie@gmail.com>
0004  * SPDX-FileCopyrightText: 2021 Nabil Maghfur Usman <nmaghfurusman@gmail.com>
0005  *
0006  *  SPDX-License-Identifier: LGPL-2.0-or-later
0007  */
0008 
0009 #include "TwoPointAssistant.h"
0010 #include "kis_debug.h"
0011 #include <klocalizedstring.h>
0012 
0013 #include <QPainter>
0014 #include <QPainterPath>
0015 #include <QLinearGradient>
0016 #include <QTransform>
0017 
0018 #include <kis_canvas2.h>
0019 #include <kis_coordinates_converter.h>
0020 #include <kis_algebra_2d.h>
0021 #include <kis_dom_utils.h>
0022 #include <math.h>
0023 #include <QtCore/qmath.h>
0024 #include <kis_assert.h>
0025 
0026 TwoPointAssistant::TwoPointAssistant()
0027     : KisPaintingAssistant("two point", i18n("Two point assistant"))
0028 {
0029 }
0030 
0031 TwoPointAssistant::TwoPointAssistant(const TwoPointAssistant &rhs, QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap)
0032     : KisPaintingAssistant(rhs, handleMap)
0033     , m_canvas(rhs.m_canvas)
0034     , m_snapLine(rhs.m_snapLine)
0035     , m_gridDensity(rhs.m_gridDensity)
0036     , m_useVertical(rhs.m_useVertical)
0037     , m_lastUsedPoint(rhs.m_lastUsedPoint)
0038 {
0039 }
0040 
0041 KisPaintingAssistantSP TwoPointAssistant::clone(QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) const
0042 {
0043     return KisPaintingAssistantSP(new TwoPointAssistant(*this, handleMap));
0044 }
0045 
0046 QPointF TwoPointAssistant::project(const QPointF& point, const QPointF& strokeBegin, const bool snapToAny, qreal moveThreshold)
0047 {
0048     Q_ASSERT(isAssistantComplete());
0049 
0050     QPointF best_pt = point;
0051     double best_dist = DBL_MAX;
0052     QList<int> possibleHandles;
0053 
0054     // must be above or equal to 0;
0055     // if useVertical, then last used point must be below 3, because 2 means vertical
0056     //     and it's the last possible point here (sanity check)
0057     // if !useVertical, then it must be below 2, because 2 means vertical
0058     bool isLastUsedPointCorrectNow = m_lastUsedPoint >= 0 && (m_useVertical ? m_lastUsedPoint < 3 : m_lastUsedPoint < 2);
0059 
0060     if (isLocal() && handles().size() == 5) {
0061         // here we can just return since we don't want to do anything
0062         // so we're returning a NaN
0063         // but only if we don't have a point/axes it was already using
0064 
0065         QRectF rect = getLocalRect();
0066         bool insideLocalRect = rect.contains(point);
0067         if (!insideLocalRect && (!isLastUsedPointCorrectNow || !m_hasBeenInsideLocalRect)) {
0068             return QPointF(qQNaN(), qQNaN());
0069         } else if (insideLocalRect) {
0070             m_hasBeenInsideLocalRect = true;
0071         }
0072     }
0073 
0074     if (!isLastUsedPointCorrectNow && KisAlgebra2D::norm(point - strokeBegin) < moveThreshold) {
0075         return strokeBegin;
0076     }
0077 
0078     if (!snapToAny && isLastUsedPointCorrectNow) {
0079         possibleHandles = QList<int>({m_lastUsedPoint});
0080     } else {
0081         if (m_useVertical) {
0082             possibleHandles = QList<int>({0, 1, 2});
0083         } else {
0084             possibleHandles = QList<int>({0, 1});
0085         }
0086     }
0087 
0088     Q_FOREACH (int vpIndex, possibleHandles) {
0089         QPointF vp = *handles()[vpIndex];
0090         double dist = 0;
0091         QPointF pt = QPointF();
0092         QLineF snapLine = QLineF();
0093 
0094         // TODO: Would be a good idea to generalize this whole routine
0095         // in KisAlgebra2d, as it's all lifted from the vanishing
0096         // point assistant and parallel ruler assistant, and by
0097         // extension the perspective assistant...
0098         qreal dx = point.x() - strokeBegin.x();
0099         qreal dy = point.y() - strokeBegin.y();
0100 
0101         if (vp != *handles()[2]) {
0102             snapLine = QLineF(vp, strokeBegin);
0103         } else {
0104             QLineF vertical = QLineF(*handles()[0],*handles()[1]).normalVector();
0105             snapLine = QLineF(vertical.p1(), vertical.p2());
0106             QPointF translation = (vertical.p1()-strokeBegin)*-1.0;
0107             snapLine = snapLine.translated(translation);
0108         }
0109 
0110         dx = snapLine.dx();
0111         dy = snapLine.dy();
0112 
0113         const qreal dx2 = dx * dx;
0114         const qreal dy2 = dy * dy;
0115         const qreal invsqrlen = 1.0 / (dx2 + dy2);
0116 
0117         pt = QPointF(dx2 * point.x() + dy2 * snapLine.x1() + dx * dy * (point.y() - snapLine.y1()),
0118                      dx2 * snapLine.y1() + dy2 * point.y() + dx * dy * (point.x() - snapLine.x1()));
0119 
0120         pt *= invsqrlen;
0121         dist = qAbs(pt.x() - point.x()) + qAbs(pt.y() - point.y());
0122 
0123         if (dist < best_dist) {
0124             best_pt = pt;
0125             best_dist = dist;
0126             m_lastUsedPoint = vpIndex;
0127         }
0128     }
0129 
0130     return best_pt;
0131 }
0132 
0133 void TwoPointAssistant::endStroke()
0134 {
0135     m_snapLine = QLineF();
0136     m_lastUsedPoint = -1;
0137     KisPaintingAssistant::endStroke();
0138 }
0139 
0140 QPointF TwoPointAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin, const bool snapToAny, qreal moveThresholdPt)
0141 {
0142     return project(pt, strokeBegin, snapToAny, moveThresholdPt);
0143 }
0144 
0145 void TwoPointAssistant::adjustLine(QPointF &point, QPointF &strokeBegin)
0146 {
0147     QPointF p = project(point, strokeBegin, true, 0.0);
0148     point = p;
0149 }
0150 
0151 void TwoPointAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible)
0152 {
0153     Q_UNUSED(updateRect);
0154     Q_UNUSED(cached);
0155     gc.save();
0156     gc.resetTransform();
0157 
0158     const QTransform initialTransform = converter->documentToWidgetTransform();
0159     bool isEditing = false;
0160     bool showLocal = isLocal() && handles().size() == 5;
0161 
0162     if (isEditing) {
0163         Q_FOREACH (const QPointF* handle, handles()) {
0164             QPointF h = initialTransform.map(*handle);
0165             QRectF ellipse = QRectF(QPointF(h.x() -15, h.y() -15), QSizeF(30, 30));
0166 
0167             QPainterPath pathCenter;
0168             pathCenter.addEllipse(ellipse);
0169             drawPath(gc, pathCenter, isSnappingActive());
0170 
0171             // Draw circle to represent center of vision
0172             if (handles().length() == 3 && handle == handles()[2]) {
0173                 const QLineF horizon = QLineF(*handles()[0],*handles()[1]);
0174                 QLineF normal = horizon.normalVector();
0175                 normal.translate(*handles()[2]-normal.p1());
0176                 QPointF cov = horizon.center();
0177                 normal.intersect(horizon,&cov);
0178                 const QPointF center = initialTransform.map(cov);
0179                 QRectF center_ellipse = QRectF(QPointF(center.x() -15, center.y() -15), QSizeF(30, 30));
0180                 QPainterPath pathCenter;
0181                 pathCenter.addEllipse(center_ellipse);
0182                 drawPath(gc, pathCenter, isSnappingActive());
0183             }
0184         }
0185 
0186         if (handles().size() <= 2) {
0187             QPainterPath path;
0188             int tempDensity = m_gridDensity * 10; // the vanishing point density seems visibly more dense, hence let's make it less dense
0189             QRect viewport = gc.viewport();
0190 
0191             for (int i = 0; i < handles().size(); i++) {
0192                 const QPointF p = initialTransform.map(*handles()[i]);
0193                 for (int currentAngle=0; currentAngle <= 180; currentAngle = currentAngle + tempDensity) {
0194 
0195                     // determine the correct angle based on the iteration
0196                     float xPos = cos(currentAngle * M_PI / 180);
0197                     float yPos = sin(currentAngle * M_PI / 180);
0198                     float length = 100;
0199                     QPointF unit = QPointF(length*xPos, length*yPos);
0200 
0201                     // find point
0202                     QLineF snapLine = QLineF(p, p + unit);
0203                     if (KisAlgebra2D::intersectLineRect(snapLine, viewport, false)) {
0204                         // make a line from VP center to edge of canvas with that angle
0205 
0206                         path.moveTo(snapLine.p1());
0207                         path.lineTo(snapLine.p2());
0208                     }
0209 
0210                     QLineF snapLine2 = QLineF(p, p - unit);
0211                     if (KisAlgebra2D::intersectLineRect(snapLine2, viewport, false)) {
0212                         // make a line from VP center to edge of canvas with that angle
0213 
0214                         path.moveTo(snapLine2.p1());
0215                         path.lineTo(snapLine2.p2());
0216                     }
0217 
0218 
0219                 }
0220 
0221                 drawPreview(gc, path);//and we draw the preview.
0222 
0223             }
0224         }
0225 
0226     }
0227 
0228     if (handles().size() >= 2) {
0229         QPointF mousePos = effectiveBrushPosition(converter, canvas);
0230         const QPointF p1 = *handles()[0];
0231         const QPointF p2 = *handles()[1];
0232         const QRect viewport= gc.viewport();
0233 
0234         const QPolygonF localPoly = (isLocal() && handles().size() == 5) ? initialTransform.map(QPolygonF(getLocalRect())) : QPolygonF();
0235         const QPolygonF viewportAndLocalPoly = !localPoly.isEmpty() ? QPolygonF(QRectF(viewport)).intersected(localPoly) : QRectF(viewport);
0236 
0237 
0238         QPainterPath path;
0239         QPainterPath previewPath; // part of the preview, instead of the assistant itself
0240 
0241         // draw the horizon
0242         if (assistantVisible == true || isEditing == true) {
0243             QLineF horizonLine = initialTransform.map(QLineF(p1,p2));
0244             KisAlgebra2D::cropLineToConvexPolygon(horizonLine, viewportAndLocalPoly, true, true);
0245             path.moveTo(horizonLine.p1());
0246             path.lineTo(horizonLine.p2());
0247         }
0248 
0249         // draw the VP-->mousePos lines
0250         if (isEditing == false && previewVisible == true && isSnappingActive() == true) {
0251             // draw the line vp <-> mouse even outside of the local rectangle
0252             // but only if the mouse pos is inside the rectangle
0253             QLineF snapMouse1 = QLineF(initialTransform.map(p1), mousePos);
0254             QLineF snapMouse2 = QLineF(initialTransform.map(p2), mousePos);
0255             KisAlgebra2D::cropLineToConvexPolygon(snapMouse1, viewportAndLocalPoly, false, true);
0256             KisAlgebra2D::cropLineToConvexPolygon(snapMouse2, viewportAndLocalPoly, false, true);
0257             previewPath.moveTo(snapMouse1.p1());
0258             previewPath.lineTo(snapMouse1.p2());
0259             previewPath.moveTo(snapMouse2.p1());
0260             previewPath.lineTo(snapMouse2.p2());
0261         }
0262 
0263         // draw the side handle bars
0264         if (isEditing == true && !sideHandles().isEmpty()) {
0265             path.moveTo(initialTransform.map(p1));
0266             path.lineTo(initialTransform.map(*sideHandles()[0]));
0267             path.lineTo(initialTransform.map(*sideHandles()[1]));
0268             path.moveTo(initialTransform.map(p2));
0269             path.lineTo(initialTransform.map(*sideHandles()[2]));
0270             path.lineTo(initialTransform.map(*sideHandles()[3]));
0271             path.moveTo(initialTransform.map(p1));
0272             path.lineTo(initialTransform.map(*sideHandles()[4]));
0273             path.lineTo(initialTransform.map(*sideHandles()[5]));
0274             path.moveTo(initialTransform.map(p2));
0275             path.lineTo(initialTransform.map(*sideHandles()[6]));
0276             path.lineTo(initialTransform.map(*sideHandles()[7]));
0277         }
0278 
0279         // draw the local rectangle
0280         if (showLocal && assistantVisible) {
0281             QPointF p1 = *handles()[(int)LocalFirstHandle];
0282             QPointF p3 = *handles()[(int)LocalSecondHandle];
0283             QPointF p2 = QPointF(p1.x(), p3.y());
0284             QPointF p4 = QPointF(p3.x(), p1.y());
0285 
0286             path.moveTo(initialTransform.map(p1));
0287 
0288             path.lineTo(initialTransform.map(p2));
0289             path.lineTo(initialTransform.map(p3));
0290             path.lineTo(initialTransform.map(p4));
0291             path.lineTo(initialTransform.map(p1));
0292         }
0293 
0294 
0295         drawPreview(gc,previewPath);
0296         drawPath(gc, path, isSnappingActive());
0297 
0298         if (handles().size() >= 3 && isSnappingActive()) {
0299             path = QPainterPath(); // clear
0300             const QPointF p3 = *handles()[2];
0301 
0302             qreal size = 0;
0303             const QTransform t = localTransform(p1,p2,p3,&size);
0304             const QTransform inv = t.inverted();
0305             const QPointF vp_a = t.map(p1);
0306             const QPointF vp_b = t.map(p2);
0307 
0308             if ((vp_a.x() < 0 && vp_b.x() > 0) ||
0309                 (vp_a.x() > 0 && vp_b.x() < 0)) {
0310                 if (m_useVertical) {
0311                     // Draw vertical line, but only if the center is between both VPs
0312                     QLineF vertical = initialTransform.map(inv.map(QLineF::fromPolar(1,90)));
0313                     if (!isEditing) vertical.translate(mousePos - vertical.p1());
0314                     KisAlgebra2D::cropLineToConvexPolygon(vertical, viewportAndLocalPoly, true, true);
0315                     if (previewVisible) {
0316                         path.moveTo(vertical.p1());
0317                         path.lineTo(vertical.p2());
0318                     }
0319 
0320                     if (assistantVisible) {
0321                         // Display a notch to represent the center of vision
0322                         path.moveTo(initialTransform.map(inv.map(QPointF(0,vp_a.y()-10))));
0323                         path.lineTo(initialTransform.map(inv.map(QPointF(0,vp_a.y()+10))));
0324                     }
0325                     drawPreview(gc,path);
0326                     path = QPainterPath(); // clear
0327                 }
0328             }
0329 
0330             const QPointF upper = QPointF(0,vp_a.y() + size);
0331             const QPointF lower = QPointF(0,vp_a.y() - size);
0332 
0333             // Set up the fading effect for the grid lines
0334             // Needed so the grid density doesn't look distracting
0335             QColor color = effectiveAssistantColor();
0336             QGradient fade = QLinearGradient(initialTransform.map(inv.map(upper)),
0337                                              initialTransform.map(inv.map(lower)));
0338             color.setAlphaF(0);
0339             fade.setColorAt(0.4, effectiveAssistantColor());
0340             fade.setColorAt(0.5, color);
0341             fade.setColorAt(0.6, effectiveAssistantColor());
0342             const QPen pen = gc.pen();
0343             const QBrush new_brush = QBrush(fade);
0344             int width = 1;
0345             const QPen new_pen = QPen(new_brush, width, pen.style());
0346             gc.setPen(new_pen);
0347 
0348             const QList<QPointF> station_points = {upper, lower};
0349             const QList<QPointF> vanishing_points = {vp_a, vp_b};
0350 
0351             // Draw grid lines above and below the horizon
0352             Q_FOREACH (const QPointF sp, station_points) {
0353 
0354                 // Draw grid lines towards each vanishing point
0355                 Q_FOREACH (const QPointF vp, vanishing_points) {
0356 
0357                     // Interval between each grid line, uses grid density specified by user
0358                     const qreal initial_angle = QLineF(sp, vp).angle();
0359                     const qreal interval = size*m_gridDensity / cos((initial_angle - 90) * M_PI/180);
0360                     const QPointF translation = QPointF(interval, 0);
0361 
0362                     // Draw grid lines originating from both the left and right of the central vertical line
0363                     Q_FOREACH (const int dir, QList<int>({-1, 1})) {
0364 
0365                         // Limit at 300 grid lines per direction, reasonable even for m_gridDensity=0.1;
0366                         for (int i = 0; i <= 300; i++) {
0367                             const QLineF gridline = QLineF(sp + translation * i * dir, vp);
0368 
0369                             // Don't bother drawing lines that are nearly parallel to horizon
0370                             const qreal angle = gridline.angle();
0371                             if (angle < 0.25 || angle > 359.75 || (angle < 180.25 && angle > 179.75)) {
0372                                 break;
0373                             }
0374 
0375                             QLineF drawn_gridline = initialTransform.map(inv.map(gridline));
0376                             KisAlgebra2D::cropLineToConvexPolygon(drawn_gridline, viewportAndLocalPoly, true, false);
0377 
0378                             if (assistantVisible || isEditing == true) {
0379                                 path.moveTo(drawn_gridline.p2());
0380                                 path.lineTo(drawn_gridline.p1());
0381                             }
0382                         }
0383                     }
0384                 }
0385             }
0386             gc.drawPath(path);
0387         }
0388     }
0389 
0390     gc.restore();
0391     //KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible);
0392 }
0393 
0394 void TwoPointAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible)
0395 {
0396     Q_UNUSED(gc);
0397     Q_UNUSED(converter);
0398     Q_UNUSED(assistantVisible);
0399     if (!m_canvas || !isAssistantComplete()) {
0400         return;
0401     }
0402 
0403     if (assistantVisible == false ||   m_canvas->paintingAssistantsDecoration()->isEditingAssistants()) {
0404         return;
0405     }
0406 }
0407 
0408 KisPaintingAssistantHandleSP TwoPointAssistant::firstLocalHandle() const
0409 {
0410     if (handles().size() > LocalFirstHandle) {
0411         return handles().at(LocalFirstHandle);
0412     } else {
0413         return nullptr;
0414     }
0415 }
0416 
0417 KisPaintingAssistantHandleSP TwoPointAssistant::secondLocalHandle() const
0418 {
0419     if (handles().size() > LocalSecondHandle) {
0420         return handles().at(LocalSecondHandle);
0421     } else {
0422         return nullptr;
0423     }
0424 }
0425 
0426 QPointF TwoPointAssistant::getDefaultEditorPosition() const
0427 {
0428     int centerOfVisionHandle = 2;
0429     if (handles().size() > centerOfVisionHandle) {
0430         return *handles().at(centerOfVisionHandle);
0431     } else if (handles().size() > 0) {
0432         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(false, *handles().at(0));
0433         return *handles().at(0);
0434     } else {
0435         KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(false, QPointF(0, 0));
0436         return QPointF(0, 0);
0437     }
0438 }
0439 
0440 void TwoPointAssistant::setGridDensity(double density)
0441 {
0442     m_gridDensity = density;
0443 }
0444 
0445 bool TwoPointAssistant::useVertical()
0446 {
0447     return m_useVertical;
0448 }
0449 
0450 void TwoPointAssistant::setUseVertical(bool value)
0451 {
0452     m_useVertical = value;
0453 }
0454 
0455 double TwoPointAssistant::gridDensity()
0456 {
0457     return m_gridDensity;
0458 }
0459 
0460 QTransform TwoPointAssistant::localTransform(QPointF vp_a, QPointF vp_b, QPointF pt_c, qreal* size)
0461 {
0462     QTransform t = QTransform();
0463     t.rotate(QLineF(vp_a, vp_b).angle());
0464     t.translate(-pt_c.x(),-pt_c.y());
0465     const QLineF horizon = QLineF(t.map(vp_a), QPointF(t.map(vp_b).x(),t.map(vp_a).y()));
0466     *size = sqrt(pow(horizon.length()/2.0,2) - pow(abs(horizon.center().x()),2));
0467 
0468     return t;
0469 }
0470 
0471 bool TwoPointAssistant::isAssistantComplete() const
0472 {
0473     return handles().size() >= numHandles();
0474 }
0475 
0476 bool TwoPointAssistant::canBeLocal() const
0477 {
0478     return true;
0479 }
0480 
0481 void TwoPointAssistant::saveCustomXml(QXmlStreamWriter* xml)
0482 {
0483     xml->writeStartElement("gridDensity");
0484     xml->writeAttribute("value", KisDomUtils::toString( this->gridDensity()));
0485     xml->writeEndElement();
0486     xml->writeStartElement("useVertical");
0487     xml->writeAttribute("value", KisDomUtils::toString( (int)this->useVertical()));
0488     xml->writeEndElement();
0489     xml->writeStartElement("isLocal");
0490     xml->writeAttribute("value", KisDomUtils::toString( (int)this->isLocal()));
0491     xml->writeEndElement();
0492 
0493 }
0494 
0495 bool TwoPointAssistant::loadCustomXml(QXmlStreamReader* xml)
0496 {
0497     if (xml && xml->name() == "gridDensity") {
0498         this->setGridDensity((float)KisDomUtils::toDouble(xml->attributes().value("value").toString()));
0499     }
0500     if (xml && xml->name() == "useVertical") {
0501         this->setUseVertical((bool)KisDomUtils::toInt(xml->attributes().value("value").toString()));
0502     }
0503     if (xml && xml->name() == "isLocal") {
0504         this->setLocal((bool)KisDomUtils::toInt(xml->attributes().value("value").toString()));
0505     }
0506     return true;
0507 }
0508 
0509 TwoPointAssistantFactory::TwoPointAssistantFactory()
0510 {
0511 }
0512 
0513 TwoPointAssistantFactory::~TwoPointAssistantFactory()
0514 {
0515 }
0516 
0517 QString TwoPointAssistantFactory::id() const
0518 {
0519     return "two point";
0520 }
0521 
0522 QString TwoPointAssistantFactory::name() const
0523 {
0524     return i18n("2 Point Perspective");
0525 }
0526 
0527 KisPaintingAssistant* TwoPointAssistantFactory::createPaintingAssistant() const
0528 {
0529     return new TwoPointAssistant;
0530 }