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 }