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 }