Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object alignment #2698

Merged
merged 12 commits into from
Jan 21, 2020
1 change: 1 addition & 0 deletions docs/reference/json-map-format.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Map
layers, array, "Array of :ref:`Layers <json-layer>`"
nextlayerid, int, "Auto-increments for each layer"
nextobjectid, int, "Auto-increments for each placed object"
objectalignment, string, "``unset``, ``top-left``, ``bottom-left``, or ``bottom-center``"
orientation, string, "``orthogonal``, ``isometric``, ``staggered`` or ``hexagonal``"
properties, array, "Array of :ref:`Properties <json-property>`"
renderorder, string, "``right-down`` (the default), ``right-up``, ``left-down`` or ``left-up`` (orthogonal maps only)"
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 @@ -61,6 +61,12 @@ might be useful for XML-namespacing anyway.*
- **nextobjectid:** Stores the next available ID for new objects. This
number is stored to prevent reuse of the same ID after objects have
been removed. (since 0.11)
- **objectalignment:** Controls the origins for tile and shape objects.
Valid values are ``unset``, ``top-left``, ``bottom-left``, and ``bottom-center``.
In isometric mode, ``bottom-left`` is treated the same as ``bottom-center``.
bjorn marked this conversation as resolved.
Show resolved Hide resolved
The default value is ``unset``, for compatibility reasons. With ``unset``, tile
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's indeed a necessary evil, but now that we've got a project file we should definitely use that as a place to store a different default value (not affecting the map format though, just affecting the initialization of new maps with that project active). No need to do that as part of this patch though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into making that change when this PR is done, then.

Reposted this because github deleted my original comment.

objects use ``bottom-left`` in orthogonal mode and ``bottom-center`` in isometric
mode, while shape objects use ``top-left`` everywhere. (since 1.4)

The ``tilewidth`` and ``tileheight`` properties determine the general
grid size of the map. The individual tiles may have different sizes.
Expand Down
82 changes: 43 additions & 39 deletions src/libtiled/isometricrenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,25 +96,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()));

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 Down Expand Up @@ -369,32 +368,26 @@ 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 rect = { pixelToScreenCoords(object->position()), object->size() };
rect.translate(-alignmentOffset(rect, object->alignment()));

