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 }