1 /* This file is part of the KDE project
2    Copyright (C) 2006-2009 Jan Hambrecht <jaham@gmx.net>
3    Copyright (C) 2009 Thomas Zander <zander@kde.org>
4 
5    This library is free software; you can redistribute it and/or
6    modify it under the terms of the GNU Library General Public
7    License as published by the Free Software Foundation; either
8    version 2 of the License, or (at your option) any later version.
9 
10    This library is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13    Library General Public License for more details.
14 
15    You should have received a copy of the GNU Library General Public License
16    along with this library; see the file COPYING.LIB.  If not, write to
17    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18  * Boston, MA 02110-1301, USA.
19 */
20 
21 #include "StarShape.h"
22 
23 #include <KoParameterShape_p.h>
24 #include <KoPathPoint.h>
25 #include <KoShapeLoadingContext.h>
26 #include <KoShapeSavingContext.h>
27 #include <KoXmlReader.h>
28 #include <KoXmlNS.h>
29 #include <KoXmlWriter.h>
30 #include <QStringList>
31 
32 #include <math.h>
33 
StarShape()34 StarShape::StarShape()
35     : m_cornerCount(5)
36     , m_zoomX(1.0)
37     , m_zoomY(1.0)
38     , m_convex(false)
39 {
40     m_radius[base] = 25.0;
41     m_radius[tip] = 50.0;
42     m_angles[base] = m_angles[tip] = defaultAngleRadian();
43     m_roundness[base] = m_roundness[tip] = 0.0f;
44 
45     m_center = QPointF(50, 50);
46     updatePath(QSize(100, 100));
47 }
48 
StarShape(const StarShape & rhs)49 StarShape::StarShape(const StarShape &rhs)
50     : KoParameterShape(rhs),
51       m_cornerCount(rhs.m_cornerCount),
52       m_radius(rhs.m_radius),
53       m_angles(rhs.m_angles),
54       m_zoomX(rhs.m_zoomX),
55       m_zoomY(rhs.m_zoomY),
56       m_roundness(rhs.m_roundness),
57       m_center(rhs.m_center),
58       m_convex(rhs.m_convex)
59 {
60 }
61 
~StarShape()62 StarShape::~StarShape()
63 {
64 }
65 
cloneShape() const66 KoShape *StarShape::cloneShape() const
67 {
68     return new StarShape(*this);
69 }
70 
71 
setCornerCount(uint cornerCount)72 void StarShape::setCornerCount(uint cornerCount)
73 {
74     if (cornerCount >= 3) {
75         double oldDefaultAngle = defaultAngleRadian();
76         m_cornerCount = cornerCount;
77         double newDefaultAngle = defaultAngleRadian();
78         m_angles[base] += newDefaultAngle - oldDefaultAngle;
79         m_angles[tip] += newDefaultAngle - oldDefaultAngle;
80 
81         updatePath(QSize());
82     }
83 }
84 
cornerCount() const85 uint StarShape::cornerCount() const
86 {
87     return m_cornerCount;
88 }
89 
setBaseRadius(qreal baseRadius)90 void StarShape::setBaseRadius(qreal baseRadius)
91 {
92     m_radius[base] = fabs(baseRadius);
93     updatePath(QSize());
94 }
95 
baseRadius() const96 qreal StarShape::baseRadius() const
97 {
98     return m_radius[base];
99 }
100 
setTipRadius(qreal tipRadius)101 void StarShape::setTipRadius(qreal tipRadius)
102 {
103     m_radius[tip] = fabs(tipRadius);
104     updatePath(QSize());
105 }
106 
tipRadius() const107 qreal StarShape::tipRadius() const
108 {
109     return m_radius[tip];
110 }
111 
setBaseRoundness(qreal baseRoundness)112 void StarShape::setBaseRoundness(qreal baseRoundness)
113 {
114     m_roundness[base] = baseRoundness;
115     updatePath(QSize());
116 }
117 
setTipRoundness(qreal tipRoundness)118 void StarShape::setTipRoundness(qreal tipRoundness)
119 {
120     m_roundness[tip] = tipRoundness;
121     updatePath(QSize());
122 }
123 
setConvex(bool convex)124 void StarShape::setConvex(bool convex)
125 {
126     m_convex = convex;
127     updatePath(QSize());
128 }
129 
convex() const130 bool StarShape::convex() const
131 {
132     return m_convex;
133 }
134 
starCenter() const135 QPointF StarShape::starCenter() const
136 {
137     return m_center;
138 }
139 
moveHandleAction(int handleId,const QPointF & point,Qt::KeyboardModifiers modifiers)140 void StarShape::moveHandleAction(int handleId, const QPointF &point, Qt::KeyboardModifiers modifiers)
141 {
142     if (modifiers & Qt::ShiftModifier) {
143         QPointF handle = handles()[handleId];
144         QPointF tangentVector = point - handle;
145         qreal distance = sqrt(tangentVector.x() * tangentVector.x() + tangentVector.y() * tangentVector.y());
146         QPointF radialVector = handle - m_center;
147         // cross product to determine in which direction the user is dragging
148         qreal moveDirection = radialVector.x() * tangentVector.y() - radialVector.y() * tangentVector.x();
149         // make the roundness stick to zero if distance is under a certain value
150         float snapDistance = 3.0;
151         if (distance >= 0.0) {
152             distance = distance < snapDistance ? 0.0 : distance - snapDistance;
153         } else {
154             distance = distance > -snapDistance ? 0.0 : distance + snapDistance;
155         }
156         // control changes roundness on both handles, else only the actual handle roundness is changed
157         if (modifiers & Qt::ControlModifier) {
158             m_roundness[handleId] = moveDirection < 0.0f ? distance : -distance;
159         } else {
160             m_roundness[base] = m_roundness[tip] = moveDirection < 0.0f ? distance : -distance;
161         }
162     } else {
163         QPointF distVector = point - m_center;
164         // unapply scaling
165         distVector.rx() /= m_zoomX;
166         distVector.ry() /= m_zoomY;
167         m_radius[handleId] = sqrt(distVector.x() * distVector.x() + distVector.y() * distVector.y());
168 
169         qreal angle = atan2(distVector.y(), distVector.x());
170         if (angle < 0.0) {
171             angle += 2.0 * M_PI;
172         }
173         qreal diffAngle = angle - m_angles[handleId];
174         qreal radianStep = M_PI / static_cast<qreal>(m_cornerCount);
175         if (handleId == tip) {
176             m_angles[tip] += diffAngle - radianStep;
177             m_angles[base] += diffAngle - radianStep;
178         } else {
179             // control make the base point move freely
180             if (modifiers & Qt::ControlModifier) {
181                 m_angles[base] += diffAngle - 2 * radianStep;
182             } else {
183                 m_angles[base] = m_angles[tip];
184             }
185         }
186     }
187 }
188 
updatePath(const QSizeF & size)189 void StarShape::updatePath(const QSizeF &size)
190 {
191     Q_UNUSED(size);
192     qreal radianStep = M_PI / static_cast<qreal>(m_cornerCount);
193 
194     createPoints(m_convex ? m_cornerCount : 2 * m_cornerCount);
195 
196     KoSubpath &points = *subpaths()[0];
197 
198     uint index = 0;
199     for (uint i = 0; i < 2 * m_cornerCount; ++i) {
200         uint cornerType = i % 2;
201         if (cornerType == base && m_convex) {
202             continue;
203         }
204         qreal radian = static_cast<qreal>((i + 1) * radianStep) + m_angles[cornerType];
205         QPointF cornerPoint = QPointF(m_zoomX * m_radius[cornerType] * cos(radian), m_zoomY * m_radius[cornerType] * sin(radian));
206 
207         points[index]->setPoint(m_center + cornerPoint);
208         points[index]->unsetProperty(KoPathPoint::StopSubpath);
209         points[index]->unsetProperty(KoPathPoint::CloseSubpath);
210         if (m_roundness[cornerType] > 1e-10 || m_roundness[cornerType] < -1e-10) {
211             // normalized cross product to compute tangential vector for handle point
212             QPointF tangentVector(cornerPoint.y() / m_radius[cornerType], -cornerPoint.x() / m_radius[cornerType]);
213             points[index]->setControlPoint2(points[index]->point() - m_roundness[cornerType] * tangentVector);
214             points[index]->setControlPoint1(points[index]->point() + m_roundness[cornerType] * tangentVector);
215         } else {
216             points[index]->removeControlPoint1();
217             points[index]->removeControlPoint2();
218         }
219         index++;
220     }
221 
222     // first path starts and closes path
223     points[0]->setProperty(KoPathPoint::StartSubpath);
224     points[0]->setProperty(KoPathPoint::CloseSubpath);
225     // last point stops and closes path
226     points.last()->setProperty(KoPathPoint::StopSubpath);
227     points.last()->setProperty(KoPathPoint::CloseSubpath);
228 
229     normalize();
230 
231     QList<QPointF> handles;
232     handles.push_back(points.at(tip)->point());
233     if (!m_convex) {
234         handles.push_back(points.at(base)->point());
235     }
236     setHandles(handles);
237 
238     m_center = computeCenter();
239 }
240 
createPoints(int requiredPointCount)241 void StarShape::createPoints(int requiredPointCount)
242 {
243     if (subpaths().count() != 1) {
244         clear();
245         subpaths().append(new KoSubpath());
246     }
247     int currentPointCount = subpaths()[0]->count();
248     if (currentPointCount > requiredPointCount) {
249         for (int i = 0; i < currentPointCount - requiredPointCount; ++i) {
250             delete subpaths()[0]->front();
251             subpaths()[0]->pop_front();
252         }
253     } else if (requiredPointCount > currentPointCount) {
254         for (int i = 0; i < requiredPointCount - currentPointCount; ++i) {
255             subpaths()[0]->append(new KoPathPoint(this, QPointF()));
256         }
257     }
258 
259     notifyPointsChanged();
260 }
261 
setSize(const QSizeF & newSize)262 void StarShape::setSize(const QSizeF &newSize)
263 {
264     QTransform matrix(resizeMatrix(newSize));
265     m_zoomX *= matrix.m11();
266     m_zoomY *= matrix.m22();
267 
268     // this transforms the handles
269     KoParameterShape::setSize(newSize);
270 
271     m_center = computeCenter();
272 }
273 
computeCenter() const274 QPointF StarShape::computeCenter() const
275 {
276     KoSubpath &points = *subpaths()[0];
277 
278     QPointF center(0, 0);
279     for (uint i = 0; i < m_cornerCount; ++i) {
280         if (m_convex) {
281             center += points[i]->point();
282         } else {
283             center += points[2 * i]->point();
284         }
285     }
286     if (m_cornerCount > 0) {
287         return center / static_cast<qreal>(m_cornerCount);
288     }
289     return center;
290 
291 }
292 
loadOdf(const KoXmlElement & element,KoShapeLoadingContext & context)293 bool StarShape::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context)
294 {
295     bool loadAsCustomShape = false;
296 
297     if (element.localName() == "custom-shape") {
298         QString drawEngine = element.attributeNS(KoXmlNS::draw, "engine", "");
299         if (drawEngine != "calligra:star") {
300             return false;
301         }
302         loadAsCustomShape = true;
303     } else if (element.localName() != "regular-polygon") {
304         return false;
305     }
306 
307     m_radius[tip] = 50;
308     m_center = QPointF(50, 50);
309 
310     if (!loadAsCustomShape) {
311         QString corners = element.attributeNS(KoXmlNS::draw, "corners", "");
312         if (!corners.isEmpty()) {
313             m_cornerCount = corners.toUInt();
314             // initialize default angles of tip and base
315             m_angles[base] = m_angles[tip] = defaultAngleRadian();
316         }
317 
318         m_convex = (element.attributeNS(KoXmlNS::draw, "concave", "false") == "false");
319 
320         if (m_convex) {
321             m_radius[base] = m_radius[tip];
322         } else {
323             // sharpness is radius of ellipse on which inner polygon points are located
324             // 0% means all polygon points are on a single ellipse
325             // 100% means inner points are located at polygon center point
326             QString sharpness = element.attributeNS(KoXmlNS::draw, "sharpness", "");
327             if (!sharpness.isEmpty() && sharpness.right(1) == "%") {
328                 float percent = sharpness.left(sharpness.length() - 1).toFloat();
329                 m_radius[base] = m_radius[tip] * (100 - percent) / 100;
330             }
331         }
332     } else {
333         QString drawData = element.attributeNS(KoXmlNS::draw, "data");
334         if (drawData.isEmpty()) {
335             return false;
336         }
337 
338         QStringList properties = drawData.split(';');
339         if (properties.count() == 0) {
340             return false;
341         }
342 
343         foreach (const QString &property, properties) {
344             QStringList pair = property.split(':');
345             if (pair.count() != 2) {
346                 continue;
347             }
348             if (pair[0] == "corners") {
349                 m_cornerCount = pair[1].toInt();
350             } else if (pair[0] == "concave") {
351                 m_convex = (pair[1] == "false");
352             } else if (pair[0] == "baseRoundness") {
353                 m_roundness[base] = pair[1].toDouble();
354             } else if (pair[0] == "tipRoundness") {
355                 m_roundness[tip] = pair[1].toDouble();
356             } else if (pair[0] == "baseAngle") {
357                 m_angles[base] = pair[1].toDouble();
358             } else if (pair[0] == "tipAngle") {
359                 m_angles[tip] = pair[1].toDouble();
360             } else if (pair[0] == "sharpness") {
361                 float percent = pair[1].left(pair[1].length() - 1).toFloat();
362                 m_radius[base] = m_radius[tip] * (100 - percent) / 100;
363             }
364         }
365 
366         if (m_convex) {
367             m_radius[base] = m_radius[tip];
368         }
369     }
370 
371     updatePath(QSizeF());
372 
373     // reset transformation
374     setTransformation(QTransform());
375 
376     loadOdfAttributes(element, context, OdfAllAttributes);
377     loadText(element, context);
378 
379     return true;
380 }
381 
saveOdf(KoShapeSavingContext & context) const382 void StarShape::saveOdf(KoShapeSavingContext &context) const
383 {
384     if (isParametricShape()) {
385         double defaultAngle = defaultAngleRadian();
386         bool hasRoundness = m_roundness[tip] != 0.0f || m_roundness[base] != 0.0f;
387         bool hasAngleOffset = m_angles[base] != defaultAngle || m_angles[tip] != defaultAngle;
388         if (hasRoundness || hasAngleOffset) {
389             // draw:regular-polygon has no means of saving roundness
390             // so we save as a custom shape with a specific draw:engine
391             context.xmlWriter().startElement("draw:custom-shape");
392             saveOdfAttributes(context, OdfAllAttributes);
393 
394             // now write the special shape data
395             context.xmlWriter().addAttribute("draw:engine", "calligra:star");
396             // create the data attribute
397             QString drawData = QString("corners:%1;").arg(m_cornerCount);
398             drawData += m_convex ? "concave:false;" : "concave:true;";
399             if (!m_convex) {
400                 // sharpness is radius of ellipse on which inner polygon points are located
401                 // 0% means all polygon points are on a single ellipse
402                 // 100% means inner points are located at polygon center point
403                 qreal percent = (m_radius[tip] - m_radius[base]) / m_radius[tip] * 100.0;
404                 drawData += QString("sharpness:%1%;").arg(percent);
405             }
406             if (m_roundness[base] != 0.0f) {
407                 drawData += QString("baseRoundness:%1;").arg(m_roundness[base]);
408             }
409             if (m_roundness[tip] != 0.0f) {
410                 drawData += QString("tipRoundness:%1;").arg(m_roundness[tip]);
411             }
412             drawData += QString("baseAngle:%1;").arg(m_angles[base]);
413             drawData += QString("tipAngle:%1;").arg(m_angles[tip]);
414 
415             context.xmlWriter().addAttribute("draw:data", drawData);
416 
417             saveOdfCommonChildElements(context);
418             saveText(context);
419 
420             // write a enhanced geometry element for compatibility with other applications
421             context.xmlWriter().startElement("draw:enhanced-geometry");
422             context.xmlWriter().addAttribute("draw:enhanced-path", toString(transformation()));
423             context.xmlWriter().endElement(); // draw:enhanced-geometry
424 
425             context.xmlWriter().endElement(); // draw:custom-shape
426         } else {
427             context.xmlWriter().startElement("draw:regular-polygon");
428             saveOdfAttributes(context, OdfAllAttributes);
429             context.xmlWriter().addAttribute("draw:corners", m_cornerCount);
430             context.xmlWriter().addAttribute("draw:concave", m_convex ? "false" : "true");
431             if (!m_convex) {
432                 // sharpness is radius of ellipse on which inner polygon points are located
433                 // 0% means all polygon points are on a single ellipse
434                 // 100% means inner points are located at polygon center point
435                 qreal percent = (m_radius[tip] - m_radius[base]) / m_radius[tip] * 100.0;
436                 context.xmlWriter().addAttribute("draw:sharpness", QString("%1%").arg(percent));
437             }
438             saveOdfCommonChildElements(context);
439             saveText(context);
440             context.xmlWriter().endElement();
441         }
442     } else {
443         KoPathShape::saveOdf(context);
444     }
445 }
446 
pathShapeId() const447 QString StarShape::pathShapeId() const
448 {
449     return StarShapeId;
450 }
451 
defaultAngleRadian() const452 double StarShape::defaultAngleRadian() const
453 {
454     qreal radianStep = M_PI / static_cast<qreal>(m_cornerCount);
455 
456     return M_PI_2 - 2 * radianStep;
457 }
458