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 &center, 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