Skip to content

Commit

Permalink
Merge pull request #2698 from Phlosioneer/object-alignment
Browse files Browse the repository at this point in the history
Added object alignment option on tilesets, which enables setting the
alignment to use for tile objects.
  • Loading branch information
bjorn authored Jan 21, 2020
2 parents e583937 + 83f21eb commit 4123bf1
Show file tree
Hide file tree
Showing 39 changed files with 546 additions and 246 deletions.
6 changes: 6 additions & 0 deletions docs/reference/json-map-format.rst
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ Tileset
imagewidth, int, "Width of source image in pixels"
margin, int, "Buffer between image edge and first tile (pixels)"
name, string, "Name given to this tileset"
objectalignment, string, "Alignment to use for tile objects (``unspecified`` (default), ``topleft``, ``top``, ``topright``, ``left``, ``center``, ``right``, ``bottomleft``, ``bottom`` or ``bottomright``) (since 1.4)"
properties, array, "Array of :ref:`Properties <json-property>`"
source, string, "The external file that contains this tilesets data"
spacing, int, "Spacing between adjacent tiles in image (pixels)"
Expand Down Expand Up @@ -728,6 +729,11 @@ A point on a polygon or a polyline, relative to the position of the object.
Changelog
---------

Tiled 1.4
~~~~~~~~~

* Added ``objectalignment`` to the :ref:`json-tileset` object.

Tiled 1.2
~~~~~~~~~

Expand Down
26 changes: 26 additions & 0 deletions docs/reference/scripting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1337,11 +1337,37 @@ Properties
**tileSize** : size |ro|, Tile size for tiles in this tileset in pixels (has ``width`` and ``height`` members).
**tileSpacing** : int |ro|, Spacing between tiles in this tileset in pixels.
**margin** : int |ro|, Margin around the tileset in pixels (only used at the top and left sides of the tileset image).
**objectAlignment** : :ref:`Alignment <script-tileset-alignment>`, "The alignment to use for tile objects (when ``Unspecified``, uses ``Bottom`` alignment on isometric maps and ``BottomLeft`` alignment for all other maps)."
**tileOffset** : :ref:`script-point`, Offset in pixels that is applied when tiles from this tileset are rendered.
**orientation** : :ref:`Orientation <script-tileset-orientation>`, The orientation of this tileset (used when rendering overlays and in the tile collision editor).
**backgroundColor** : color, Background color for this tileset in the *Tilesets* view.
**isCollection** : bool, Whether this tileset is a collection of images.
**selectedTiles** : [:ref:`script-tile`], Selected tiles (in the tileset editor).

.. _script-tileset-alignment:

.. csv-table::
:header: "Tileset.Alignment"

Tileset.Unspecified
Tileset.TopLeft
Tileset.Top
Tileset.TopRight
Tileset.Left
Tileset.Center
Tileset.Right
Tileset.BottomLeft
Tileset.Bottom
Tileset.BottomRight

.. _script-tileset-orientation:

.. csv-table::
:header: "Tileset.Orientation"

Tileset.Orthogonal
Tileset.Isometric

Functions
~~~~~~~~~

Expand Down
6 changes: 6 additions & 0 deletions docs/reference/tmx-changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ TMX Changelog
Below are described the changes/additions that were made to the
:doc:`tmx-map-format` for recent versions of Tiled.

Tiled 1.4
---------

- Added the ``objectalignment`` attribute to the :ref:`tmx-tileset` element,
allowing the tileset to control the alignment used for tile objects.

Tiled 1.2.1
-----------

