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