Skip to content

Commit 61c199f

Browse files
committed
Add support for Tileset Grid Atlas
Useful for y-sorting, multi tile objects, base tile images changing, etc. - Add new "atlas" property to tileset that determines if tileset is atlas tileset or not - Atlas property is set on tileset creation through type - If generate tiles based on grid option is enabled, atlas tileset will pre-generate grid tiles - Atlas tileset always saves all image rects For controls, go to rearrange tiles mode: - Left-click and drag to move tiles - Left-click tile corners to resize - Left-click and drag empty space to create new tiles - Right-click and drag to delete tiles - Hold Shift to disable grid snapping Signed-off-by: Tomas Slusny <[email protected]>
1 parent f7c4e4d commit 61c199f

18 files changed

+760
-111
lines changed

docs/manual/editing-tilesets.rst

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ tileset.
1010
Two Types of Tileset
1111
--------------------
1212

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

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

24+
Tileset Atlas
25+
This tileset uses a single image but allows you to define custom tile
26+
regions that can vary in size and position. This is useful for tilesets
27+
with varied tile sizes, y-sorting, multi-tile objects, or when you need
28+
to dynamically adjust tile boundaries. Tiles can be arranged, moved, and
29+
resized using the *Rearrange Tiles* mode (see :ref:`rearranging-tiles`).
30+
2431
Collection of Images
2532
In this type of tileset each tile refers to its own image file. It
2633
is useful when the tiles aren't the same size, or when the packing
@@ -175,6 +182,34 @@ both tile layers and tile objects.
175182
Collision shapes rendered on the map. This map is from `Owyn's Adventure
176183
<https://store.steampowered.com/app/1020940/Owyns_Adventure/>`__.
177184

185+
.. _rearranging-tiles:
186+
187+
Rearranging Tiles (Atlas Tilesets)
188+
-----------------------------------
189+
190+
When working with a Tileset Atlas, you can customize the tile regions using the
191+
*Rearrange Tiles* mode. This mode allows you to move, resize, create, and delete
192+
tile regions within the tileset image. Click the *Rearrange Tiles* |rearrange-tiles-icon|
193+
button in the toolbar to enter this mode.
194+
195+
When in Rearrange Tiles mode, the following mouse controls are available:
196+
197+
* **Left-click and drag on a tile** - Moves the tile to a new position
198+
* **Left-click and drag on a tile corner** - Resizes the tile
199+
* **Left-click and drag on empty space** - Creates a new tile region
200+
* **Right-click and drag** - Deletes all tiles overlapping the selection area
201+
* **Hold Shift** - Disables grid snapping for precise positioning
202+
203+
This is particularly useful for:
204+
205+
* Creating tilesets with varied tile sizes
206+
* Implementing y-sorting with tiles of different heights
207+
* Defining multi-tile objects as single tiles
208+
* Adjusting tile boundaries after importing an image
209+
210+
.. |rearrange-tiles-icon|
211+
image:: ../../src/tiled/resources/images/22/stock-tool-move-22.png
212+
178213
.. _tile-animation-editor:
179214

180215
Tile Animation Editor

src/libtiled/map.cpp

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,21 @@ void Map::recomputeDrawMargins() const
143143
QMargins offsetMargins;
144144

