File indexing completed on 2024-12-22 04:14:23

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  * SPDX-FileCopyrightText: 2022 Julian Schmidt <julisch1107@web.de>
0007  *
0008  *  SPDX-License-Identifier: LGPL-2.0-or-later
0009  */
0010 
0011 #include "InfiniteRulerAssistant.h"
0012 
0013 #include "kis_debug.h"
0014 #include <klocalizedstring.h>
0015 
0016 #include <QPainter>
0017 #include <QPainterPath>
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 
0026 InfiniteRulerAssistant::InfiniteRulerAssistant()
0027     : RulerAssistant("infinite ruler", i18n("Infinite Ruler assistant"))
0028 {
0029 }
0030 
0031 InfiniteRulerAssistant::InfiniteRulerAssistant(const InfiniteRulerAssistant &rhs, QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap)
0032     : RulerAssistant(rhs, handleMap)
0033 {
0034 }
0035 
0036 KisPaintingAssistantSP InfiniteRulerAssistant::clone(QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) const
0037 {
0038     return KisPaintingAssistantSP(new InfiniteRulerAssistant(*this, handleMap));
0039 }
0040 
0041 QPointF InfiniteRulerAssistant::project(const QPointF& pt, const QPointF& strokeBegin, const bool checkForInitialMovement, qreal moveThresholdPt)
0042 {
0043     Q_ASSERT(isAssistantComplete());
0044     //code nicked from the perspective ruler.
0045     qreal dx = pt.x() - strokeBegin.x();
0046     qreal dy = pt.y() - strokeBegin.y();
0047     if (checkForInitialMovement && KisAlgebra2D::norm(QPointF(dx, dy)) < moveThresholdPt) {
0048         // allow some movement before snapping
0049         return strokeBegin;
0050     }
0051 
0052     QLineF snapLine = QLineF(*handles()[0], *handles()[1]);
0053     
0054         dx = snapLine.dx();
0055         dy = snapLine.dy();
0056     const qreal
0057         dx2 = dx * dx,
0058         dy2 = dy * dy,
0059         invsqrlen = 1.0 / (dx2 + dy2);
0060     QPointF r(dx2 * pt.x() + dy2 * snapLine.x1() + dx * dy * (pt.y() - snapLine.y1()),
0061               dx2 * snapLine.y1() + dy2 * pt.y() + dx * dy * (pt.x() - snapLine.x1()));
0062     r *= invsqrlen;
0063     return r;
0064     //return pt;
0065 }
0066 
0067 QPointF InfiniteRulerAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin, const bool /*snapToAny*/, qreal moveThresholdPt)
0068 {
0069     return project(pt, strokeBegin, true, moveThresholdPt);
0070 }
0071 
0072 void InfiniteRulerAssistant::adjustLine(QPointF &point, QPointF &strokeBegin)
0073 {
0074 
0075     point = project(point, strokeBegin, false, 0.0);
0076     strokeBegin = project(strokeBegin, strokeBegin, false, 0.0);
0077 }
0078 
0079 void InfiniteRulerAssistant::drawSubdivisions(QPainter& gc, const KisCoordinatesConverter *converter) {
0080     if (subdivisions() == 0) {
0081         return;
0082     }
0083     
0084     // Get handle positions
0085     QTransform document2widget = converter->documentToWidgetTransform();
0086     
0087     QPointF p1 = document2widget.map(*handles()[0]);
0088     QPointF p2 = document2widget.map(*handles()[1]);
0089     
0090     const qreal scale = 16.0 / 2;
0091     const qreal minorScale = scale / 2;
0092     const QRectF clipping = QRectF(gc.viewport()).adjusted(-scale, -scale, scale, scale);
0093     // If the lines would end up closer to each other than this threshold (in
0094     // screen coordinates), they are not rendered, as they wouldn't be
0095     // distinguishable anymore.
0096     const qreal threshold = 3.0;
0097     
0098     // Calculate line direction and normal vector
0099     QPointF delta = p2 - p1;
0100     qreal length = sqrt(KisPaintingAssistant::norm2(delta));
0101     qreal stepsize = length / subdivisions();
0102     
0103     // Only draw if lines are far enough apart
0104     if (stepsize >= threshold) {
0105         QPointF normal = QPointF(delta.y(), -delta.x());
0106         normal /= length;
0107         
0108         // Clip the line to the viewport and find the t parameters for these
0109         // points
0110         ClippingResult res = clipLineParametric(QLineF(p1, p2), clipping);
0111         // Abort if line is outside clipping area
0112         if (!res.intersects) {
0113             return;
0114         }
0115         // Calculate indices to start and end the subdivisions on screen by
0116         // rounding further "away" from the visible area, ensuring that all
0117         // divisions that could be visible are actually drawn
0118         int istart = (int) floor(res.tmin * subdivisions());
0119         int iend =   (int)  ceil(res.tmax * subdivisions());
0120   
0121         QPainterPath path;
0122         QPainterPath highlight;
0123         
0124         // Draw the major subdivisions
0125         for (int ii = istart; ii < iend; ++ii) {
0126             QPointF pos = p1 + delta * ((qreal)ii / subdivisions());
0127             // No additional clipping check needed, since we're already
0128             // constrained inside it by the ii values.
0129             // However, don't draw over the thicker lines where the actual
0130             // ruler is located and already drawn!
0131             if (0 <= ii && ii < subdivisions()) {
0132                 continue;
0133             }
0134             // Special case at ii == subdivs: minor subdivisions are needed
0135             // here, but not the major one, as this is the last line drawn of
0136             // the main ruler.
0137             if (ii != subdivisions()) {
0138                 // Highlight the integer multiples of the ruler length
0139                 if (ii % subdivisions() == 0) {
0140                     highlight.moveTo(pos - normal * scale);
0141                     highlight.lineTo(pos + normal * scale);
0142                 } else {
0143                     path.moveTo(pos - normal * scale);
0144                     path.lineTo(pos + normal * scale);
0145                 }
0146             }
0147   
0148             // Draw minor subdivisions, if they exist (implicit check due to
0149             // the loop bounds)
0150             // Skip if major subdivisions are too close already
0151             if (stepsize / minorSubdivisions() < threshold)
0152                 continue;
0153             // Draw minor marks in between the major ones
0154             for (int jj = 1; jj < minorSubdivisions(); ++jj) {
0155                 QPointF mpos = pos + delta * ((qreal)jj / (subdivisions() * minorSubdivisions()));
0156         
0157                 path.moveTo(mpos - normal * minorScale);
0158                 path.lineTo(mpos + normal * minorScale);
0159             }
0160         }
0161   
0162         // Draw highlight as regular path (2 px wide)
0163         drawPath(gc, highlight);
0164         // Draw normal lines as preview (1 px wide)
0165         drawPreview(gc, path);
0166     }
0167 }
0168 
0169 void InfiniteRulerAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible)
0170 {
0171     gc.save();
0172     gc.resetTransform();
0173     
0174     if (isAssistantComplete() && isSnappingActive() && previewVisible) {
0175       
0176         // Extend the line to the full viewport
0177         QTransform initialTransform = converter->documentToWidgetTransform();
0178         QLineF snapLine = QLineF(initialTransform.map(*handles()[0]), initialTransform.map(*handles()[1]));
0179         QRect viewport = gc.viewport();
0180         KisAlgebra2D::intersectLineRect(snapLine, viewport, true);
0181   
0182         // Draw as preview (thin lines)
0183         QPainterPath path;
0184         path.moveTo(snapLine.p1());
0185         path.lineTo(snapLine.p2());
0186         drawPreview(gc, path);
0187         
0188         // Add the extended subdivisions, if active
0189         // When the number of subdivisions (or minor subdivisions) is set to
0190         // 0, the respective feature is turned off and won't be rendered.
0191         if (subdivisions() > 0) {
0192             drawSubdivisions(gc, converter);
0193         }
0194     }
0195     
0196     gc.restore();
0197     
0198     RulerAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible);
0199 }
0200 
0201 InfiniteRulerAssistant::ClippingResult InfiniteRulerAssistant::clipLineParametric(QLineF line, QRectF rect, bool extendFirst, bool extendSecond) {
0202     double dx = line.x2() - line.x1();
0203     double dy = line.y2() - line.y1();
0204     
0205     double q1 = line.x1() - rect.x();
0206     double q2 = rect.x() + rect.width() - line.x1();
0207     double q3 = line.y1() - rect.y();
0208     double q4 = rect.y() + rect.height() - line.y1();
0209     
0210     QVector<double> p = QVector<double>({-dx, dx, -dy, dy});
0211     QVector<double> q = QVector<double>({q1, q2, q3, q4});
0212     
0213     double tmin = extendFirst ? -std::numeric_limits<double>::infinity() : 0.0;
0214     double tmax = extendSecond ? +std::numeric_limits<double>::infinity() : 1.0;
0215     
0216     for (int i = 0; i < p.length(); i++) {
0217         
0218         if (p[i] == 0 && q[i] < 0) {
0219             // Line is parallel to this boundary and outside of it
0220             return ClippingResult{false, 0, 0};
0221             
0222         } else if (p[i] < 0) {
0223             // Line moves into this boundary with increasing t
0224             // Set minimum t where it just comes in
0225             double t = q[i] / p[i];
0226             if (t > tmin) {
0227                 tmin = t;
0228             }
0229           
0230         } else if (p[i] > 0) {
0231             // Line moves out of this boundary with increasing t
0232             // Set maximum t where it is still inside
0233             double t = q[i] / p[i];
0234             if (t < tmax) {
0235                 tmax = t;
0236             }
0237         }
0238     }
0239     
0240     // The line intersects the rectangle if tmin < tmax.
0241     return ClippingResult{tmin < tmax, tmin, tmax};
0242 }
0243 
0244 QPointF InfiniteRulerAssistant::getDefaultEditorPosition() const
0245 {
0246     return (*handles()[0]);
0247 }
0248 
0249 bool InfiniteRulerAssistant::isAssistantComplete() const
0250 {
0251     return handles().size() >= 2;
0252 }
0253 
0254 InfiniteRulerAssistantFactory::InfiniteRulerAssistantFactory() = default;
0255 
0256 InfiniteRulerAssistantFactory::~InfiniteRulerAssistantFactory() = default;
0257 
0258 QString InfiniteRulerAssistantFactory::id() const
0259 {
0260     return "infinite ruler";
0261 }
0262 
0263 QString InfiniteRulerAssistantFactory::name() const
0264 {
0265     return i18n("Infinite Ruler");
0266 }
0267 
0268 KisPaintingAssistant* InfiniteRulerAssistantFactory::createPaintingAssistant() const
0269 {
0270     return new InfiniteRulerAssistant;
0271 }