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>
39
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 stroke->setColor(m_strokeColor);
441 }
442 return stroke;
443}
444
446{
447 return m_shape;
448}
449
451{
452 QRectF roi = handleGrabRect(position);
453 QList<KoShape *> shapes = canvas()->shapeManager()->shapesAt(roi);
454
455 KoPathPoint * nearestPoint = 0;
456 qreal minDistance = HUGE_VAL;
457 qreal maxDistance = canvas()->viewConverter()->viewToDocumentX(grabSensitivity());
458
459 Q_FOREACH(KoShape * shape, shapes) {
460 KoPathShape * path = dynamic_cast<KoPathShape*>(shape);
461 if (!path)
462 continue;
463 KoParameterShape *paramShape = dynamic_cast<KoParameterShape*>(shape);
464 if (paramShape && paramShape->isParametricShape())
465 continue;
466
467 KoPathPoint * p = 0;
468 uint subpathCount = path->subpathCount();
469 for (uint i = 0; i < subpathCount; ++i) {
470 if (path->isClosedSubpath(i))
471 continue;
472 p = path->pointByIndex(KoPathPointIndex(i, 0));
473 // check start of subpath
474 qreal d = squareDistance(position, path->shapeToDocument(p->point()));
475 if (d < minDistance && d < maxDistance) {
476 nearestPoint = p;
477 minDistance = d;
478 }
479 // check end of subpath
480 p = path->pointByIndex(KoPathPointIndex(i, path->subpathPointCount(i) - 1));
481 d = squareDistance(position, path->shapeToDocument(p->point()));
482 if (d < minDistance && d < maxDistance) {
483 nearestPoint = p;
484 minDistance = d;
485 }
486 }
487 }
488
489 return nearestPoint;
490}
491
492bool KoPencilTool::connectPaths(KoPathShape *pathShape, KoPathPoint *pointAtStart, KoPathPoint *pointAtEnd)
493{
494 // at least one point must be valid
495 if (!pointAtStart && !pointAtEnd)
496 return false;
497 // do not allow connecting to the same point twice
498 if (pointAtStart == pointAtEnd)
499 pointAtEnd = 0;
500
501 // we have hit an existing path point on start/finish
502 // what we now do is:
503 // 1. combine the new created path with the ones we hit on start/finish
504 // 2. merge the endpoints of the corresponding subpaths
505
506 uint newPointCount = pathShape->subpathPointCount(0);
507 KoPathPointIndex newStartPointIndex(0, 0);
508 KoPathPointIndex newEndPointIndex(0, newPointCount - 1);
509 KoPathPoint * newStartPoint = pathShape->pointByIndex(newStartPointIndex);
510 KoPathPoint * newEndPoint = pathShape->pointByIndex(newEndPointIndex);
511
512 KoPathShape * startShape = pointAtStart ? pointAtStart->parent() : 0;
513 KoPathShape * endShape = pointAtEnd ? pointAtEnd->parent() : 0;
514
515 // combine with the path we hit on start
516 KoPathPointIndex startIndex(-1, -1);
517 if (pointAtStart) {
518 startIndex = startShape->pathPointIndex(pointAtStart);
519 pathShape->combine(startShape);
520 pathShape->moveSubpath(0, pathShape->subpathCount() - 1);
521 }
522 // combine with the path we hit on finish
523 KoPathPointIndex endIndex(-1, -1);
524 if (pointAtEnd) {
525 endIndex = endShape->pathPointIndex(pointAtEnd);
526 if (endShape != startShape) {
527 endIndex.first += pathShape->subpathCount();
528 pathShape->combine(endShape);
529 }
530 }
531 // do we connect twice to a single subpath ?
532 bool connectToSingleSubpath = (startShape == endShape && startIndex.first == endIndex.first);
533
534 if (startIndex.second == 0 && !connectToSingleSubpath) {
535 pathShape->reverseSubpath(startIndex.first);
536 startIndex.second = pathShape->subpathPointCount(startIndex.first) - 1;
537 }
538 if (endIndex.second > 0 && !connectToSingleSubpath) {
539 pathShape->reverseSubpath(endIndex.first);
540 endIndex.second = 0;
541 }
542
543 // after combining we have a path where with the subpaths in the following
544 // order:
545 // 1. the subpaths of the pathshape we started the new path at
546 // 2. the subpath we just created
547 // 3. the subpaths of the pathshape we finished the new path at
548
549 // get the path points we want to merge, as these are not going to
550 // change while merging
551 KoPathPoint * existingStartPoint = pathShape->pointByIndex(startIndex);
552 KoPathPoint * existingEndPoint = pathShape->pointByIndex(endIndex);
553
554 // merge first two points
555 if (existingStartPoint) {
556 KoPathPointData pd1(pathShape, pathShape->pathPointIndex(existingStartPoint));
557 KoPathPointData pd2(pathShape, pathShape->pathPointIndex(newStartPoint));
558 KoPathPointMergeCommand cmd1(pd1, pd2);
559 cmd1.redo();
560 }
561 // merge last two points
562 if (existingEndPoint) {
563 KoPathPointData pd3(pathShape, pathShape->pathPointIndex(newEndPoint));
564 KoPathPointData pd4(pathShape, pathShape->pathPointIndex(existingEndPoint));
565 KoPathPointMergeCommand cmd2(pd3, pd4);
566 cmd2.redo();
567 }
568
569 return true;
570}
571
573{
574 return this->m_fittingError;
575}
576
578{
579 m_strokeColor = color;
580}
581
582void KoPencilTool::setFittingError(qreal fittingError)
583{
584 this->m_fittingError = fittingError;
585}
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
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.
void setStrokeColor(QColor color)
~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
QColor m_strokeColor
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:1006
virtual KoShapeStrokeModelSP stroke() const
Definition KoShape.cpp:890
virtual void setStroke(KoShapeStrokeModelSP stroke)
Definition KoShape.cpp:904
QTransform absoluteTransformation() const
Definition KoShape.cpp:335
static KisHandlePainterHelper createHandlePainterHelperView(QPainter *painter, KoShape *shape, const KoViewConverter &converter, qreal handleRadius=0.0, int decorationThickness=1)
Definition KoShape.cpp:982
void setShapeId(const QString &id)
Definition KoShape.cpp:885
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