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>
21#include <KoPathPoint.h>
22#include <KoPathPointData.h>
26
27#include <klocalizedstring.h>
28
29#include <QDoubleSpinBox>
30#include <QComboBox>
31#include <QStackedWidget>
32#include <QGroupBox>
33#include <QCheckBox>
34#include <QVBoxLayout>
35#include <QPainter>
36#include <QLabel>
37#include <QKeyEvent>
38
39#include <math.h>
41#include "KoCreatePathTool_p.h"
43
48
52
53void KoPencilTool::paint(QPainter &painter, const KoViewConverter &converter)
54{
55 if (m_shape) {
56 painter.save();
57
58 painter.setTransform(m_shape->absoluteTransformation() *
59 converter.documentToView() *
60 painter.transform());
61
62 painter.save();
63 m_shape->paint(painter);
64 painter.restore();
65
66 if (m_shape->stroke()) {
67 painter.save();
68 m_shape->stroke()->paint(m_shape, painter);
69 painter.restore();
70 }
71
72 painter.restore();
73 }
74
75 if (m_hoveredPoint) {
78
79 helper.setHandleStyle(KisHandleStyle::primarySelection(canvas()->displayRendererInterface()->handlePaletteForDisplayColorSpace()));
81 }
82}
83
85{
87
88 if (!m_shape && stroke && stroke->isVisible()) {
89 m_shape = new KoPathShape();
92 m_points.clear();
93
94 QPointF point = event->point;
98
99 addPoint(point);
100 }
101}
102
104{
105 if (event->buttons() & Qt::LeftButton)
106 addPoint(event->point);
107
108 KoPathPoint * endPoint = endPointAtPosition(event->point);
109 if (m_hoveredPoint != endPoint) {
110 if (m_hoveredPoint) {
111 QPointF nodePos = m_hoveredPoint->parent()->shapeToDocument(m_hoveredPoint->point());
113 }
114 m_hoveredPoint = endPoint;
115 if (m_hoveredPoint) {
116 QPointF nodePos = m_hoveredPoint->parent()->shapeToDocument(m_hoveredPoint->point());
118 }
119 }
120}
121
123{
124 if (! m_shape)
125 return;
126
127 QPointF point = event->point;
131
132 addPoint(point);
133 finish(event->modifiers() & Qt::ShiftModifier);
134
137 m_hoveredPoint = 0;
138
139 // the original path may be different from the one added
140 if (canvas() && m_shape) {
142 }
143 delete m_shape;
144 m_shape = 0;
145 m_points.clear();
146}
147
148void KoPencilTool::keyPressEvent(QKeyEvent *event)
149{
150 if (m_shape) {
151 event->accept();
152 } else {
153 event->ignore();
154 }
155}
156
157void KoPencilTool::activate(const QSet<KoShape*> &shapes)
158{
159 KoToolBase::activate(shapes);
160
161 m_points.clear();
162 m_close = false;
164
165 if (m_strokeWidget) {
167 }
168 m_configGroup = KSharedConfig::openConfig()->group(toolId());
169}
170
172{
173 m_points.clear();
174 delete m_shape;
175 m_shape = 0;
178 m_hoveredPoint = 0;
179
180 if (m_strokeWidget) {
182 }
183
185}
186
188{
189 KoShapeStrokeSP stroke = createStroke();
190 useCursor((stroke && stroke->isVisible()) ? Qt::ArrowCursor : Qt::ForbiddenCursor);
191}
192
193void KoPencilTool::addPoint(const QPointF & point)
194{
195 if (! m_shape)
196 return;
197
198 // do a moveTo for the first point added
199 if (m_points.empty())
200 m_shape->moveTo(point);
201 // do not allow coincident points
202 else if (point != m_points.last())
203 m_shape->lineTo(point);
204 else
205 return;
206
207 m_points.append(point);
209}
210
211qreal KoPencilTool::lineAngle(const QPointF &p1, const QPointF &p2)
212{
213 qreal angle = atan2(p2.y() - p1.y(), p2.x() - p1.x());
214 if (angle < 0.0)
215 angle += 2 * M_PI;
216
217 return angle * 180.0 / M_PI;
218}
219
220void KoPencilTool::finish(bool closePath)
221{
222 if (m_points.count() < 2)
223 return;
224
225 KoPathShape * path = 0;
226 QList<QPointF> complete;
227 QList<QPointF> *points = &m_points;
228
230 float combineAngle;
231
232 if (m_mode == ModeStraight)
233 combineAngle = m_combineAngle;
234 else
235 combineAngle = 0.50f;
236
237 //Add the first two points
238 complete.append(m_points[0]);
239 complete.append(m_points[1]);
240
241 //Now we need to get the angle of the first line
242 float lastAngle = lineAngle(complete[0], complete[1]);
243
244 uint pointCount = m_points.count();
245 for (uint i = 2; i < pointCount; ++i) {
246 float angle = lineAngle(complete.last(), m_points[i]);
247 if (qAbs(angle - lastAngle) < combineAngle)
248 complete.removeLast();
249 complete.append(m_points[i]);
250 lastAngle = angle;
251 }
252
253 m_points.clear();
254 points = &complete;
255 }
256
257 switch (m_mode) {
258 case ModeCurve: {
259 path = bezierFit(*points, m_fittingError);
260 }
261 break;
262 case ModeStraight:
263 case ModeRaw: {
264 path = new KoPathShape();
265 uint pointCount = points->count();
266 path->moveTo(points->at(0));
267 for (uint i = 1; i < pointCount; ++i)
268 path->lineTo(points->at(i));
269 }
270 break;
271 }
272
273 if (! path)
274 return;
275
276 path->setShapeId(KoPathShapeId);
277 path->setStroke(createStroke());
278 addPathShape(path, closePath);
279}
280
282{
283 m_mode = static_cast<PencilMode>(m_configGroup.readEntry<int>("pencilMode", m_mode));
284 m_optimizeRaw = m_configGroup.readEntry<bool>("optimizeRaw", m_optimizeRaw);
285 m_optimizeCurve = m_configGroup.readEntry<bool>("optimizeCurve", m_optimizeCurve);
286 m_combineAngle = m_configGroup.readEntry<qreal>("combineAngle", m_combineAngle);
287 m_fittingError = m_configGroup.readEntry<qreal>("fittingError", m_fittingError);
288
289 QList<QPointer<QWidget> > widgets;
290 QWidget *optionWidget = new QWidget();
291 QVBoxLayout * layout = new QVBoxLayout(optionWidget);
292
293 QHBoxLayout *modeLayout = new QHBoxLayout;
294 modeLayout->setSpacing(3);
295 QLabel *modeLabel = new QLabel(i18n("Precision:"), optionWidget);
296 QComboBox * modeBox = new QComboBox(optionWidget);
297 modeBox->addItem(i18nc("The raw line data", "Raw"));
298 modeBox->addItem(i18n("Curve"));
299 modeBox->addItem(i18n("Straight"));
300 modeLayout->addWidget(modeLabel);
301 modeLayout->addWidget(modeBox, 1);
302 layout->addLayout(modeLayout);
303
304 QStackedWidget * stackedWidget = new QStackedWidget(optionWidget);
305
306 QWidget * rawBox = new QWidget(stackedWidget);
307 QVBoxLayout * rawLayout = new QVBoxLayout(rawBox);
308 QCheckBox * optimizeRaw = new QCheckBox(i18n("Optimize"), rawBox);
309 optimizeRaw->setChecked(m_optimizeRaw);
310 rawLayout->addWidget(optimizeRaw);
311 rawLayout->setContentsMargins(0, 0, 0, 0);
312
313 QWidget * curveBox = new QWidget(stackedWidget);
314 QHBoxLayout * curveLayout = new QHBoxLayout(curveBox);
315 QCheckBox * optimizeCurve = new QCheckBox(i18n("Optimize"), curveBox);
316 optimizeCurve->setChecked(m_optimizeCurve);
317 QDoubleSpinBox * fittingError = new KisDoubleParseSpinBox(curveBox);
318 fittingError->setSingleStep(0.50);
319 fittingError->setMaximum(400.0);
320 fittingError->setMinimum(0.0);
321 fittingError->setValue(m_fittingError);
322 fittingError->setToolTip(i18n("Exactness:"));
323 curveLayout->addWidget(optimizeCurve);
324 curveLayout->addWidget(fittingError);
325 curveLayout->setContentsMargins(0, 0, 0, 0);
326
327 QWidget *straightBox = new QWidget(stackedWidget);
328 QVBoxLayout *straightLayout = new QVBoxLayout(straightBox);
329 QDoubleSpinBox *combineAngle = new KisDoubleParseSpinBox(straightBox);
330 combineAngle->setSingleStep(0.50);
331 combineAngle->setMaximum(360.0);
332 combineAngle->setMinimum(0.0);
333 combineAngle->setValue(m_combineAngle);
334 combineAngle->setSuffix(" deg");
335 // QT5TODO
336 //combineAngle->setLabel(i18n("Combine angle:"), Qt::AlignLeft | Qt::AlignVCenter);
337 straightLayout->addWidget(combineAngle);
338 straightLayout->setContentsMargins(0, 0, 0, 0);
339
340 stackedWidget->addWidget(rawBox);
341 stackedWidget->addWidget(curveBox);
342 stackedWidget->addWidget(straightBox);
343 layout->addWidget(stackedWidget);
344 layout->addStretch(1);
345
346 connect(modeBox, SIGNAL(activated(int)), stackedWidget, SLOT(setCurrentIndex(int)));
347 connect(modeBox, SIGNAL(activated(int)), this, SLOT(selectMode(int)));
348 connect(optimizeRaw, SIGNAL(stateChanged(int)), this, SLOT(setOptimize(int)));
349 connect(optimizeCurve, SIGNAL(stateChanged(int)), this, SLOT(setOptimize(int)));
350 connect(fittingError, SIGNAL(valueChanged(double)), this, SLOT(setDelta(double)));
351 connect(combineAngle, SIGNAL(valueChanged(double)), this, SLOT(setDelta(double)));
352
353 modeBox->setCurrentIndex(m_mode);
354 stackedWidget->setCurrentIndex(m_mode);
355 optionWidget->setObjectName(i18n("Pencil"));
356 optionWidget->setWindowTitle(i18n("Pencil"));
357 widgets.append(optionWidget);
358
361 m_strokeWidget->setWindowTitle(i18n("Line"));
362 connect(m_strokeWidget, SIGNAL(sigStrokeChanged()), SLOT(slotUpdatePencilCursor()));
363 if (isActivated()) {
365 }
366 widgets.append(m_strokeWidget);
367 return widgets;
368}
369
370void KoPencilTool::addPathShape(KoPathShape* path, bool closePath)
371{
372 KoShape * startShape = 0;
373 KoShape * endShape = 0;
374
375 if (closePath) {
376 path->close();
377 path->normalize();
378 } else {
379 path->normalize();
382 startShape = m_existingStartPoint->parent();
384 endShape = m_existingEndPoint->parent();
385 }
386 }
387
388 KUndo2Command * cmd = canvas()->shapeController()->addShape(path, 0);
389 if (cmd) {
391 selection->deselectAll();
392 selection->select(path);
393
394 if (startShape)
395 canvas()->shapeController()->removeShape(startShape, cmd);
396 if (endShape && startShape != endShape)
397 canvas()->shapeController()->removeShape(endShape, cmd);
398
399 canvas()->addCommand(cmd);
400 } else {
401 canvas()->updateCanvas(path->boundingRect());
402 delete path;
403 }
404}
405
407{
408 m_mode = static_cast<PencilMode>(mode);
409 m_configGroup.writeEntry("pencilMode", mode);
410}
411
413{
414 if (m_mode == ModeRaw) {
415 m_optimizeRaw = state == Qt::Checked ? true : false;
416 m_configGroup.writeEntry("optimizeRaw", m_optimizeRaw);
417 }
418 else {
419 m_optimizeCurve = state == Qt::Checked ? true : false;
420 m_configGroup.writeEntry("optimizeCurve", m_optimizeCurve);
421 }
422}
423
424void KoPencilTool::setDelta(double delta)
425{
426 if (m_mode == ModeCurve) {
427 m_fittingError = delta;
428 m_configGroup.writeEntry("fittingError", m_fittingError);
429 }
430 else if (m_mode == ModeStraight) {
431 m_combineAngle = delta;
432 m_configGroup.writeEntry("combineAngle", m_combineAngle);
433 }
434}
435
437{
438 KoShapeStrokeSP stroke;
439 if (m_strokeWidget) {
441 stroke->setColor(m_strokeColor);
442 }
443 return stroke;
444}
445
447{
448 return m_shape;
449}
450
452{
453 QRectF roi = handleGrabRect(position);
454 QList<KoShape *> shapes = canvas()->shapeManager()->shapesAt(roi);
455
456 KoPathPoint * nearestPoint = 0;
457 qreal minDistance = HUGE_VAL;
458 qreal maxDistance = canvas()->viewConverter()->viewToDocumentX(grabSensitivity());
459
460 Q_FOREACH(KoShape * shape, shapes) {
461 KoPathShape * path = dynamic_cast<KoPathShape*>(shape);
462 if (!path)
463 continue;
464 KoParameterShape *paramShape = dynamic_cast<KoParameterShape*>(shape);
465 if (paramShape && paramShape->isParametricShape())
466 continue;
467
468 KoPathPoint * p = 0;
469 uint subpathCount = path->subpathCount();
470 for (uint i = 0; i < subpathCount; ++i) {
471 if (path->isClosedSubpath(i))
472 continue;
473 p = path->pointByIndex(KoPathPointIndex(i, 0));
474 // check start of subpath
475 qreal d = squareDistance(position, path->shapeToDocument(p->point()));
476 if (d < minDistance && d < maxDistance) {
477 nearestPoint = p;
478 minDistance = d;
479 }
480 // check end of subpath
481 p = path->pointByIndex(KoPathPointIndex(i, path->subpathPointCount(i) - 1));
482 d = squareDistance(position, path->shapeToDocument(p->point()));
483 if (d < minDistance && d < maxDistance) {
484 nearestPoint = p;
485 minDistance = d;
486 }
487 }
488 }
489
490 return nearestPoint;
491}
492
493bool KoPencilTool::connectPaths(KoPathShape *pathShape, KoPathPoint *pointAtStart, KoPathPoint *pointAtEnd)
494{
495 // at least one point must be valid
496 if (!pointAtStart && !pointAtEnd)
497 return false;
498 // do not allow connecting to the same point twice
499 if (pointAtStart == pointAtEnd)
500 pointAtEnd = 0;
501
502 // we have hit an existing path point on start/finish
503 // what we now do is:
504 // 1. combine the new created path with the ones we hit on start/finish
505 // 2. merge the endpoints of the corresponding subpaths
506
507 uint newPointCount = pathShape->subpathPointCount(0);
508 KoPathPointIndex newStartPointIndex(0, 0);
509 KoPathPointIndex newEndPointIndex(0, newPointCount - 1);
510 KoPathPoint * newStartPoint = pathShape->pointByIndex(newStartPointIndex);
511 KoPathPoint * newEndPoint = pathShape->pointByIndex(newEndPointIndex);
512
513 KoPathShape * startShape = pointAtStart ? pointAtStart->parent() : 0;
514 KoPathShape * endShape = pointAtEnd ? pointAtEnd->parent() : 0;
515
516 // combine with the path we hit on start
517 KoPathPointIndex startIndex(-1, -1);
518 if (pointAtStart) {
519 startIndex = startShape->pathPointIndex(pointAtStart);
520 pathShape->combine(startShape);
521 pathShape->moveSubpath(0, pathShape->subpathCount() - 1);
522 }
523 // combine with the path we hit on finish
524 KoPathPointIndex endIndex(-1, -1);
525 if (pointAtEnd) {
526 endIndex = endShape->pathPointIndex(pointAtEnd);
527 if (endShape != startShape) {
528 endIndex.first += pathShape->subpathCount();
529 pathShape->combine(endShape);
530 }
531 }
532 // do we connect twice to a single subpath ?
533 bool connectToSingleSubpath = (startShape == endShape && startIndex.first == endIndex.first);
534
535 if (startIndex.second == 0 && !connectToSingleSubpath) {
536 pathShape->reverseSubpath(startIndex.first);
537 startIndex.second = pathShape->subpathPointCount(startIndex.first) - 1;
538 }
539 if (endIndex.second > 0 && !connectToSingleSubpath) {
540 pathShape->reverseSubpath(endIndex.first);
541 endIndex.second = 0;
542 }
543
544 // after combining we have a path where with the subpaths in the following
545 // order:
546 // 1. the subpaths of the pathshape we started the new path at
547 // 2. the subpath we just created
548 // 3. the subpaths of the pathshape we finished the new path at
549
550 // get the path points we want to merge, as these are not going to
551 // change while merging
552 KoPathPoint * existingStartPoint = pathShape->pointByIndex(startIndex);
553 KoPathPoint * existingEndPoint = pathShape->pointByIndex(endIndex);
554
555 // merge first two points
556 if (existingStartPoint) {
557 KoPathPointData pd1(pathShape, pathShape->pathPointIndex(existingStartPoint));
558 KoPathPointData pd2(pathShape, pathShape->pathPointIndex(newStartPoint));
559 KoPathPointMergeCommand cmd1(pd1, pd2);
560 cmd1.redo();
561 }
562 // merge last two points
563 if (existingEndPoint) {
564 KoPathPointData pd3(pathShape, pathShape->pathPointIndex(newEndPoint));
565 KoPathPointData pd4(pathShape, pathShape->pathPointIndex(existingEndPoint));
566 KoPathPointMergeCommand cmd2(pd3, pd4);
567 cmd2.redo();
568 }
569
570 return true;
571}
572
574{
575 return this->m_fittingError;
576}
577
579{
580 m_strokeColor = color;
581}
582
583void KoPencilTool::setFittingError(qreal fittingError)
584{
585 this->m_fittingError = fittingError;
586}
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(KisHandlePalette palette=KisHandlePalette())
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:1001
virtual KoShapeStrokeModelSP stroke() const
Definition KoShape.cpp:885
virtual void setStroke(KoShapeStrokeModelSP stroke)
Definition KoShape.cpp:899
QTransform absoluteTransformation() const
Definition KoShape.cpp:330
static KisHandlePainterHelper createHandlePainterHelperView(QPainter *painter, KoShape *shape, const KoViewConverter &converter, qreal handleRadius=0.0, int decorationThickness=1)
Definition KoShape.cpp:977
void setShapeId(const QString &id)
Definition KoShape.cpp:880
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