Krita Source Code Documentation
Loading...
Searching...
No Matches
CurvilinearPerspectiveAssistant.cc
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2008 Cyrille Berger <cberger@cberger.net>
3 * SPDX-FileCopyrightText: 2010 Geoffry Song <goffrie@gmail.com>
4 * SPDX-FileCopyrightText: 2014 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
5 * SPDX-FileCopyrightText: 2017 Scott Petrovic <scottpetrovic@gmail.com>
6 *
7 * SPDX-License-Identifier: LGPL-2.0-or-later
8 */
9
11
12#include "kis_debug.h"
13#include <klocalizedstring.h>
14
15#include <QPainter>
16#include <QPainterPath>
17#include <QLinearGradient>
18#include <QTransform>
19
20#include <kis_canvas2.h>
22#include <kis_algebra_2d.h>
23
24#include <math.h>
25#include <limits>
26
28 : KisPaintingAssistant("curvilinear-perspective", i18n("Curvilinear Perspective assistant"))
29{
30}
31
32CurvilinearPerspectiveAssistant::CurvilinearPerspectiveAssistant(const CurvilinearPerspectiveAssistant &rhs, QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap)
33 : KisPaintingAssistant(rhs, handleMap)
34{
35}
36
37KisPaintingAssistantSP CurvilinearPerspectiveAssistant::clone(QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) const
38{
39 return KisPaintingAssistantSP(new CurvilinearPerspectiveAssistant(*this, handleMap));
40}
41
42void CurvilinearPerspectiveAssistant::adjustLine(QPointF &point, QPointF &strokeBegin)
43{
44 point = QPointF();
45 strokeBegin = QPointF();
46}
47
48void CurvilinearPerspectiveAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible)
49{
50 Q_UNUSED(cached);
51 Q_UNUSED(updateRect);
52
53 gc.save();
54 gc.resetTransform();
55
56 if (isSnappingActive()) {
57
58 QTransform initialTransform = converter->documentToWidgetTransform();
59 QPainterPath baseGuidePath;
60 QPainterPath mouseGuidePath;
61
62 gc.setTransform(initialTransform);
63
64 /*
65 * Curvilinear perspective is created by circular arcs that intersect 2 vanishing points.
66 * As such, the center of the circle and the radius of the circle need to be determined.
67 *
68 * Create guidelines by selecting incremental multipliers for the assistant size between [-1, 1),
69 * and calculating the center location and radius of the circle to include the point
70 * at the location specified by the multiplier (the "arbitrary point").
71 *
72 * Formulas:
73 * Radius^2 = HalfHandleDist^2 + CenterDist^2 (b.c. The circle must include both vanishing points.)
74 * Radius^2 = (CenterDist + Multiplier * HalfHandleDist)^2 (b.c. The circle must include the arbitrary point)
75 *
76 * Solve for CenterDist and Radius:
77 * CenterDist = HalfHandleDist * (1 - Multiplier * Multiplier) / ( 2 * Multiplier)
78 * Radius = HalfHandleDist * (1 + Multiplier * Multiplier) / ( 2 * Multiplier)
79 *
80 */
81
82 QPointF p1 = *handles()[0];
83 QPointF p2 = *handles()[1];
84
85 double deltaX = p2.x() - p1.x();
86 double deltaY = p2.y() - p1.y();
87
88 // Copied from Two-Point Perspective's fading effect for approaching vanishing points.
89 // Set up the fading effect for the grid lines
90 // Needed so the grid density doesn't look distracting
91 QColor color = effectiveAssistantColor();
92 QGradient fade = QLinearGradient(
93 QPointF(p1.x() - deltaY, p1.y() + deltaX),
94 QPointF(p1.x() + deltaY, p1.y() - deltaX));
95
96 color.setAlphaF(0.0);
97 fade.setColorAt(0.42, effectiveAssistantColor());
98 fade.setColorAt(0.5, color);
99 fade.setColorAt(0.58, effectiveAssistantColor());
100 const QPen pen = gc.pen();
101 const QBrush new_brush = QBrush(fade);
102 int width = 0;
103 const QPen new_pen = QPen(new_brush, width, pen.style());
104 gc.setPen(new_pen);
105
106 double handleDistance = KisAlgebra2D::norm(QPointF(deltaX, deltaY));
107 double halfHandleDist = handleDistance / 2.0;
108
109 double avgX = deltaX / 2.0 + p1.x();
110 double avgY = deltaY / 2.0 + p1.y();
111
112 // Rotate 90 degrees by formula: (-y, x)
113 // Then normalize vector.
114 double dirX = -deltaY / handleDistance;
115 double dirY = deltaX / handleDistance;
116
117 int resolution = halfHandleDist / 3;
118
119 if(assistantVisible) {
120
121 for(int i = -resolution; i < resolution; i++) {
122 // If i = 0, the circle would be infinitely far away with an infinite radius (aka a line)
123 if(i == 0) {
124 baseGuidePath.moveTo(QPointF(p1.x() - deltaX*2, p1.y() - deltaY*2));
125 baseGuidePath.lineTo(QPointF(p2.x() + deltaX*2, p2.y() + deltaY*2));
126 continue;
127 }
128 // Map loop iterator to multiplier. This line gives the depth-like effect.
129 double mult = 1.0 / i;
130 // Use formula to calculate CenterDist
131 double centerDist = halfHandleDist * (1 - pow2(mult)) / (2*mult);
132
133 // Use the distance to the center (from the average point) to calculate the center location.
134 double circleCenterX = centerDist * dirX + avgX;
135 double circleCenterY = centerDist * dirY + avgY;
136 // Use formula to calculate Radius
137 double radius = halfHandleDist * (1 + pow2(mult)) / (2*mult);
138
139 baseGuidePath.addEllipse(QPointF(circleCenterX, circleCenterY), radius, radius);
140
141 }
142 gc.drawPath(baseGuidePath);//drawPath(gc, baseGuidePath);
143 }
144
145 if(previewVisible) {
146 // Draw guideline for the mouse, based on mouse position.
147 QPointF mousePos = effectiveBrushPosition(converter, canvas);
148 // Get location on the screen of handles.
149 QPointF screenP1 = initialTransform.map(*handles()[0]);
150 QPointF screenP2 = initialTransform.map(*handles()[1]);
151 // Don't draw if mouse is too close to vanishing points (will flicker if not)
152 // Use distance squared to avoid expensive sqrt.
153 if(
154 kisSquareDistance(mousePos, screenP2) > 9 &&
155 kisSquareDistance(mousePos, screenP1) > 9
156 ) {
157 QLineF circle = identifyCircle(initialTransform.inverted().map(mousePos));
158 double radius = circle.length();
159 mouseGuidePath.addEllipse(circle.p1(), radius, radius);
160 }
161
162 gc.drawPath(mouseGuidePath);//drawPath(gc, mouseGuidePath);
163 }
164
165 }
166 gc.restore();
167
168 //KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible);
169
170}
171
172void CurvilinearPerspectiveAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible)
173{
174 Q_UNUSED(gc);
175 Q_UNUSED(converter);
176 Q_UNUSED(assistantVisible);
177}
178
179QLineF CurvilinearPerspectiveAssistant::identifyCircle(const QPointF thirdPoint) {
180 /*
181 * Calculate center location and radius for an arbitrary point (usually the mouse location).
182 * Given Formulas:
183 * Radius^2 = HalfHandleDist^2 + CenterDist^2
184 * avgX + CenterDist * dirX = CenterX
185 * avgY + CenterDist * dirY = CenterY
186 *
187 * For ease of use, let BetaX = MouseX - AvgX, BetaY = MouseY - AvgY
188 * Calculated Formula for CenterDist:
189 * CenterDist = (BetaX^2 + BetaY^2 - HalfHandleDist^2) / (2 * DirY * BetaX + 2 * DirY * BetaY)
190 *
191 * Returns line from center to the arbitrary point.
192 *
193 */
194 QPointF p1 = *handles()[0];
195 QPointF p2 = *handles()[1];
196
197 double deltaX = p2.x() - p1.x();
198 double deltaY = p2.y() - p1.y();
199
200 double handleDistance = KisAlgebra2D::norm(QPointF(deltaX, deltaY));
201 double halfHandleDist = handleDistance / 2.0;
202
203 double avgX = deltaX / 2.0 + p1.x();
204 double avgY = deltaY / 2.0 + p1.y();
205
206 double dirX = -deltaY / handleDistance;
207 double dirY = deltaX / handleDistance;
208
209 double betaX = thirdPoint.x() - avgX;
210 double betaY = thirdPoint.y() - avgY;
211
212 double centerDist =
213 (pow2(betaX) + pow2(betaY) - pow2(halfHandleDist))
214 /
215 (2 * dirX * betaX + 2 * dirY * betaY);
216
217 double circleCenterX = centerDist*dirX + avgX;
218 double circleCenterY = centerDist*dirY + avgY;
219 return QLineF(QPointF(circleCenterX, circleCenterY), thirdPoint);
220}
221
222QPointF CurvilinearPerspectiveAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin, const bool /*snapToAny*/, qreal /*moveThresholdPt*/)
223{
224 // Get the center and radius for the given point
225 QLineF initialCircle = identifyCircle(strokeBegin);
226
227 // Set the new point onto the circle.
228 QLineF magnetizedCircle(initialCircle.p1(), pt);
229 magnetizedCircle.setLength(initialCircle.length());
230
231 return magnetizedCircle.p2();
232
233}
234
236{
237 return (*handles()[0] + *handles()[1]) * 0.5;
238}
239
241{
242 return handles().size() >= 2;
243}
244
245
249
253
255{
256 return "curvilinear-perspective";
257}
258
260{
261 return i18n("Curvilinear Perspective");
262}
263
QPointF p2
QPointF p1
KisPaintingAssistant * createPaintingAssistant() const override
KisPaintingAssistantSP clone(QMap< KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP > &handleMap) const override
QLineF identifyCircle(const QPointF thirdPoint)
QPointF adjustPosition(const QPointF &point, const QPointF &strokeBegin, const bool snapToAny, qreal moveThresholdPt) override
void drawCache(QPainter &gc, const KisCoordinatesConverter *converter, bool assistantVisible=true) override
performance layer where the graphics can be drawn from a cache instead of generated every render upda...
void drawAssistant(QPainter &gc, const QRectF &updateRect, const KisCoordinatesConverter *converter, bool cached=true, KisCanvas2 *canvas=0, bool assistantVisible=true, bool previewVisible=true) override
void adjustLine(QPointF &point, QPointF &strokeBegin) override
QPointF effectiveBrushPosition(const KisCoordinatesConverter *converter, KisCanvas2 *canvas) const
Query the effective brush position to be used for preview lines. This is intended to be used for pain...
const QList< KisPaintingAssistantHandleSP > & handles() const
T pow2(const T &x)
Definition kis_global.h:166
qreal kisSquareDistance(const QPointF &pt1, const QPointF &pt2)
Definition kis_global.h:194
QSharedPointer< KisPaintingAssistant > KisPaintingAssistantSP
Definition kis_types.h:189
qreal norm(const T &a)