145145
for (const SharedTileset &tileset : mTilesets) {
146-
const bool useGridSize = tileset->tileRenderSize() == Tileset::GridSize;
147-
const QSize tileSize = useGridSize ? this->tileSize()
148-
: tileset->tileSize();
146+
if (tileset->isAtlas()) {
147+
// For atlas tilesets, check all tile image rects
148+
for (const Tile *tile : tileset->tiles()) {
149+
const QRect rect = tile->imageRect();
150+
maxTileSize = std::max(maxTileSize,
151+
std::max(rect.width(), rect.height()));
152+
}
153+
} else {
154+
const bool useGridSize = tileset->tileRenderSize() == Tileset::GridSize;
155+
const QSize tileSize = useGridSize ? this->tileSize()
156+
: tileset->tileSize();
149157

150-
maxTileSize = std::max(maxTileSize, std::max(tileSize.width(),
151-
tileSize.height()));
158+
maxTileSize = std::max(maxTileSize, std::max(tileSize.width(),
159+
tileSize.height()));
160+
}
152161

153162
const QPoint offset = tileset->tileOffset();
154163
offsetMargins = maxMargins(QMargins(-offset.x(),

src/libtiled/mapreader.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,14 +390,15 @@ SharedTileset MapReaderPrivate::readTileset()
390390
const QString className = atts.value(QLatin1String("class")).toString();
391391
const int tileSpacing = atts.value(QLatin1String("spacing")).toInt();
392392
const int margin = atts.value(QLatin1String("margin")).toInt();
393+
const bool atlas = atts.value(QLatin1String("atlas")).toInt();
393394
const int columns = atts.value(QLatin1String("columns")).toInt();
394395
const QString backgroundColor = atts.value(QLatin1String("backgroundcolor")).toString();
395396
const QString alignment = atts.value(QLatin1String("objectalignment")).toString();
396397
const QString tileRenderSize = atts.value(QLatin1String("tilerendersize")).toString();
397398
const QString fillMode = atts.value(QLatin1String("fillmode")).toString();
398399

399400
tileset = Tileset::create(name, tileWidth, tileHeight,
400-
tileSpacing, margin);
401+
tileSpacing, margin, atlas);
401402

402403
tileset->setClassName(className);
403404
tileset->setColumnCount(columns);

src/libtiled/maptovariantconverter.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ QVariant MapToVariantConverter::toVariant(const Tileset &tileset,
185185
tilesetVariant[QStringLiteral("name")] = tileset.name();
186186
if (!tileset.className().isEmpty())
187187
tilesetVariant[QStringLiteral("class")] = tileset.className();
188+
tilesetVariant[QStringLiteral("atlas")] = tileset.isAtlas();
188189
tilesetVariant[QStringLiteral("tilewidth")] = tileset.tileWidth();
189190
tilesetVariant[QStringLiteral("tileheight")] = tileset.tileHeight();
190191
tilesetVariant[QStringLiteral("spacing")] = tileset.tileSpacing();
@@ -302,14 +303,13 @@ QVariant MapToVariantConverter::toVariant(const Tileset &tileset,
302303
tileVariant[QStringLiteral("imagewidth")] = imageSize.width();
303304
tileVariant[QStringLiteral("imageheight")] = imageSize.height();
304305
}
305-
306-
const QRect &imageRect = tile->imageRect();
307-
if (!imageRect.isNull() && imageRect != tile->image().rect() && tileset.isCollection()) {
308-
tileVariant[QStringLiteral("x")] = imageRect.x();
309-
tileVariant[QStringLiteral("y")] = imageRect.y();
310-
tileVariant[QStringLiteral("width")] = imageRect.width();
311-
tileVariant[QStringLiteral("height")] = imageRect.height();
312-
}
306+
}
307+
const QRect &imageRect = tile->imageRect();
308+
if (!imageRect.isNull() && imageRect != tile->image().rect() && (tileset.isCollection() || tileset.isAtlas())) {
309+
tileVariant[QStringLiteral("x")] = imageRect.x();
310+
tileVariant[QStringLiteral("y")] = imageRect.y();
311+
tileVariant[QStringLiteral("width")] = imageRect.width();
312+
tileVariant[QStringLiteral("height")] = imageRect.height();
313313
}
314314
if (tile->objectGroup())
315315
tileVariant[QStringLiteral("objectgroup")] = toVariant(*tile->objectGroup());

src/libtiled/mapwriter.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset &tileset,
337337
if (margin != 0)
338338
w.writeAttribute(QStringLiteral("margin"), QString::number(margin));
339339

340+
if (tileset.isAtlas())
341+
w.writeAttribute(QStringLiteral("atlas"), QString::number(tileset.isAtlas()));
342+
340343
w.writeAttribute(QStringLiteral("tilecount"),
341344
QString::number(tileset.tileCount()));
342345
w.writeAttribute(QStringLiteral("columns"),
@@ -413,15 +416,16 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset &tileset,
413416
QSize(tileset.imageWidth(), tileset.imageHeight()));
414417

415418
const bool isCollection = tileset.isCollection();
416-
const bool includeAllTiles = isCollection || tileset.anyTileOutOfOrder();
419+
const bool isAtlas = tileset.isAtlas();
420+
const bool includeAllTiles = isCollection || isAtlas || tileset.anyTileOutOfOrder();
417421

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

423427
const QRect &imageRect = tile->imageRect();
424-
if (!imageRect.isNull() && imageRect != tile->image().rect() && isCollection) {
428+
if (!imageRect.isNull() && imageRect != tile->image().rect() && (isCollection || isAtlas)) {
425429
w.writeAttribute(QStringLiteral("x"),
426430
QString::number(imageRect.x()));
427431
w.writeAttribute(QStringLiteral("y"),

src/libtiled/tileset.cpp

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,15 @@
3838
namespace Tiled {
3939

4040
Tileset::Tileset(QString name, int tileWidth, int tileHeight,
41-
int tileSpacing, int margin)
41+
int tileSpacing, int margin, bool isAtlas)
4242
: Object(TilesetType)
4343
, mName(std::move(name))
4444
, mTileWidth(tileWidth)
4545
, mTileHeight(tileHeight)
4646
, mTileSpacing(tileSpacing)
4747
, mMargin(margin)
4848
, mGridSize(tileWidth, tileHeight)
49+
, mAtlas(isAtlas)
4950
{
5051
Q_ASSERT(tileSpacing >= 0);
5152
Q_ASSERT(margin >= 0);
@@ -245,48 +246,58 @@ bool Tileset::loadImage()
245246
return initializeTilesetTiles();
246247
}
247248

248-
bool Tileset::initializeTilesetTiles()
249+
bool Tileset::initializeTilesetTiles(bool forceGeneration)
249250
{
250251
if (mImage.isNull() || mTileWidth <= 0 || mTileHeight <= 0)
251252
return false;
252253

253254
if (mImageReference.transparentColor.isValid())
254255
mImage.setMask(mImage.createMaskFromColor(mImageReference.transparentColor));
255256

256-
QVector<QRect> tileRects;
257-
258-
for (int y = mMargin; y <= mImage.height() - mTileHeight; y += mTileHeight + mTileSpacing)
259-
for (int x = mMargin; x <= mImage.width() - mTileWidth; x += mTileWidth + mTileSpacing)
260-
tileRects.append(QRect(x, y, mTileWidth, mTileHeight));
261-
262-
for (int tileNum = 0; tileNum < tileRects.size(); ++tileNum) {
263-
auto it = mTilesById.find(tileNum);
264-
if (it != mTilesById.end()) {
265-
it.value()->setImage(QPixmap()); // make sure it uses the tileset's image
266-
it.value()->setImageRect(tileRects.at(tileNum));
267-
} else {
268-
auto tile = new Tile(tileNum, this);
269-
tile->setImageRect(tileRects.at(tileNum));
270-
mTilesById.insert(tileNum, tile);
271-
mTiles.insert(tileNum, tile);
272-
}
257+
bool needsRectGeneration = true;
258+
if (isAtlas()) {
259+
needsRectGeneration = forceGeneration;
273260
}
274261

275-
QPixmap blank;
262+
if (needsRectGeneration) {
263+
QVector<QRect> tileRects;
264+
265+
for (int y = mMargin; y <= mImage.height() - mTileHeight; y += mTileHeight + mTileSpacing)
266+
for (int x = mMargin; x <= mImage.width() - mTileWidth; x += mTileWidth + mTileSpacing)
267+
tileRects.append(QRect(x, y, mTileWidth, mTileHeight));
268+
269+
for (int tileNum = 0; tileNum < tileRects.size(); ++tileNum) {
270+
auto it = mTilesById.find(tileNum);
271+
if (it != mTilesById.end()) {
272+
it.value()->setImage(QPixmap()); // make sure it uses the tileset's image
273+
it.value()->setImageRect(tileRects.at(tileNum));
274+
} else {
275+
auto tile = new Tile(tileNum, this);
276+
tile->setImageRect(tileRects.at(tileNum));
277+
mTilesById.insert(tileNum, tile);
278+
mTiles.insert(tileNum, tile);
279+
}
280+
}
276281

277-
// Blank out any remaining tiles to avoid confusion (todo: could be more clear)
278-
for (Tile *tile : std::as_const(mTiles)) {
279-
if (tile->id() >= tileRects.size()) {
280-
if (blank.isNull()) {
281-
blank = QPixmap(mTileWidth, mTileHeight);
282-
blank.fill();
282+
QPixmap blank;
283+
284+
// Blank out any remaining tiles to avoid confusion (todo: could be more clear)
285+
if (!isAtlas()) {
286+
for (Tile *tile : std::as_const(mTiles)) {
287+
if (tile->id() >= tileRects.size()) {
288+
if (blank.isNull()) {
289+
blank = QPixmap(mTileWidth, mTileHeight);
290+
blank.fill();
291+
}
292+
tile->setImage(blank);
293+
tile->setImageRect(QRect(0, 0, mTileWidth, mTileHeight));
294+
}
283295
}
284-
tile->setImage(blank);
285-
tile->setImageRect(QRect(0, 0, mTileWidth, mTileHeight));
286296
}
287297
}
288298

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

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

555566
void Tileset::maybeUpdateTileSize(QSize previousTileSize, QSize newTileSize)
556567
{
568+
if (isAtlas())
569+
return;
570+
557571
if (previousTileSize == newTileSize)
558572
return;
559573

@@ -681,6 +695,9 @@ SharedTileset Tileset::clone() const
681695
*/
682696
void Tileset::updateTileSize()
683697
{
698+
if (isAtlas())
699+
return;
700+
684701
int maxWidth = 0;
685702
int maxHeight = 0;
686703
for (Tile *tile : std::as_const(mTiles)) {

src/libtiled/tileset.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class TILEDSHARED_EXPORT Tileset : public Object, public QEnableSharedFromThis<T
120120
* pointer is initialized, which enables the sharedPointer() function.
121121
*/
122122
Tileset(QString name, int tileWidth, int tileHeight,
123-
int tileSpacing = 0, int margin = 0);
123+
int tileSpacing = 0, int margin = 0, bool isAtlas = false);
124124

125125
public:
126126
QString exportFileName;
@@ -138,6 +138,9 @@ class TILEDSHARED_EXPORT Tileset : public Object, public QEnableSharedFromThis<T
138138
void setFormat(const QString &format);
139139
QString format() const;
140140

141+
bool isAtlas() const { return mAtlas; }
142+
void setAtlas(bool atlas) { mAtlas = atlas; }
143+
141144
int tileWidth() const;
142145
int tileHeight() const;
143146

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

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

@@ -311,6 +314,7 @@ class TILEDSHARED_EXPORT Tileset : public Object, public QEnableSharedFromThis<T
311314
TileRenderSize mTileRenderSize = TileSize;
312315
FillMode mFillMode = Stretch;
313316
QSize mGridSize;
317+
bool mAtlas;
314318
int mColumnCount = 0;
315319
int mExpectedColumnCount = 0;
316320
int mExpectedRowCount = 0;

src/libtiled/varianttomapconverter.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ SharedTileset VariantToMapConverter::toTileset(const QVariant &variant)
220220
}
221221

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

245246
SharedTileset tileset(Tileset::create(name,
246247
tileWidth, tileHeight,
247-
spacing, margin));
248+
spacing, margin, atlas));
248249

249250
tileset->setClassName(className);
250251
tileset->setObjectAlignment(alignmentFromString(objectAlignment));

src/tiled/documentmanager.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1372,7 +1372,7 @@ void DocumentManager::onWorldUnloaded(WorldDocument *worldDocument)
13721372

13731373
static bool mayNeedColumnCountAdjustment(const Tileset &tileset)
13741374
{
1375-
if (tileset.isCollection())
1375+
if (tileset.isCollection() || tileset.isAtlas())
13761376
return false;
13771377
if (tileset.imageStatus() != LoadingReady)
13781378
return false;

0 commit comments

Comments
 (0)