1 /*
2 * Hedgewars, a free turn based strategy game
3 * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 2 of the License
8 *
9 * This program 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
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19 #include <QGraphicsSceneMouseEvent>
20 #include <QGraphicsPathItem>
21 #include <QtEndian>
22 #include <QDebug>
23 #include <QTransform>
24 #include <math.h>
25
26 #include "drawmapscene.h"
27
28 #define DRAWN_MAP_COLOR_LAND (Qt::yellow)
29 #define DRAWN_MAP_COLOR_CURSOR_PEN (Qt::green)
30 #define DRAWN_MAP_COLOR_CURSOR_ERASER (Qt::red)
31
32 #ifndef M_PI
33 #define M_PI 3.14159265358979323846
34 #endif
35
sqr(const T & x)36 template <class T> T sqr(const T & x)
37 {
38 return x*x;
39 }
40
DrawMapScene(QObject * parent)41 DrawMapScene::DrawMapScene(QObject *parent) :
42 QGraphicsScene(parent),
43 m_pen(DRAWN_MAP_COLOR_LAND),
44 m_brush(DRAWN_MAP_COLOR_LAND),
45 m_cursor(new QGraphicsEllipseItem(-5, -5, 5, 5))
46 {
47 setSceneRect(0, 0, 4096, 2048);
48
49 QLinearGradient gradient(0, 0, 0, 2048);
50 gradient.setColorAt(0, QColor(60, 60, 155));
51 gradient.setColorAt(1, QColor(155, 155, 60));
52
53 m_eraser = QBrush(gradient);
54 setBackgroundBrush(m_eraser);
55 m_isErasing = false;
56
57 m_pathType = Polyline;
58
59 m_pen.setWidth(DRAWN_MAP_BRUSH_SIZE_START);
60 m_pen.setJoinStyle(Qt::RoundJoin);
61 m_pen.setCapStyle(Qt::RoundCap);
62 m_currPath = 0;
63
64 m_isCursorShown = false;
65 QPen cursorPen = QPen(DRAWN_MAP_COLOR_CURSOR_PEN);
66 cursorPen.setJoinStyle(Qt::RoundJoin);
67 cursorPen.setCapStyle(Qt::RoundCap);
68 cursorPen.setWidth(brushSize());
69 m_cursor->setPen(cursorPen);
70 m_cursor->setZValue(1);
71 }
72
mouseMoveEvent(QGraphicsSceneMouseEvent * mouseEvent)73 void DrawMapScene::mouseMoveEvent(QGraphicsSceneMouseEvent * mouseEvent)
74 {
75 if(m_currPath && (mouseEvent->buttons() & Qt::LeftButton))
76 {
77 QPainterPath path = m_currPath->path();
78 QPointF currentPos = mouseEvent->scenePos();
79
80 if(mouseEvent->modifiers() & Qt::ControlModifier)
81 currentPos = putSomeConstraints(paths.first().initialPoint, currentPos);
82
83 switch (m_pathType)
84 {
85 case Polyline:
86 if(mouseEvent->modifiers() & Qt::ControlModifier)
87 {
88 int c = path.elementCount();
89 path.setElementPositionAt(c - 1, currentPos.x(), currentPos.y());
90
91 }
92 else
93 {
94 path.lineTo(currentPos);
95 paths.first().points.append(mouseEvent->scenePos().toPoint());
96 }
97 break;
98 case Rectangle: {
99 path = QPainterPath();
100 QPointF p1 = paths.first().initialPoint;
101 QPointF p2 = currentPos;
102 path.moveTo(p1);
103 path.lineTo(p1.x(), p2.y());
104 path.lineTo(p2);
105 path.lineTo(p2.x(), p1.y());
106 path.lineTo(p1);
107 break;
108 }
109 case Ellipse: {
110 path = QPainterPath();
111 QList<QPointF> points = makeEllipse(paths.first().initialPoint, currentPos);
112 path.addPolygon(QPolygonF(QVector<QPointF>::fromList(points)));
113 break;
114 }
115 }
116
117 m_currPath->setPath(path);
118
119 emit pathChanged();
120 }
121
122 if(!m_isCursorShown)
123 showCursor();
124 m_cursor->setPos(mouseEvent->scenePos());
125 }
126
mousePressEvent(QGraphicsSceneMouseEvent * mouseEvent)127 void DrawMapScene::mousePressEvent(QGraphicsSceneMouseEvent * mouseEvent)
128 {
129 m_currPath = addPath(QPainterPath(), m_pen);
130
131 QPainterPath path = m_currPath->path();
132 QPointF p = mouseEvent->scenePos();
133 p += QPointF(0.01, 0.01);
134 path.moveTo(p);
135 path.lineTo(mouseEvent->scenePos());
136
137 PathParams params;
138 params.width = serializePenWidth(brushSize());
139 params.erasing = m_isErasing;
140 params.initialPoint = mouseEvent->scenePos().toPoint();
141 params.points = QList<QPoint>() << params.initialPoint;
142 paths.prepend(params);
143 m_currPath->setPath(path);
144
145 emit pathChanged();
146 }
147
mouseReleaseEvent(QGraphicsSceneMouseEvent * mouseEvent)148 void DrawMapScene::mouseReleaseEvent(QGraphicsSceneMouseEvent * mouseEvent)
149 {
150 if (m_currPath)
151 {
152 QPointF currentPos = mouseEvent->scenePos();
153
154 if(mouseEvent->modifiers() & Qt::ControlModifier)
155 currentPos = putSomeConstraints(paths.first().initialPoint, currentPos);
156
157 switch (m_pathType)
158 {
159 case Polyline: {
160 QPainterPath path = m_currPath->path();
161 path.lineTo(mouseEvent->scenePos());
162 paths.first().points.append(currentPos.toPoint());
163 m_currPath->setPath(path);
164 simplifyLast();
165 break;
166 }
167 case Rectangle: {
168 QPoint p1 = paths.first().initialPoint;
169 QPoint p2 = currentPos.toPoint();
170 QList<QPoint> rpoints;
171 rpoints << p1 << QPoint(p1.x(), p2.y()) << p2 << QPoint(p2.x(), p1.y()) << p1;
172 paths.first().points = rpoints;
173 break;
174 }
175 case Ellipse:
176 QPoint p1 = paths.first().initialPoint;
177 QPoint p2 = currentPos.toPoint();
178 QList<QPointF> points = makeEllipse(p1, p2);
179 QList<QPoint> epoints;
180 foreach(const QPointF & p, points)
181 epoints.append(p.toPoint());
182 paths.first().points = epoints;
183 break;
184 }
185
186 m_currPath = 0;
187
188 emit pathChanged();
189 }
190 }
191
setBrushSize(int newBrushSize)192 void DrawMapScene::setBrushSize(int newBrushSize)
193 {
194 if(newBrushSize > DRAWN_MAP_BRUSH_SIZE_MAX)
195 newBrushSize = DRAWN_MAP_BRUSH_SIZE_MAX;
196 if(newBrushSize < DRAWN_MAP_BRUSH_SIZE_MIN)
197 newBrushSize = DRAWN_MAP_BRUSH_SIZE_MIN;
198
199 m_pen.setWidth(newBrushSize);
200 QPen cursorPen = m_cursor->pen();
201 cursorPen.setWidth(m_pen.width());
202 m_cursor->setPen(cursorPen);
203 if(m_currPath)
204 {
205 m_currPath->setPen(m_pen);
206 paths.first().width = serializePenWidth(m_pen.width());
207 }
208
209 emit brushSizeChanged(newBrushSize);
210 }
211
brushSize()212 int DrawMapScene::brushSize()
213 {
214 return m_pen.width();
215 }
216
wheelEvent(QGraphicsSceneWheelEvent * wheelEvent)217 void DrawMapScene::wheelEvent(QGraphicsSceneWheelEvent * wheelEvent)
218 {
219 int b = brushSize();
220 if(wheelEvent->delta() > 0)
221 setBrushSize(b + DRAWN_MAP_BRUSH_SIZE_STEP);
222 else if(wheelEvent->delta() < 0 && b >= DRAWN_MAP_BRUSH_SIZE_MIN)
223 setBrushSize(b - DRAWN_MAP_BRUSH_SIZE_STEP);
224 }
225
showCursor()226 void DrawMapScene::showCursor()
227 {
228 if(!m_isCursorShown)
229 addItem(m_cursor);
230
231 m_isCursorShown = true;
232 }
233
hideCursor()234 void DrawMapScene::hideCursor()
235 {
236 if(m_isCursorShown)
237 removeItem(m_cursor);
238
239 m_isCursorShown = false;
240 }
241
undo()242 void DrawMapScene::undo()
243 {
244 // cursor is a part of items()
245 if(m_isCursorShown)
246 return;
247
248 if(paths.size())
249 {
250 removeItem(items().first());
251 paths.removeFirst();
252
253 emit pathChanged();
254 }
255 else if(oldItems.size())
256 {
257 while(oldItems.size())
258 addItem(oldItems.takeFirst());
259 paths = oldPaths;
260
261 emit pathChanged();
262 }
263 }
264
clearMap()265 void DrawMapScene::clearMap()
266 {
267 // cursor is a part of items()
268 if(m_isCursorShown)
269 return;
270
271 // don't clear if already cleared
272 if(!items().size())
273 return;
274
275 m_specialPoints.clear();
276 oldItems.clear();
277
278 // do this since clear() would _destroy_ all items
279 for(int i = paths.size() - 1; i >= 0; --i)
280 {
281 oldItems.push_front(items().first());
282 removeItem(items().first());
283 }
284
285 items().clear();
286
287 oldPaths = paths;
288
289 paths.clear();
290
291 emit pathChanged();
292 }
293
294
setErasing(bool erasing)295 void DrawMapScene::setErasing(bool erasing)
296 {
297 m_isErasing = erasing;
298 QPen cursorPen = m_cursor->pen();
299 if(erasing) {
300 m_pen.setBrush(m_eraser);
301 cursorPen.setColor(DRAWN_MAP_COLOR_CURSOR_ERASER);
302 } else {
303 m_pen.setBrush(m_brush);
304 cursorPen.setColor(DRAWN_MAP_COLOR_CURSOR_PEN);
305 }
306 m_cursor->setPen(cursorPen);
307 }
308
encode()309 QByteArray DrawMapScene::encode()
310 {
311 QByteArray b(m_specialPoints);
312
313 for(int i = paths.size() - 1; i >= 0; --i)
314 {
315 int cnt = 0;
316 PathParams params = paths.at(i);
317 foreach(QPoint point, params.points)
318 {
319 qint16 px = qToBigEndian((qint16)point.x());
320 qint16 py = qToBigEndian((qint16)point.y());
321 quint8 flags = 0;
322 if(!cnt)
323 {
324 flags = 0x80 + params.width;
325 if(params.erasing) flags |= 0x40;
326 }
327 b.append((const char *)&px, 2);
328 b.append((const char *)&py, 2);
329 b.append((const char *)&flags, 1);
330
331 ++cnt;
332 }
333
334 }
335
336 return b;
337 }
338
decode(QByteArray data)339 void DrawMapScene::decode(QByteArray data)
340 {
341 hideCursor();
342
343 // Remember erasing mode
344 bool erasing = m_isErasing;
345
346 // Use seperate for decoding the map, don't mess with the user pen
347 QPen load_pen = QPen(m_pen);
348
349 oldItems.clear();
350 oldPaths.clear();
351 clear();
352 paths.clear();
353 m_specialPoints.clear();
354
355 PathParams params;
356
357 bool isSpecial = true;
358
359 while(data.size() >= 5)
360 {
361 qint16 px = qFromBigEndian(*(qint16 *)data.data());
362 data.remove(0, 2);
363 qint16 py = qFromBigEndian(*(qint16 *)data.data());
364 data.remove(0, 2);
365 quint8 flags = *(quint8 *)data.data();
366 data.remove(0, 1);
367 //qDebug() << px << py;
368 if(flags & 0x80)
369 {
370 isSpecial = false;
371
372 if(params.points.size())
373 {
374 addPath(pointsToPath(params.points), load_pen);
375
376 paths.prepend(params);
377
378 params.points.clear();
379 }
380
381 quint8 penWidth = flags & 0x3f;
382 load_pen.setWidth(deserializePenWidth(penWidth));
383 params.erasing = flags & 0x40;
384 if(params.erasing)
385 load_pen.setBrush(m_eraser);
386 else
387 load_pen.setBrush(m_brush);
388 params.width = penWidth;
389 } else
390 if(isSpecial)
391 {
392 QPainterPath path;
393 path.addEllipse(QPointF(px, py), 10, 10);
394
395 addPath(path);
396
397 qint16 x = qToBigEndian(px);
398 qint16 y = qToBigEndian(py);
399 m_specialPoints.append((const char *)&x, 2);
400 m_specialPoints.append((const char *)&y, 2);
401 m_specialPoints.append((const char *)&flags, 1);
402 }
403
404 if(!isSpecial)
405 params.points.append(QPoint(px, py));
406 }
407
408 if(params.points.size())
409 {
410 addPath(pointsToPath(params.points), load_pen);
411 paths.prepend(params);
412 }
413
414 emit pathChanged();
415
416 // Restore erasing mode
417 setErasing(erasing);
418 }
419
simplifyLast()420 void DrawMapScene::simplifyLast()
421 {
422 if(!paths.size()) return;
423
424 QList<QPoint> points = paths.at(0).points;
425
426 QPoint prevPoint = points.first();
427 int i = 1;
428 while(i < points.size())
429 {
430 if( ((i != points.size() - 1) || (prevPoint == points[i]))
431 && (sqr(prevPoint.x() - points[i].x()) + sqr(prevPoint.y() - points[i].y()) < 1000)
432 )
433 points.removeAt(i);
434 else
435 {
436 prevPoint = points[i];
437 ++i;
438 }
439 }
440
441 paths[0].points = points;
442
443
444 // redraw path
445 {
446 QGraphicsPathItem * pathItem = static_cast<QGraphicsPathItem *>(items()[m_isCursorShown ? 1 : 0]);
447 pathItem->setPath(pointsToPath(paths[0].points));
448 }
449 }
450
pointsCount()451 int DrawMapScene::pointsCount()
452 {
453 int cnt = 0;
454 foreach(PathParams p, paths)
455 cnt += p.points.size();
456
457 return cnt;
458 }
459
pointsToPath(const QList<QPoint> points)460 QPainterPath DrawMapScene::pointsToPath(const QList<QPoint> points)
461 {
462 QPainterPath path;
463
464 if(points.size())
465 {
466 QPointF p = points[0] + QPointF(0.01, 0.01);
467 path.moveTo(p);
468
469 foreach(QPoint p, points)
470 path.lineTo(p);
471 }
472
473 return path;
474 }
475
serializePenWidth(int width)476 quint8 DrawMapScene::serializePenWidth(int width)
477 {
478 return (width - 6) / 10;
479 }
480
deserializePenWidth(quint8 width)481 int DrawMapScene::deserializePenWidth(quint8 width)
482 {
483 return width * 10 + 6;
484 }
485
setPathType(PathType pathType)486 void DrawMapScene::setPathType(PathType pathType)
487 {
488 m_pathType = pathType;
489 }
490
makeEllipse(const QPointF & center,const QPointF & corner)491 QList<QPointF> DrawMapScene::makeEllipse(const QPointF ¢er, const QPointF &corner)
492 {
493 QList<QPointF> l;
494 qreal rx = qAbs(center.x() - corner.x());
495 qreal ry = qAbs(center.y() - corner.y());
496 qreal r = qMax(rx, ry);
497
498 if(r < 4)
499 {
500 l.append(center);
501 } else
502 {
503 qreal angleDelta = qMax(static_cast<qreal> (0.1), qMin(static_cast<qreal> (0.7), 120 / r));
504 for(qreal angle = 0.0; angle < 2*M_PI; angle += angleDelta)
505 l.append(center + QPointF(rx * cos(angle), ry * sin(angle)));
506 l.append(l.first());
507 }
508
509 return l;
510 }
511
putSomeConstraints(const QPointF & initialPoint,const QPointF & point)512 QPointF DrawMapScene::putSomeConstraints(const QPointF &initialPoint, const QPointF &point)
513 {
514 QPointF vector = point - initialPoint;
515
516 for(int angle = 0; angle < 180; angle += 15)
517 {
518 QTransform transform;
519 transform.rotate(angle);
520
521 QPointF rotated = transform.map(vector);
522
523 if(rotated.x() == 0) return point;
524 if(qAbs(rotated.y() / rotated.x()) < 0.05) return initialPoint + transform.inverted().map(QPointF(rotated.x(), 0));
525 }
526
527 return point;
528 }
529
optimize()530 void DrawMapScene::optimize()
531 {
532 if(!paths.size()) return;
533
534 // break paths into segments
535 Paths pth;
536
537 foreach(const PathParams & pp, paths)
538 {
539 int l = pp.points.size();
540
541 if(l == 1)
542 {
543 pth.prepend(pp);
544 } else
545 {
546 for(int i = l - 2; i >= 0; --i)
547 {
548 PathParams p = pp;
549 p.points = QList<QPoint>() << p.points[i] << p.points[i + 1];
550 pth.prepend(pp);
551 }
552 }
553 }
554
555 // clear the scene
556 oldItems.clear();
557 oldPaths.clear();
558 clear();
559 paths.clear();
560 m_specialPoints.clear();
561
562 // render the result
563 foreach(const PathParams & p, pth)
564 {
565 if(p.erasing)
566 m_pen.setBrush(m_eraser);
567 else
568 m_pen.setBrush(m_brush);
569
570 m_pen.setWidth(deserializePenWidth(p.width));
571
572 addPath(pointsToPath(p.points), m_pen);
573 }
574
575 emit pathChanged();
576 }
577