File indexing completed on 2025-02-23 04:05:38

0001 /* This file is part of the KDE project
0002  * SPDX-FileCopyrightText: 2007, 2009, 2011 Jan Hambrecht <jaham@gmx.net>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "KoPencilTool.h"
0008 #include "KoCurveFit.h"
0009 
0010 #include <KoPathShape.h>
0011 #include <KoParameterShape.h>
0012 #include <KoShapeStroke.h>
0013 #include <KoPointerEvent.h>
0014 #include <KoCanvasBase.h>
0015 #include <KoShapeController.h>
0016 #include <KoShapeManager.h>
0017 #include <KoSelection.h>
0018 #include <KoCanvasResourceProvider.h>
0019 #include <KoColor.h>
0020 #include <KoPathPoint.h>
0021 #include <KoPathPointData.h>
0022 #include <KoPathPointMergeCommand.h>
0023 #include <widgets/KoStrokeConfigWidget.h>
0024 #include <KisHandlePainterHelper.h>
0025 
0026 #include <klocalizedstring.h>
0027 
0028 #include <QDoubleSpinBox>
0029 #include <QComboBox>
0030 #include <QStackedWidget>
0031 #include <QGroupBox>
0032 #include <QCheckBox>
0033 #include <QVBoxLayout>
0034 #include <QPainter>
0035 #include <QLabel>
0036 
0037 #include <math.h>
0038 
0039 #include "KoCreatePathTool_p.h"
0040 #include "kis_double_parse_spin_box.h"
0041 
0042 KoPencilTool::KoPencilTool(KoCanvasBase *canvas)
0043     : KoToolBase(canvas)
0044     , m_mode(ModeCurve)
0045     , m_optimizeRaw(false)
0046     , m_optimizeCurve(false)
0047     , m_combineAngle(15.0)
0048     , m_fittingError(5.0)
0049     , m_close(false)
0050     , m_shape(0)
0051     , m_existingStartPoint(0)
0052     , m_existingEndPoint(0)
0053     , m_hoveredPoint(0)
0054     , m_strokeWidget(0)
0055 {
0056 }
0057 
0058 KoPencilTool::~KoPencilTool()
0059 {
0060 }
0061 
0062 void KoPencilTool::paint(QPainter &painter, const KoViewConverter &converter)
0063 {
0064     if (m_shape) {
0065         painter.save();
0066 
0067         painter.setTransform(m_shape->absoluteTransformation() *
0068                              converter.documentToView() *
0069                              painter.transform());
0070 
0071         painter.save();
0072         m_shape->paint(painter);
0073         painter.restore();
0074 
0075         if (m_shape->stroke()) {
0076             painter.save();
0077             m_shape->stroke()->paint(m_shape, painter);
0078             painter.restore();
0079         }
0080 
0081         painter.restore();
0082     }
0083 
0084     if (m_hoveredPoint) {
0085         KisHandlePainterHelper helper =
0086             KoShape::createHandlePainterHelperView(&painter, m_hoveredPoint->parent(), converter, handleRadius(), decorationThickness());
0087 
0088         helper.setHandleStyle(KisHandleStyle::primarySelection());
0089         m_hoveredPoint->paint(helper, KoPathPoint::Node);
0090     }
0091 }
0092 
0093 void KoPencilTool::mousePressEvent(KoPointerEvent *event)
0094 {
0095     KoShapeStrokeSP stroke = createStroke();
0096 
0097     if (!m_shape && stroke && stroke->isVisible()) {
0098         m_shape = new KoPathShape();
0099         m_shape->setShapeId(KoPathShapeId);
0100         m_shape->setStroke(createStroke());
0101         m_points.clear();
0102 
0103         QPointF point = event->point;
0104         m_existingStartPoint = endPointAtPosition(point);
0105         if (m_existingStartPoint)
0106             point = m_existingStartPoint->parent()->shapeToDocument(m_existingStartPoint->point());
0107 
0108         addPoint(point);
0109     }
0110 }
0111 
0112 void KoPencilTool::mouseMoveEvent(KoPointerEvent *event)
0113 {
0114     if (event->buttons() & Qt::LeftButton)
0115         addPoint(event->point);
0116 
0117     KoPathPoint * endPoint = endPointAtPosition(event->point);
0118     if (m_hoveredPoint != endPoint) {
0119         if (m_hoveredPoint) {
0120             QPointF nodePos = m_hoveredPoint->parent()->shapeToDocument(m_hoveredPoint->point());
0121             canvas()->updateCanvas(handlePaintRect(nodePos));
0122         }
0123         m_hoveredPoint = endPoint;
0124         if (m_hoveredPoint) {
0125             QPointF nodePos = m_hoveredPoint->parent()->shapeToDocument(m_hoveredPoint->point());
0126             canvas()->updateCanvas(handlePaintRect(nodePos));
0127         }
0128     }
0129 }
0130 
0131 void KoPencilTool::mouseReleaseEvent(KoPointerEvent *event)
0132 {
0133     if (! m_shape)
0134         return;
0135 
0136     QPointF point = event->point;
0137     m_existingEndPoint = endPointAtPosition(point);
0138     if (m_existingEndPoint)
0139         point = m_existingEndPoint->parent()->shapeToDocument(m_existingEndPoint->point());
0140 
0141     addPoint(point);
0142     finish(event->modifiers() & Qt::ShiftModifier);
0143 
0144     m_existingStartPoint = 0;
0145     m_existingEndPoint = 0;
0146     m_hoveredPoint = 0;
0147 
0148     // the original path may be different from the one added
0149     if (canvas() && m_shape) {
0150         canvas()->updateCanvas(m_shape->boundingRect());
0151     }
0152     delete m_shape;
0153     m_shape = 0;
0154     m_points.clear();
0155 }
0156 
0157 void KoPencilTool::keyPressEvent(QKeyEvent *event)
0158 {
0159     if (m_shape) {
0160         event->accept();
0161     } else {
0162         event->ignore();
0163     }
0164 }
0165 
0166 void KoPencilTool::activate(const QSet<KoShape*> &shapes)
0167 {
0168     KoToolBase::activate(shapes);
0169 
0170     m_points.clear();
0171     m_close = false;
0172     slotUpdatePencilCursor();
0173 
0174     if (m_strokeWidget) {
0175         m_strokeWidget->activate();
0176     }
0177 }
0178 
0179 void KoPencilTool::deactivate()
0180 {
0181     m_points.clear();
0182     delete m_shape;
0183     m_shape = 0;
0184     m_existingStartPoint = 0;
0185     m_existingEndPoint = 0;
0186     m_hoveredPoint = 0;
0187 
0188     if (m_strokeWidget) {
0189         m_strokeWidget->deactivate();
0190     }
0191 
0192     KoToolBase::deactivate();
0193 }
0194 
0195 void KoPencilTool::slotUpdatePencilCursor()
0196 {
0197     KoShapeStrokeSP stroke = createStroke();
0198     useCursor((stroke && stroke->isVisible()) ? Qt::ArrowCursor : Qt::ForbiddenCursor);
0199 }
0200 
0201 void KoPencilTool::addPoint(const QPointF & point)
0202 {
0203     if (! m_shape)
0204         return;
0205 
0206     // do a moveTo for the first point added
0207     if (m_points.empty())
0208         m_shape->moveTo(point);
0209     // do not allow coincident points
0210     else if (point != m_points.last())
0211         m_shape->lineTo(point);
0212     else
0213         return;
0214 
0215     m_points.append(point);
0216     canvas()->updateCanvas(m_shape->boundingRect());
0217 }
0218 
0219 qreal KoPencilTool::lineAngle(const QPointF &p1, const QPointF &p2)
0220 {
0221     qreal angle = atan2(p2.y() - p1.y(), p2.x() - p1.x());
0222     if (angle < 0.0)
0223         angle += 2 * M_PI;
0224 
0225     return angle * 180.0 / M_PI;
0226 }
0227 
0228 void KoPencilTool::finish(bool closePath)
0229 {
0230     if (m_points.count() < 2)
0231         return;
0232 
0233     KoPathShape * path = 0;
0234     QList<QPointF> complete;
0235     QList<QPointF> *points = &m_points;
0236 
0237     if (m_mode == ModeStraight || m_optimizeRaw || m_optimizeCurve) {
0238         float combineAngle;
0239 
0240         if (m_mode == ModeStraight)
0241             combineAngle = m_combineAngle;
0242         else
0243             combineAngle = 0.50f;
0244 
0245         //Add the first two points
0246         complete.append(m_points[0]);
0247         complete.append(m_points[1]);
0248 
0249         //Now we need to get the angle of the first line
0250         float lastAngle = lineAngle(complete[0], complete[1]);
0251 
0252         uint pointCount = m_points.count();
0253         for (uint i = 2; i < pointCount; ++i) {
0254             float angle = lineAngle(complete.last(), m_points[i]);
0255             if (qAbs(angle - lastAngle) < combineAngle)
0256                 complete.removeLast();
0257             complete.append(m_points[i]);
0258             lastAngle = angle;
0259         }
0260 
0261         m_points.clear();
0262         points = &complete;
0263     }
0264 
0265     switch (m_mode) {
0266     case ModeCurve: {
0267         path = bezierFit(*points, m_fittingError);
0268     }
0269     break;
0270     case ModeStraight:
0271     case ModeRaw: {
0272         path = new KoPathShape();
0273         uint pointCount = points->count();
0274         path->moveTo(points->at(0));
0275         for (uint i = 1; i < pointCount; ++i)
0276             path->lineTo(points->at(i));
0277     }
0278     break;
0279     }
0280 
0281     if (! path)
0282         return;
0283 
0284     path->setShapeId(KoPathShapeId);
0285     path->setStroke(createStroke());
0286     addPathShape(path, closePath);
0287 }
0288 
0289 QList<QPointer<QWidget> > KoPencilTool::createOptionWidgets()
0290 {
0291     QList<QPointer<QWidget> > widgets;
0292     QWidget *optionWidget = new QWidget();
0293     QVBoxLayout * layout = new QVBoxLayout(optionWidget);
0294 
0295     QHBoxLayout *modeLayout = new QHBoxLayout;
0296     modeLayout->setSpacing(3);
0297     QLabel *modeLabel = new QLabel(i18n("Precision:"), optionWidget);
0298     QComboBox * modeBox = new QComboBox(optionWidget);
0299     modeBox->addItem(i18nc("The raw line data", "Raw"));
0300     modeBox->addItem(i18n("Curve"));
0301     modeBox->addItem(i18n("Straight"));
0302     modeLayout->addWidget(modeLabel);
0303     modeLayout->addWidget(modeBox, 1);
0304     layout->addLayout(modeLayout);
0305 
0306     QStackedWidget * stackedWidget = new QStackedWidget(optionWidget);
0307 
0308     QWidget * rawBox = new QWidget(stackedWidget);
0309     QVBoxLayout * rawLayout = new QVBoxLayout(rawBox);
0310     QCheckBox * optimizeRaw = new QCheckBox(i18n("Optimize"), rawBox);
0311     rawLayout->addWidget(optimizeRaw);
0312     rawLayout->setContentsMargins(0, 0, 0, 0);
0313 
0314     QWidget * curveBox = new QWidget(stackedWidget);
0315     QHBoxLayout * curveLayout = new QHBoxLayout(curveBox);
0316     QCheckBox * optimizeCurve = new QCheckBox(i18n("Optimize"), curveBox);
0317     QDoubleSpinBox * fittingError = new KisDoubleParseSpinBox(curveBox);
0318     fittingError->setValue(0.50);
0319     fittingError->setMaximum(400.0);
0320     fittingError->setMinimum(0.0);
0321     fittingError->setSingleStep(m_fittingError);
0322     fittingError->setToolTip(i18n("Exactness:"));
0323     curveLayout->addWidget(optimizeCurve);
0324     curveLayout->addWidget(fittingError);
0325     curveLayout->setContentsMargins(0, 0, 0, 0);
0326 
0327     QWidget *straightBox = new QWidget(stackedWidget);
0328     QVBoxLayout *straightLayout = new QVBoxLayout(straightBox);
0329     QDoubleSpinBox *combineAngle = new KisDoubleParseSpinBox(straightBox);
0330     combineAngle->setValue(0.50);
0331     combineAngle->setMaximum(360.0);
0332     combineAngle->setMinimum(0.0);
0333     combineAngle->setSingleStep(m_combineAngle);
0334     combineAngle->setSuffix(" deg");
0335     // QT5TODO
0336     //combineAngle->setLabel(i18n("Combine angle:"), Qt::AlignLeft | Qt::AlignVCenter);
0337     straightLayout->addWidget(combineAngle);
0338     straightLayout->setContentsMargins(0, 0, 0, 0);
0339 
0340     stackedWidget->addWidget(rawBox);
0341     stackedWidget->addWidget(curveBox);
0342     stackedWidget->addWidget(straightBox);
0343     layout->addWidget(stackedWidget);
0344     layout->addStretch(1);
0345 
0346     connect(modeBox, SIGNAL(activated(int)), stackedWidget, SLOT(setCurrentIndex(int)));
0347     connect(modeBox, SIGNAL(activated(int)), this, SLOT(selectMode(int)));
0348     connect(optimizeRaw, SIGNAL(stateChanged(int)), this, SLOT(setOptimize(int)));
0349     connect(optimizeCurve, SIGNAL(stateChanged(int)), this, SLOT(setOptimize(int)));
0350     connect(fittingError, SIGNAL(valueChanged(double)), this, SLOT(setDelta(double)));
0351     connect(combineAngle, SIGNAL(valueChanged(double)), this, SLOT(setDelta(double)));
0352 
0353     modeBox->setCurrentIndex(m_mode);
0354     stackedWidget->setCurrentIndex(m_mode);
0355     optionWidget->setObjectName(i18n("Pencil"));
0356     optionWidget->setWindowTitle(i18n("Pencil"));
0357     widgets.append(optionWidget);
0358 
0359     m_strokeWidget = new KoStrokeConfigWidget(canvas(), 0);
0360     m_strokeWidget->setNoSelectionTrackingMode(true);
0361     m_strokeWidget->setWindowTitle(i18n("Line"));
0362     connect(m_strokeWidget, SIGNAL(sigStrokeChanged()), SLOT(slotUpdatePencilCursor()));
0363     if (isActivated()) {
0364         m_strokeWidget->activate();
0365     }
0366     widgets.append(m_strokeWidget);
0367     return widgets;
0368 }
0369 
0370 void KoPencilTool::addPathShape(KoPathShape* path, bool closePath)
0371 {
0372     KoShape * startShape = 0;
0373     KoShape * endShape = 0;
0374 
0375     if (closePath) {
0376         path->close();
0377         path->normalize();
0378     } else {
0379         path->normalize();
0380         if (connectPaths(path, m_existingStartPoint, m_existingEndPoint)) {
0381             if (m_existingStartPoint)
0382                 startShape = m_existingStartPoint->parent();
0383             if (m_existingEndPoint && m_existingEndPoint != m_existingStartPoint)
0384                 endShape = m_existingEndPoint->parent();
0385         }
0386     }
0387 
0388     KUndo2Command * cmd = canvas()->shapeController()->addShape(path, 0);
0389     if (cmd) {
0390         KoSelection *selection = canvas()->shapeManager()->selection();
0391         selection->deselectAll();
0392         selection->select(path);
0393 
0394         if (startShape)
0395             canvas()->shapeController()->removeShape(startShape, cmd);
0396         if (endShape && startShape != endShape)
0397             canvas()->shapeController()->removeShape(endShape, cmd);
0398 
0399         canvas()->addCommand(cmd);
0400     } else {
0401         canvas()->updateCanvas(path->boundingRect());
0402         delete path;
0403     }
0404 }
0405 
0406 void KoPencilTool::selectMode(int mode)
0407 {
0408     m_mode = static_cast<PencilMode>(mode);
0409 }
0410 
0411 void KoPencilTool::setOptimize(int state)
0412 {
0413     if (m_mode == ModeRaw)
0414         m_optimizeRaw = state == Qt::Checked ? true : false;
0415     else
0416         m_optimizeCurve = state == Qt::Checked ? true : false;
0417 }
0418 
0419 void KoPencilTool::setDelta(double delta)
0420 {
0421     if (m_mode == ModeCurve)
0422         m_fittingError = delta;
0423     else if (m_mode == ModeStraight)
0424         m_combineAngle = delta;
0425 }
0426 
0427 KoShapeStrokeSP KoPencilTool::createStroke()
0428 {
0429     KoShapeStrokeSP stroke;
0430     if (m_strokeWidget) {
0431         stroke = m_strokeWidget->createShapeStroke();
0432     }
0433     return stroke;
0434 }
0435 
0436 KoPathShape * KoPencilTool::path()
0437 {
0438     return m_shape;
0439 }
0440 
0441 KoPathPoint* KoPencilTool::endPointAtPosition(const QPointF &position)
0442 {
0443     QRectF roi = handleGrabRect(position);
0444     QList<KoShape *> shapes = canvas()->shapeManager()->shapesAt(roi);
0445 
0446     KoPathPoint * nearestPoint = 0;
0447     qreal minDistance = HUGE_VAL;
0448     qreal maxDistance = canvas()->viewConverter()->viewToDocumentX(grabSensitivity());
0449 
0450     Q_FOREACH(KoShape * shape, shapes) {
0451         KoPathShape * path = dynamic_cast<KoPathShape*>(shape);
0452         if (!path)
0453             continue;
0454         KoParameterShape *paramShape = dynamic_cast<KoParameterShape*>(shape);
0455         if (paramShape && paramShape->isParametricShape())
0456             continue;
0457 
0458         KoPathPoint * p = 0;
0459         uint subpathCount = path->subpathCount();
0460         for (uint i = 0; i < subpathCount; ++i) {
0461             if (path->isClosedSubpath(i))
0462                 continue;
0463             p = path->pointByIndex(KoPathPointIndex(i, 0));
0464             // check start of subpath
0465             qreal d = squareDistance(position, path->shapeToDocument(p->point()));
0466             if (d < minDistance && d < maxDistance) {
0467                 nearestPoint = p;
0468                 minDistance = d;
0469             }
0470             // check end of subpath
0471             p = path->pointByIndex(KoPathPointIndex(i, path->subpathPointCount(i) - 1));
0472             d = squareDistance(position, path->shapeToDocument(p->point()));
0473             if (d < minDistance && d < maxDistance) {
0474                 nearestPoint = p;
0475                 minDistance = d;
0476             }
0477         }
0478     }
0479 
0480     return nearestPoint;
0481 }
0482 
0483 bool KoPencilTool::connectPaths(KoPathShape *pathShape, KoPathPoint *pointAtStart, KoPathPoint *pointAtEnd)
0484 {
0485     // at least one point must be valid
0486     if (!pointAtStart && !pointAtEnd)
0487         return false;
0488     // do not allow connecting to the same point twice
0489     if (pointAtStart == pointAtEnd)
0490         pointAtEnd = 0;
0491 
0492     // we have hit an existing path point on start/finish
0493     // what we now do is:
0494     // 1. combine the new created path with the ones we hit on start/finish
0495     // 2. merge the endpoints of the corresponding subpaths
0496 
0497     uint newPointCount = pathShape->subpathPointCount(0);
0498     KoPathPointIndex newStartPointIndex(0, 0);
0499     KoPathPointIndex newEndPointIndex(0, newPointCount - 1);
0500     KoPathPoint * newStartPoint = pathShape->pointByIndex(newStartPointIndex);
0501     KoPathPoint * newEndPoint = pathShape->pointByIndex(newEndPointIndex);
0502 
0503     KoPathShape * startShape = pointAtStart ? pointAtStart->parent() : 0;
0504     KoPathShape * endShape = pointAtEnd ? pointAtEnd->parent() : 0;
0505 
0506     // combine with the path we hit on start
0507     KoPathPointIndex startIndex(-1, -1);
0508     if (pointAtStart) {
0509         startIndex = startShape->pathPointIndex(pointAtStart);
0510         pathShape->combine(startShape);
0511         pathShape->moveSubpath(0, pathShape->subpathCount() - 1);
0512     }
0513     // combine with the path we hit on finish
0514     KoPathPointIndex endIndex(-1, -1);
0515     if (pointAtEnd) {
0516         endIndex = endShape->pathPointIndex(pointAtEnd);
0517         if (endShape != startShape) {
0518             endIndex.first += pathShape->subpathCount();
0519             pathShape->combine(endShape);
0520         }
0521     }
0522     // do we connect twice to a single subpath ?
0523     bool connectToSingleSubpath = (startShape == endShape && startIndex.first == endIndex.first);
0524 
0525     if (startIndex.second == 0 && !connectToSingleSubpath) {
0526         pathShape->reverseSubpath(startIndex.first);
0527         startIndex.second = pathShape->subpathPointCount(startIndex.first) - 1;
0528     }
0529     if (endIndex.second > 0 && !connectToSingleSubpath) {
0530         pathShape->reverseSubpath(endIndex.first);
0531         endIndex.second = 0;
0532     }
0533 
0534     // after combining we have a path where with the subpaths in the following
0535     // order:
0536     // 1. the subpaths of the pathshape we started the new path at
0537     // 2. the subpath we just created
0538     // 3. the subpaths of the pathshape we finished the new path at
0539 
0540     // get the path points we want to merge, as these are not going to
0541     // change while merging
0542     KoPathPoint * existingStartPoint = pathShape->pointByIndex(startIndex);
0543     KoPathPoint * existingEndPoint = pathShape->pointByIndex(endIndex);
0544 
0545     // merge first two points
0546     if (existingStartPoint) {
0547         KoPathPointData pd1(pathShape, pathShape->pathPointIndex(existingStartPoint));
0548         KoPathPointData pd2(pathShape, pathShape->pathPointIndex(newStartPoint));
0549         KoPathPointMergeCommand cmd1(pd1, pd2);
0550         cmd1.redo();
0551     }
0552     // merge last two points
0553     if (existingEndPoint) {
0554         KoPathPointData pd3(pathShape, pathShape->pathPointIndex(newEndPoint));
0555         KoPathPointData pd4(pathShape, pathShape->pathPointIndex(existingEndPoint));
0556         KoPathPointMergeCommand cmd2(pd3, pd4);
0557         cmd2.redo();
0558     }
0559 
0560     return true;
0561 }
0562 
0563 qreal KoPencilTool::getFittingError()
0564 {
0565     return this->m_fittingError;
0566 }
0567 
0568 void KoPencilTool::setFittingError(qreal fittingError)
0569 {
0570     this->m_fittingError = fittingError;
0571 }