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 }