File indexing completed on 2025-02-02 04:14:40

0001 /* This file is part of the KDE project
0002  *
0003  * SPDX-FileCopyrightText: 2006 Thorsten Zachmann <zachmann@kde.org>
0004  * SPDX-FileCopyrightText: 2008-2010 Jan Hambrecht <jaham@gmx.net>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.0-or-later
0007  */
0008 
0009 #ifndef KOCREATEPATHTOOL_P_H
0010 #define KOCREATEPATHTOOL_P_H
0011 
0012 #include <QPainterPath>
0013 
0014 #include "KoCreatePathTool.h"
0015 #include "KoPathPoint.h"
0016 #include "KoPathPointData.h"
0017 #include "KoPathPointMergeCommand.h"
0018 #include "KoParameterShape.h"
0019 #include "KoShapeManager.h"
0020 #include "KoSnapStrategy.h"
0021 #include "KoToolBase_p.h"
0022 #include <KoViewConverter.h>
0023 #include "kis_config.h"
0024 
0025 #include "math.h"
0026 
0027 class KoStrokeConfigWidget;
0028 class KoConverter;
0029 
0030 /// Small helper to keep track of a path point and its parent path shape
0031 struct PathConnectionPoint {
0032     PathConnectionPoint()
0033         : path(0), point(0) {
0034     }
0035 
0036     // reset state to invalid
0037     void reset() {
0038         path = 0;
0039         point = 0;
0040     }
0041 
0042     PathConnectionPoint& operator =(KoPathPoint * pathPoint) {
0043         if (!pathPoint || ! pathPoint->parent()) {
0044             reset();
0045         } else {
0046             path = pathPoint->parent();
0047             point = pathPoint;
0048         }
0049         return *this;
0050     }
0051 
0052     bool operator != (const PathConnectionPoint &rhs) const {
0053         return rhs.path != path || rhs.point != point;
0054     }
0055 
0056     bool operator == (const PathConnectionPoint &rhs) const {
0057         return rhs.path == path && rhs.point == point;
0058     }
0059 
0060     bool isValid() const {
0061         return path && point;
0062     }
0063 
0064     // checks if the path and point are still valid
0065     void validate(KoCanvasBase *canvas) {
0066         // no point in validating an already invalid state
0067         if (!isValid()) {
0068             return;
0069         }
0070         // we need canvas to validate
0071         if (!canvas) {
0072             reset();
0073             return;
0074         }
0075         // check if path is still part of the document
0076         if (!canvas->shapeManager()->shapes().contains(path)) {
0077             reset();
0078             return;
0079         }
0080         // check if point is still part of the path
0081         if (path->pathPointIndex(point) == KoPathPointIndex(-1, -1)) {
0082             reset();
0083             return;
0084         }
0085     }
0086 
0087     KoPathShape * path;
0088     KoPathPoint * point;
0089 };
0090 
0091 inline qreal squareDistance(const QPointF &p1, const QPointF &p2)
0092 {
0093     qreal dx = p1.x() - p2.x();
0094     qreal dy = p1.y() - p2.y();
0095     return dx * dx + dy * dy;
0096 }
0097 
0098 class AngleSnapStrategy : public KoSnapStrategy
0099 {
0100 public:
0101     explicit AngleSnapStrategy(qreal angleStep, bool active)
0102         : KoSnapStrategy(KoSnapGuide::CustomSnapping), m_angleStep(angleStep), m_active(active) {
0103     }
0104 
0105     void setStartPoint(const QPointF &startPoint) {
0106         m_startPoint = startPoint;
0107     }
0108 
0109     void setAngleStep(qreal angleStep) {
0110         m_angleStep = qAbs(angleStep);
0111     }
0112 
0113     bool snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance) override {
0114         Q_UNUSED(proxy);
0115 
0116         if (!m_active)
0117             return false;
0118 
0119         QLineF line(m_startPoint, mousePosition);
0120         qreal currentAngle = line.angle();
0121         int prevStep = qAbs(currentAngle / m_angleStep);
0122         int nextStep = prevStep + 1;
0123         qreal prevAngle = prevStep * m_angleStep;
0124         qreal nextAngle = nextStep * m_angleStep;
0125 
0126         if (qAbs(currentAngle - prevAngle) <= qAbs(currentAngle - nextAngle)) {
0127             line.setAngle(prevAngle);
0128         } else {
0129             line.setAngle(nextAngle);
0130         }
0131 
0132         qreal maxSquareSnapDistance = maxSnapDistance * maxSnapDistance;
0133         qreal snapDistance = squareDistance(mousePosition, line.p2());
0134         if (snapDistance > maxSquareSnapDistance)
0135             return false;
0136 
0137         setSnappedPosition(line.p2());
0138         return true;
0139     }
0140 
0141     QPainterPath decoration(const KoViewConverter &converter) const override {
0142         Q_UNUSED(converter);
0143 
0144         QPainterPath decoration;
0145         decoration.moveTo(m_startPoint);
0146         decoration.lineTo(snappedPosition());
0147         return decoration;
0148     }
0149 
0150     void deactivate() {
0151         m_active = false;
0152     }
0153 
0154     void activate() {
0155         m_active = true;
0156     }
0157 
0158 private:
0159     QPointF m_startPoint;
0160     qreal m_angleStep;
0161     bool m_active;
0162 };
0163 
0164 
0165 class KoCreatePathToolPrivate : public KoToolBasePrivate
0166 {
0167     KoCreatePathTool * const q;
0168 public:
0169     KoCreatePathToolPrivate(KoCreatePathTool * const qq, KoCanvasBase* canvas)
0170         : KoToolBasePrivate(qq, canvas),
0171           q(qq),
0172           shape(0),
0173           activePoint(0),
0174           firstPoint(0),
0175           handleRadius(3),
0176           decorationThickness(1),
0177           mouseOverFirstPoint(false),
0178           pointIsDragged(false),
0179           finishAfterThisPoint(false),
0180           hoveredPoint(0),
0181           angleSnapStrategy(0),
0182           angleSnappingDelta(15),
0183           angleSnapStatus(false),
0184           enableClosePathShortcut(true)
0185     {
0186     }
0187 
0188     KoPathShape *shape;
0189     KoPathPoint *activePoint;
0190     KoPathPoint *firstPoint;
0191     int handleRadius;
0192     int decorationThickness;
0193     bool mouseOverFirstPoint;
0194     bool pointIsDragged;
0195     bool finishAfterThisPoint;
0196     PathConnectionPoint existingStartPoint; ///< an existing path point we started a new path at
0197     PathConnectionPoint existingEndPoint;   ///< an existing path point we finished a new path at
0198     KoPathPoint *hoveredPoint; ///< an existing path end point the mouse is hovering on
0199     bool prevPointWasDragged = false;
0200     bool autoSmoothCurves = false;
0201 
0202     QPointF dragStartPoint;
0203 
0204     AngleSnapStrategy *angleSnapStrategy;
0205     int angleSnappingDelta;
0206     bool angleSnapStatus;
0207     bool enableClosePathShortcut;
0208 
0209     void repaintActivePoint() const {
0210         const bool isFirstPoint = (activePoint == firstPoint);
0211 
0212         if (!isFirstPoint && !pointIsDragged)
0213             return;
0214 
0215         QRectF rect = activePoint->boundingRect(false);
0216 
0217         // make sure that we have the second control point inside our
0218         // update rect, as KoPathPoint::boundingRect will not include
0219         // the second control point of the last path point if the path
0220         // is not closed
0221         const QPointF &point = activePoint->point();
0222         const QPointF &controlPoint = activePoint->controlPoint2();
0223         rect = rect.united(QRectF(point, controlPoint).normalized());
0224 
0225         // when painting the first point we want the
0226         // first control point to be painted as well
0227         if (isFirstPoint) {
0228             const QPointF &controlPoint = activePoint->controlPoint1();
0229             rect = rect.united(QRectF(point, controlPoint).normalized());
0230         }
0231 
0232         QPointF border = q->canvas()->viewConverter()
0233                          ->viewToDocument(QPointF(handleRadius, handleRadius));
0234 
0235         rect.adjust(-border.x(), -border.y(), border.x(), border.y());
0236         q->canvas()->updateCanvas(rect);
0237     }
0238 
0239     /// returns the nearest existing path point
0240     KoPathPoint* endPointAtPosition(const QPointF &position) const {
0241         QRectF roi = q->handleGrabRect(position);
0242         QList<KoShape *> shapes = q->canvas()->shapeManager()->shapesAt(roi);
0243 
0244         KoPathPoint * nearestPoint = 0;
0245         qreal minDistance = HUGE_VAL;
0246         uint grabSensitivity = q->grabSensitivity();
0247         qreal maxDistance = q->canvas()->viewConverter()->viewToDocumentX(grabSensitivity);
0248 
0249         Q_FOREACH(KoShape * s, shapes) {
0250             KoPathShape * path = dynamic_cast<KoPathShape*>(s);
0251             if (!path)
0252                 continue;
0253             KoParameterShape *paramShape = dynamic_cast<KoParameterShape*>(s);
0254             if (paramShape && paramShape->isParametricShape())
0255                 continue;
0256 
0257             KoPathPoint * p = 0;
0258             uint subpathCount = path->subpathCount();
0259             for (uint i = 0; i < subpathCount; ++i) {
0260                 if (path->isClosedSubpath(i))
0261                     continue;
0262                 p = path->pointByIndex(KoPathPointIndex(i, 0));
0263                 // check start of subpath
0264                 qreal d = squareDistance(position, path->shapeToDocument(p->point()));
0265                 if (d < minDistance && d < maxDistance) {
0266                     nearestPoint = p;
0267                     minDistance = d;
0268                 }
0269                 // check end of subpath
0270                 p = path->pointByIndex(KoPathPointIndex(i, path->subpathPointCount(i) - 1));
0271                 d = squareDistance(position, path->shapeToDocument(p->point()));
0272                 if (d < minDistance && d < maxDistance) {
0273                     nearestPoint = p;
0274                     minDistance = d;
0275                 }
0276             }
0277         }
0278 
0279         return nearestPoint;
0280     }
0281 
0282     /// Connects given path with the ones we hit when starting/finishing
0283     bool connectPaths(KoPathShape *pathShape, const PathConnectionPoint &pointAtStart, const PathConnectionPoint &pointAtEnd) const {
0284         KoPathShape * startShape = 0;
0285         KoPathShape * endShape = 0;
0286         KoPathPoint * startPoint = 0;
0287         KoPathPoint * endPoint = 0;
0288 
0289         if (pointAtStart.isValid()) {
0290             startShape = pointAtStart.path;
0291             startPoint = pointAtStart.point;
0292         }
0293         if (pointAtEnd.isValid()) {
0294             endShape = pointAtEnd.path;
0295             endPoint = pointAtEnd.point;
0296         }
0297 
0298         // at least one point must be valid
0299         if (!startPoint && !endPoint)
0300             return false;
0301         // do not allow connecting to the same point twice
0302         if (startPoint == endPoint)
0303             endPoint = 0;
0304 
0305         // we have hit an existing path point on start/finish
0306         // what we now do is:
0307         // 1. combine the new created path with the ones we hit on start/finish
0308         // 2. merge the endpoints of the corresponding subpaths
0309 
0310         uint newPointCount = pathShape->subpathPointCount(0);
0311         KoPathPointIndex newStartPointIndex(0, 0);
0312         KoPathPointIndex newEndPointIndex(0, newPointCount - 1);
0313         KoPathPoint * newStartPoint = pathShape->pointByIndex(newStartPointIndex);
0314         KoPathPoint * newEndPoint = pathShape->pointByIndex(newEndPointIndex);
0315 
0316         // combine with the path we hit on start
0317         KoPathPointIndex startIndex(-1, -1);
0318         if (startShape && startPoint) {
0319             startIndex = startShape->pathPointIndex(startPoint);
0320             pathShape->combine(startShape);
0321             pathShape->moveSubpath(0, pathShape->subpathCount() - 1);
0322         }
0323         // combine with the path we hit on finish
0324         KoPathPointIndex endIndex(-1, -1);
0325         if (endShape && endPoint) {
0326             endIndex = endShape->pathPointIndex(endPoint);
0327             if (endShape != startShape) {
0328                 endIndex.first += pathShape->subpathCount();
0329                 pathShape->combine(endShape);
0330             }
0331         }
0332         // do we connect twice to a single subpath ?
0333         bool connectToSingleSubpath = (startShape == endShape && startIndex.first == endIndex.first);
0334 
0335         if (startIndex.second == 0 && !connectToSingleSubpath) {
0336             pathShape->reverseSubpath(startIndex.first);
0337             startIndex.second = pathShape->subpathPointCount(startIndex.first) - 1;
0338         }
0339         if (endIndex.second > 0 && !connectToSingleSubpath) {
0340             pathShape->reverseSubpath(endIndex.first);
0341             endIndex.second = 0;
0342         }
0343 
0344         // after combining we have a path where with the subpaths in the following
0345         // order:
0346         // 1. the subpaths of the pathshape we started the new path at
0347         // 2. the subpath we just created
0348         // 3. the subpaths of the pathshape we finished the new path at
0349 
0350         // get the path points we want to merge, as these are not going to
0351         // change while merging
0352         KoPathPoint * existingStartPoint = pathShape->pointByIndex(startIndex);
0353         KoPathPoint * existingEndPoint = pathShape->pointByIndex(endIndex);
0354 
0355         // merge first two points
0356         if (existingStartPoint) {
0357             KoPathPointData pd1(pathShape, pathShape->pathPointIndex(existingStartPoint));
0358             KoPathPointData pd2(pathShape, pathShape->pathPointIndex(newStartPoint));
0359             KoPathPointMergeCommand cmd1(pd1, pd2);
0360             cmd1.redo();
0361         }
0362         // merge last two points
0363         if (existingEndPoint) {
0364             KoPathPointData pd3(pathShape, pathShape->pathPointIndex(newEndPoint));
0365             KoPathPointData pd4(pathShape, pathShape->pathPointIndex(existingEndPoint));
0366             KoPathPointMergeCommand cmd2(pd3, pd4);
0367             cmd2.redo();
0368         }
0369 
0370         return true;
0371     }
0372 
0373     void addPathShape() {
0374         if (!shape) return;
0375 
0376         if (shape->pointCount() < 2) {
0377             cleanUp();
0378             return;
0379         }
0380 
0381         // this is done so that nothing happens when the mouseReleaseEvent for the this event is received
0382         KoPathShape *pathShape = shape;
0383         shape = 0;
0384 
0385         q->addPathShape(pathShape);
0386 
0387         cleanUp();
0388 
0389         return;
0390     }
0391 
0392     void cleanUp() {
0393         // reset snap guide
0394         q->canvas()->updateCanvas(q->canvas()->snapGuide()->boundingRect());
0395         q->canvas()->snapGuide()->reset();
0396         angleSnapStrategy = 0;
0397 
0398         delete shape;
0399         shape = 0;
0400         existingStartPoint = 0;
0401         existingEndPoint = 0;
0402         hoveredPoint = 0;
0403         activePoint = 0;
0404     }
0405 
0406     void angleDeltaChanged(qreal value) {
0407         angleSnappingDelta = static_cast<int>(value);
0408         if (angleSnapStrategy)
0409             angleSnapStrategy->setAngleStep(angleSnappingDelta);
0410     }
0411 
0412     void autoSmoothCurvesChanged(bool value) {
0413         autoSmoothCurves = value;
0414 
0415         KisConfig cfg(false);
0416         cfg.setAutoSmoothBezierCurves(value);
0417     }
0418 
0419     void loadAutoSmoothValueFromConfig() {
0420         KisConfig cfg(true);
0421         autoSmoothCurves = cfg.autoSmoothBezierCurves();
0422 
0423         emit q->sigUpdateAutoSmoothCurvesGUI(autoSmoothCurves);
0424     }
0425 
0426     void angleSnapChanged(int angleSnap) {
0427         angleSnapStatus = ! angleSnapStatus;
0428         if (angleSnapStrategy) {
0429             if (angleSnap == Qt::Checked)
0430                 angleSnapStrategy->activate();
0431             else
0432                 angleSnapStrategy->deactivate();
0433         }
0434     }
0435 };
0436 
0437 #endif // KOCREATEPATHTOOL_P_H