Skip to content

Commit accee95

Browse files
devnewtonbjorndevnewton
authored
Added support for sub-images in image collection tilesets (mapeditor#3339)
The new 'forest' example demonstrates the use of sub-images. Co-authored-by: Thorbjørn Lindeijer <[email protected]> Co-authored-by: devnewton <[email protected]>
1 parent 51fc8c6 commit accee95

36 files changed

+330
-91
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
### Unreleased
22

33
* Ignore transparent pixels when selecting tile objects (#1477)
4+
* Added support for sub-images in image collection tilesets (#1008)
45
* Added %worldfile variable for custom commands (by Pixel-Nori, #3352)
56
* Scripting: Added -e,--evaluate to run a script from command-line
67
* Scripting: Added Tool.toolBarActions property (#3318)

docs/reference/json-map-format.rst

+12-1
Original file line numberDiff line numberDiff line change
@@ -548,9 +548,13 @@ Tile (Definition)
548548

549549
animation, array, "Array of :ref:`Frames <json-frame>`"
550550
id, int, "Local ID of the tile"
551-
image, string, "Image representing this tile (optional)"
551+
image, string, "Image representing this tile (optional, used for image collection tilesets)"
552552
imageheight, int, "Height of the tile image in pixels"
553553
imagewidth, int, "Width of the tile image in pixels"
554+
x, int, "The X position of the sub-rectangle representing this tile (default: 0)"
555+
y, int, "The Y position of the sub-rectangle representing this tile (default: 0)"
556+
width, int, "The width of the sub-rectangle representing this tile (defaults to the image width)"
557+
height, int, "The height of the sub-rectangle representing this tile (defaults to the image height)"
554558
objectgroup, :ref:`json-layer`, "Layer with type ``objectgroup``, when collision shapes are specified (optional)"
555559
probability, double, "Percentage chance this tile is chosen when competing with others in the editor (optional)"
556560
properties, array, "Array of :ref:`Properties <json-property>`"
@@ -725,6 +729,13 @@ A point on a polygon or a polyline, relative to the position of the object.
725729
Changelog
726730
---------
727731

732+
Tiled 1.9
733+
~~~~~~~~~
734+
735+
* Added ``x``, ``y``, ``width`` and ``height`` properties to :ref:`json-tile`,
736+
which store the sub-rectangle of a tile's image used to represent this tile.
737+
By default the entire image is used.
738+
728739
Tiled 1.8
729740
~~~~~~~~~
730741

docs/reference/tmx-changelog.rst

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ TMX Changelog
44
Below are described the changes/additions that were made to the
55
:doc:`tmx-map-format` for recent versions of Tiled.
66

7+
Tiled 1.9
8+
---------
9+
10+
- Added ``x``, ``y``, ``width`` and ``height`` attributes to the
11+
:ref:`tmx-tileset-tile` element, which store the sub-rectangle of a tile's
12+
image used to represent this tile. By default the entire image is used.
13+
714
Tiled 1.8
815
---------
916

docs/reference/tmx-map-format.rst

+4
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ tiles (e.g. to extend a Wang set by transforming existing tiles).
297297
- **probability:** A percentage indicating the probability that this
298298
tile is chosen when it competes with others while editing with the
299299
terrain tool. (defaults to 0)
300+
- **x:** The X position of the sub-rectangle representing this tile (default: 0)
301+
- **y:** The Y position of the sub-rectangle representing this tile (default: 0)
302+
- **width:** The width of the sub-rectangle representing this tile (defaults to the image width)
303+
- **height:** The height of the sub-rectangle representing this tile (defaults to the image height)
300304

301305
Can contain at most one: :ref:`tmx-properties`, :ref:`tmx-image` (since
302306
0.9), :ref:`tmx-objectgroup`, :ref:`tmx-animation`

examples/forest/forest.tmx

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<map version="1.8" tiledversion="1.8.4" orientation="orthogonal" renderorder="right-down" width="40" height="16" tilewidth="16" tileheight="16" infinite="0" parallaxoriginx="320" parallaxoriginy="128" nextlayerid="11" nextobjectid="42">
3+
<tileset firstgid="1" source="forest.tsx"/>
4+
<objectgroup id="7" name="bg0" parallaxx="0.12" parallaxy="0.12">
5+
<object id="35" gid="7" x="0" y="176" width="160" height="208"/>
6+
<object id="36" gid="7" x="160" y="176" width="160" height="208"/>
7+
<object id="37" gid="7" x="320" y="176" width="160" height="208"/>
8+
<object id="38" gid="7" x="480" y="176" width="160" height="208"/>
9+
</objectgroup>
10+
<objectgroup id="9" name="bg1" parallaxx="0.25" parallaxy="0.25">
11+
<object id="27" gid="9" x="0" y="192" width="160" height="112"/>
12+
<object id="28" gid="9" x="160" y="192" width="160" height="112"/>
13+
<object id="29" gid="9" x="320" y="192" width="160" height="112"/>
14+
<object id="30" gid="9" x="480" y="192" width="160" height="112"/>
15+
</objectgroup>
16+
<objectgroup id="8" name="bg2" parallaxx="0.5" parallaxy="0.5">
17+
<object id="21" gid="10" x="0" y="256" width="160" height="112"/>
18+
<object id="22" gid="10" x="160" y="256" width="160" height="112"/>
19+
<object id="23" gid="10" x="320" y="256" width="160" height="112"/>
20+
<object id="24" gid="10" x="480" y="256" width="160" height="112"/>
21+
</objectgroup>
22+
<layer id="1" name="platforms" width="40" height="16">
23+
<data encoding="csv">
24+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
25+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
26+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
27+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
28+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
29+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
30+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
31+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
32+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
33+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
34+
0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
35+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
36+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
37+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,
38+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
39+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
40+
</data>
41+
</layer>
42+
<objectgroup id="10" name="characters">
43+
<object id="39" gid="14" x="192" y="160" width="25" height="25"/>
44+
</objectgroup>
45+
</map>

examples/forest/forest.tsx

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<tileset version="1.8" tiledversion="1.8.4" name="forest" tilewidth="160" tileheight="208" tilecount="7" columns="0">
3+
<grid orientation="orthogonal" width="1" height="1"/>
4+
<tile id="0" x="1" y="1" width="16" height="16">
5+
<image width="1024" height="1024" source="squirrel.png"/>
6+
</tile>
7+
<tile id="6" x="521" y="114" width="160" height="208">
8+
<image width="1024" height="1024" source="squirrel.png"/>
9+
</tile>
10+
<tile id="8" x="682" y="1" width="160" height="112">
11+
<image width="1024" height="1024" source="squirrel.png"/>
12+
</tile>
13+
<tile id="9" x="521" y="1" width="160" height="112">
14+
<image width="1024" height="1024" source="squirrel.png"/>
15+
</tile>
16+
<tile id="10" x="116" y="824" width="25" height="25">
17+
<image width="1024" height="1024" source="squirrel.png"/>
18+
</tile>
19+
<tile id="11" x="116" y="850" width="25" height="25">
20+
<image width="1024" height="1024" source="squirrel.png"/>
21+
</tile>
22+
<tile id="13" x="116" y="824" width="25" height="25">
23+
<image width="1024" height="1024" source="squirrel.png"/>
24+
<animation>
25+
<frame tileid="10" duration="150"/>
26+
<frame tileid="11" duration="150"/>
27+
</animation>
28+
</tile>
29+
</tileset>

examples/forest/squirrel.license

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Authors Luis Zuno @ansimuz;
2+
Licence Public domain;
3+
Url https://opengameart.org/content/sunnyland-woods;

examples/forest/squirrel.png

48.7 KB
Loading

src/libtiled/imagereference.cpp

+5-4
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ bool ImageReference::hasImage() const
3232

3333
QPixmap ImageReference::create() const
3434
{
35+
QPixmap pixmap;
3536
if (source.isLocalFile())
36-
return ImageCache::loadPixmap(source.toLocalFile());
37+
pixmap = ImageCache::loadPixmap(source.toLocalFile());
3738
else if (source.scheme() == QLatin1String("qrc"))
38-
return ImageCache::loadPixmap(QLatin1Char(':') + source.path());
39+
pixmap = ImageCache::loadPixmap(QLatin1Char(':') + source.path());
3940
else if (!data.isEmpty())
40-
return QPixmap::fromImage(QImage::fromData(data, format));
41+
pixmap = QPixmap::fromImage(QImage::fromData(data, format));
4142

42-
return QPixmap();
43+
return pixmap;
4344
}
4445

4546
} // namespace Tiled

src/libtiled/mapreader.cpp

+5-1
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,11 @@ void MapReaderPrivate::readTilesetTile(Tileset &tileset)
539539
if (imageReference.source.isEmpty())
540540
xml.raiseError(tr("Error reading embedded image for tile %1").arg(id));
541541
}
542-
tileset.setTileImage(tile, image, imageReference.source);
542+
const QRect imageRect(atts.value(QLatin1String("x")).toInt(),
543+
atts.value(QLatin1String("y")).toInt(),
544+
atts.value(QLatin1String("width")).toInt(),
545+
atts.value(QLatin1String("height")).toInt());
546+
tileset.setTileImage(tile, image, imageReference.source, imageRect);
543547
}
544548
} else if (xml.name() == QLatin1String("objectgroup")) {
545549
std::unique_ptr<ObjectGroup> objectGroup = readObjectGroup();

src/libtiled/maprenderer.cpp

+15-14
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,11 @@ void CellRenderer::render(const Cell &cell, const QPointF &screenPos, const QSiz
378378
flush();
379379

380380
const QPixmap &image = tile->image();
381-
const QSizeF imageSize = image.size();
382-
if (imageSize.isEmpty())
381+
const QRect imageRect = tile->imageRect().isNull() ? image.rect()
382+
: tile->imageRect();
383+
if (imageRect.isEmpty())
383384
return;
384385

385-
const QSizeF scale(size.width() / imageSize.width(), size.height() / imageSize.height());
386386
const QPoint offset = tile->offset();
387387
const QPointF sizeHalf = QPointF(size.width() / 2, size.height() / 2);
388388

@@ -391,14 +391,14 @@ void CellRenderer::render(const Cell &cell, const QPointF &screenPos, const QSiz
391391

392392
QPainter::PixmapFragment fragment;
393393
// Calculate the position as if the origin is TopLeft, and correct it later.
394-
fragment.x = screenPos.x() + (offset.x() * scale.width()) + sizeHalf.x();
395-
fragment.y = screenPos.y() + (offset.y() * scale.height()) + sizeHalf.y();
396-
fragment.sourceLeft = 0;
397-
fragment.sourceTop = 0;
398-
fragment.width = imageSize.width();
399-
fragment.height = imageSize.height();
400-
fragment.scaleX = flippedHorizontally ? -1 : 1;
401-
fragment.scaleY = flippedVertically ? -1 : 1;
394+
fragment.scaleX = size.width() / imageRect.width();
395+
fragment.scaleY = size.height() / imageRect.height();
396+
fragment.x = screenPos.x() + (offset.x() * fragment.scaleX) + sizeHalf.x();
397+
fragment.y = screenPos.y() + (offset.y() * fragment.scaleY) + sizeHalf.y();
398+
fragment.sourceLeft = imageRect.x();
399+
fragment.sourceTop = imageRect.y();
400+
fragment.width = imageRect.width();
401+
fragment.height = imageRect.height();
402402
fragment.rotation = 0;
403403
fragment.opacity = 1;
404404

@@ -426,8 +426,8 @@ void CellRenderer::render(const Cell &cell, const QPointF &screenPos, const QSiz
426426
fragment.x += halfDiff;
427427
}
428428

429-
fragment.scaleX = scale.width() * (flippedHorizontally ? -1 : 1);
430-
fragment.scaleY = scale.height() * (flippedVertically ? -1 : 1);
429+
fragment.scaleX *= flippedHorizontally ? -1 : 1;
430+
fragment.scaleY *= flippedVertically ? -1 : 1;
431431

432432
if (mIsOpenGL || (fragment.scaleX > 0 && fragment.scaleY > 0)) {
433433
mTile = tile;
@@ -448,7 +448,8 @@ void CellRenderer::render(const Cell &cell, const QPointF &screenPos, const QSiz
448448

449449
const QRectF target(fragment.width * -0.5, fragment.height * -0.5,
450450
fragment.width, fragment.height);
451-
const QRectF source(0, 0, fragment.width, fragment.height);
451+
const QRectF source(fragment.sourceLeft, fragment.sourceTop,
452+
fragment.width, fragment.height);
452453

453454
mPainter->setTransform(transform);
454455
mPainter->drawPixmap(target, tinted(image, mTintColor), source);

src/libtiled/maptovariantconverter.cpp

+13-4
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,19 @@ QVariant MapToVariantConverter::toVariant(const Tileset &tileset,
282282
const QString rel = toFileReference(tile->imageSource(), mDir);
283283
tileVariant[QStringLiteral("image")] = rel;
284284

285-
const QSize tileSize = tile->size();
286-
if (!tileSize.isNull()) {
287-
tileVariant[QStringLiteral("imagewidth")] = tileSize.width();
288-
tileVariant[QStringLiteral("imageheight")] = tileSize.height();
285+
const QSize imageSize = tile->image().isNull() ? tile->size()
286+
: tile->image().size();
287+
if (!imageSize.isNull()) {
288+
tileVariant[QStringLiteral("imagewidth")] = imageSize.width();
289+
tileVariant[QStringLiteral("imageheight")] = imageSize.height();
290+
}
291+
292+
const QRect &imageRect = tile->imageRect();
293+
if (!imageRect.isNull() && imageRect != tile->image().rect()) {
294+
tileVariant[QStringLiteral("x")] = imageRect.x();
295+
tileVariant[QStringLiteral("y")] = imageRect.y();
296+
tileVariant[QStringLiteral("width")] = imageRect.width();
297+
tileVariant[QStringLiteral("height")] = imageRect.height();
289298
}
290299
}
291300
if (tile->objectGroup())

src/libtiled/mapwriter.cpp

+17-4
Original file line numberDiff line numberDiff line change
@@ -423,14 +423,27 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset &tileset,
423423
if (!tile->properties().isEmpty())
424424
writeProperties(w, tile->properties());
425425
if (imageSource.isEmpty()) {
426+
const QRect &imageRect = tile->imageRect();
427+
if (!imageRect.isNull() && imageRect != tile->image().rect()) {
428+
w.writeAttribute(QStringLiteral("x"),
429+
QString::number(imageRect.x()));
430+
w.writeAttribute(QStringLiteral("y"),
431+
QString::number(imageRect.y()));
432+
w.writeAttribute(QStringLiteral("width"),
433+
QString::number(imageRect.width()));
434+
w.writeAttribute(QStringLiteral("height"),
435+
QString::number(imageRect.height()));
436+
}
437+
426438
w.writeStartElement(QStringLiteral("image"));
427439

428-
const QSize tileSize = tile->size();
429-
if (!tileSize.isNull()) {
440+
const QSize imageSize = tile->image().isNull() ? tile->size()
441+
: tile->image().size();
442+
if (!imageSize.isNull()) {
430443
w.writeAttribute(QStringLiteral("width"),
431-
QString::number(tileSize.width()));
444+
QString::number(imageSize.width()));
432445
w.writeAttribute(QStringLiteral("height"),
433-
QString::number(tileSize.height()));
446+
QString::number(imageSize.height()));
434447
}
435448

436449
if (tile->imageSource().isEmpty()) {

src/libtiled/tile.cpp

+27
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,27 @@ const QPainterPath &Tile::imageShape() const
8484
// heavy-weight paths since it just uses rectangles.
8585
mImageShape.emplace().addRegion(mImage.mask());
8686
#endif
87+
88+
if (!mImageRect.isNull() && mImageRect != mImage.rect()) {
89+
QPainterPath rect;
90+
rect.addRect(mImageRect);
91+
mImageShape.value() &= rect;
92+
mImageShape->translate(-mImageRect.topLeft());
93+
}
8794
}
8895
return *mImageShape;
8996
}
9097

98+
/**
99+
* Sets the image of this tile.
100+
*/
101+
void Tile::setImage(const QPixmap &image)
102+
{
103+
mImage = image;
104+
mImageStatus = image.isNull() ? LoadingError : LoadingReady;
105+
mImageShape.reset();
106+
}
107+
91108
/**
92109
* Returns the tile to render when taking into account tile animations.
93110
*
@@ -103,6 +120,15 @@ const Tile *Tile::currentFrameTile() const
103120
return this;
104121
}
105122

123+
void Tile::setImageRect(const QRect &imageRect)
124+
{
125+
if (mImageRect == imageRect)
126+
return;
127+
128+
mImageRect = imageRect;
129+
mImageShape.reset();
130+
}
131+
106132
/**
107133
* Returns the drawing offset of the tile (in pixels).
108134
*/
@@ -197,6 +223,7 @@ Tile *Tile::clone(Tileset *tileset) const
197223
c->setProperties(properties());
198224

199225
c->mImageSource = mImageSource;
226+
c->mImageRect = mImageRect;
200227
c->mImageStatus = mImageStatus;
201228
c->mType = mType;
202229
c->mProbability = mProbability;

0 commit comments

Comments
 (0)