File indexing completed on 2024-12-22 04:14:22
0001 /* 0002 * SPDX-FileCopyrightText: 2008 Cyrille Berger <cberger@cberger.net> 0003 * SPDX-FileCopyrightText: 2010 Geoffry Song <goffrie@gmail.com> 0004 * SPDX-FileCopyrightText: 2014 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com> 0005 * SPDX-FileCopyrightText: 2017 Scott Petrovic <scottpetrovic@gmail.com> 0006 * 0007 * SPDX-License-Identifier: LGPL-2.0-or-later 0008 */ 0009 0010 #include "CurvilinearPerspectiveAssistant.h" 0011 0012 #include "kis_debug.h" 0013 #include <klocalizedstring.h> 0014 0015 #include <QPainter> 0016 #include <QPainterPath> 0017 #include <QLinearGradient> 0018 #include <QTransform> 0019 0020 #include <kis_canvas2.h> 0021 #include <kis_coordinates_converter.h> 0022 #include <kis_algebra_2d.h> 0023 0024 #include <math.h> 0025 #include <limits> 0026 0027 CurvilinearPerspectiveAssistant::CurvilinearPerspectiveAssistant() 0028 : KisPaintingAssistant("curvilinear-perspective", i18n("Curvilinear Perspective assistant")) 0029 { 0030 } 0031 0032 CurvilinearPerspectiveAssistant::CurvilinearPerspectiveAssistant(const CurvilinearPerspectiveAssistant &rhs, QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) 0033 : KisPaintingAssistant(rhs, handleMap) 0034 { 0035 } 0036 0037 KisPaintingAssistantSP CurvilinearPerspectiveAssistant::clone(QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) const 0038 { 0039 return KisPaintingAssistantSP(new CurvilinearPerspectiveAssistant(*this, handleMap)); 0040 } 0041 0042 void CurvilinearPerspectiveAssistant::adjustLine(QPointF &point, QPointF &strokeBegin) 0043 { 0044 point = QPointF(); 0045 strokeBegin = QPointF(); 0046 } 0047 0048 void CurvilinearPerspectiveAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible) 0049 { 0050 Q_UNUSED(cached); 0051 Q_UNUSED(updateRect); 0052 0053 gc.save(); 0054 gc.resetTransform(); 0055 0056 if (isSnappingActive()) { 0057 0058 QTransform initialTransform = converter->documentToWidgetTransform(); 0059 QPainterPath baseGuidePath; 0060 QPainterPath mouseGuidePath; 0061 0062 gc.setTransform(initialTransform); 0063 0064 /* 0065 * Curvilinear perspective is created by circular arcs that intersect 2 vanishing points. 0066 * As such, the center of the circle and the radius of the circle need to be determined. 0067 * 0068 * Create guidelines by selecting incremental multipliers for the assistant size between [-1, 1), 0069 * and calculating the center location and radius of the circle to include the point 0070 * at the location specified by the multiplier (the "arbitary point"). 0071 * 0072 * Formulas: 0073 * Radius^2 = HalfHandleDist^2 + CenterDist^2 (b.c. The circle must include both vanishing points.) 0074 * Radius^2 = (CenterDist + Multiplier * HalfHandleDist)^2 (b.c. The circle must include the arbitrary point) 0075 * 0076 * Solve for CenterDist and Radius: 0077 * CenterDist = HalfHandleDist * (1 - Multipler * Multiplier) / ( 2 * Multiplier) 0078 * Radius = HalfHandleDist * (1 + Multipler * Multiplier) / ( 2 * Multiplier) 0079 * 0080 */ 0081 0082 QPointF p1 = *handles()[0]; 0083 QPointF p2 = *handles()[1]; 0084 0085 double deltaX = p2.x() - p1.x(); 0086 double deltaY = p2.y() - p1.y(); 0087 0088 // Copied from Two-Point Perspective's fading effect for approaching vanishing points. 0089 // Set up the fading effect for the grid lines 0090 // Needed so the grid density doesn't look distracting 0091 QColor color = effectiveAssistantColor(); 0092 QGradient fade = QLinearGradient( 0093 QPointF(p1.x() - deltaY, p1.y() + deltaX), 0094 QPointF(p1.x() + deltaY, p1.y() - deltaX)); 0095 0096 color.setAlphaF(0.0); 0097 fade.setColorAt(0.42, effectiveAssistantColor()); 0098 fade.setColorAt(0.5, color); 0099 fade.setColorAt(0.58, effectiveAssistantColor()); 0100 const QPen pen = gc.pen(); 0101 const QBrush new_brush = QBrush(fade); 0102 int width = 0; 0103 const QPen new_pen = QPen(new_brush, width, pen.style()); 0104 gc.setPen(new_pen); 0105 0106 double handleDistance = KisAlgebra2D::norm(QPointF(deltaX, deltaY)); 0107 double halfHandleDist = handleDistance / 2.0; 0108 0109 double avgX = deltaX / 2.0 + p1.x(); 0110 double avgY = deltaY / 2.0 + p1.y(); 0111 0112 // Rotate 90 degrees by formula: (-y, x) 0113 // Then normalize vector. 0114 double dirX = -deltaY / handleDistance; 0115 double dirY = deltaX / handleDistance; 0116 0117 int resolution = halfHandleDist / 3; 0118 0119 if(assistantVisible) { 0120 0121 for(int i = -resolution; i < resolution; i++) { 0122 // If i = 0, the circle would be infinitely far away with an infinite radius (aka a line) 0123 if(i == 0) { 0124 baseGuidePath.moveTo(QPointF(p1.x() - deltaX*2, p1.y() - deltaY*2)); 0125 baseGuidePath.lineTo(QPointF(p2.x() + deltaX*2, p2.y() + deltaY*2)); 0126 continue; 0127 } 0128 // Map loop iterator to multiplier. This line gives the depth-like effect. 0129 double mult = 1.0 / i; 0130 // Use formula to calculate CenterDist 0131 double centerDist = halfHandleDist * (1 - pow2(mult)) / (2*mult); 0132 0133 // Use the distance to the center (from the average point) to calculate the center location. 0134 double circleCenterX = centerDist * dirX + avgX; 0135 double circleCenterY = centerDist * dirY + avgY; 0136 // Use formula to calculate Radius 0137 double radius = halfHandleDist * (1 + pow2(mult)) / (2*mult); 0138 0139 baseGuidePath.addEllipse(QPointF(circleCenterX, circleCenterY), radius, radius); 0140 0141 } 0142 gc.drawPath(baseGuidePath);//drawPath(gc, baseGuidePath); 0143 } 0144 0145 if(previewVisible) { 0146 // Draw guideline for the mouse, based on mouse position. 0147 QPointF mousePos = effectiveBrushPosition(converter, canvas); 0148 // Get location on the screen of handles. 0149 QPointF screenP1 = initialTransform.map(*handles()[0]); 0150 QPointF screenP2 = initialTransform.map(*handles()[1]); 0151 // Don't draw if mouse is too close to vanishing points (will flicker if not) 0152 // Use distance squared to avoid expensive sqrt. 0153 if( 0154 kisSquareDistance(mousePos, screenP2) > 9 && 0155 kisSquareDistance(mousePos, screenP1) > 9 0156 ) { 0157 QLineF circle = identifyCircle(initialTransform.inverted().map(mousePos)); 0158 double radius = circle.length(); 0159 mouseGuidePath.addEllipse(circle.p1(), radius, radius); 0160 } 0161 0162 gc.drawPath(mouseGuidePath);//drawPath(gc, mouseGuidePath); 0163 } 0164 0165 } 0166 gc.restore(); 0167 0168 //KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible); 0169 0170 } 0171 0172 void CurvilinearPerspectiveAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible) 0173 { 0174 Q_UNUSED(gc); 0175 Q_UNUSED(converter); 0176 Q_UNUSED(assistantVisible); 0177 } 0178 0179 QLineF CurvilinearPerspectiveAssistant::identifyCircle(const QPointF thirdPoint) { 0180 /* 0181 * Calculate center location and radius for an arbitrary point (usually the mouse location). 0182 * Given Formulas: 0183 * Radius^2 = HalfHandleDist^2 + CenterDist^2 0184 * avgX + CenterDist * dirX = CenterX 0185 * avgY + CenterDist * dirY = CenterY 0186 * 0187 * For ease of use, let BetaX = MouseX - AvgX, BetaY = MouseY - AvgY 0188 * Calculated Formula for CenterDist: 0189 * CenterDist = (BetaX^2 + BetaY^2 - HalfHandleDist^2) / (2 * DirY * BetaX + 2 * DirY * BetaY) 0190 * 0191 * Returns line from center to the arbitrary point. 0192 * 0193 */ 0194 QPointF p1 = *handles()[0]; 0195 QPointF p2 = *handles()[1]; 0196 0197 double deltaX = p2.x() - p1.x(); 0198 double deltaY = p2.y() - p1.y(); 0199 0200 double handleDistance = KisAlgebra2D::norm(QPointF(deltaX, deltaY)); 0201 double halfHandleDist = handleDistance / 2.0; 0202 0203 double avgX = deltaX / 2.0 + p1.x(); 0204 double avgY = deltaY / 2.0 + p1.y(); 0205 0206 double dirX = -deltaY / handleDistance; 0207 double dirY = deltaX / handleDistance; 0208 0209 double betaX = thirdPoint.x() - avgX; 0210 double betaY = thirdPoint.y() - avgY; 0211 0212 double centerDist = 0213 (pow2(betaX) + pow2(betaY) - pow2(halfHandleDist)) 0214 / 0215 (2 * dirX * betaX + 2 * dirY * betaY); 0216 0217 double circleCenterX = centerDist*dirX + avgX; 0218 double circleCenterY = centerDist*dirY + avgY; 0219 return QLineF(QPointF(circleCenterX, circleCenterY), thirdPoint); 0220 } 0221 0222 QPointF CurvilinearPerspectiveAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin, const bool /*snapToAny*/, qreal /*moveThresholdPt*/) 0223 { 0224 // Get the center and radius for the given point 0225 QLineF initialCircle = identifyCircle(strokeBegin); 0226 0227 // Set the new point onto the circle. 0228 QLineF magnetizedCircle(initialCircle.p1(), pt); 0229 magnetizedCircle.setLength(initialCircle.length()); 0230 0231 return magnetizedCircle.p2(); 0232 0233 } 0234 0235 QPointF CurvilinearPerspectiveAssistant::getDefaultEditorPosition() const 0236 { 0237 return (*handles()[0] + *handles()[1]) * 0.5; 0238 } 0239 0240 bool CurvilinearPerspectiveAssistant::isAssistantComplete() const 0241 { 0242 return handles().size() >= 2; 0243 } 0244 0245 0246 CurvilinearPerspectiveAssistantFactory::CurvilinearPerspectiveAssistantFactory() 0247 { 0248 } 0249 0250 CurvilinearPerspectiveAssistantFactory::~CurvilinearPerspectiveAssistantFactory() 0251 { 0252 } 0253 0254 QString CurvilinearPerspectiveAssistantFactory::id() const 0255 { 0256 return "curvilinear-perspective"; 0257 } 0258 0259 QString CurvilinearPerspectiveAssistantFactory::name() const 0260 { 0261 return i18n("Curvilinear Perspective"); 0262 } 0263 0264 KisPaintingAssistant* CurvilinearPerspectiveAssistantFactory::createPaintingAssistant() const 0265 { 0266 return new CurvilinearPerspectiveAssistant; 0267 }