Krita Source Code Documentation
Loading...
Searching...
No Matches
KarbonCalligraphyTool.cpp
Go to the documentation of this file.
1/* This file is part of the KDE project
2 * SPDX-FileCopyrightText: 2008 Fela Winkelmolen <fela.kde@gmail.com>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
10
11#include <KoPathShape.h>
12#include <KoShapeGroup.h>
13#include <KoPointerEvent.h>
14#include <KoPathPoint.h>
15#include <KoCanvasBase.h>
16#include <KoShapeController.h>
17#include <KoShapeManager.h>
19#include <KoSelection.h>
20#include <KoCurveFit.h>
21#include <KoColorBackground.h>
23#include <KoColor.h>
24#include <KoViewConverter.h>
26
27#include <QAction>
28#include <QDebug>
29#include <klocalizedstring.h>
30#include <QPainter>
31
32#include <cmath>
33
34#undef M_PI
35const qreal M_PI = 3.1415927;
36using std::pow;
37using std::sqrt;
38
40 : KoToolBase(canvas)
41 , m_shape(0)
42 , m_angle(0)
43 , m_selectedPath(0)
44 , m_isDrawing(false)
45 , m_speed(0, 0)
46{
48
50}
51
55
56void KarbonCalligraphyTool::paint(QPainter &painter, const KoViewConverter &converter)
57{
58 if (m_selectedPath) {
59 painter.save();
60 painter.setRenderHints(QPainter::Antialiasing, false);
61 painter.setPen(Qt::red); // TODO make configurable
63 QPointF p1 = converter.documentToView(rect.topLeft());
64 QPointF p2 = converter.documentToView(rect.bottomRight());
65 painter.drawRect(QRectF(p1, p2));
66 painter.restore();
67 }
68
69 if (!m_shape) {
70 return;
71 }
72
73 painter.save();
74
75 painter.setTransform(m_shape->absoluteTransformation() *
76 converter.documentToView() *
77 painter.transform());
78
79 m_shape->paint(painter);
80
81 painter.restore();
82}
83
85{
86 if (m_isDrawing) {
87 return;
88 }
89
90 m_lastPoint = event->point;
91 m_speed = QPointF(0, 0);
92
93 m_isDrawing = true;
94 m_pointCount = 0;
96 m_shape->setBackground(QSharedPointer<KoShapeBackground>(new KoColorBackground(canvas()->resourceManager()->foregroundColor().toQColor())));
97 //addPoint( event );
98}
99
101{
102 if (!m_isDrawing) {
103 return;
104 }
105
106 addPoint(event);
107}
108
110{
111 if (!m_isDrawing) {
112 return;
113 }
114
115 if (m_pointCount == 0) {
116 // handle click: select shape (if any)
117 if (event->point == m_lastPoint) {
118 KoShapeManager *shapeManager = canvas()->shapeManager();
119 KoShape *selectedShape = shapeManager->shapeAt(event->point);
120 if (selectedShape != 0) {
121 shapeManager->selection()->deselectAll();
122 shapeManager->selection()->select(selectedShape);
123 }
124 }
125
126 delete m_shape;
127 m_shape = 0;
128 m_isDrawing = false;
129 return;
130 } else {
131 m_endOfPath = false; // allow last point being added
132 addPoint(event); // add last point
133 m_isDrawing = false;
134 }
135
137
138 KUndo2Command *cmd = canvas()->shapeController()->addShape(m_shape, 0);
139 if (cmd) {
140 canvas()->addCommand(cmd);
142 } else {
143 // don't leak shape when command could not be created
144 delete m_shape;
145 }
146
147 m_shape = 0;
148}
149
151{
152 if (m_pointCount == 0) {
153 if (m_usePath && m_selectedPath) {
155 }
156 m_pointCount = 1;
157 m_endOfPath = false;
159 m_lastMousePos = event->point;
161 m_deviceSupportsTilt = (event->xTilt() != 0 || event->yTilt() != 0);
162 return;
163 }
164
165 if (m_endOfPath) {
166 return;
167 }
168
169 ++m_pointCount;
170
171 setAngle(event);
172
173 QPointF newSpeed;
174 QPointF newPoint = calculateNewPoint(event->point, &newSpeed);
175 qreal width = calculateWidth(event->pressure());
176 qreal angle = calculateAngle(m_speed, newSpeed);
177
178 // add the previous point
179 m_shape->appendPoint(m_lastPoint, angle, width);
180
181 m_speed = newSpeed;
182 m_lastPoint = newPoint;
184
185 if (m_usePath && m_selectedPath) {
186 m_speed = QPointF(0, 0); // following path
187 }
188}
189
191{
192 if (!m_useAngle) {
193 m_angle = (360.0 - m_customAngle + 90.0) / 180.0 * M_PI;
194 return;
195 }
196
197 // setting m_angle to the angle of the device
198 if (event->xTilt() != 0 || event->yTilt() != 0) {
200 }
201
203 if (event->xTilt() == 0 && event->yTilt() == 0) {
204 return; // leave as is
205 }
206 if (event->x() == 0) {
207 m_angle = M_PI / 2.0;
208 return;
209 }
210
211 // y is inverted in qt painting
212 m_angle = std::atan(static_cast<double>(-event->yTilt()) / static_cast<double>(event->xTilt())) + M_PI / 2.0;
213 } else {
214 m_angle = event->rotation() + M_PI / 2.0;
215 }
216}
217
218QPointF KarbonCalligraphyTool::calculateNewPoint(const QPointF &mousePos, QPointF *speed)
219{
220 if (!m_usePath || !m_selectedPath) { // don't follow path
221 QPointF force = mousePos - m_lastPoint;
222 QPointF dSpeed = force / m_mass;
223 *speed = m_speed * (1.0 - m_drag) + dSpeed;
224 return m_lastPoint + *speed;
225 }
226
227 QPointF sp = mousePos - m_lastMousePos;
228 m_lastMousePos = mousePos;
229
230 // follow selected path
231 qreal step = QLineF(QPointF(0, 0), sp).length();
232 m_followPathPosition += step;
233
234 qreal t;
236 t = 1.0;
237 m_endOfPath = true;
238 } else {
239 t = m_selectedPathOutline.percentAtLength(m_followPathPosition);
240 }
241
242 QPointF res = m_selectedPathOutline.pointAtPercent(t);
243 *speed = res - m_lastPoint;
244 return res;
245}
246
248{
249 // calculate the modulo of the speed
250 qreal speed = std::sqrt(pow(m_speed.x(), 2) + pow(m_speed.y(), 2));
251 qreal thinning = m_thinning * (speed + 1) / 10.0; // can be negative
252
253 if (thinning > 1) {
254 thinning = 1;
255 }
256
257 if (!m_usePressure) {
258 pressure = 1.0;
259 }
260
261 qreal strokeWidth = m_strokeWidth * pressure * (1 - thinning);
262
263 const qreal MINIMUM_STROKE_WIDTH = 1.0;
264 if (strokeWidth < MINIMUM_STROKE_WIDTH) {
265 strokeWidth = MINIMUM_STROKE_WIDTH;
266 }
267
268 return strokeWidth;
269}
270
271qreal KarbonCalligraphyTool::calculateAngle(const QPointF &oldSpeed, const QPointF &newSpeed)
272{
273 // calculate the average of the speed (sum of the normalized values)
274 qreal oldLength = QLineF(QPointF(0, 0), oldSpeed).length();
275 qreal newLength = QLineF(QPointF(0, 0), newSpeed).length();
276 QPointF oldSpeedNorm = !qFuzzyCompare(oldLength + 1, 1) ?
277 oldSpeed / oldLength : QPointF(0, 0);
278 QPointF newSpeedNorm = !qFuzzyCompare(newLength + 1, 1) ?
279 newSpeed / newLength : QPointF(0, 0);
280 QPointF speed = oldSpeedNorm + newSpeedNorm;
281
282 // angle solely based on the speed
283 qreal speedAngle = 0;
284 if (speed.x() != 0) { // avoid division by zero
285 speedAngle = std::atan(speed.y() / speed.x());
286 } else if (speed.y() > 0) {
287 // x == 0 && y != 0
288 speedAngle = M_PI / 2;
289 } else if (speed.y() < 0) {
290 // x == 0 && y != 0
291 speedAngle = -M_PI / 2;
292 }
293 if (speed.x() < 0) {
294 speedAngle += M_PI;
295 }
296
297 // move 90 degrees
298 speedAngle += M_PI / 2;
299
300 qreal fixedAngle = m_angle;
301 // check if the fixed angle needs to be flipped
302 qreal diff = fixedAngle - speedAngle;
303 while (diff >= M_PI) { // normalize diff between -180 and 180
304 diff -= 2 * M_PI;
305 }
306 while (diff < -M_PI) {
307 diff += 2 * M_PI;
308 }
309
310 if (std::abs(diff) > M_PI / 2) { // if absolute value < 90
311 fixedAngle += M_PI; // += 180
312 }
313
314 qreal dAngle = speedAngle - fixedAngle;
315
316 // normalize dAngle between -90 and +90
317 while (dAngle >= M_PI / 2) {
318 dAngle -= M_PI;
319 }
320 while (dAngle < -M_PI / 2) {
321 dAngle += M_PI;
322 }
323
324 qreal angle = fixedAngle + dAngle * (1.0 - m_fixation);
325
326 return angle;
327}
328
329void KarbonCalligraphyTool::activate(const QSet<KoShape*> &shapes)
330{
331 KoToolBase::activate(shapes);
332
333 if (!m_widget) {
335 }
336
337 QAction *a = action("calligraphy_increase_width");
338 connect(a, SIGNAL(triggered()), m_widget, SLOT(increaseWidth()), Qt::UniqueConnection);
339
340 a = action("calligraphy_decrease_width");
341 connect(a, SIGNAL(triggered()), m_widget, SLOT(decreaseWidth()), Qt::UniqueConnection);
342
343 a = action("calligraphy_increase_angle");
344 connect(a, SIGNAL(triggered()), m_widget, SLOT(increaseAngle()), Qt::UniqueConnection);
345
346 a = action("calligraphy_decrease_angle");
347 connect(a, SIGNAL(triggered()), m_widget, SLOT(decreaseAngle()), Qt::UniqueConnection);
348
349
350 useCursor(Qt::CrossCursor);
351}
352
354{
355 QAction *a = action("calligraphy_increase_width");
356 disconnect(a, 0, this, 0);
357
358 a = action("calligraphy_decrease_width");
359 disconnect(a, 0, this, 0);
360
361 a = action("calligraphy_increase_angle");
362 disconnect(a, 0, this, 0);
363
364 a = action("calligraphy_decrease_angle");
365 disconnect(a, 0, this, 0);
366
368}
369
371{
372 // if the widget don't exists yet create it
373 QList<QPointer<QWidget> > widgets;
374
375 //KoFillConfigWidget *fillWidget = new KoFillConfigWidget(0);
376 //fillWidget->setWindowTitle(i18n("Fill"));
377 //widgets.append(fillWidget);
378
380 connect(m_widget, SIGNAL(usePathChanged(bool)),
381 this, SLOT(setUsePath(bool)));
382
383 connect(m_widget, SIGNAL(usePressureChanged(bool)),
384 this, SLOT(setUsePressure(bool)));
385
386 connect(m_widget, SIGNAL(useAngleChanged(bool)),
387 this, SLOT(setUseAngle(bool)));
388
389 connect(m_widget, SIGNAL(widthChanged(double)),
390 this, SLOT(setStrokeWidth(double)));
391
392 connect(m_widget, SIGNAL(thinningChanged(double)),
393 this, SLOT(setThinning(double)));
394
395 connect(m_widget, SIGNAL(angleChanged(int)),
396 this, SLOT(setAngle(int)));
397
398 connect(m_widget, SIGNAL(fixationChanged(double)),
399 this, SLOT(setFixation(double)));
400
401 connect(m_widget, SIGNAL(capsChanged(double)),
402 this, SLOT(setCaps(double)));
403
404 connect(m_widget, SIGNAL(massChanged(double)),
405 this, SLOT(setMass(double)));
406
407 connect(m_widget, SIGNAL(dragChanged(double)),
408 this, SLOT(setDrag(double)));
409
410 connect(this, SIGNAL(pathSelectedChanged(bool)),
411 m_widget, SLOT(setUsePathEnabled(bool)));
412
413 // sync all parameters with the loaded profile
414 m_widget->emitAll();
415 m_widget->setObjectName(i18nc("Object name of Calligraphy", "Calligraphy"));
416 m_widget->setWindowTitle(i18nc("Tool Option title of Calligraphy", "Calligraphy"));
417 widgets.append(m_widget);
418
419 return widgets;
420}
421
426
428{
429 m_strokeWidth = width;
430}
431
433{
434 m_thinning = thinning;
435}
436
438{
439 m_customAngle = angle;
440}
441
443{
444 m_fixation = fixation;
445}
446
448{
449 m_mass = mass * mass + 1;
450}
451
453{
454 m_drag = drag;
455}
456
458{
459 m_usePath = usePath;
460}
461
463{
464 m_usePressure = usePressure;
465}
466
468{
469 m_useAngle = useAngle;
470}
471
473{
474 m_caps = caps;
475}
476
478{
479 KoPathShape *oldSelectedPath = m_selectedPath; // save old value
480
482 if (selection) {
483 // null pointer if it the selection isn't a KoPathShape
484 // or if the selection is empty
486 dynamic_cast<KoPathShape *>(selection->firstSelectedShape());
487
488 // or if it's a KoPathShape but with no or more than one subpaths
490 m_selectedPath = 0;
491 }
492
493 // or if there ora none or more than 1 shapes selected
494 if (selection->count() != 1) {
495 m_selectedPath = 0;
496 }
497
498 // Q_EMIT signal it there wasn't a selected path and now there is
499 // or the other way around
500 if ((m_selectedPath != 0) != (oldSelectedPath != 0)) {
502 }
503 }
504}
const qreal M_PI
QPointF p2
QPointF p1
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
void appendPoint(const QPointF &p1, qreal angle, qreal width)
KarbonCalligraphicShape * m_shape
void setUseAngle(bool useAngle)
KarbonCalligraphyTool(KoCanvasBase *canvas)
qreal calculateAngle(const QPointF &oldSpeed, const QPointF &newSpeed)
void setStrokeWidth(double width)
KarbonCalligraphyOptionWidget * m_widget
void addPoint(KoPointerEvent *event)
void mouseReleaseEvent(KoPointerEvent *event) override
void mouseMoveEvent(KoPointerEvent *event) override
void mousePressEvent(KoPointerEvent *event) override
void setThinning(double thinning)
void activate(const QSet< KoShape * > &shapes) override
void pathSelectedChanged(bool selection)
QPointF calculateNewPoint(const QPointF &mousePos, QPointF *speed)
QList< QPointer< QWidget > > createOptionWidgets() override
KisPopupWidgetInterface * popupWidget() override
void setUsePressure(bool usePressure)
void paint(QPainter &painter, const KoViewConverter &converter) override
void setFixation(double fixation)
qreal calculateWidth(qreal pressure)
The PopupWidgetInterface abstract class defines the basic interface that will be used by all popup wi...
QPointer< KoShapeController > shapeController
virtual KoShapeManager * shapeManager() const =0
virtual void updateCanvas(const QRectF &rc)=0
virtual void addCommand(KUndo2Command *command)=0
virtual KoSelectedShapesProxy * selectedShapesProxy() const =0
selectedShapesProxy() is a special interface for keeping a persistent connections to selectionChanged...
A simple solid color shape background.
The position of a path point within a path shape.
Definition KoPathShape.h:63
void paint(QPainter &painter) const override
reimplemented
QPainterPath outline() const override
reimplemented
QRectF boundingRect() const override
reimplemented
int subpathCount() const
Returns the number of subpaths in the path.
qreal pressure() const
qreal yTilt() const
QPointF point
The point in document coordinates.
qreal xTilt() const
void deselectAll()
clear the selections list
void select(KoShape *shape)
KoShape * shapeAt(const QPointF &position, KoFlake::ShapeSelection selection=KoFlake::ShapeOnTop, bool omitHiddenShapes=true)
KoSelection * selection
QTransform absoluteTransformation() const
Definition KoShape.cpp:382
virtual void setBackground(QSharedPointer< KoShapeBackground > background)
Definition KoShape.cpp:918
KoCanvasBase * canvas() const
Returns the canvas the tool is working on.
void selectionChanged(bool hasSelection)
virtual KoToolSelection * selection()
void useCursor(const QCursor &cursor)
virtual void activate(const QSet< KoShape * > &shapes)
virtual void deactivate()
QAction * action(const QString &name) const
virtual QPointF documentToView(const QPointF &documentPoint) const
static bool qFuzzyCompare(half p1, half p2)
#define M_PI
Definition kis_global.h:111