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