1 /*
2 * mapobject.cpp
3 * Copyright 2008-2013, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
4 * Copyright 2008, Roderic Morris <roderic@ccs.neu.edu>
5 * Copyright 2017, Klimov Viktor <vitek.fomino@bk.ru>
6 *
7 * This file is part of libtiled.
8 *
9 * Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions are met:
11 *
12 * 1. Redistributions of source code must retain the above copyright notice,
13 * this list of conditions and the following disclaimer.
14 *
15 * 2. Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in the
17 * documentation and/or other materials provided with the distribution.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR
20 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
21 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
22 * EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
25 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
27 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
28 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31 #include "mapobject.h"
32
33 #include "map.h"
34 #include "maprenderer.h"
35 #include "objectgroup.h"
36 #include "objecttemplate.h"
37 #include "tile.h"
38
39 #include <QFontMetricsF>
40 #include <qmath.h>
41
42 namespace Tiled {
43
TextData()44 TextData::TextData()
45 : font(QStringLiteral("sans-serif"))
46 {
47 font.setPixelSize(16);
48 }
49
flags() const50 int TextData::flags() const
51 {
52 return wordWrap ? (alignment | Qt::TextWordWrap) : alignment;
53 }
54
textOption() const55 QTextOption TextData::textOption() const
56 {
57 QTextOption option(alignment);
58
59 if (wordWrap)
60 option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
61 else
62 option.setWrapMode(QTextOption::ManualWrap);
63
64 return option;
65 }
66
67 /**
68 * Returns the size of the text when drawn without wrapping.
69 */
textSize() const70 QSizeF TextData::textSize() const
71 {
72 QFontMetricsF fontMetrics(font);
73 return fontMetrics.size(0, text);
74 }
75
76
MapObject(const QString & name,const QString & type,const QPointF & pos,const QSizeF & size)77 MapObject::MapObject(const QString &name, const QString &type,
78 const QPointF &pos,
79 const QSizeF &size):
80 Object(MapObjectType),
81 mId(0),
82 mShape(Rectangle),
83 mName(name),
84 mType(type),
85 mPos(pos),
86 mSize(size),
87 mObjectTemplate(nullptr),
88 mObjectGroup(nullptr),
89 mRotation(0.0),
90 mVisible(true),
91 mTemplateBase(false)
92 {
93 }
94
index() const95 int MapObject::index() const
96 {
97 if (mObjectGroup)
98 return mObjectGroup->objects().indexOf(const_cast<MapObject*>(this));
99 return -1;
100 }
101
102 /**
103 * Returns the affective type of this object. This may be the type of its tile,
104 * if the object does not have a type set explicitly.
105 */
effectiveType() const106 const QString &MapObject::effectiveType() const
107 {
108 if (mType.isEmpty())
109 if (const Tile *tile = mCell.tile())
110 return tile->type();
111
112 return mType;
113 }
114
115 /**
116 * Sets the text data associated with this object.
117 */
setTextData(const TextData & textData)118 void MapObject::setTextData(const TextData &textData)
119 {
120 mTextData = textData;
121 }
122
123 /**
124 * Shortcut to getting a QRectF from position() and size() that uses cell tile
125 * if present.
126 *
127 * \deprecated See problems in comment.
128 */
boundsUseTile() const129 QRectF MapObject::boundsUseTile() const
130 {
131 // FIXME: This is outdated code:
132 // * It does not take into account that a tile object can be scaled.
133 // * It neglects that origin is not the same in orthogonal and isometric
134 // maps (see MapObject::alignment).
135 // * It does not deal with rotation.
136
137 if (const Tile *tile = mCell.tile()) {
138 // Using the tile for determing boundary
139 // Note the position given is the bottom-left corner so correct for that
140 return QRectF(QPointF(mPos.x(),
141 mPos.y() - tile->height()),
142 tile->size());
143 }
144
145 // No tile so just use regular bounds
146 return bounds();
147 }
148
align(QRectF & r,Alignment alignment)149 static void align(QRectF &r, Alignment alignment)
150 {
151 r.translate(-alignmentOffset(r, alignment));
152 }
153
154 /**
155 * Returns the bounds of the object in screen space when using the given
156 * \a renderer. Does not take into account rotation!
157 *
158 * This is slightly different from the bounds that should be used when
159 * rendering the object, which are returned by the MapRenderer::boundingRect
160 * function.
161 *
162 * \todo Look into unduplicating this code, which is also present in
163 * objectselectiontool.cpp in very similar form (objectBounds).
164 */
screenBounds(const MapRenderer & renderer) const165 QRectF MapObject::screenBounds(const MapRenderer &renderer) const
166 {
167 if (!mCell.isEmpty()) {
168 // Tile objects can have a tile offset, which is scaled along with the image
169 QSizeF imgSize;
170 QPoint tileOffset;
171
172 if (const Tile *tile = mCell.tile()) {
173 imgSize = tile->size();
174 tileOffset = tile->offset();
175 } else {
176 imgSize = size();
177 }
178
179 const QPointF position = renderer.pixelToScreenCoords(mPos);
180 const QSizeF objectSize = size();
181 const qreal scaleX = imgSize.width() > 0 ? objectSize.width() / imgSize.width() : 0;
182 const qreal scaleY = imgSize.height() > 0 ? objectSize.height() / imgSize.height() : 0;
183
184 QRectF bounds(position.x() + (tileOffset.x() * scaleX),
185 position.y() + (tileOffset.y() * scaleY),
186 objectSize.width(),
187 objectSize.height());
188
189 align(bounds, alignment(renderer.map()));
190
191 return bounds;
192 } else {
193 switch (mShape) {
194 case MapObject::Ellipse:
195 case MapObject::Rectangle: {
196 QRectF bounds(this->bounds());
197 align(bounds, alignment(renderer.map()));
198 QPolygonF screenPolygon = renderer.pixelToScreenCoords(bounds);
199 return screenPolygon.boundingRect();
200 }
201 case MapObject::Point:
202 return renderer.shape(this).boundingRect();
203 case MapObject::Polygon:
204 case MapObject::Polyline: {
205 // Alignment is irrelevant for polygon objects since they have no size
206 const QPolygonF polygon = mPolygon.translated(mPos);
207 QPolygonF screenPolygon = renderer.pixelToScreenCoords(polygon);
208 return screenPolygon.boundingRect();
209 }
210 case MapObject::Text:
211 return renderer.boundingRect(this);
212 }
213 }
214
215 return QRectF();
216 }
217
map() const218 Map *MapObject::map() const
219 {
220 return mObjectGroup ? mObjectGroup->map() : nullptr;
221 }
222
223 /*
224 * Returns the effective alignment for this object on the given \a map.
225 *
226 * By default, non-tile objects have top-left alignment, while tile objects
227 * have bottom-left alignment on orthogonal maps and bottom-center alignment
228 * on isometric maps.
229 *
230 * For tile objects, the default alignment can be overridden by setting an
231 * alignment on the tileset.
232 */
alignment(const Map * map) const233 Alignment MapObject::alignment(const Map *map) const
234 {
235 Alignment alignment = Unspecified;
236
237 if (Tileset *tileset = mCell.tileset())
238 alignment = tileset->objectAlignment();
239
240 if (!map && mObjectGroup)
241 map = mObjectGroup->map();
242
243 if (alignment == Unspecified) {
244 if (mCell.isEmpty())
245 return TopLeft;
246 else if (map && map->orientation() == Map::Isometric)
247 return Bottom;
248
249 return BottomLeft;
250 }
251
252 return alignment;
253 }
254
255 /**
256 * A helper function to determine the color of a map object. The color is
257 * determined first of all by the object type, and otherwise by the group
258 * that the object is in. If still no color is defined, it defaults to
259 * gray.
260 */
effectiveColor() const261 QColor MapObject::effectiveColor() const
262 {
263 const QString effectiveType = this->effectiveType();
264
265 // See if this object type has a color associated with it
266 for (const ObjectType &type : Object::objectTypes()) {
267 if (type.name.compare(effectiveType, Qt::CaseInsensitive) == 0)
268 return type.color;
269 }
270
271 // If not, get color from object group
272 if (mObjectGroup && mObjectGroup->color().isValid())
273 return mObjectGroup->color();
274
275 // Fallback color
276 return Qt::gray;
277 }
278
mapObjectProperty(Property property) const279 QVariant MapObject::mapObjectProperty(Property property) const
280 {
281 switch (property) {
282 case NameProperty: return mName;
283 case TypeProperty: return mType;
284 case VisibleProperty: return mVisible;
285 case TextProperty: return mTextData.text;
286 case TextFontProperty: return mTextData.font;
287 case TextAlignmentProperty: return QVariant::fromValue(mTextData.alignment);
288 case TextWordWrapProperty: return mTextData.wordWrap;
289 case TextColorProperty: return mTextData.color;
290 case PositionProperty: return mPos;
291 case SizeProperty: return mSize;
292 case RotationProperty: return mRotation;
293 case CellProperty: Q_ASSERT(false); break;
294 case ShapeProperty: return mShape;
295 case TemplateProperty: Q_ASSERT(false); break;
296 case CustomProperties: Q_ASSERT(false); break;
297 case AllProperties: Q_ASSERT(false); break;
298 }
299 return QVariant();
300 }
301
setMapObjectProperty(Property property,const QVariant & value)302 void MapObject::setMapObjectProperty(Property property, const QVariant &value)
303 {
304 switch (property) {
305 case NameProperty: setName(value.toString()); break;
306 case TypeProperty: setType(value.toString()); break;
307 case VisibleProperty: setVisible(value.toBool()); break;
308 case TextProperty: mTextData.text = value.toString(); break;
309 case TextFontProperty: mTextData.font = value.value<QFont>(); break;
310 case TextAlignmentProperty: mTextData.alignment = value.value<Qt::Alignment>(); break;
311 case TextWordWrapProperty: mTextData.wordWrap = value.toBool(); break;
312 case TextColorProperty: mTextData.color = value.value<QColor>(); break;
313 case PositionProperty: setPosition(value.toPointF()); break;
314 case SizeProperty: setSize(value.toSizeF()); break;
315 case RotationProperty: setRotation(value.toReal()); break;
316 case CellProperty: Q_ASSERT(false); break;
317 case ShapeProperty: setShape(value.value<Shape>()); break;
318 case TemplateProperty: Q_ASSERT(false); break;
319 case CustomProperties: Q_ASSERT(false); break;
320 case AllProperties: Q_ASSERT(false); break;
321 }
322 }
323
324 /**
325 * Flip this object in the given \a direction. This doesn't change the size
326 * of the object.
327 */
flip(FlipDirection direction,const QPointF & origin)328 void MapObject::flip(FlipDirection direction, const QPointF &origin)
329 {
330 //computing new rotation and flip transform
331 QTransform flipTransform;
332 flipTransform.translate(origin.x(), origin.y());
333 qreal newRotation = 0;
334 if (direction == FlipHorizontally) {
335 newRotation = 180.0 - rotation();
336 flipTransform.scale(-1, 1);
337 } else { //direction == FlipVertically
338 flipTransform.scale(1, -1);
339 newRotation = -rotation();
340 }
341 flipTransform.translate(-origin.x(), -origin.y());
342
343
344 if (!mCell.isEmpty())
345 flipTileObject(flipTransform);
346 else if (!mPolygon.isEmpty())
347 flipPolygonObject(flipTransform);
348 else
349 flipRectObject(flipTransform);
350
351 //installing new rotation after computing new position
352 setRotation(newRotation);
353 }
354
355 /**
356 * Returns a duplicate of this object. The caller is responsible for the
357 * ownership of this newly created object.
358 */
clone() const359 MapObject *MapObject::clone() const
360 {
361 MapObject *o = new MapObject(mName, mType, mPos, mSize);
362 o->setId(mId);
363 o->setProperties(properties());
364 o->setTextData(mTextData);
365 o->setPolygon(mPolygon);
366 o->setShape(mShape);
367 o->setCell(mCell);
368 o->setRotation(mRotation);
369 o->setVisible(mVisible);
370 o->setChangedProperties(mChangedProperties);
371 o->setObjectTemplate(mObjectTemplate);
372 return o;
373 }
374
copyPropertiesFrom(const MapObject * object)375 void MapObject::copyPropertiesFrom(const MapObject *object)
376 {
377 setName(object->name());
378 setSize(object->size());
379 setType(object->type());
380 setTextData(object->textData());
381 setPolygon(object->polygon());
382 setShape(object->shape());
383 setCell(object->cell());
384 setRotation(object->rotation());
385 setVisible(object->isVisible());
386 setProperties(object->properties());
387 setChangedProperties(object->changedProperties());
388 setObjectTemplate(object->objectTemplate());
389 }
390
templateObject() const391 const MapObject *MapObject::templateObject() const
392 {
393 if (mObjectTemplate)
394 return mObjectTemplate->object();
395 return nullptr;
396 }
397
syncWithTemplate()398 void MapObject::syncWithTemplate()
399 {
400 const MapObject *base = templateObject();
401 if (!base)
402 return;
403
404 if (!propertyChanged(MapObject::NameProperty))
405 setName(base->name());
406
407 if (!propertyChanged(MapObject::SizeProperty))
408 setSize(base->size());
409
410 if (!propertyChanged(MapObject::TypeProperty))
411 setType(base->type());
412
413 if (!propertyChanged(MapObject::TextProperty))
414 setTextData(base->textData());
415
416 if (!propertyChanged(MapObject::ShapeProperty)) {
417 setShape(base->shape());
418 setPolygon(base->polygon());
419 }
420
421 if (!propertyChanged(MapObject::CellProperty))
422 setCell(base->cell());
423
424 if (!propertyChanged(MapObject::RotationProperty))
425 setRotation(base->rotation());
426
427 if (!propertyChanged(MapObject::VisibleProperty))
428 setVisible(base->isVisible());
429 }
430
detachFromTemplate()431 void MapObject::detachFromTemplate()
432 {
433 // Can't detach when template object not loaded
434 const MapObject *base = templateObject();
435 if (!base)
436 return;
437
438 // All non-overridden properties are already synchronized, so we only need
439 // to merge the custom properties.
440 Properties newProperties = base->properties();
441 Tiled::mergeProperties(newProperties, properties());
442 setProperties(newProperties);
443
444 setObjectTemplate(nullptr);
445 }
446
flipRectObject(const QTransform & flipTransform)447 void MapObject::flipRectObject(const QTransform &flipTransform)
448 {
449 QPointF oldBottomLeftPoint = QPointF(cos(qDegreesToRadians(rotation() + 90)) * height() + x(),
450 sin(qDegreesToRadians(rotation() + 90)) * height() + y());
451 QPointF newPos = flipTransform.map(oldBottomLeftPoint);
452
453 setPosition(newPos);
454 }
455
flipPolygonObject(const QTransform & flipTransform)456 void MapObject::flipPolygonObject(const QTransform &flipTransform)
457 {
458 QTransform polygonToMapTransform;
459 polygonToMapTransform.translate(x(), y());
460 polygonToMapTransform.rotate(rotation());
461
462 QPointF localPolygonCenter = mPolygon.boundingRect().center();
463 QTransform polygonFlip;
464 polygonFlip.translate(localPolygonCenter.x(), localPolygonCenter.y());
465 polygonFlip.scale(1, -1);
466 polygonFlip.translate(-localPolygonCenter.x(), -localPolygonCenter.y());
467
468 QPointF oldBottomLeftPoint = polygonToMapTransform.map(polygonFlip.map(QPointF(0, 0)));
469 QPointF newPos = flipTransform.map(oldBottomLeftPoint);
470
471 setPolygon(polygonFlip.map(mPolygon));
472 setPosition(newPos);
473 }
474
flipTileObject(const QTransform & flipTransform)475 void MapObject::flipTileObject(const QTransform &flipTransform)
476 {
477 mCell.setFlippedVertically(!mCell.flippedVertically());
478
479 //old tile position is bottomLeftPoint
480 QPointF topLeftTilePoint = QPointF(cos(qDegreesToRadians(rotation() - 90)) * height() + x(),
481 sin(qDegreesToRadians(rotation() - 90)) * height() + y());
482 QPointF newPos = flipTransform.map(topLeftTilePoint);
483
484 setPosition(newPos);
485 }
486
487 } // namespace Tiled
488