Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion docs/manual/editing-tilesets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ tileset.
Two Types of Tileset
--------------------

A tileset is a collection of tiles. Tiled currently supports two types
A tileset is a collection of tiles. Tiled currently supports three types
of tilesets, which are chosen when creating a new tileset:

Based on Tileset Image
Expand All @@ -21,6 +21,13 @@ Based on Tileset Image
between or around their tiles or those that have extruded the border
pixels of each tile to avoid color bleeding.

Tileset Atlas
This tileset uses a single image but allows you to define custom tile
regions that can vary in size and position. This is useful for tilesets
with varied tile sizes, y-sorting, multi-tile objects, or when you need
to dynamically adjust tile boundaries. Tiles can be arranged, moved, and
resized using the *Rearrange Tiles* mode (see :ref:`rearranging-tiles`).

Collection of Images
In this type of tileset each tile refers to its own image file. It
is useful when the tiles aren't the same size, or when the packing
Expand Down Expand Up @@ -175,6 +182,34 @@ both tile layers and tile objects.
Collision shapes rendered on the map. This map is from `Owyn's Adventure
<https://store.steampowered.com/app/1020940/Owyns_Adventure/>`__.

.. _rearranging-tiles:

Rearranging Tiles (Atlas Tilesets)
-----------------------------------

When working with a Tileset Atlas, you can customize the tile regions using the
*Rearrange Tiles* mode. This mode allows you to move, resize, create, and delete
tile regions within the tileset image. Click the *Rearrange Tiles* |rearrange-tiles-icon|
button in the toolbar to enter this mode.

When in Rearrange Tiles mode, the following mouse controls are available:

* **Left-click and drag on a tile** - Moves the tile to a new position
* **Left-click and drag on a tile corner** - Resizes the tile
* **Left-click and drag on empty space** - Creates a new tile region
* **Right-click and drag** - Deletes all tiles overlapping the selection area
* **Hold Shift** - Disables grid snapping for precise positioning

This is particularly useful for:

* Creating tilesets with varied tile sizes
* Implementing y-sorting with tiles of different heights
* Defining multi-tile objects as single tiles
* Adjusting tile boundaries after importing an image

.. |rearrange-tiles-icon|
image:: ../../src/tiled/resources/images/22/stock-tool-move-22.png

.. _tile-animation-editor:

Tile Animation Editor
Expand Down
19 changes: 14 additions & 5 deletions src/libtiled/map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,21 @@ void Map::recomputeDrawMargins() const
QMargins offsetMargins;

