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