Krita Source Code Documentation
Loading...
Searching...
No Matches
KarbonCalligraphicShape.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
8
9#include <KoPathPoint.h>
10
11#include <KoParameterShape_p.h>
12#include "KarbonSimplifyPath.h"
13#include <KoCurveFit.h>
14#include <KoColorBackground.h>
15
16#include <QDebug>
17#include <QColor>
18#include <QPainterPath>
19
20#include <cmath>
21#include <cstdlib>
22
23#undef M_PI
24const qreal M_PI = 3.1415927;
25
26struct KarbonCalligraphicShape::Private : public QSharedData
27{
28 Private(qreal _caps)
29 : lastWasFlip(false),
30 caps(_caps)
31
32 {
33 }
34
35 Private(const Private &rhs) = default;
36
38 qreal caps = 0.0;
39 // the actual data then determines it's shape (guide path + data for points)
41};
42
51
57
61
66
67void KarbonCalligraphicShape::appendPoint(const QPointF &point, qreal angle, qreal width)
68{
69 // convert the point from canvas to shape coordinates
70 QPointF p = point - position();
71 KarbonCalligraphicPoint calligraphicPoint(p, angle, width);
72
74 handles.append(p);
76 s->points.append(calligraphicPoint);
77 appendPointToPath(calligraphicPoint);
78
79 // make the angle of the first point more in line with the actual
80 // direction
81 if (s->points.count() == 4) {
82 s->points[0].setAngle(angle);
83 s->points[1].setAngle(angle);
84 s->points[2].setAngle(angle);
85 }
86
87 normalize();
88}
89
91{
92 qreal dx = std::cos(p.angle()) * p.width();
93 qreal dy = std::sin(p.angle()) * p.width();
94
95 // find the outline points
96 QPointF p1 = p.point() - QPointF(dx / 2, dy / 2);
97 QPointF p2 = p.point() + QPointF(dx / 2, dy / 2);
98
99 if (pointCount() == 0) {
100 moveTo(p1);
101 lineTo(p2);
102 return;
103 }
104 // pointCount > 0
105
106 bool flip = (pointCount() >= 2) ? flipDetected(p1, p2) : false;
107
108 // if there was a flip add additional points
109 if (flip) {
111 if (pointCount() > 4) {
113 }
114 }
115
117
118 if (pointCount() > 4) {
120
121 if (flip) {
122 int index = pointCount() / 2;
123 // find the last two points
124 KoPathPoint *last1 = pointByIndex(KoPathPointIndex(0, index - 1));
125 KoPathPoint *last2 = pointByIndex(KoPathPointIndex(0, index));
126
127 last1->removeControlPoint1();
128 last1->removeControlPoint2();
129 last2->removeControlPoint1();
130 last2->removeControlPoint2();
131 s->lastWasFlip = true;
132 }
133
134 if (s->lastWasFlip) {
135 int index = pointCount() / 2;
136 // find the previous two points
137 KoPathPoint *prev1 = pointByIndex(KoPathPointIndex(0, index - 2));
138 KoPathPoint *prev2 = pointByIndex(KoPathPointIndex(0, index + 1));
139
140 prev1->removeControlPoint1();
141 prev1->removeControlPoint2();
142 prev2->removeControlPoint1();
143 prev2->removeControlPoint2();
144
145 if (!flip) {
146 s->lastWasFlip = false;
147 }
148 }
149 }
150
151 // add initial cap if it's the fourth added point
152 // this code is here because this function is called from different places
153 // pointCount() == 8 may causes crashes because it doesn't take possible
154 // flips into account
155
156 if (s->points.count() >= 4 && p == s->points[3]) {
157 addCap(3, 0, 0, true);
158 // duplicate the last point to make the points remain "balanced"
159 // needed to keep all indexes code (else I would need to change
160 // everything in the code...)
162 KoPathPoint *newPoint = new KoPathPoint(this, last->point());
163 insertPoint(newPoint, KoPathPointIndex(0, pointCount()));
164 close();
165 }
166}
167
168void KarbonCalligraphicShape::appendPointsToPathAux(const QPointF &p1, const QPointF &p2)
169{
170 KoPathPoint *pathPoint1 = new KoPathPoint(this, p1);
171 KoPathPoint *pathPoint2 = new KoPathPoint(this, p2);
172
173 // calculate the index of the insertion position
174 int index = pointCount() / 2;
175
176 insertPoint(pathPoint2, KoPathPointIndex(0, index));
177 insertPoint(pathPoint1, KoPathPointIndex(0, index));
178}
179
181{
182 int index = pointCount() / 2;
183 smoothPoint(index - 2);
184 smoothPoint(index + 1);
185}
186
188{
189 if (pointCount() < index + 2) {
190 return;
191 } else if (index < 1) {
192 return;
193 }
194
195 const KoPathPointIndex PREV(0, index - 1);
196 const KoPathPointIndex INDEX(0, index);
197 const KoPathPointIndex NEXT(0, index + 1);
198
199 QPointF prev = pointByIndex(PREV)->point();
200 QPointF point = pointByIndex(INDEX)->point();
201 QPointF next = pointByIndex(NEXT)->point();
202
203 QPointF vector = next - prev;
204 qreal dist = (QLineF(prev, next)).length();
205 // normalize the vector (make it's size equal to 1)
206 if (!qFuzzyCompare(dist + 1, 1)) {
207 vector /= dist;
208 }
209 qreal mult = 0.35; // found by trial and error, might not be perfect...
210 // distance of the control points from the point
211 qreal dist1 = (QLineF(point, prev)).length() * mult;
212 qreal dist2 = (QLineF(point, next)).length() * mult;
213 QPointF vector1 = vector * dist1;
214 QPointF vector2 = vector * dist2;
215 QPointF controlPoint1 = point - vector1;
216 QPointF controlPoint2 = point + vector2;
217
218 pointByIndex(INDEX)->setControlPoint1(controlPoint1);
219 pointByIndex(INDEX)->setControlPoint2(controlPoint2);
220}
221
223{
224 if (pointCount() < 6) {
225 return QRectF();
226 }
227
228 int index = pointCount() / 2;
229
230 QPointF p1 = pointByIndex(KoPathPointIndex(0, index - 3))->point();
231 QPointF p2 = pointByIndex(KoPathPointIndex(0, index - 2))->point();
232 QPointF p3 = pointByIndex(KoPathPointIndex(0, index - 1))->point();
233 QPointF p4 = pointByIndex(KoPathPointIndex(0, index))->point();
234 QPointF p5 = pointByIndex(KoPathPointIndex(0, index + 1))->point();
235 QPointF p6 = pointByIndex(KoPathPointIndex(0, index + 2))->point();
236
237 // TODO: also take the control points into account
238 QPainterPath p;
239 p.moveTo(p1);
240 p.lineTo(p2);
241 p.lineTo(p3);
242 p.lineTo(p4);
243 p.lineTo(p5);
244 p.lineTo(p6);
245
246 return p.boundingRect().translated(position());
247}
248
249bool KarbonCalligraphicShape::flipDetected(const QPointF &p1, const QPointF &p2)
250{
251 // detect the flip caused by the angle changing 180 degrees
252 // thus detect the boundary crossing
253 int index = pointCount() / 2;
254 QPointF last1 = pointByIndex(KoPathPointIndex(0, index - 1))->point();
255 QPointF last2 = pointByIndex(KoPathPointIndex(0, index))->point();
256
257 int sum1 = std::abs(ccw(p1, p2, last1) + ccw(p1, last2, last1));
258 int sum2 = std::abs(ccw(p2, p1, last2) + ccw(p2, last1, last2));
259 // if there was a flip
260 return sum1 < 2 && sum2 < 2;
261}
262
263int KarbonCalligraphicShape::ccw(const QPointF &p1, const QPointF &p2,const QPointF &p3)
264{
265 // calculate two times the area of the triangle formed by the points given
266 qreal area2 = (p2.x() - p1.x()) * (p3.y() - p1.y()) -
267 (p2.y() - p1.y()) * (p3.x() - p1.x());
268 if (area2 > 0) {
269 return +1; // the points are given in counterclockwise order
270 } else if (area2 < 0) {
271 return -1; // the points are given in clockwise order
272 } else {
273 return 0; // the points form a degenerate triangle
274 }
275}
276
277void KarbonCalligraphicShape::setSize(const QSizeF &newSize)
278{
279 // QSizeF oldSize = size();
280 // TODO: check
282}
283
285{
286 QPointF offset(KoParameterShape::normalize());
287 QTransform matrix;
288 matrix.translate(-offset.x(), -offset.y());
289
290 for (int i = 0; i < s->points.size(); ++i) {
291 s->points[i].setPoint(matrix.map(s->points[i].point()));
292 }
293
294 return offset;
295}
296
298 const QPointF &point,
299 Qt::KeyboardModifiers modifiers)
300{
301 Q_UNUSED(modifiers);
302 s->points[handleId].setPoint(point);
303}
304
306{
307 Q_UNUSED(size);
308
309 QPointF pos = position();
310
311 // remove all points
312 clear();
313 setPosition(QPoint(0, 0));
314
315 Q_FOREACH (const KarbonCalligraphicPoint &p, s->points) {
317 }
318
320 Q_FOREACH (const KarbonCalligraphicPoint &p, s->points) {
321 handles.append(p.point());
322 }
324
325 setPosition(pos);
326 normalize();
327}
328
330{
331 if (s->points.count() < 2) {
332 return;
333 }
334
335 close();
336
337 // add final cap
338 addCap(s->points.count() - 2, s->points.count() - 1, pointCount() / 2);
339
340 // TODO: the error should be proportional to the width
341 // and it shouldn't be a magic number
342 karbonSimplifyPath(this, 0.3);
343}
344
345void KarbonCalligraphicShape::addCap(int index1, int index2, int pointIndex, bool inverted)
346{
347 QPointF p1 = s->points[index1].point();
348 QPointF p2 = s->points[index2].point();
349
350 // TODO: review why spikes can appear with a lower limit
351 QPointF delta = p2 - p1;
352 if (delta.manhattanLength() < 1.0) {
353 return;
354 }
355
356 QPointF direction = QLineF(QPointF(0, 0), delta).unitVector().p2();
357 qreal width = s->points[index2].width();
358 QPointF p = p2 + direction * s->caps * width;
359
360 KoPathPoint *newPoint = new KoPathPoint(this, p);
361
362 qreal angle = s->points[index2].angle();
363 if (inverted) {
364 angle += M_PI;
365 }
366
367 qreal dx = std::cos(angle) * width;
368 qreal dy = std::sin(angle) * width;
369 newPoint->setControlPoint1(QPointF(p.x() - dx / 2, p.y() - dy / 2));
370 newPoint->setControlPoint2(QPointF(p.x() + dx / 2, p.y() + dy / 2));
371
372 insertPoint(newPoint, KoPathPointIndex(0, pointIndex));
373}
374
379
381{
382 // do not attempt to simplify if there are too few points
383 if (s->points.count() < 3) {
384 return;
385 }
386
387 QList<QPointF> points;
388 Q_FOREACH (const KarbonCalligraphicPoint &p, s->points) {
389 points.append(p.point());
390 }
391
392 // cumulative data used to determine if the point can be removed
393 qreal widthChange = 0;
394 qreal directionChange = 0;
395 QList<KarbonCalligraphicPoint>::iterator i = s->points.begin() + 2;
396
397 while (i != std::prev(s->points.end())) {
398 QPointF point = i->point();
399
400 qreal width = i->width();
401 qreal prevWidth = std::prev(i)->width();
402 qreal widthDiff = width - prevWidth;
403 widthDiff /= qMax(width, prevWidth);
404
405 qreal directionDiff = 0;
406 if (std::next(i) != s->points.end()) {
407 QPointF prev = std::prev(i)->point();
408 QPointF next = std::next(i)->point();
409
410 directionDiff = QLineF(prev, point).angleTo(QLineF(point, next));
411 if (directionDiff > 180) {
412 directionDiff -= 360;
413 }
414 }
415
416 if (directionChange * directionDiff >= 0 &&
417 qAbs(directionChange + directionDiff) < 20 &&
418 widthChange * widthDiff >= 0 &&
419 qAbs(widthChange + widthDiff) < 0.1) {
420 // deleted point
421 i = s->points.erase(i);
422 directionChange += directionDiff;
423 widthChange += widthDiff;
424 } else {
425 // keep point
426 directionChange = 0;
427 widthChange = 0;
428 ++i;
429 }
430 }
431
432 updatePath(QSizeF());
433}
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
const qreal M_PI
#define KarbonCalligraphicShapeId
void karbonSimplifyPath(KoPathShape *path, qreal error)
const Params2D p
QPointF p2
QPointF p3
QPointF p1
QSharedPointer< KoShapeStrokeModel > KoShapeStrokeModelSP
#define KoPathShapeId
Definition KoPathShape.h:20
QPair< int, int > KoPathPointIndex
Definition KoPathShape.h:28
void setSize(const QSizeF &newSize) override
Resize the shape.
KoShape * cloneShape() const override
creates a deep copy of the shape or shape's subtree
void appendPointsToPathAux(const QPointF &p1, const QPointF &p2)
void appendPoint(const QPointF &p1, qreal angle, qreal width)
static int ccw(const QPointF &p1, const QPointF &p2, const QPointF &p3)
bool flipDetected(const QPointF &p1, const QPointF &p2)
void updatePath(const QSizeF &size) override
Update the path of the parameter shape.
QString pathShapeId() const override
void addCap(int index1, int index2, int pointIndex, bool inverted=false)
void appendPointToPath(const KarbonCalligraphicPoint &p)
QPointF normalize() override
Normalizes the path data.
void moveHandleAction(int handleId, const QPointF &point, Qt::KeyboardModifiers modifiers=Qt::NoModifier) override
Updates the internal state of a KoParameterShape.
QSharedDataPointer< Private > s
A simple solid color shape background.
QPointF normalize() override
Normalizes the path data.
QList< QPointF > handles
the handles that the user can grab and change
void setSize(const QSizeF &size) override
reimplemented from KoShape
void setHandles(const QList< QPointF > &handles)
A KoPathPoint represents a point in a path.
void setControlPoint1(const QPointF &point)
Set the control point 1.
QPointF point
void setControlPoint2(const QPointF &point)
Set the control point 2.
void removeControlPoint1()
Removes the first control point.
void removeControlPoint2()
Removes the second control point.
KoPathPoint * lineTo(const QPointF &p)
Adds a new line segment.
QSizeF size() const override
reimplemented
void close()
Closes the current subpath.
void setFillRule(Qt::FillRule fillRule)
Sets the fill rule to be used for painting the background.
KoPathPoint * moveTo(const QPointF &p)
Starts a new Subpath.
int pointCount() const
Returns the number of points in the path.
KoPathPoint * pointByIndex(const KoPathPointIndex &pointIndex) const
Returns the path point specified by a path point index.
void clear()
Removes all subpaths and their points from the path.
bool insertPoint(KoPathPoint *point, const KoPathPointIndex &pointIndex)
Inserts a new point into the given subpath at the specified position.
virtual void setStroke(KoShapeStrokeModelSP stroke)
Definition KoShape.cpp:1081
virtual void setBackground(QSharedPointer< KoShapeBackground > background)
Definition KoShape.cpp:918
virtual void setPosition(const QPointF &position)
Set the position of the shape in pt.
Definition KoShape.cpp:295
void setShapeId(const QString &id)
Definition KoShape.cpp:1062
QPointF position() const
Get the position of the shape in pt.
Definition KoShape.cpp:825
static bool qFuzzyCompare(half p1, half p2)
#define M_PI
Definition kis_global.h:111
Private(const Private &rhs)=default
QList< KarbonCalligraphicPoint > points