for (const SharedTileset &tileset : mTilesets) {
const bool useGridSize = tileset->tileRenderSize() == Tileset::GridSize;
const QSize tileSize = useGridSize ? this->tileSize()
: tileset->tileSize();
if (tileset->isAtlas()) {
// For atlas tilesets, check all tile image rects
for (const Tile *tile : tileset->tiles()) {
const QRect rect = tile->imageRect();
maxTileSize = std::max(maxTileSize,
std::max(rect.width(), rect.height()));
}
} else {
const bool useGridSize = tileset->tileRenderSize() == Tileset::GridSize;
const QSize tileSize = useGridSize ? this->tileSize()
: tileset->tileSize();

maxTileSize = std::max(maxTileSize, std::max(tileSize.width(),
tileSize.height()));
maxTileSize = std::max(maxTileSize, std::max(tileSize.width(),
tileSize.height()));
}

const QPoint offset = tileset->tileOffset();
offsetMargins = maxMargins(QMargins(-offset.x(),
Expand Down
3 changes: 2 additions & 1 deletion src/libtiled/mapreader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -390,14 +390,15 @@ SharedTileset MapReaderPrivate::readTileset()
const QString className = atts.value(QLatin1String("class")).toString();
const int tileSpacing = atts.value(QLatin1String("spacing")).toInt();
const int margin = atts.value(QLatin1String("margin")).toInt();
const bool atlas = atts.value(QLatin1String("atlas")).toInt();
const int columns = atts.value(QLatin1String("columns")).toInt();
const QString backgroundColor = atts.value(QLatin1String("backgroundcolor")).toString();
const QString alignment = atts.value(QLatin1String("objectalignment")).toString();
const QString tileRenderSize = atts.value(QLatin1String("tilerendersize")).toString();
const QString fillMode = atts.value(QLatin1String("fillmode")).toString();

tileset = Tileset::create(name, tileWidth, tileHeight,
tileSpacing, margin);
tileSpacing, margin, atlas);

tileset->setClassName(className);
tileset->setColumnCount(columns);
Expand Down
16 changes: 8 additions & 8 deletions src/libtiled/maptovariantconverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ QVariant MapToVariantConverter::toVariant(const Tileset &tileset,
tilesetVariant[QStringLiteral("name")] = tileset.name();
if (!tileset.className().isEmpty())
tilesetVariant[QStringLiteral("class")] = tileset.className();
tilesetVariant[QStringLiteral("atlas")] = tileset.isAtlas();
tilesetVariant[QStringLiteral("tilewidth")] = tileset.tileWidth();
tilesetVariant[QStringLiteral("tileheight")] = tileset.tileHeight();
tilesetVariant[QStringLiteral("spacing")] = tileset.tileSpacing();
Expand Down Expand Up @@ -302,14 +303,13 @@ QVariant MapToVariantConverter::toVariant(const Tileset &tileset,
tileVariant[QStringLiteral("imagewidth")] = imageSize.width();
tileVariant[QStringLiteral("imageheight")] = imageSize.height();
}

const QRect &imageRect = tile->imageRect();
if (!imageRect.isNull() && imageRect != tile->image().rect() && tileset.isCollection()) {
tileVariant[QStringLiteral("x")] = imageRect.x();
tileVariant[QStringLiteral("y")] = imageRect.y();
tileVariant[QStringLiteral("width")] = imageRect.width();
tileVariant[QStringLiteral("height")] = imageRect.height();
}
}
const QRect &imageRect = tile->imageRect();
if (!imageRect.isNull() && imageRect != tile->image().rect() && (tileset.isCollection() || tileset.isAtlas())) {
tileVariant[QStringLiteral("x")] = imageRect.x();
tileVariant[QStringLiteral("y")] = imageRect.y();
tileVariant[QStringLiteral("width")] = imageRect.width();
tileVariant[QStringLiteral("height")] = imageRect.height();
}
if (tile->objectGroup())
tileVariant[QStringLiteral("objectgroup")] = toVariant(*tile->objectGroup());
Expand Down
8 changes: 6 additions & 2 deletions src/libtiled/mapwriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,9 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset &tileset,
if (margin != 0)
w.writeAttribute(QStringLiteral("margin"), QString::number(margin));

if (tileset.isAtlas())
w.writeAttribute(QStringLiteral("atlas"), QString::number(tileset.isAtlas()));

w.writeAttribute(QStringLiteral("tilecount"),
QString::number(tileset.tileCount()));
w.writeAttribute(QStringLiteral("columns"),
Expand Down Expand Up @@ -413,15 +416,16 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset &tileset,
QSize(tileset.imageWidth(), tileset.imageHeight()));

const bool isCollection = tileset.isCollection();
const bool includeAllTiles = isCollection || tileset.anyTileOutOfOrder();
const bool isAtlas = tileset.isAtlas();
const bool includeAllTiles = isCollection || isAtlas || tileset.anyTileOutOfOrder();

for (const Tile *tile : tileset.tiles()) {
if (includeAllTiles || includeTile(tile)) {
w.writeStartElement(QStringLiteral("tile"));
w.writeAttribute(QStringLiteral("id"), QString::number(tile->id()));

const QRect &imageRect = tile->imageRect();
if (!imageRect.isNull() && imageRect != tile->image().rect() && isCollection) {
if (!imageRect.isNull() && imageRect != tile->image().rect() && (isCollection || isAtlas)) {
w.writeAttribute(QStringLiteral("x"),
QString::number(imageRect.x()));
w.writeAttribute(QStringLiteral("y"),
Expand Down
75 changes: 46 additions & 29 deletions src/libtiled/tileset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@
namespace Tiled {

Tileset::Tileset(QString name, int tileWidth, int tileHeight,
int tileSpacing, int margin)
int tileSpacing, int margin, bool isAtlas)
: Object(TilesetType)
, mName(std::move(name))
, mTileWidth(tileWidth)
, mTileHeight(tileHeight)
, mTileSpacing(tileSpacing)
, mMargin(margin)
, mGridSize(tileWidth, tileHeight)
, mAtlas(isAtlas)
{
Q_ASSERT(tileSpacing >= 0);
Q_ASSERT(margin >= 0);
Expand Down Expand Up @@ -245,48 +246,58 @@ bool Tileset::loadImage()
return initializeTilesetTiles();
}

bool Tileset::initializeTilesetTiles()
bool Tileset::initializeTilesetTiles(bool forceGeneration)
{
if (mImage.isNull() || mTileWidth <= 0 || mTileHeight <= 0)
return false;

if (mImageReference.transparentColor.isValid())
mImage.setMask(mImage.createMaskFromColor(mImageReference.transparentColor));

QVector<QRect> tileRects;

for (int y = mMargin; y <= mImage.height() - mTileHeight; y += mTileHeight + mTileSpacing)
for (int x = mMargin; x <= mImage.width() - mTileWidth; x += mTileWidth + mTileSpacing)
tileRects.append(QRect(x, y, mTileWidth, mTileHeight));

for (int tileNum = 0; tileNum < tileRects.size(); ++tileNum) {
auto it = mTilesById.find(tileNum);
if (it != mTilesById.end()) {
it.value()->setImage(QPixmap()); // make sure it uses the tileset's image
it.value()->setImageRect(tileRects.at(tileNum));
} else {
auto tile = new Tile(tileNum, this);
tile->setImageRect(tileRects.at(tileNum));
mTilesById.insert(tileNum, tile);
mTiles.insert(tileNum, tile);
}
bool needsRectGeneration = true;
if (isAtlas()) {
needsRectGeneration = forceGeneration;
}

QPixmap blank;
if (needsRectGeneration) {
QVector<QRect> tileRects;

for (int y = mMargin; y <= mImage.height() - mTileHeight; y += mTileHeight + mTileSpacing)
for (int x = mMargin; x <= mImage.width() - mTileWidth; x += mTileWidth + mTileSpacing)
tileRects.append(QRect(x, y, mTileWidth, mTileHeight));

for (int tileNum = 0; tileNum < tileRects.size(); ++tileNum) {
auto it = mTilesById.find(tileNum);
if (it != mTilesById.end()) {
it.value()->setImage(QPixmap()); // make sure it uses the tileset's image
it.value()->setImageRect(tileRects.at(tileNum));
} else {
auto tile = new Tile(tileNum, this);
tile->setImageRect(tileRects.at(tileNum));
mTilesById.insert(tileNum, tile);
mTiles.insert(tileNum, tile);
}
}

// Blank out any remaining tiles to avoid confusion (todo: could be more clear)
for (Tile *tile : std::as_const(mTiles)) {
if (tile->id() >= tileRects.size()) {
if (blank.isNull()) {
blank = QPixmap(mTileWidth, mTileHeight);
blank.fill();
QPixmap blank;

// Blank out any remaining tiles to avoid confusion (todo: could be more clear)
if (!isAtlas()) {
for (Tile *tile : std::as_const(mTiles)) {
if (tile->id() >= tileRects.size()) {
if (blank.isNull()) {
blank = QPixmap(mTileWidth, mTileHeight);
blank.fill();
}
tile->setImage(blank);
tile->setImageRect(QRect(0, 0, mTileWidth, mTileHeight));
}
}
tile->setImage(blank);
tile->setImageRect(QRect(0, 0, mTileWidth, mTileHeight));
}
}

mNextTileId = std::max<int>(mNextTileId, tileRects.size());
for (Tile *tile : std::as_const(mTiles))
mNextTileId = std::max(mNextTileId, tile->id() + 1);

mImageReference.size = mImage.size();
mColumnCount = columnCountForWidth(mImageReference.size.width());
Expand Down Expand Up @@ -554,6 +565,9 @@ void Tileset::setTileImageRect(Tile *tile, const QRect &imageRect)

void Tileset::maybeUpdateTileSize(QSize previousTileSize, QSize newTileSize)
{
if (isAtlas())
return;

if (previousTileSize == newTileSize)
return;

Expand Down Expand Up @@ -681,6 +695,9 @@ SharedTileset Tileset::clone() const
*/
void Tileset::updateTileSize()
{
if (isAtlas())
return;

int maxWidth = 0;
int maxHeight = 0;
for (Tile *tile : std::as_const(mTiles)) {
Expand Down
8 changes: 6 additions & 2 deletions src/libtiled/tileset.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class TILEDSHARED_EXPORT Tileset : public Object, public QEnableSharedFromThis<T
* pointer is initialized, which enables the sharedPointer() function.
*/
Tileset(QString name, int tileWidth, int tileHeight,
int tileSpacing = 0, int margin = 0);
int tileSpacing = 0, int margin = 0, bool isAtlas = false);

public:
QString exportFileName;
Expand All @@ -138,6 +138,9 @@ class TILEDSHARED_EXPORT Tileset : public Object, public QEnableSharedFromThis<T
void setFormat(const QString &format);
QString format() const;

bool isAtlas() const { return mAtlas; }
void setAtlas(bool atlas) { mAtlas = atlas; }

int tileWidth() const;
int tileHeight() const;

Expand Down Expand Up @@ -198,7 +201,7 @@ class TILEDSHARED_EXPORT Tileset : public Object, public QEnableSharedFromThis<T
bool loadFromImage(const QImage &image, const QString &source);
bool loadFromImage(const QString &fileName);
bool loadImage();
bool initializeTilesetTiles();
bool initializeTilesetTiles(bool forceGeneration=false);

SharedTileset findSimilarTileset(const QVector<SharedTileset> &tilesets) const;

Expand Down Expand Up @@ -311,6 +314,7 @@ class TILEDSHARED_EXPORT Tileset : public Object, public QEnableSharedFromThis<T
TileRenderSize mTileRenderSize = TileSize;
FillMode mFillMode = Stretch;
QSize mGridSize;
bool mAtlas;
int mColumnCount = 0;
int mExpectedColumnCount = 0;
int mExpectedRowCount = 0;
Expand Down
3 changes: 2 additions & 1 deletion src/libtiled/varianttomapconverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ SharedTileset VariantToMapConverter::toTileset(const QVariant &variant)
}

const QString name = variantMap[QStringLiteral("name")].toString();
const bool atlas = variantMap[QStringLiteral("atlas")].toBool();
const QString className = variantMap[QStringLiteral("class")].toString();
const int tileWidth = variantMap[QStringLiteral("tilewidth")].toInt();
const int tileHeight = variantMap[QStringLiteral("tileheight")].toInt();
Expand All @@ -244,7 +245,7 @@ SharedTileset VariantToMapConverter::toTileset(const QVariant &variant)

SharedTileset tileset(Tileset::create(name,
tileWidth, tileHeight,
spacing, margin));
spacing, margin, atlas));

tileset->setClassName(className);
tileset->setObjectAlignment(alignmentFromString(objectAlignment));
Expand Down
2 changes: 1 addition & 1 deletion src/tiled/documentmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1372,7 +1372,7 @@ void DocumentManager::onWorldUnloaded(WorldDocument *worldDocument)

static bool mayNeedColumnCountAdjustment(const Tileset &tileset)
{
if (tileset.isCollection())
if (tileset.isCollection() || tileset.isAtlas())
return false;
if (tileset.imageStatus() != LoadingReady)
return false;
Expand Down
Loading
Loading