File indexing completed on 2024-06-16 04:15:52

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 "SplineAssistant.h"
0010 
0011 #include <klocalizedstring.h>
0012 
0013 #include <QPainter>
0014 #include <QPainterPath>
0015 #include <QLinearGradient>
0016 #include <QTransform>
0017 
0018 #include <kis_canvas2.h>
0019 #include <kis_coordinates_converter.h>
0020 #include "kis_debug.h"
0021 #include "KisBezierUtils.h"
0022 
0023 #include <math.h>
0024 #include <limits>
0025 #include <algorithm>
0026 
0027 struct GoldenSearchParams
0028 {
0029     GoldenSearchParams(qreal lbound
0030                 ,qreal ubound)
0031         :lbound(lbound)
0032         ,ubound(ubound)
0033     {
0034         samples = QVector<GoldenSearchPoint>(4);
0035     }
0036 
0037     struct GoldenSearchPoint {
0038         GoldenSearchPoint(qreal xval)
0039             : x(xval)
0040         {
0041         }
0042 
0043         GoldenSearchPoint(){}
0044 
0045         void inv_norm(qreal l, qreal u)
0046         {
0047             this->xnorm = this->x * (u - l) + l;
0048         }
0049 
0050         qreal fval;
0051         qreal xnorm;
0052         qreal x;
0053     };
0054 
0055 
0056     qreal lbound;
0057     qreal ubound;
0058     QVector<GoldenSearchPoint> samples;
0059 };
0060 
0061 
0062 struct SplineAssistant::Private {
0063     Private();
0064 
0065     QPointF prevStrokebegin;
0066     qreal prev_t {0};
0067 };
0068 
0069 SplineAssistant::Private::Private()
0070     : prevStrokebegin(0,0)
0071 {
0072 }
0073 
0074 SplineAssistant::SplineAssistant()
0075     : KisPaintingAssistant("spline", i18n("Spline assistant"))
0076     , m_d(new Private)
0077 {
0078 }
0079 
0080 SplineAssistant::SplineAssistant(const SplineAssistant &rhs, QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap)
0081     : KisPaintingAssistant(rhs, handleMap)
0082     , m_canvas(rhs.m_canvas)
0083     , m_d(new Private)
0084 {
0085 }
0086 
0087 KisPaintingAssistantSP SplineAssistant::clone(QMap<KisPaintingAssistantHandleSP, KisPaintingAssistantHandleSP> &handleMap) const
0088 {
0089     return KisPaintingAssistantSP(new SplineAssistant(*this, handleMap));
0090 }
0091 
0092 // parametric form of a cubic spline (B(t) = (1-t)^3 P0 + 3 (1-t)^2 t P1 + 3 (1-t) t^2 P2 + t^3 P3)
0093 inline QPointF B(qreal t, const QPointF& P0, const QPointF& P1, const QPointF& P2, const QPointF& P3)
0094 {
0095     const qreal tp = 1 - t;
0096     const qreal tp2 = tp * tp;
0097     const qreal t2 = t * t;
0098 
0099     return  (    tp2 * tp) * P0 +
0100             (3 * tp2 * t ) * P1 +
0101             (3 * tp  * t2) * P2 +
0102             (    t   * t2) * P3;
0103 }
0104 // squared distance from a point on the spline to given point: we want to minimize this
0105 inline qreal D(qreal t, const QPointF& P0, const QPointF& P1, const QPointF& P2, const QPointF& P3, const QPointF& p)
0106 {
0107     const qreal
0108             tp =  1 - t,
0109             tp2 = tp * tp,
0110             t2 =  t * t,
0111             a =   tp2 * tp,
0112             b =   3 * tp2 * t,
0113             c =   3 * tp  * t2,
0114             d =   t   * t2,
0115             x_dist = a*P0.x() + b*P1.x() + c*P2.x() + d*P3.x() - p.x(),
0116             y_dist = a*P0.y() + b*P1.y() + c*P2.y() + d*P3.y() - p.y();
0117 
0118     return x_dist * x_dist + y_dist * y_dist;
0119 }
0120 
0121 
0122 inline qreal goldenSearch(const QPointF& pt
0123                           , const QList<KisPaintingAssistantHandleSP> handles
0124                           , qreal low
0125                           , qreal high
0126                           , qreal tolerance
0127                           , uint max_iter)
0128 {
0129     GoldenSearchParams ovalues = GoldenSearchParams(low,high);
0130     QVector<GoldenSearchParams::GoldenSearchPoint> p = ovalues.samples;
0131     qreal u = ovalues.ubound;
0132     qreal l = ovalues.lbound;
0133 
0134     const qreal ratio = 1 - 2/(1 + sqrt(5));
0135     p[0].x = 0;
0136     p[1].x = ratio;
0137     p[2].x = 1 - p[1].x;
0138     p[3].x = 1;
0139 
0140     p[1].inv_norm(l,u);
0141     p[2].inv_norm(l,u);
0142 
0143     p[1].fval = D(p[1].xnorm, *handles[0], *handles[2], *handles[3], *handles[1], pt);
0144     p[2].fval = D(p[2].xnorm, *handles[0], *handles[2], *handles[3], *handles[1], pt);
0145 
0146     GoldenSearchParams::GoldenSearchPoint xtemp = GoldenSearchParams::GoldenSearchPoint(0);
0147 
0148     uint i = 0; // used to force early exit
0149     while ( qAbs(p[2].xnorm - p[1].xnorm) > tolerance && i < max_iter) {
0150 
0151         if (p[1].fval < p[2].fval) {
0152             xtemp = p[1];
0153             p[3] = p[2];
0154             p[1].x = p[0].x + (p[2].x - p[1].x);
0155             p[2] = xtemp;
0156 
0157             p[1].inv_norm(l,u);
0158             p[1].fval = D(p[1].xnorm, *handles[0], *handles[2], *handles[3], *handles[1], pt);
0159 
0160         } else {
0161             xtemp = p[2];
0162             p[0] = p[1];
0163             p[2].x = p[1].x + (p[3].x - p[2].x);
0164             p[1] = xtemp;
0165 
0166             p[2].inv_norm(l,u);
0167             p[2].fval = D(p[2].xnorm, *handles[0], *handles[2], *handles[3], *handles[1], pt);
0168 
0169         }
0170         i++;
0171     }
0172     return (p[2].xnorm + p[1].xnorm) / 2;
0173 }
0174 
0175 
0176 QPointF SplineAssistant::project(const QPointF& pt, const QPointF& strokeBegin) const
0177 {
0178     Q_ASSERT(isAssistantComplete());
0179 
0180     // minimize d(t), but keep t in the same neighbourhood as before (unless starting a new stroke)
0181     bool stayClose = (m_d->prevStrokebegin == strokeBegin)? true : false;
0182     qreal min_t;
0183 
0184     QList<QPointF> refs;
0185     QVector<int> hindex = {0,2,3,1}; // order handles as expected by KisBezierUtils
0186     Q_FOREACH(int i, hindex) {
0187         refs.append(*handles()[i]);
0188     }
0189 
0190     if (stayClose){
0191         // Search in the vicinity of previous t value.
0192         // This ensure unimodality for proper goldenSearch algorithm
0193         qreal delta = 1/10.0;
0194         qreal lbound = qBound(0.0,1.0, m_d->prev_t - delta);
0195         qreal ubound = qBound(0.0,1.0, m_d->prev_t + delta);
0196         min_t = goldenSearch(pt,handles(), lbound , ubound, 1e-6,1e+2);
0197 
0198     } else {
0199         min_t = KisBezierUtils::nearestPoint(refs,pt);
0200     }
0201 
0202     QPointF draw_pos = B(min_t, *handles()[0], *handles()[2], *handles()[3], *handles()[1]);
0203 
0204     m_d->prev_t = min_t;
0205     m_d->prevStrokebegin = strokeBegin;
0206 
0207     return draw_pos;
0208 }
0209 
0210 QPointF SplineAssistant::adjustPosition(const QPointF& pt, const QPointF& strokeBegin, const bool /*snapToAny*/, qreal /*moveThresholdPt*/)
0211 {
0212     return project(pt, strokeBegin);
0213 }
0214 
0215 void SplineAssistant::adjustLine(QPointF &point, QPointF &strokeBegin)
0216 {
0217     point = QPointF();
0218     strokeBegin = QPointF();
0219 }
0220 
0221 void SplineAssistant::drawAssistant(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter* converter, bool cached, KisCanvas2* canvas, bool assistantVisible, bool previewVisible)
0222 {
0223     gc.save();
0224     gc.resetTransform();
0225     QPoint mousePos;
0226     
0227     if (canvas){
0228         //simplest, cheapest way to get the mouse-position//
0229         mousePos= canvas->canvasWidget()->mapFromGlobal(QCursor::pos());
0230         m_canvas = canvas;
0231     }
0232     else {
0233         //...of course, you need to have access to a canvas-widget for that.//
0234         mousePos = QCursor::pos();//this'll give an offset//
0235         dbgFile<<"canvas does not exist in spline, you may have passed arguments incorrectly:"<<canvas;
0236     }
0237     
0238 
0239     if (handles().size() > 1) {
0240 
0241         QTransform initialTransform = converter->documentToWidgetTransform();
0242 
0243         // first we find the path that our point create.
0244         QPointF pts[4];
0245         pts[0] = *handles()[0];
0246         pts[1] = *handles()[1];
0247         pts[2] = (handles().size() >= 3) ? (*handles()[2]) : (*handles()[0]);
0248         pts[3] = (handles().size() >= 4) ? (*handles()[3]) : (handles().size() >= 3) ? (*handles()[2]) : (*handles()[1]);
0249         gc.setTransform(initialTransform);
0250 
0251         // Draw the spline
0252         QPainterPath path;
0253         path.moveTo(pts[0]);
0254         path.cubicTo(pts[2], pts[3], pts[1]);
0255         
0256         //then we use this path to check the bounding rectangle//
0257         if (isSnappingActive() && path.boundingRect().contains(initialTransform.inverted().map(mousePos)) && previewVisible==true){
0258             drawPreview(gc, path);//and we draw the preview.
0259         }
0260     }
0261     gc.restore();
0262 
0263    // there is some odd rectangle that is getting rendered when there is only one point, so don't start rendering the line until after 2
0264    // this issue only exists with this spline assistant...none of the others
0265    if (handles().size() > 2) {
0266       KisPaintingAssistant::drawAssistant(gc, updateRect, converter, cached, canvas, assistantVisible, previewVisible);
0267    }
0268 }
0269 
0270 void SplineAssistant::drawCache(QPainter& gc, const KisCoordinatesConverter *converter, bool assistantVisible)
0271 {
0272     if (assistantVisible == false || handles().size() < 2 ){
0273         return;
0274     }
0275 
0276     QTransform initialTransform = converter->documentToWidgetTransform();
0277 
0278     QPointF pts[4];
0279     pts[0] = *handles()[0];
0280     pts[1] = *handles()[1];
0281     pts[2] = (handles().size() >= 3) ? (*handles()[2]) : (*handles()[0]);
0282     pts[3] = (handles().size() >= 4) ? (*handles()[3]) : (handles().size() >= 3) ? (*handles()[2]) : (*handles()[1]);
0283 
0284     gc.setTransform(initialTransform);
0285 
0286 
0287     {  // Draw bezier handles control lines only if we are editing the assistant
0288         gc.save();
0289         QColor assistantColor = effectiveAssistantColor();
0290         QPen bezierlinePen(assistantColor);
0291         bezierlinePen.setStyle(Qt::DotLine);
0292         bezierlinePen.setWidth(2);
0293 
0294         if (m_canvas->paintingAssistantsDecoration()->isEditingAssistants()) {
0295 
0296             if (!isSnappingActive()) {
0297                 QColor snappingColor = assistantColor;
0298                 snappingColor.setAlpha(snappingColor.alpha() * 0.2);
0299 
0300                 bezierlinePen.setColor(snappingColor);
0301             }
0302             bezierlinePen.setCosmetic(true);
0303 
0304             gc.setPen(bezierlinePen);
0305             gc.drawLine(pts[0], pts[2]);
0306 
0307             if (isAssistantComplete()) {
0308                 gc.drawLine(pts[1], pts[3]);
0309             }
0310             gc.setPen(QColor(0, 0, 0, 125));
0311         }
0312         gc.restore();
0313     }
0314 
0315 
0316     // Draw the spline
0317     QPainterPath path;
0318     path.moveTo(pts[0]);
0319     path.cubicTo(pts[2], pts[3], pts[1]);
0320     drawPath(gc, path, isSnappingActive());
0321 
0322 
0323 }
0324 
0325 QPointF SplineAssistant::getDefaultEditorPosition() const
0326 {
0327     return B(0.5, *handles()[0], *handles()[2], *handles()[3], *handles()[1]);
0328 }
0329 
0330 bool SplineAssistant::isAssistantComplete() const
0331 {
0332     return handles().size() >= 4; // specify 4 corners to make assistant complete
0333 }
0334 
0335 SplineAssistantFactory::SplineAssistantFactory()
0336 {
0337 }
0338 
0339 SplineAssistantFactory::~SplineAssistantFactory()
0340 {
0341 }
0342 
0343 QString SplineAssistantFactory::id() const
0344 {
0345     return "spline";
0346 }
0347 
0348 QString SplineAssistantFactory::name() const
0349 {
0350     return i18nc("A type of drawing assistants", "Spline");
0351 }
0352 
0353 KisPaintingAssistant* SplineAssistantFactory::createPaintingAssistant() const
0354 {
0355     return new SplineAssistant;
0356 }