CellRenderer(painter, this).render(cell, pos, size,
CellRenderer::BottomCenter);
CellRenderer(painter, this).render(cell, rect.topLeft(), rect.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 {
rect.width() / tileSize.width(),
rect.height() / tileSize.height()
};
tileOffset.rx() *= scale.width();
tileOffset.ry() *= scale.height();
}
rect.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);
Expand Down Expand Up @@ -433,11 +426,22 @@ void IsometricRenderer::drawMapObject(QPainter *painter,
painter->setPen(pen);
painter->setRenderHint(QPainter::Antialiasing);

QRectF bounds(object->bounds());
switch (map()->objectAlignment()) {
case Map::TopLeft:
bounds.moveTopLeft(QPointF(-bounds.width(), -bounds.height()));
break;
case Map::Unset:
case Map::BottomLeft:
case Map::BottomCenter:
break;
}

// 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 @@ -453,11 +457,11 @@ void IsometricRenderer::drawMapObject(QPainter *painter,
break;
}
case MapObject::Point:
painter->translate(pixelToScreenCoords(object->position()));
painter->translate(pixelToScreenCoords(bounds.topLeft()));
drawPointObject(painter, color);
break;
case MapObject::Rectangle: {
QPolygonF polygon = pixelRectToScreenPolygon(object->bounds());
QPolygonF polygon = pixelRectToScreenPolygon(bounds);
painter->drawPolygon(polygon);

painter->setPen(colorPen);
Expand All @@ -467,7 +471,7 @@ void IsometricRenderer::drawMapObject(QPainter *painter,
break;
}
case MapObject::Polygon: {
const QPointF &pos = object->position();
const QPointF &pos = bounds.topLeft();
const QPolygonF polygon = object->polygon().translated(pos);
QPolygonF screenPolygon = pixelToScreenCoords(polygon);

Expand All @@ -491,7 +495,7 @@ void IsometricRenderer::drawMapObject(QPainter *painter,
break;
}
case MapObject::Polyline: {
const QPointF &pos = object->position();
const QPointF &pos = bounds.topLeft();
bjorn marked this conversation as resolved.
Show resolved Hide resolved
const QPolygonF polygon = object->polygon().translated(pos);
QPolygonF screenPolygon = pixelToScreenCoords(polygon);

Expand Down
31 changes: 31 additions & 0 deletions src/libtiled/map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Map::Map(Orientation orientation,
Object(MapType),
mOrientation(orientation),
mRenderOrder(RightDown),
mObjectAlignment(Unset),
mCompressionLevel(-1),
mWidth(width),
mHeight(height),
Expand Down Expand Up @@ -338,6 +339,8 @@ std::unique_ptr<Map> Map::clone() const
o->exportFileName = exportFileName;
o->exportFormat = exportFormat;
o->mRenderOrder = mRenderOrder;
o->mObjectAlignment = mObjectAlignment;
o->mCompressionLevel = mCompressionLevel;
o->mHexSideLength = mHexSideLength;
o->mStaggerAxis = mStaggerAxis;
o->mStaggerIndex = mStaggerIndex;
Expand Down Expand Up @@ -529,3 +532,31 @@ Map::RenderOrder Tiled::renderOrderFromString(const QString &string)
}
return renderOrder;
}

QString Tiled::objectAlignmentToString(Map::ObjectAlignment objectAlignment)
{
switch (objectAlignment) {
case Map::Unset:
return QLatin1String("unset");
case Map::TopLeft:
return QLatin1String("top-left");
case Map::BottomLeft:
return QLatin1String("bottom-left");
case Map::BottomCenter:
return QLatin1String("bottom-center");
}
return QString();
}

Map::ObjectAlignment Tiled::objectAlignmentFromString(const QString &string)
{
Map::ObjectAlignment objectAlignment = Map::Unset;
if (string == QLatin1String("top-left")) {
objectAlignment = Map::TopLeft;
} else if (string == QLatin1String("bottom-left")) {
objectAlignment = Map::BottomLeft;
} else if (string == QLatin1String("bottom-center")) {
objectAlignment = Map::BottomCenter;
}
return objectAlignment;
}
28 changes: 28 additions & 0 deletions src/libtiled/map.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ class TILEDSHARED_EXPORT Map : public Object
};
Q_ENUM(StaggerIndex)

/**
* Where objects are aligned. For compatibility reasons, the default option
* is Unset. Unset aligns tile objects using BottomLeft for orthogonal maps and
* BottomCenter for isometric maps, while all other objects use TopLeft.
*/
enum ObjectAlignment {
Unset = 0,
TopLeft = 1,
BottomLeft = 2,
BottomCenter = 3
};
Q_ENUM(ObjectAlignment)

Map();

/**
Expand Down Expand Up @@ -184,6 +197,17 @@ class TILEDSHARED_EXPORT Map : public Object
void setRenderOrder(RenderOrder renderOrder)
{ mRenderOrder = renderOrder; }

/**
* Returns the alignment for objects in the map.
*/
ObjectAlignment objectAlignment() const { return mObjectAlignment; }

/**
* Sets the alignment for objects in the map.
*/
void setObjectAlignment(ObjectAlignment objectAlignment)
{ mObjectAlignment = objectAlignment; }

/**
* Returns the compression level of this map.
*/
Expand Down Expand Up @@ -486,6 +510,7 @@ class TILEDSHARED_EXPORT Map : public Object

Orientation mOrientation;
RenderOrder mRenderOrder;
ObjectAlignment mObjectAlignment;
int mCompressionLevel;
int mWidth;
int mHeight;
Expand Down Expand Up @@ -687,6 +712,9 @@ TILEDSHARED_EXPORT QString compressionToString(Map::LayerDataFormat);
TILEDSHARED_EXPORT QString renderOrderToString(Map::RenderOrder renderOrder);
TILEDSHARED_EXPORT Map::RenderOrder renderOrderFromString(const QString &);

TILEDSHARED_EXPORT QString objectAlignmentToString(Map::ObjectAlignment);
TILEDSHARED_EXPORT Map::ObjectAlignment objectAlignmentFromString(const QString &);

typedef QSharedPointer<Map> SharedMap;

} // namespace Tiled
Expand Down
16 changes: 16 additions & 0 deletions src/libtiled/mapobject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,22 @@ QRectF MapObject::screenBounds(const MapRenderer &renderer) const
*/
Alignment MapObject::alignment() const
{
Map::ObjectAlignment objectAlignment = Map::Unset;
if (mObjectGroup)
if (Map *map = mObjectGroup->map())
objectAlignment = map->objectAlignment();

switch (objectAlignment) {
case Map::Unset:
break;
case Map::TopLeft:
return TopLeft;
case Map::BottomLeft:
return BottomLeft;
case Map::BottomCenter:
return Bottom;
}

if (mCell.isEmpty()) {
return TopLeft;
} else if (mObjectGroup) {
Expand Down
6 changes: 6 additions & 0 deletions src/libtiled/mapreader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@ std::unique_ptr<Map> MapReaderPrivate::readMap()
const QString compressionLevelString =
atts.value(QLatin1String("compressionlevel")).toString();

const QString objectAlignmentString =
atts.value(QLatin1String("objectalignment")).toString();
const Map::ObjectAlignment objectAlignment =
objectAlignmentFromString(objectAlignmentString);

const int nextLayerId = atts.value(QLatin1String("nextlayerid")).toInt();
const int nextObjectId = atts.value(QLatin1String("nextobjectid")).toInt();

Expand All @@ -284,6 +289,7 @@ std::unique_ptr<Map> MapReaderPrivate::readMap()
mMap->setStaggerAxis(staggerAxis);
mMap->setStaggerIndex(staggerIndex);
mMap->setRenderOrder(renderOrder);
mMap->setObjectAlignment(objectAlignment);
mMap->setCompressionLevel(compressionLevelString.toUInt());
if (nextLayerId)
mMap->setNextLayerId(nextLayerId);
Expand Down
33 changes: 28 additions & 5 deletions src/libtiled/maprenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ CellRenderer::CellRenderer(QPainter *painter, const MapRenderer *renderer, CellT
* kind of tile has to be drawn. For this reason it is necessary to call
* flush when finished doing drawCell calls. This function is also called by
* the destructor so usually an explicit call is not needed.
*
* This call expects `painter.translate(pos)` to correspond to the Origin point.
*/
void CellRenderer::render(const Cell &cell, const QPointF &pos, const QSizeF &size, Origin origin)
{
Expand All @@ -249,9 +251,19 @@ void CellRenderer::render(const Cell &cell, const QPointF &pos, const QSizeF &si
tile = tile->currentFrameTile();

if (!tile || tile->image().isNull()) {
QRectF target { pos - QPointF(0, size.height()), size };
if (origin == BottomCenter)
target.moveLeft(target.left() - size.width() / 2);
QRectF target { pos, size };

switch (origin) {
case TopLeft:
break;
case BottomLeft:
target.translate(0.0, -size.height());
break;
case BottomCenter:
target.translate(-size.width() / 2, -size.height());
break;
}

renderMissingImageMarker(*mPainter, target);
return;
}
Expand All @@ -274,8 +286,9 @@ void CellRenderer::render(const Cell &cell, const QPointF &pos, const QSizeF &si
bool flippedVertically = cell.flippedVertically();

QPainter::PixmapFragment fragment;
// Calculate the position as if the origin is TopLeft, and correct it later.
fragment.x = pos.x() + (offset.x() * scale.width()) + sizeHalf.x();
fragment.y = pos.y() + (offset.y() * scale.height()) + sizeHalf.y() - size.height();
fragment.y = pos.y() + (offset.y() * scale.height()) + sizeHalf.y();
fragment.sourceLeft = 0;
fragment.sourceTop = 0;
fragment.width = imageSize.width();
Expand All @@ -285,8 +298,18 @@ void CellRenderer::render(const Cell &cell, const QPointF &pos, const QSizeF &si
fragment.rotation = 0;
fragment.opacity = 1;

if (origin == BottomCenter)
// Correct the position if the origin is not TopLeft.
switch (origin) {
case TopLeft:
break;
case BottomLeft:
fragment.y -= size.height();
break;
case BottomCenter:
fragment.x -= sizeHalf.x();
fragment.y -= size.height();
break;
}

if (mCellType == HexagonalCells) {

Expand Down
4 changes: 3 additions & 1 deletion src/libtiled/maprenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ class CellRenderer
{
public:
enum Origin {
TopLeft,
BottomLeft,
BottomCenter
};
Expand All @@ -304,7 +305,8 @@ class CellRenderer

~CellRenderer() { flush(); }

void render(const Cell &cell, const QPointF &pos, const QSizeF &size, Origin origin);
void render(const Cell &cell, const QPointF &pos, const QSizeF &size,
Origin origin = TopLeft);
void flush();

private:
Expand Down
1 change: 1 addition & 0 deletions src/libtiled/maptovariantconverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ QVariant MapToVariantConverter::toVariant(const Map &map, const QDir &mapDir)
mapVariant[QLatin1String("tiledversion")] = QCoreApplication::applicationVersion();
mapVariant[QLatin1String("orientation")] = orientationToString(map.orientation());
mapVariant[QLatin1String("renderorder")] = renderOrderToString(map.renderOrder());
mapVariant[QLatin1String("objectalignment")] = objectAlignmentToString(map.objectAlignment());
mapVariant[QLatin1String("width")] = map.width();
mapVariant[QLatin1String("height")] = map.height();
mapVariant[QLatin1String("tilewidth")] = map.tileWidth();
Expand Down
Loading