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