Krita Source Code Documentation
Loading...
Searching...
No Matches
KoPencilTool.cpp
Go to the documentation of this file.
1/* This file is part of the KDE project
2 * SPDX-FileCopyrightText: 2007, 2009, 2011 Jan Hambrecht <jaham@gmx.net>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7#include "KoPencilTool.h"
8#include "KoCurveFit.h"
9
10#include <KoPathShape.h>
11#include <KoParameterShape.h>
12#include <KoShapeStroke.h>
13#include <KoPointerEvent.h>
14#include <KoCanvasBase.h>
15#include <KoShapeController.h>
16#include <KoShapeManager.h>
17#include <KoSelection.h>
19#include <KoColor.h>
20#include <KoPathPoint.h>
21#include <KoPathPointData.h>
25
26#include <klocalizedstring.h>
27
28#include <QDoubleSpinBox>
29#include <QComboBox>
30#include <QStackedWidget>
31#include <QGroupBox>
32#include <QCheckBox>
33#include <QVBoxLayout>
34#include <QPainter>
35#include <QLabel>
36#include <QKeyEvent>
37
38#include <math.h>
40#include "KoCreatePathTool_p.h"
42
47
51
52void KoPencilTool::paint(QPainter &painter, const KoViewConverter &converter)
53{
54 if (m_shape) {
55 painter.save();
56
57 painter.setTransform(m_shape->absoluteTransformation() *
58 converter.documentToView() *
59 painter.transform());
60
61 painter.save();
62 m_shape->paint(painter);
63 painter.restore();
64
65 if (m_shape->stroke()) {
66 painter.save();
67 m_shape->stroke()->paint(m_shape, painter);
68 painter.restore();
69 }
70
71 painter.restore();
72 }
73
74 if (m_hoveredPoint) {
77
80 }
81}
82
84{
86
87 if (!m_shape && stroke && stroke->isVisible()) {
88 m_shape = new KoPathShape();
91 m_points.clear();
92
93 QPointF point = event->point;
97
98 addPoint(point);
99 }
100}
101
103{
104 if (event->buttons() & Qt::LeftButton)
105 addPoint(event->point);
106
107 KoPathPoint * endPoint = endPointAtPosition(event->point);
108 if (m_hoveredPoint != endPoint) {
109 if (m_hoveredPoint) {
110 QPointF nodePos = m_hoveredPoint->parent()->shapeToDocument(m_hoveredPoint->point());
112 }
113 m_hoveredPoint = endPoint;
114 if (m_hoveredPoint) {
115 QPointF nodePos = m_hoveredPoint->parent()->shapeToDocument(m_hoveredPoint->point());
117 }
118 }
119}
120
122{
123 if (! m_shape)
124 return;
125
126 QPointF point = event->point;
130
131 addPoint(point);
132 finish(event->modifiers() & Qt::ShiftModifier);
133
136 m_hoveredPoint = 0;
137
138 // the original path may be different from the one added
139 if (canvas() && m_shape) {
141 }
142 delete m_shape;
143 m_shape = 0;
144 m_points.clear();
145}
146
147void KoPencilTool::keyPressEvent(QKeyEvent *event)
148{
149 if (m_shape) {
150 event->accept();
151 } else {
152 event->ignore();
153 }
154}
155
156void KoPencilTool::activate(const QSet<KoShape*> &shapes)
157{
158 KoToolBase::activate(shapes);
159
160 m_points.clear();
161 m_close = false;
163
164 if (m_strokeWidget) {
166 }
167 m_configGroup = KSharedConfig::openConfig()->group(toolId());
168}
169
171{
172 m_points.clear();
173 delete m_shape;
174 m_shape = 0;
177 m_hoveredPoint = 0;
178
179 if (m_strokeWidget) {
181 }
182
184}
185
187{
188 KoShapeStrokeSP stroke = createStroke();
189 useCursor((stroke && stroke->isVisible()) ? Qt::ArrowCursor : Qt::ForbiddenCursor);
190}
191
192void KoPencilTool::addPoint(const QPointF & point)
193{
194 if (! m_shape)
195 return;
196
197 // do a moveTo for the first point added
198 if (m_points.empty())
199 m_shape->moveTo(point);
200 // do not allow coincident points
201 else if (point != m_points.last())
202 m_shape->lineTo(point);
203 else
204 return;
205
206 m_points.append(point);
208}
209
210qreal KoPencilTool::lineAngle(const QPointF &p1, const QPointF &p2)
211{
212 qreal angle = atan2(p2.y() - p1.y(), p2.x() - p1.x());
213 if (angle < 0.0)
214 angle += 2 * M_PI;
215
216 return angle * 180.0 / M_PI;
217}
218
219void KoPencilTool::finish(bool closePath)
220{
221 if (m_points.count() < 2)
222 return;
223
224 KoPathShape * path = 0;
225 QList<QPointF> complete;
226 QList<QPointF> *points = &m_points;
227
229 float combineAngle;
230
231 if (m_mode == ModeStraight)
232 combineAngle = m_combineAngle;
233 else
234 combineAngle = 0.50f;
235
236 //Add the first two points
237 complete.append(m_points[0]);
238 complete.append(m_points[1]);
239
240 //Now we need to get the angle of the first line
241 float lastAngle = lineAngle(complete[0], complete[1]);
242
243 uint pointCount = m_points.count();
244 for (uint i = 2; i < pointCount; ++i) {
245 float angle = lineAngle(complete.last(), m_points[i]);
246 if (qAbs(angle - lastAngle) < combineAngle)
247 complete.removeLast();
248 complete.append(m_points[i]);
249 lastAngle = angle;
250 }
251
252 m_points.clear();
253 points = &complete;
254 }
255
256 switch (m_mode) {
257 case ModeCurve: {
258 path = bezierFit(*points, m_fittingError);
259 }
260 break;
261 case ModeStraight:
262 case ModeRaw: {
263 path = new KoPathShape();
264 uint pointCount = points->count();
265 path->moveTo(points->at(0));
266 for (uint i = 1; i < pointCount; ++i)
267 path->lineTo(points->at(i));
268 }
269 break;
270 }
271
272 if (! path)
273 return;
274
275 path->setShapeId(KoPathShapeId);
276 path->setStroke(createStroke());
277 addPathShape(path, closePath);
278}
279
281{
282 m_mode = static_cast<PencilMode>(m_configGroup.readEntry<int>("pencilMode", m_mode));
283 m_optimizeRaw = m_configGroup.readEntry<bool>("optimizeRaw", m_optimizeRaw);
284 m_optimizeCurve = m_configGroup.readEntry<bool>("optimizeCurve", m_optimizeCurve);
285 m_combineAngle = m_configGroup.readEntry<qreal>("combineAngle", m_combineAngle);
286 m_fittingError = m_configGroup.readEntry<qreal>("fittingError", m_fittingError);
287
288 QList<QPointer<QWidget> > widgets;
289 QWidget *optionWidget = new QWidget();
290 QVBoxLayout * layout = new QVBoxLayout(optionWidget);
291
292 QHBoxLayout *modeLayout = new QHBoxLayout;
293 modeLayout->setSpacing(3);
294 QLabel *modeLabel = new QLabel(i18n("Precision:"), optionWidget);
295 QComboBox * modeBox = new QComboBox(optionWidget);
296 modeBox->addItem(i18nc("The raw line data", "Raw"));
297 modeBox->addItem(i18n("Curve"));
298 modeBox->addItem(i18n("Straight"));
299 modeLayout->addWidget(modeLabel);
300 modeLayout->addWidget(modeBox, 1);
301 layout->addLayout(modeLayout);
302
303 QStackedWidget * stackedWidget = new QStackedWidget(optionWidget);
304
305 QWidget * rawBox = new QWidget(stackedWidget);
306 QVBoxLayout * rawLayout = new QVBoxLayout(rawBox);
307 QCheckBox * optimizeRaw = new QCheckBox(i18n("Optimize"), rawBox);
308 optimizeRaw->setChecked(m_optimizeRaw);
309 rawLayout->addWidget(optimizeRaw);
310 rawLayout->setContentsMargins(0, 0, 0, 0);
311
312 QWidget * curveBox = new QWidget(stackedWidget);
313 QHBoxLayout * curveLayout = new QHBoxLayout(curveBox);
314 QCheckBox * optimizeCurve = new QCheckBox(i18n("Optimize"), curveBox);
315 optimizeCurve->setChecked(m_optimizeCurve);
316 QDoubleSpinBox * fittingError = new KisDoubleParseSpinBox(curveBox);
317 fittingError->setSingleStep(0.50);
318 fittingError->setMaximum(400.0);
319 fittingError->setMinimum(0.0);
320 fittingError->setValue(m_fittingError);
321 fittingError->setToolTip(i18n("Exactness:"));
322 curveLayout->addWidget(optimizeCurve);
323 curveLayout->addWidget(fittingError);
324 curveLayout->setContentsMargins(0, 0, 0, 0);
325
326 QWidget *straightBox = new QWidget(stackedWidget);
327 QVBoxLayout *straightLayout = new QVBoxLayout(straightBox);
328 QDoubleSpinBox *combineAngle = new KisDoubleParseSpinBox(straightBox);
329 combineAngle->setSingleStep(0.50);
330 combineAngle->setMaximum(360.0);
331 combineAngle->setMinimum(0.0);
332 combineAngle->setValue(m_combineAngle);
333 combineAngle->setSuffix(" deg");
334 // QT5TODO
335 //combineAngle->setLabel(i18n("Combine angle:"), Qt::AlignLeft | Qt::AlignVCenter);
336 straightLayout->addWidget(combineAngle);
337 straightLayout->setContentsMargins(0, 0, 0, 0);
338
339 stackedWidget->addWidget(rawBox);
340 stackedWidget->addWidget(curveBox);
341 stackedWidget->addWidget(straightBox);
342 layout->addWidget(stackedWidget);
343 layout->addStretch(1);
344
345 connect(modeBox, SIGNAL(activated(int)), stackedWidget, SLOT(setCurrentIndex(int)));
346 connect(modeBox, SIGNAL(activated(int)), this, SLOT(selectMode(int)));
347 connect(optimizeRaw, SIGNAL(stateChanged(int)), this, SLOT(setOptimize(int)));
348 connect(optimizeCurve, SIGNAL(stateChanged(int)), this, SLOT(setOptimize(int)));
349 connect(fittingError, SIGNAL(valueChanged(double)), this, SLOT(setDelta(double)));
350 connect(combineAngle, SIGNAL(valueChanged(double)), this, SLOT(setDelta(double)));
351
352 modeBox->setCurrentIndex(m_mode);
353 stackedWidget->setCurrentIndex(m_mode);
354 optionWidget->setObjectName(i18n("Pencil"));
355 optionWidget->setWindowTitle(i18n("Pencil"));
356 widgets.append(optionWidget);
357
360 m_strokeWidget->setWindowTitle(i18n("Line"));
361 connect(m_strokeWidget, SIGNAL(sigStrokeChanged()), SLOT(slotUpdatePencilCursor()));
362 if (isActivated()) {
364 }
365 widgets.append(m_strokeWidget);
366 return widgets;
367}
368
369void KoPencilTool::addPathShape(KoPathShape* path, bool closePath)
370{
371 KoShape * startShape = 0;
372 KoShape * endShape = 0;
373
374 if (closePath) {
375 path->close();
376 path->normalize();
377 } else {
378 path->normalize();
381 startShape = m_existingStartPoint->parent();
383 endShape = m_existingEndPoint->parent();
384 }
385 }
386
387 KUndo2Command * cmd = canvas()->shapeController()->addShape(path, 0);
388 if (cmd) {
390 selection->deselectAll();
391 selection->select(path);
392
393 if (startShape)
394 canvas()->shapeController()->removeShape(startShape, cmd);
395 if (endShape && startShape != endShape)
396 canvas()->shapeController()->removeShape(endShape, cmd);
397
398 canvas()->addCommand(cmd);
399 } else {
400 canvas()->updateCanvas(path->boundingRect());
401 delete path;
402 }
403}
404
406{
407 m_mode = static_cast<PencilMode>(mode);
408 m_configGroup.writeEntry("pencilMode", mode);
409}
410
412{
413 if (m_mode == ModeRaw) {
414 m_optimizeRaw = state == Qt::Checked ? true : false;
415 m_configGroup.writeEntry("optimizeRaw", m_optimizeRaw);
416 }
417 else {
418 m_optimizeCurve = state == Qt::Checked ? true : false;
419 m_configGroup.writeEntry("optimizeCurve", m_optimizeCurve);
420 }
421}
422
423void KoPencilTool::setDelta(double delta)
424{
425 if (m_mode == ModeCurve) {
426 m_fittingError = delta;
427 m_configGroup.writeEntry("fittingError", m_fittingError);
428 }
429 else if (m_mode == ModeStraight) {
430 m_combineAngle = delta;
431 m_configGroup.writeEntry("combineAngle", m_combineAngle);
432 }
433}
434
436{
437 KoShapeStrokeSP stroke;
438 if (m_strokeWidget) {
440 }
441 return stroke;
442}
443
445{
446 return m_shape;
447}
448
450{
451 QRectF roi = handleGrabRect(position);
452 QList<KoShape *> shapes = canvas()->shapeManager()->shapesAt(roi);
453
454 KoPathPoint * nearestPoint = 0;
455 qreal minDistance = HUGE_VAL;
456 qreal maxDistance = canvas()->viewConverter()->viewToDocumentX(grabSensitivity());
457
458 Q_FOREACH(KoShape * shape, shapes) {
459 KoPathShape * path = dynamic_cast<KoPathShape*>(shape);
460 if (!path)
461 continue;
462 KoParameterShape *paramShape = dynamic_cast<KoParameterShape*>(shape);
463 if (paramShape && paramShape->isParametricShape())
464 continue;
465
466 KoPathPoint * p = 0;
467 uint subpathCount = path->subpathCount();
468 for (uint i = 0; i < subpathCount; ++i) {
469 if (path->isClosedSubpath(i))
470 continue;
471 p = path->pointByIndex(KoPathPointIndex(i, 0));
472 // check start of subpath
473 qreal d = squareDistance(position, path->shapeToDocument(p->point()));
474 if (d < minDistance && d < maxDistance) {
475 nearestPoint = p;
476 minDistance = d;
477 }
478 // check end of subpath
479 p = path->pointByIndex(KoPathPointIndex(i, path->subpathPointCount(i) - 1));
480 d = squareDistance(position, path->shapeToDocument(p->point()));
481 if (d < minDistance && d < maxDistance) {
482 nearestPoint = p;
483 minDistance = d;
484 }
485 }
486 }
487
488 return nearestPoint;
489}
490
491bool KoPencilTool::connectPaths(KoPathShape *pathShape, KoPathPoint *pointAtStart, KoPathPoint *pointAtEnd)
492{
493 // at least one point must be valid
494 if (!pointAtStart && !pointAtEnd)
495 return false;
496 // do not allow connecting to the same point twice
497 if (pointAtStart == pointAtEnd)
498 pointAtEnd = 0;
499
500 // we have hit an existing path point on start/finish
501 // what we now do is:
502 // 1. combine the new created path with the ones we hit on start/finish
503 // 2. merge the endpoints of the corresponding subpaths
504
505 uint newPointCount = pathShape->subpathPointCount(0);
506 KoPathPointIndex newStartPointIndex(0, 0);
507 KoPathPointIndex newEndPointIndex(0, newPointCount - 1);
508 KoPathPoint * newStartPoint = pathShape->pointByIndex(newStartPointIndex);
509 KoPathPoint * newEndPoint = pathShape->pointByIndex(newEndPointIndex);
510
511 KoPathShape * startShape = pointAtStart ? pointAtStart->parent() : 0;
512 KoPathShape * endShape = pointAtEnd ? pointAtEnd->parent() : 0;
513
514 // combine with the path we hit on start
515 KoPathPointIndex startIndex(-1, -1);
516 if (pointAtStart) {
517 startIndex = startShape->pathPointIndex(pointAtStart);
518 pathShape->combine(startShape);
519 pathShape->moveSubpath(0, pathShape->subpathCount() - 1);
520 }
521 // combine with the path we hit on finish
522 KoPathPointIndex endIndex(-1, -1);
523 if (pointAtEnd) {
524 endIndex = endShape->pathPointIndex(pointAtEnd);
525 if (endShape != startShape) {
526 endIndex.first += pathShape->subpathCount();
527 pathShape->combine(endShape);
528 }
529 }
530 // do we connect twice to a single subpath ?
531 bool connectToSingleSubpath = (startShape == endShape && startIndex.first == endIndex.first);
532
533 if (startIndex.second == 0 && !connectToSingleSubpath) {
534 pathShape->reverseSubpath(startIndex.first);
535 startIndex.second = pathShape->subpathPointCount(startIndex.first) - 1;
536 }
537 if (endIndex.second > 0 && !connectToSingleSubpath) {
538 pathShape->reverseSubpath(endIndex.first);
539 endIndex.second = 0;
540 }
541
542 // after combining we have a path where with the subpaths in the following
543 // order:
544 // 1. the subpaths of the pathshape we started the new path at
545 // 2. the subpath we just created
546 // 3. the subpaths of the pathshape we finished the new path at
547
548 // get the path points we want to merge, as these are not going to
549 // change while merging
550 KoPathPoint * existingStartPoint = pathShape->pointByIndex(startIndex);
551 KoPathPoint * existingEndPoint = pathShape->pointByIndex(endIndex);
552
553 // merge first two points
554 if (existingStartPoint) {
555 KoPathPointData pd1(pathShape, pathShape->pathPointIndex(existingStartPoint));
556 KoPathPointData pd2(pathShape, pathShape->pathPointIndex(newStartPoint));
557 KoPathPointMergeCommand cmd1(pd1, pd2);
558 cmd1.redo();
559 }
560 // merge last two points
561 if (existingEndPoint) {
562 KoPathPointData pd3(pathShape, pathShape->pathPointIndex(newEndPoint));
563 KoPathPointData pd4(pathShape, pathShape->pathPointIndex(existingEndPoint));
564 KoPathPointMergeCommand cmd2(pd3, pd4);
565 cmd2.redo();
566 }
567
568 return true;
569}
570
572{
573 return this->m_fittingError;
574}
575
576void KoPencilTool::setFittingError(qreal fittingError)
577{
578 this->m_fittingError = fittingError;
579}
const Params2D p
QPointF p2
QPointF p1
qreal squareDistance(const QPointF &p1, const QPointF &p2)
KoPathShape * bezierFit(const QList< QPointF > &points, float error)
unsigned int uint
#define KoPathShapeId
Definition KoPathShape.h:20
QPair< int, int > KoPathPointIndex
Definition KoPathShape.h:28
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
The KisDoubleParseSpinBox class is a cleverer doubleSpinBox, able to parse arithmetic expressions.
The KisHandlePainterHelper class is a special helper for painting handles around objects....
void setHandleStyle(const KisHandleStyle &style)
static KisHandleStyle & primarySelection()
QPointer< KoShapeController > shapeController
virtual KoShapeManager * shapeManager() const =0
virtual const KoViewConverter * viewConverter() const =0
virtual void updateCanvas(const QRectF &rc)=0
virtual void addCommand(KUndo2Command *command)=0
bool isParametricShape() const
Check if object is a parametric shape.
Describe a KoPathPoint by a KoPathShape and its indices.
The undo / redo command for merging two subpath end points.
void redo() override
redo the command
A KoPathPoint represents a point in a path.
QPointF point
KoPathShape * parent() const
Get the path shape the point belongs to.
@ Node
the node point
Definition KoPathPoint.h:49
void paint(KisHandlePainterHelper &handlesHelper, PointTypes types, bool active=true)
The position of a path point within a path shape.
Definition KoPathShape.h:63
int subpathPointCount(int subpathIndex) const
Returns the number of points in a subpath.
bool reverseSubpath(int subpathIndex)
Reverse subpath.
KoPathPoint * lineTo(const QPointF &p)
Adds a new line segment.
KoPathPoint * moveTo(const QPointF &p)
Starts a new Subpath.
bool moveSubpath(int oldSubpathIndex, int newSubpathIndex)
Moves the position of a subpath within a path.
void paint(QPainter &painter) const override
reimplemented
QRectF boundingRect() const override
reimplemented
int subpathCount() const
Returns the number of subpaths in the path.
KoPathPointIndex pathPointIndex(const KoPathPoint *point) const
Returns the path point index of a given path point.
KoPathPoint * pointByIndex(const KoPathPointIndex &pointIndex) const
Returns the path point specified by a path point index.
int combine(KoPathShape *path)
Combines two path shapes by appending the data of the specified path.
~KoPencilTool() override
void setDelta(double delta)
bool connectPaths(KoPathShape *pathShape, KoPathPoint *pointAtStart, KoPathPoint *pointAtEnd)
Connects given path with the ones we hit when starting/finishing.
void selectMode(int mode)
void addPoint(const QPointF &point)
QList< QPointer< QWidget > > createOptionWidgets() override
KoPathPoint * endPointAtPosition(const QPointF &position)
returns the nearest existing path point
qreal getFittingError()
KoPathPoint * m_existingEndPoint
an existing path point we finished a new path at
QList< QPointF > m_points
KoPathShape * path()
virtual void slotUpdatePencilCursor()
void mousePressEvent(KoPointerEvent *event) override
KoPathPoint * m_existingStartPoint
an existing path point we started a new path at
void paint(QPainter &painter, const KoViewConverter &converter) override
KoPencilTool(KoCanvasBase *canvas)
void mouseReleaseEvent(KoPointerEvent *event) override
KoShapeStrokeSP createStroke()
void activate(const QSet< KoShape * > &shapes) override
KoPathPoint * m_hoveredPoint
an existing path end point the mouse is hovering on
qreal m_fittingError
void keyPressEvent(QKeyEvent *event) override
KoStrokeConfigWidget * m_strokeWidget
PencilMode m_mode
void mouseMoveEvent(KoPointerEvent *event) override
KConfigGroup m_configGroup
virtual void addPathShape(KoPathShape *path, bool closePath)
KoPathShape * m_shape
qreal lineAngle(const QPointF &p1, const QPointF &p2)
bool m_optimizeCurve
void setFittingError(qreal fittingError)
void setOptimize(int state)
qreal m_combineAngle
void finish(bool closePath)
void deactivate() override
Qt::MouseButtons buttons() const
return buttons pressed (see QMouseEvent::buttons());
Qt::KeyboardModifiers modifiers() const
QPointF point
The point in document coordinates.
QList< KoShape * > shapesAt(const QRectF &rect, bool omitHiddenShapes=true, bool containedMode=false)
KoSelection * selection
QPointF shapeToDocument(const QPointF &point) const
Transforms point from shape coordinates to document coordinates.
Definition KoShape.cpp:1201
virtual KoShapeStrokeModelSP stroke() const
Definition KoShape.cpp:1067
virtual void setStroke(KoShapeStrokeModelSP stroke)
Definition KoShape.cpp:1081
QTransform absoluteTransformation() const
Definition KoShape.cpp:382
static KisHandlePainterHelper createHandlePainterHelperView(QPainter *painter, KoShape *shape, const KoViewConverter &converter, qreal handleRadius=0.0, int decorationThickness=1)
Definition KoShape.cpp:1177
void setShapeId(const QString &id)
Definition KoShape.cpp:1062
A widget for configuring the stroke of a shape.
void setNoSelectionTrackingMode(bool value)
KoShapeStrokeSP createShapeStroke()
KoCanvasBase * canvas() const
Returns the canvas the tool is working on.
Q_INVOKABLE QString toolId() const
QRectF handlePaintRect(const QPointF &position) const
int grabSensitivity() const
Convenience function to get the current grab sensitivity.
virtual KoToolSelection * selection()
int handleRadius() const
Convenience function to get the current handle radius.
void useCursor(const QCursor &cursor)
virtual void activate(const QSet< KoShape * > &shapes)
QRectF handleGrabRect(const QPointF &position) const
virtual void deactivate()
bool isActivated() const
int decorationThickness() const
decorationThickness The minimum thickness for tool decoration lines, this is derived from the screen ...
virtual qreal viewToDocumentX(qreal viewX) const
virtual QPointF documentToView(const QPointF &documentPoint) const
#define M_PI
Definition kis_global.h:111