Expand Down
6 changes: 6 additions & 0 deletions docs/reference/tmx-map-format.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ Can contain any number: :ref:`tmx-tileset`, :ref:`tmx-layer`,
- **columns:** The number of tile columns in the tileset. For image
collection tilesets it is editable and is used when displaying the
tileset. (since 0.15)
- **objectalignment:** Controls the alignment for tile objects.
Valid values are ``unspecified``, ``topleft``, ``top``, ``topright``,
``left``, ``center``, ``right``, ``bottomleft``, ``bottom`` and
``bottomright``. The default value is ``unspecified``, for compatibility
reasons. When unspecified, tile objects use ``bottomleft`` in orthogonal mode
and ``bottom`` in isometric mode. (since 1.4)

If there are multiple ``<tileset>`` elements, they are in ascending
order of their ``firstgid`` attribute. The first tileset always has a
Expand Down
107 changes: 60 additions & 47 deletions src/libtiled/isometricrenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ QRect IsometricRenderer::boundingRect(const QRect &rect) const
QRectF IsometricRenderer::boundingRect(const MapObject *object) const
{
if (object->shape() == MapObject::Text) {
const QPointF topLeft = pixelToScreenCoords(object->position());
return QRectF(topLeft, object->size());
QRectF bounds { pixelToScreenCoords(object->position()), object->size() };
bounds.translate(-alignmentOffset(bounds, object->alignment(map())));
return bounds;
} else if (object->shape() == MapObject::Point) {
const qreal extraSpace = qMax(objectLineWidth() / 2, qreal(1));
return shape(object).boundingRect()
Expand All @@ -97,25 +98,24 @@ QRectF IsometricRenderer::boundingRect(const MapObject *object) const
extraSpace,
extraSpace);
} else if (!object->cell().isEmpty()) {
const QSizeF objectSize { object->size() };

QSizeF scale { 1.0, 1.0 };
QPoint tileOffset;
QRectF bounds { pixelToScreenCoords(object->position()), object->size() };
bounds.translate(-alignmentOffset(bounds, object->alignment(map())));

if (const Tile *tile = object->cell().tile()) {
QSize imgSize = tile->size();
if (!imgSize.isNull()) {
scale = QSizeF(objectSize.width() / imgSize.width(),
objectSize.height() / imgSize.height());
QPointF tileOffset = tile->offset();
const QSize tileSize = tile->size();
if (!tileSize.isNull()) {
const QSizeF scale {
bounds.width() / tileSize.width(),
bounds.height() / tileSize.height()
};
tileOffset.rx() *= scale.width();
tileOffset.ry() *= scale.height();
}
tileOffset = tile->offset();
bounds.translate(tileOffset);
}

const QPointF bottomCenter = pixelToScreenCoords(object->position());
return QRectF(bottomCenter.x() + (tileOffset.x() * scale.width()) - objectSize.width() / 2,
bottomCenter.y() + (tileOffset.y() * scale.height()) - objectSize.height(),
objectSize.width(),
objectSize.height()).adjusted(-1, -1, 1, 1);
return bounds.adjusted(-1, -1, 1, 1);
} else if (!object->polygon().isEmpty()) {
qreal extraSpace = qMax(objectLineWidth(), qreal(1));

Expand All @@ -132,7 +132,10 @@ QRectF IsometricRenderer::boundingRect(const MapObject *object) const
} else {
// Take the bounding rect of the projected object, and then add a few
// pixels on all sides to correct for the line width.
const QRectF base = pixelRectToScreenPolygon(object->bounds()).boundingRect();
QRectF bounds = object->bounds();
bounds.translate(-alignmentOffset(bounds, object->alignment(map())));

const QRectF base = pixelRectToScreenPolygon(bounds).boundingRect();
const qreal extraSpace = qMax(objectLineWidth() / 2, qreal(1));

return base.adjusted(-extraSpace,
Expand All @@ -147,12 +150,18 @@ QPainterPath IsometricRenderer::shape(const MapObject *object) const

switch (object->shape()) {
case MapObject::Ellipse: {
path.addEllipse(object->bounds());
QRectF bounds = object->bounds();
bounds.translate(-alignmentOffset(bounds, object->alignment(map())));

path.addEllipse(bounds);
path = transform().map(path);
break;
}
case MapObject::Rectangle: {
QPolygonF polygon = pixelRectToScreenPolygon(object->bounds());
QRectF bounds = object->bounds();
bounds.translate(-alignmentOffset(bounds, object->alignment(map())));

QPolygonF polygon = pixelRectToScreenPolygon(bounds);
polygon.append(polygon.first());
path.addPolygon(polygon);
break;
Expand Down Expand Up @@ -186,9 +195,13 @@ QPainterPath IsometricRenderer::interactionShape(const MapObject *object) const
} else {
switch (object->shape()) {
case MapObject::Rectangle:
case MapObject::Ellipse:
path.addPolygon(pixelRectToScreenPolygon(object->bounds()));
case MapObject::Ellipse: {
QRectF bounds = object->bounds();
bounds.translate(-alignmentOffset(bounds, object->alignment(map())));

path.addPolygon(pixelRectToScreenPolygon(bounds));
break;
}
case MapObject::Polygon:
case MapObject::Text:
path = shape(object);
Expand Down Expand Up @@ -370,49 +383,46 @@ void IsometricRenderer::drawMapObject(QPainter *painter,
const Cell &cell = object->cell();

if (!cell.isEmpty()) {
const QSizeF size = object->size();
const QPointF pos = pixelToScreenCoords(object->position());
QRectF bounds = { pixelToScreenCoords(object->position()), object->size() };
bounds.translate(-alignmentOffset(bounds, object->alignment(map())));

CellRenderer(painter, this, object->objectGroup()->effectiveTintColor()).render(cell, pos, size,
CellRenderer::BottomCenter);
CellRenderer(painter, this, object->objectGroup()->effectiveTintColor())
.render(cell, bounds.topLeft(), bounds.size());

if (testFlag(ShowTileObjectOutlines)) {
QPointF tileOffset;
QPointF scale(1.0, 1.0);

if (const Tile *tile = cell.tile()) {
tileOffset = tile->offset();

const QPixmap &image = tile->image();
const QSizeF imageSize = image.size();

if (!imageSize.isEmpty()) {
scale = QPointF(size.width() / imageSize.width(),
size.height() / imageSize.height());
if (const Tile *tile = object->cell().tile()) {
QPointF tileOffset = tile->offset();
const QSize tileSize = tile->size();
if (!tileSize.isNull()) {
const QSizeF scale {
bounds.width() / tileSize.width(),
bounds.height() / tileSize.height()
};
tileOffset.rx() *= scale.width();
tileOffset.ry() *= scale.height();
}
bounds.translate(tileOffset);
}

QRectF rect(QPointF(pos.x() - size.width() / 2 + tileOffset.x() * scale.x(),
pos.y() - size.height() + tileOffset.y() * scale.y()),
size);

pen.setStyle(Qt::SolidLine);
painter->setRenderHint(QPainter::Antialiasing, false);
painter->setBrush(Qt::NoBrush);
painter->setPen(pen);
painter->drawRect(rect);
painter->drawRect(bounds);
pen.setStyle(Qt::DotLine);
pen.setColor(color);
painter->setPen(pen);
painter->drawRect(rect);
painter->drawRect(bounds);
}
} else if (object->shape() == MapObject::Text) {
const QPointF pos = pixelToScreenCoords(object->position());
QRectF bounds = { pixelToScreenCoords(object->position()), object->size() };
bounds.translate(-alignmentOffset(bounds, object->alignment(map())));

const auto& textData = object->textData();

painter->setFont(textData.font);
painter->setPen(textData.color);
painter->drawText(QRectF(pos, object->size()),
painter->drawText(bounds,
textData.text,
textData.textOption());
} else {
Expand All @@ -434,11 +444,14 @@ void IsometricRenderer::drawMapObject(QPainter *painter,
painter->setPen(pen);
painter->setRenderHint(QPainter::Antialiasing);

QRectF bounds = object->bounds();
bounds.translate(-alignmentOffset(bounds, object->alignment(map())));

// TODO: Do something sensible to make null-sized objects usable

switch (object->shape()) {
case MapObject::Ellipse: {
const QPolygonF rect = pixelRectToScreenPolygon(object->bounds());
const QPolygonF rect = pixelRectToScreenPolygon(bounds);
const QPainterPath ellipse = shape(object);

painter->drawPath(ellipse);
Expand All @@ -458,7 +471,7 @@ void IsometricRenderer::drawMapObject(QPainter *painter,
drawPointObject(painter, color);
break;
case MapObject::Rectangle: {
QPolygonF polygon = pixelRectToScreenPolygon(object->bounds());
QPolygonF polygon = pixelRectToScreenPolygon(bounds);
painter->drawPolygon(polygon);

painter->setPen(colorPen);
Expand Down
1 change: 1 addition & 0 deletions src/libtiled/map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ std::unique_ptr<Map> Map::clone() const
o->exportFileName = exportFileName;
o->exportFormat = exportFormat;
o->mRenderOrder = mRenderOrder;
o->mCompressionLevel = mCompressionLevel;
o->mHexSideLength = mHexSideLength;
o->mStaggerAxis = mStaggerAxis;
o->mStaggerIndex = mStaggerIndex;
Expand Down
44 changes: 26 additions & 18 deletions src/libtiled/mapobject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,15 @@ QRectF MapObject::screenBounds(const MapRenderer &renderer) const
objectSize.width(),
objectSize.height());

align(bounds, alignment());
align(bounds, alignment(renderer.map()));

return bounds;
} else {
switch (mShape) {
case MapObject::Ellipse:
case MapObject::Rectangle: {
QRectF bounds(this->bounds());
align(bounds, alignment());
align(bounds, alignment(renderer.map()));
QPolygonF screenPolygon = renderer.pixelToScreenCoords(bounds);
return screenPolygon.boundingRect();
}
Expand All @@ -221,27 +221,35 @@ Map *MapObject::map() const
}

/*
* This is somewhat of a workaround for dealing with the ways different objects
* align.
* Returns the effective alignment for this object on the given \a map.
*
* Traditional rectangle objects have top-left alignment.
* Tile objects have bottom-left alignment on orthogonal maps, but
* bottom-center alignment on isometric maps.
* By default, non-tile objects have top-left alignment, while tile objects
* have bottom-left alignment on orthogonal maps and bottom-center alignment
* on isometric maps.
*
* Eventually, the object alignment should probably be configurable. For
* backwards compatibility, it will need to be configurable on a per-object
* level.
* For tile objects, the default alignment can be overridden by setting an
* alignment on the tileset.
*/
Alignment MapObject::alignment() const
Alignment MapObject::alignment(const Map *map) const
{
if (mCell.isEmpty()) {
return TopLeft;
} else if (mObjectGroup) {
if (Map *map = mObjectGroup->map())
if (map->orientation() == Map::Isometric)
return Bottom;
Alignment alignment = Unspecified;

if (Tileset *tileset = mCell.tileset())
alignment = tileset->objectAlignment();

if (!map && mObjectGroup)
map = mObjectGroup->map();

if (alignment == Unspecified) {
if (mCell.isEmpty())
return TopLeft;
else if (map && map->orientation() == Map::Isometric)
return Bottom;

return BottomLeft;
}
return BottomLeft;

return alignment;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/libtiled/mapobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ class TILEDSHARED_EXPORT MapObject : public Object
qreal rotation() const;
void setRotation(qreal rotation);

Alignment alignment() const;
Alignment alignment(const Map *map = nullptr) const;

bool isVisible() const;
void setVisible(bool visible);
Expand Down
Loading

0 comments on commit 4123bf1

Please sign in to comment.