1 /* This file is part of the KDE project
2 Copyright (C) 2008 Fela Winkelmolen <fela.kde@gmail.com>
3
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
8
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Library General Public License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20 #include "KarbonCalligraphicShape.h"
21
22 #include <KoPathPoint.h>
23
24 #include "KarbonSimplifyPath.h"
25 #include <KoCurveFit.h>
26 #include <KoColorBackground.h>
27
28 #include <QDebug>
29 #include <QColor>
30 #include <QPainterPath>
31
32 #include <cmath>
33 #include <cstdlib>
34
35 #undef M_PI
36 const qreal M_PI = 3.1415927;
37
KarbonCalligraphicShape(qreal caps)38 KarbonCalligraphicShape::KarbonCalligraphicShape(qreal caps)
39 : m_lastWasFlip(false),
40 m_caps(caps)
41 {
42 setShapeId(KoPathShapeId);
43 setFillRule(Qt::WindingFill);
44 setBackground(QSharedPointer<KoShapeBackground>(new KoColorBackground(QColor(Qt::black))));
45 setStroke(0);
46 }
47
~KarbonCalligraphicShape()48 KarbonCalligraphicShape::~KarbonCalligraphicShape()
49 {
50 }
51
appendPoint(const QPointF & point,qreal angle,qreal width)52 void KarbonCalligraphicShape::appendPoint(const QPointF &point,
53 qreal angle,
54 qreal width)
55 {
56 // convert the point from canvas to shape coordinates
57 QPointF p = point - position();
58 KarbonCalligraphicPoint *calligraphicPoint =
59 new KarbonCalligraphicPoint(p, angle, width);
60
61 QVector<QPointF> handles = this->handles();
62 handles.append(p);
63 setHandles(handles);
64 m_points.append(calligraphicPoint);
65 appendPointToPath(*calligraphicPoint);
66
67 // make the angle of the first point more in line with the actual
68 // direction
69 if (m_points.count() == 4) {
70 m_points[0]->setAngle(angle);
71 m_points[1]->setAngle(angle);
72 m_points[2]->setAngle(angle);
73 }
74 }
75
76 void KarbonCalligraphicShape::
appendPointToPath(const KarbonCalligraphicPoint & p)77 appendPointToPath(const KarbonCalligraphicPoint &p)
78 {
79 qreal dx = std::cos(p.angle()) * p.width();
80 qreal dy = std::sin(p.angle()) * p.width();
81
82 // find the outline points
83 QPointF p1 = p.point() - QPointF(dx / 2, dy / 2);
84 QPointF p2 = p.point() + QPointF(dx / 2, dy / 2);
85
86 if (pointCount() == 0) {
87 moveTo(p1);
88 lineTo(p2);
89 normalize();
90 return;
91 }
92 // pointCount > 0
93
94 bool flip = (pointCount() >= 2) ? flipDetected(p1, p2) : false;
95
96 // if there was a flip add additional points
97 if (flip) {
98 appendPointsToPathAux(p2, p1);
99 if (pointCount() > 4)
100 smoothLastPoints();
101 }
102
103 appendPointsToPathAux(p1, p2);
104
105 if (pointCount() > 4) {
106 smoothLastPoints();
107
108 if (flip) {
109 int index = pointCount() / 2;
110 // find the last two points
111 KoPathPoint *last1 = pointByIndex(KoPathPointIndex(0, index - 1));
112 KoPathPoint *last2 = pointByIndex(KoPathPointIndex(0, index));
113
114 last1->removeControlPoint1();
115 last1->removeControlPoint2();
116 last2->removeControlPoint1();
117 last2->removeControlPoint2();
118 m_lastWasFlip = true;
119 }
120
121 if (m_lastWasFlip) {
122 int index = pointCount() / 2;
123 // find the previous two points
124 KoPathPoint *prev1 = pointByIndex(KoPathPointIndex(0, index - 2));
125 KoPathPoint *prev2 = pointByIndex(KoPathPointIndex(0, index + 1));
126
127 prev1->removeControlPoint1();
128 prev1->removeControlPoint2();
129 prev2->removeControlPoint1();
130 prev2->removeControlPoint2();
131
132 if (! flip)
133 m_lastWasFlip = false;
134 }
135 }
136 normalize();
137
138 // add initial cap if it's the fourth added point
139 // this code is here because this function is called from different places
140 // pointCount() == 8 may causes crashes because it doesn't take possible
141 // flips into account
142 if (m_points.count() >= 4 && &p == m_points[3]) {
143 qDebug() << "Adding caps!!!!!!!!!!!!!!!!" << m_points.count();
144 addCap(3, 0, 0, true);
145 // duplicate the last point to make the points remain "balanced"
146 // needed to keep all indexes code (else I would need to change
147 // everything in the code...)
148 KoPathPoint *last = pointByIndex(KoPathPointIndex(0, pointCount() - 1));
149 KoPathPoint *newPoint = new KoPathPoint(this, last->point());
150 insertPoint(newPoint, KoPathPointIndex(0, pointCount()));
151 close();
152 }
153 }
154
appendPointsToPathAux(const QPointF & p1,const QPointF & p2)155 void KarbonCalligraphicShape::appendPointsToPathAux(const QPointF &p1,
156 const QPointF &p2)
157 {
158 KoPathPoint *pathPoint1 = new KoPathPoint(this, p1);
159 KoPathPoint *pathPoint2 = new KoPathPoint(this, p2);
160
161 // calculate the index of the insertion position
162 int index = pointCount() / 2;
163
164 insertPoint(pathPoint2, KoPathPointIndex(0, index));
165 insertPoint(pathPoint1, KoPathPointIndex(0, index));
166 }
167
smoothLastPoints()168 void KarbonCalligraphicShape::smoothLastPoints()
169 {
170 int index = pointCount() / 2;
171 smoothPoint(index - 2);
172 smoothPoint(index + 1);
173 }
174
smoothPoint(const int index)175 void KarbonCalligraphicShape::smoothPoint(const int index)
176 {
177 if (pointCount() < index + 2) {
178 qDebug() << "index to high";
179 return;
180 } else if (index < 1) {
181 qDebug() << "index to low";
182 return;
183 }
184
185 const KoPathPointIndex PREV(0, index - 1);
186 const KoPathPointIndex INDEX(0, index);
187 const KoPathPointIndex NEXT(0, index + 1);
188
189 QPointF prev = pointByIndex(PREV)->point();
190 QPointF point = pointByIndex(INDEX)->point();
191 QPointF next = pointByIndex(NEXT)->point();
192
193 QPointF vector = next - prev;
194 qreal dist = (QLineF(prev, next)).length();
195 // normalize the vector (make it's size equal to 1)
196 if (! qFuzzyCompare(dist + 1, 1))
197 vector /= dist;
198 qreal mult = 0.35; // found by trial and error, might not be perfect...
199 // distance of the control points from the point
200 qreal dist1 = (QLineF(point, prev)).length() * mult;
201 qreal dist2 = (QLineF(point, next)).length() * mult;
202 QPointF vector1 = vector * dist1;
203 QPointF vector2 = vector * dist2;
204 QPointF controlPoint1 = point - vector1;
205 QPointF controlPoint2 = point + vector2;
206
207 pointByIndex(INDEX)->setControlPoint1(controlPoint1);
208 pointByIndex(INDEX)->setControlPoint2(controlPoint2);
209 }
210
211
lastPieceBoundingRect()212 const QRectF KarbonCalligraphicShape::lastPieceBoundingRect()
213 {
214 if (pointCount() < 6)
215 return QRectF();
216
217 int index = pointCount() / 2;
218
219 QPointF p1 = pointByIndex(KoPathPointIndex(0, index - 3))->point();
220 QPointF p2 = pointByIndex(KoPathPointIndex(0, index - 2))->point();
221 QPointF p3 = pointByIndex(KoPathPointIndex(0, index - 1))->point();
222 QPointF p4 = pointByIndex(KoPathPointIndex(0, index))->point();
223 QPointF p5 = pointByIndex(KoPathPointIndex(0, index + 1))->point();
224 QPointF p6 = pointByIndex(KoPathPointIndex(0, index + 2))->point();
225
226 // TODO: also take the control points into account
227 QPainterPath p;
228 p.moveTo(p1);
229 p.lineTo(p2);
230 p.lineTo(p3);
231 p.lineTo(p4);
232 p.lineTo(p5);
233 p.lineTo(p6);
234
235 return p.boundingRect().translated(position());
236 }
237
238
flipDetected(const QPointF & p1,const QPointF & p2)239 bool KarbonCalligraphicShape::flipDetected(const QPointF &p1, const QPointF &p2)
240 {
241 // detect the flip caused by the angle changing 180 degrees
242 // thus detect the boundary crossing
243 int index = pointCount() / 2;
244 QPointF last1 = pointByIndex(KoPathPointIndex(0, index - 1))->point();
245 QPointF last2 = pointByIndex(KoPathPointIndex(0, index))->point();
246
247 int sum1 = std::abs(ccw(p1, p2, last1) + ccw(p1, last2, last1));
248 int sum2 = std::abs(ccw(p2, p1, last2) + ccw(p2, last1, last2));
249 // if there was a flip
250 return sum1 < 2 && sum2 < 2;
251 }
252
ccw(const QPointF & p1,const QPointF & p2,const QPointF & p3)253 int KarbonCalligraphicShape::ccw(const QPointF &p1,
254 const QPointF &p2,
255 const QPointF &p3)
256 {
257 // calculate two times the area of the triangle fomed by the points given
258 qreal area2 = (p2.x() - p1.x()) * (p3.y() - p1.y()) -
259 (p2.y() - p1.y()) * (p3.x() - p1.x());
260 if (area2 > 0) {
261 return +1; // the points are given in counterclockwise order
262 } else if (area2 < 0) {
263 return -1; // the points are given in clockwise order
264 } else {
265 return 0; // the points form a degenerate triangle
266 }
267 }
268
269
270
setSize(const QSizeF & newSize)271 void KarbonCalligraphicShape::setSize(const QSizeF &newSize)
272 {
273 // QSizeF oldSize = size();
274 // TODO: check
275 KoParameterShape::setSize(newSize);
276 }
277
normalize()278 QPointF KarbonCalligraphicShape::normalize()
279 {
280 QPointF offset(KoParameterShape::normalize());
281 QTransform matrix;
282 matrix.translate(-offset.x(), -offset.y());
283
284 for (int i = 0; i < m_points.size(); ++i) {
285 m_points[i]->setPoint(matrix.map(m_points[i]->point()));
286 }
287
288 return offset;
289 }
290
moveHandleAction(int handleId,const QPointF & point,Qt::KeyboardModifiers modifiers)291 void KarbonCalligraphicShape::moveHandleAction(int handleId,
292 const QPointF & point,
293 Qt::KeyboardModifiers modifiers)
294 {
295 Q_UNUSED(modifiers);
296 m_points[handleId]->setPoint(point);
297 }
298
updatePath(const QSizeF & size)299 void KarbonCalligraphicShape::updatePath(const QSizeF &size)
300 {
301 Q_UNUSED(size);
302
303 QPointF pos = position();
304
305 // remove all points
306 clear();
307 setPosition(QPoint(0, 0));
308
309 foreach(KarbonCalligraphicPoint *p, m_points)
310 appendPointToPath(*p);
311
312 simplifyPath();
313
314 QVector<QPointF> handles;
315 handles.reserve(m_points.count());
316 foreach(KarbonCalligraphicPoint *p, m_points) {
317 handles.append(p->point());
318 }
319 setHandles(handles);
320
321 setPosition(pos);
322 }
323
simplifyPath()324 void KarbonCalligraphicShape::simplifyPath()
325 {
326 if (m_points.count() < 2)
327 return;
328
329 close();
330
331 // add final cap
332 addCap(m_points.count() - 2, m_points.count() - 1, pointCount() / 2);
333
334 // TODO: the error should be proportional to the width
335 // and it shouldn't be a magic number
336 karbonSimplifyPath(this, 0.3);
337 }
338
addCap(int index1,int index2,int pointIndex,bool inverted)339 void KarbonCalligraphicShape::addCap(int index1, int index2, int pointIndex,
340 bool inverted)
341 {
342 QPointF p1 = m_points[index1]->point();
343 QPointF p2 = m_points[index2]->point();
344
345 // TODO: review why spikes can appear with a lower limit
346 QPointF delta = p2 - p1;
347 if (delta.manhattanLength() < 1.0)
348 return;
349
350 QPointF direction = QLineF(QPointF(0, 0), delta).unitVector().p2();
351 qreal width = m_points[index2]->width();
352 QPointF p = p2 + direction * m_caps * width;
353
354 KoPathPoint * newPoint = new KoPathPoint(this, p);
355
356 qreal angle = m_points[index2]->angle();
357 if (inverted)
358 angle += M_PI;
359
360 qreal dx = std::cos(angle) * width;
361 qreal dy = std::sin(angle) * width;
362 newPoint->setControlPoint1(QPointF(p.x() - dx / 2, p.y() - dy / 2));
363 newPoint->setControlPoint2(QPointF(p.x() + dx / 2, p.y() + dy / 2));
364
365 insertPoint(newPoint, KoPathPointIndex(0, pointIndex));
366 }
367
pathShapeId() const368 QString KarbonCalligraphicShape::pathShapeId() const
369 {
370 return KarbonCalligraphicShapeId;
371 }
372
simplifyGuidePath()373 void KarbonCalligraphicShape::simplifyGuidePath()
374 {
375 // do not attempt to simplify if there are too few points
376 if (m_points.count() < 3)
377 return;
378
379 // cumulative data used to determine if the point can be removed
380 qreal widthChange = 0;
381 qreal directionChange = 0;
382 QList<KarbonCalligraphicPoint *>::iterator i = m_points.begin() + 2;
383
384 while (i != m_points.end() - 1) {
385 QPointF point = (*i)->point();
386
387 qreal width = (*i)->width();
388 qreal prevWidth = (*(i - 1))->width();
389 qreal widthDiff = width - prevWidth;
390 widthDiff /= qMax(width, prevWidth);
391
392 qreal directionDiff = 0;
393 if ((i + 1) != m_points.end()) {
394 QPointF prev = (*(i - 1))->point();
395 QPointF next = (*(i + 1))->point();
396
397 directionDiff = QLineF(prev, point).angleTo(QLineF(point, next));
398 if (directionDiff > 180)
399 directionDiff -= 360;
400 }
401
402 if (directionChange * directionDiff >= 0 &&
403 qAbs(directionChange + directionDiff) < 20 &&
404 widthChange * widthDiff >= 0 &&
405 qAbs(widthChange + widthDiff) < 0.1) {
406 // deleted point
407 delete *i;
408 i = m_points.erase(i);
409 directionChange += directionDiff;
410 widthChange += widthDiff;
411 } else {
412 // keep point
413 directionChange = 0;
414 widthChange = 0;
415 ++i;
416 }
417 }
418
419 updatePath(QSizeF());
420 }
421