1 /*
2  * mapobjectitem.cpp
3  * Copyright 2008, Roderic Morris <roderic@ccs.neu.edu>
4  * Copyright 2008-2011, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
5  * Copyright 2010, Jeff Bland <jksb@member.fsf.org>
6  *
7  * This file is part of Tiled.
8  *
9  * This program is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU General Public License as published by the Free
11  * Software Foundation; either version 2 of the License, or (at your option)
12  * any later version.
13  *
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
17  * more details.
18  *
19  * You should have received a copy of the GNU General Public License along with
20  * this program. If not, see <http://www.gnu.org/licenses/>.
21  */
22 
23 #include "mapobjectitem.h"
24 
25 #include "geometry.h"
26 #include "mapdocument.h"
27 #include "mapobject.h"
28 #include "maprenderer.h"
29 #include "mapscene.h"
30 #include "mapview.h"
31 #include "objectgroup.h"
32 #include "objectgroupitem.h"
33 #include "tile.h"
34 #include "utils.h"
35 #include "zoomable.h"
36 
37 #include <QPainter>
38 
39 #include <cmath>
40 #include <memory>
41 
42 using namespace Tiled;
43 
MapObjectItem(MapObject * object,MapDocument * mapDocument,QGraphicsItem * parent)44 MapObjectItem::MapObjectItem(MapObject *object, MapDocument *mapDocument,
45                              QGraphicsItem *parent):
46     QGraphicsItem(parent),
47     mObject(object),
48     mMapDocument(mapDocument)
49 {
50     setAcceptedMouseButtons(Qt::MouseButtons());
51     setAcceptHoverEvents(true);     // Accept hover events otherwise going to the MapItem
52     syncWithMapObject();
53 }
54 
syncWithMapObject()55 void MapObjectItem::syncWithMapObject()
56 {
57     const QColor color = mObject->effectiveColor();
58 
59     // Update the whole object when the name, polygon or color has changed
60     if (mPolygon != mObject->polygon() || mColor != color) {
61         mPolygon = mObject->polygon();
62         mColor = color;
63         update();
64     }
65 
66     QString toolTip = mObject->name();
67     const QString &type = mObject->type();
68     if (!type.isEmpty())
69         toolTip += QStringLiteral(" (") + type + QLatin1Char(')');
70     setToolTip(toolTip);
71 
72     MapRenderer *renderer = mMapDocument->renderer();
73     const QPointF pixelPos = renderer->pixelToScreenCoords(mObject->position());
74     QRectF bounds = renderer->boundingRect(mObject);
75 
76     bounds.translate(-pixelPos);
77 
78     if (renderer->flags().testFlag(ShowTileCollisionShapes))
79         expandBoundsToCoverTileCollisionObjects(bounds);
80 
81     setPos(pixelPos);
82     setRotation(mObject->rotation());
83 
84     if (ObjectGroup *objectGroup = mObject->objectGroup()) {
85         if (objectGroup->drawOrder() == ObjectGroup::TopDownOrder)
86             setZValue(pixelPos.y());
87 
88         if (mIsHoveredIndicator) {
89             auto totalOffset = static_cast<MapScene*>(scene())->absolutePositionForLayer(*objectGroup);
90             setTransform(QTransform::fromTranslate(totalOffset.x(), totalOffset.y()));
91         }
92     }
93 
94     if (mBoundingRect != bounds) {
95         // Notify the graphics scene about the geometry change in advance
96         prepareGeometryChange();
97         mBoundingRect = bounds;
98     }
99 
100     setVisible(mObject->isVisible());
101 }
102 
setIsHoverIndicator(bool isHoverIndicator)103 void MapObjectItem::setIsHoverIndicator(bool isHoverIndicator)
104 {
105     if (mIsHoveredIndicator == isHoverIndicator)
106         return;
107 
108     mIsHoveredIndicator = isHoverIndicator;
109 
110     if (isHoverIndicator) {
111         auto totalOffset = static_cast<MapScene*>(scene())->absolutePositionForLayer(*mObject->objectGroup());
112         setTransform(QTransform::fromTranslate(totalOffset.x(), totalOffset.y()));
113     } else {
114         setTransform(QTransform());
115     }
116 
117     update();
118 }
119 
boundingRect() const120 QRectF MapObjectItem::boundingRect() const
121 {
122     return mBoundingRect;
123 }
124 
shape() const125 QPainterPath MapObjectItem::shape() const
126 {
127     QPainterPath path = mMapDocument->renderer()->interactionShape(mObject);
128     path.translate(-pos());
129     return path;
130 }
131 
paint(QPainter * painter,const QStyleOptionGraphicsItem *,QWidget * widget)132 void MapObjectItem::paint(QPainter *painter,
133                           const QStyleOptionGraphicsItem *,
134                           QWidget *widget)
135 {
136     const qreal scale = static_cast<MapView*>(widget->parent())->zoomable()->scale();
137     const QColor color = mIsHoveredIndicator ? mColor.lighter() : mColor;
138     const qreal previousOpacity = painter->opacity();
139 
140     if (mIsHoveredIndicator)
141         painter->setOpacity(0.4);
142 
143     painter->translate(-pos());
144     mMapDocument->renderer()->setPainterScale(scale);
145     mMapDocument->renderer()->drawMapObject(painter, mObject, color);
146     painter->translate(pos());
147 
148     if (mIsHoveredIndicator) {
149         painter->setOpacity(0.6);
150 
151         // TODO: Code mostly duplicated in MapObjectOutline
152         const QPointF pixelPos = mMapDocument->renderer()->pixelToScreenCoords(mObject->position());
153         QRectF bounds = mObject->screenBounds(*mMapDocument->renderer());
154         bounds.translate(-pixelPos);
155 
156         const QLineF lines[4] = {
157             QLineF(bounds.topLeft(), bounds.topRight()),
158             QLineF(bounds.bottomLeft(), bounds.bottomRight()),
159             QLineF(bounds.topLeft(), bounds.bottomLeft()),
160             QLineF(bounds.topRight(), bounds.bottomRight())
161         };
162 
163         const qreal devicePixelRatio = painter->device()->devicePixelRatioF();
164         const qreal dashLength = std::ceil(Utils::dpiScaled(2) * devicePixelRatio);
165 
166         // Draw a solid white line
167         QPen pen(Qt::white, 1.5 * devicePixelRatio, Qt::SolidLine);
168         pen.setCosmetic(true);
169         painter->setRenderHint(QPainter::Antialiasing);
170         painter->setPen(pen);
171         painter->drawLines(lines, 4);
172 
173         // Draw a black dashed line above the white line
174         pen.setColor(Qt::black);
175         pen.setCapStyle(Qt::FlatCap);
176         pen.setDashPattern({dashLength, dashLength});
177         painter->setPen(pen);
178         painter->drawLines(lines, 4);
179 
180         painter->setOpacity(previousOpacity);
181     }
182 }
183 
setPolygon(const QPolygonF & polygon)184 void MapObjectItem::setPolygon(const QPolygonF &polygon)
185 {
186     // Not using the MapObjectModel because it is used during object creation,
187     // when the object is not actually part of the map yet.
188     mObject->setPolygon(polygon);
189     syncWithMapObject();
190 }
191 
expandBoundsToCoverTileCollisionObjects(QRectF & bounds)192 void MapObjectItem::expandBoundsToCoverTileCollisionObjects(QRectF &bounds)
193 {
194     const Cell &cell = mObject->cell();
195     const Tile *tile = cell.tile();
196     if (!tile || !tile->objectGroup())
197         return;
198 
199     const Tileset *tileset = cell.tileset();
200 
201     Map::Parameters mapParameters;
202     mapParameters.orientation = tileset->orientation() == Tileset::Orthogonal ? Map::Orthogonal
203                                                                               : Map::Isometric;
204     mapParameters.tileWidth = tileset->gridSize().width();
205     mapParameters.tileHeight = tileset->gridSize().height();
206 
207     const Map map(mapParameters);
208     const auto renderer = MapRenderer::create(&map);
209     const QTransform tileTransform = tileCollisionObjectsTransform(*tile);
210 
211     for (MapObject *object : tile->objectGroup()->objects()) {
212         auto transform = rotateAt(object->position(), object->rotation());
213         transform *= tileTransform;
214 
215         bounds |= transform.mapRect(renderer->boundingRect(object));
216     }
217 }
218 
tileCollisionObjectsTransform(const Tile & tile) const219 QTransform MapObjectItem::tileCollisionObjectsTransform(const Tile &tile) const
220 {
221     const Tileset *tileset = tile.tileset();
222 
223     QTransform tileTransform;
224 
225     tileTransform.scale(mObject->width() / tile.width(),
226                         mObject->height() / tile.height());
227 
228     if (mMapDocument->map()->orientation() == Map::Isometric)
229         tileTransform.translate(-tile.width() / 2, 0.0);
230 
231     tileTransform.translate(tileset->tileOffset().x(), tileset->tileOffset().y());
232 
233     if (mObject->cell().flippedVertically()) {
234         tileTransform.scale(1, -1);
235         tileTransform.translate(0, tile.height());
236     }
237     if (mObject->cell().flippedHorizontally()) {
238         tileTransform.scale(-1, 1);
239         tileTransform.translate(-tile.width(), 0);
240     }
241 
242     if (tileset->orientation() == Tileset::Isometric)
243         tileTransform.translate(0.0, -tile.tileset()->gridSize().height());
244     else
245         tileTransform.translate(0.0, -tile.height());
246 
247     return tileTransform;
248 }
249