Skip to content

Commit

Permalink
Added support for rotating and flipping Wang tiles (#2912)
Browse files Browse the repository at this point in the history
All the possible variations are pre-computed, meaning performance
shouldn't generally suffer.

The options are added at the tileset level. Support for per-tile
overrides was evaluated as well and can be added later.

The previous mechanism for storing various flipped variations of tiles,
which was only partly supported and not at all by the UI is now removed.

Closes #2833

Co-authored-by: Thorbjørn Lindeijer <bjorn@lindeijer.nl>
  • Loading branch information
cpetig and bjorn committed Nov 25, 2020
1 parent 308553d commit e65120d
Show file tree
Hide file tree
Showing 29 changed files with 596 additions and 370 deletions.
29 changes: 23 additions & 6 deletions docs/reference/json-map-format.rst
Expand Up @@ -441,6 +441,7 @@ Tileset
tileoffset, :ref:`json-tileset-tileoffset`, "(optional)"
tiles, array, "Array of :ref:`Tiles <json-tile>` (optional)"
tilewidth, int, "Maximum width of tiles in this set"
transformations, :ref:`json-tileset-transformations`, "Allowed transformations (optional)"
transparentcolor, string, "Hex-formatted color (#RRGGBB) (optional)"
type, string, "``tileset`` (for tileset files, since 1.0)"
version, number, "The JSON format version"
Expand Down Expand Up @@ -483,6 +484,22 @@ See :ref:`tmx-tileoffset` in the TMX Map Format.
x, int, "Horizontal offset in pixels"
y, int, "Vertical offset in pixels (positive is down)"

.. _json-tileset-transformations:

Transformations
~~~~~~~~~~~~~~~

See :ref:`tmx-tileset-transformations` in the TMX Map Format.

.. csv-table::
:header: Field, Type, Description
:widths: 1, 1, 4

hflip, bool, "Tiles can be flipped horizontally"
vflip, bool, "Tiles can be flipped vertically"
rotate, bool, "Tiles can be rotated in 90-degree increments"
preferuntransformed, bool, "Whether untransformed tiles remain preferred, otherwise transformed tiles are used to produce more variations"

Tileset Example
~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -667,21 +684,15 @@ Wang Tile
:header: Field, Type, Description
:widths: 1, 1, 4

dflip, bool, "Tile is flipped diagonally (default: ``false``)"
hflip, bool, "Tile is flipped horizontally (default: ``false``)"
tileid, int, "Local ID of tile"
vflip, bool, "Tile is flipped vertically (default: ``false``)"
wangid, array, "Array of Wang color indexes (``uchar[8]``)"

Example:

.. code:: json

{
"dflip": false,
"hflip": false,
"tileid": 0,
"vflip": false,
"wangid": [2, 0, 1, 0, 1, 0, 2, 0]
}

Expand Down Expand Up @@ -739,6 +750,12 @@ Tiled 1.5

* :ref:`json-wangcolor` can now store ``properties``.

* Added ``transformations`` property to :ref:`json-tileset` (see
:ref:`json-tileset-transformations`).

* Removed ``dflip``, ``hflip`` and ``vflip`` properties from
:ref:`json-wangtile` (no longer supported).

Tiled 1.4
~~~~~~~~~

Expand Down
5 changes: 5 additions & 0 deletions docs/reference/tmx-changelog.rst
Expand Up @@ -17,6 +17,11 @@ Tiled 1.5
hex format. This is because the number of colors supported in a Wang set was
increased from 15 to 255.

- Valid transformations of tiles in a set (flipping, rotation) are specified
in a :ref:`tmx-tileset-transformations` element. The partial support for the
``vflip``, ``hflip`` and ``dflip`` attributes on the :ref:`tmx-wangtile`
element has been removed.

Tiled 1.4
---------

Expand Down
28 changes: 18 additions & 10 deletions docs/reference/tmx-map-format.rst
Expand Up @@ -169,7 +169,7 @@ an ``<image>`` tag.

Can contain at most one: :ref:`tmx-image`, :ref:`tmx-tileoffset`,
:ref:`tmx-grid` (since 1.0), :ref:`tmx-properties`, :ref:`tmx-terraintypes`,
:ref:`tmx-wangsets` (since 1.1),
:ref:`tmx-wangsets` (since 1.1), :ref:`tmx-tileset-transformations` (since 1.5)

Can contain any number: :ref:`tmx-tileset-tile`

Expand Down Expand Up @@ -245,6 +245,20 @@ Can contain any number: :ref:`tmx-terrain`

Can contain at most one: :ref:`tmx-properties`

.. _tmx-tileset-transformations:

<transformations>
~~~~~~~~~~~~~~~~~

This element is used to describe which transformations can be applied to the
tiles (e.g. to extend a Wang set by transforming existing tiles).

- **hflip:** Whether the tiles in this set can be flipped horizontally (default 0)
- **vflip:** Whether the tiles in this set can be flipped vertically (default 0)
- **rotate:** Whether the tiles in this set can be rotated in 90 degree increments (default 0)
- **preferuntransformed:** Whether untransformed tiles remain preferred, otherwise
transformed tiles are used to produce more variations (default 0)

.. _tmx-tileset-tile:

<tile>
Expand Down Expand Up @@ -343,15 +357,9 @@ associating it with a certain Wang ID.
Wang ID was saved as a 32-bit unsigned integer stored in the format
``0xCECECECE`` (where each C is a corner color and each E is an edge color,
in reverse order)."
- **hflip:** Whether the tile is flipped horizontally. This only affects
the tile image, it does not change the meaning of the wangid. See
:ref:`Tile flipping <tmx-tile-flipping>` for more info. (defaults to false)
- **vflip:** Whether the tile is flipped vertically. This only affects
the tile image, it does not change the meaning of the wangid. See
:ref:`Tile flipping <tmx-tile-flipping>` for more info. (defaults to false)
- **dflip:** Whether the tile is flipped on its diagonal. This only affects
the tile image, it does not change the meaning of the wangid. See
:ref:`Tile flipping <tmx-tile-flipping>` for more info. (defaults to false)
- *hflip:* Whether the tile is flipped horizontally (removed in Tiled 1.5).
- *vflip:* Whether the tile is flipped vertically (removed in Tiled 1.5).
- *dflip:* Whether the tile is flipped on its diagonal (removed in Tiled 1.5).

.. _tmx-layer:

Expand Down
125 changes: 70 additions & 55 deletions src/libtiled/mapreader.cpp
Expand Up @@ -92,6 +92,7 @@ class MapReaderPrivate
void readTilesetEditorSettings(Tileset &tileset);
void readTilesetTile(Tileset &tileset);
void readTilesetGrid(Tileset &tileset);
void readTilesetTransformations(Tileset &tileset);
void readTilesetImage(Tileset &tileset);
void readTilesetTerrainTypes(Tileset &tileset);
void readTilesetWangSets(Tileset &tileset);
Expand Down Expand Up @@ -376,58 +377,62 @@ SharedTileset MapReaderPrivate::readTileset()
const QString name = atts.value(QLatin1String("name")).toString();
const int tileWidth = atts.value(QLatin1String("tilewidth")).toInt();
const int tileHeight = atts.value(QLatin1String("tileheight")).toInt();

if (tileWidth < 0 || tileHeight < 0
|| (firstGid == 0 && !mReadingExternalTileset)) {
xml.raiseError(tr("Invalid tileset parameters for tileset"
" '%1'").arg(name));
return {};
}

const int tileSpacing = atts.value(QLatin1String("spacing")).toInt();
const int margin = atts.value(QLatin1String("margin")).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();

if (tileWidth < 0 || tileHeight < 0
|| (firstGid == 0 && !mReadingExternalTileset)) {
xml.raiseError(tr("Invalid tileset parameters for tileset"
" '%1'").arg(name));
} else {
tileset = Tileset::create(name, tileWidth, tileHeight,
tileSpacing, margin);
tileset = Tileset::create(name, tileWidth, tileHeight,
tileSpacing, margin);

tileset->setColumnCount(columns);
tileset->setColumnCount(columns);

if (QColor::isValidColor(backgroundColor))
tileset->setBackgroundColor(QColor(backgroundColor));
if (QColor::isValidColor(backgroundColor))
tileset->setBackgroundColor(QColor(backgroundColor));

tileset->setObjectAlignment(alignmentFromString(alignment));
tileset->setObjectAlignment(alignmentFromString(alignment));

while (xml.readNextStartElement()) {
if (xml.name() == QLatin1String("editorsettings")) {
readTilesetEditorSettings(*tileset);
} else if (xml.name() == QLatin1String("tile")) {
readTilesetTile(*tileset);
} else if (xml.name() == QLatin1String("tileoffset")) {
const QXmlStreamAttributes oa = xml.attributes();
int x = oa.value(QLatin1String("x")).toInt();
int y = oa.value(QLatin1String("y")).toInt();
tileset->setTileOffset(QPoint(x, y));
xml.skipCurrentElement();
} else if (xml.name() == QLatin1String("grid")) {
readTilesetGrid(*tileset);
} else if (xml.name() == QLatin1String("properties")) {
tileset->mergeProperties(readProperties());
} else if (xml.name() == QLatin1String("image")) {
if (tileWidth == 0 || tileHeight == 0) {
xml.raiseError(tr("Invalid tileset parameters for tileset"
" '%1'").arg(name));
tileset.clear();
break;
} else {
readTilesetImage(*tileset);
}
} else if (xml.name() == QLatin1String("terraintypes")) {
readTilesetTerrainTypes(*tileset);
} else if (xml.name() == QLatin1String("wangsets")) {
readTilesetWangSets(*tileset);
while (xml.readNextStartElement()) {
if (xml.name() == QLatin1String("editorsettings")) {
readTilesetEditorSettings(*tileset);
} else if (xml.name() == QLatin1String("tile")) {
readTilesetTile(*tileset);
} else if (xml.name() == QLatin1String("tileoffset")) {
const QXmlStreamAttributes oa = xml.attributes();
int x = oa.value(QLatin1String("x")).toInt();
int y = oa.value(QLatin1String("y")).toInt();
tileset->setTileOffset(QPoint(x, y));
xml.skipCurrentElement();
} else if (xml.name() == QLatin1String("grid")) {
readTilesetGrid(*tileset);
} else if (xml.name() == QLatin1String("transformations")) {
readTilesetTransformations(*tileset);
} else if (xml.name() == QLatin1String("properties")) {
tileset->mergeProperties(readProperties());
} else if (xml.name() == QLatin1String("image")) {
if (tileWidth == 0 || tileHeight == 0) {
xml.raiseError(tr("Invalid tileset parameters for tileset"
" '%1'").arg(name));
tileset.clear();
break;
} else {
readUnknownElement();
readTilesetImage(*tileset);
}
} else if (xml.name() == QLatin1String("terraintypes")) {
readTilesetTerrainTypes(*tileset);
} else if (xml.name() == QLatin1String("wangsets")) {
readTilesetWangSets(*tileset);
} else {
readUnknownElement();
}
}
} else { // External tileset
Expand Down Expand Up @@ -504,7 +509,7 @@ void MapReaderPrivate::readTilesetTile(Tileset &tileset)
}

if (wangId)
tileset.wangSet(0)->addTile(tile, wangId);
tileset.wangSet(0)->setWangId(id, wangId);
}

// Read tile probability
Expand Down Expand Up @@ -589,6 +594,27 @@ void MapReaderPrivate::readTilesetGrid(Tileset &tileset)
xml.skipCurrentElement();
}

void MapReaderPrivate::readTilesetTransformations(Tileset &tileset)
{
Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("transformations"));

const QXmlStreamAttributes atts = xml.attributes();

Tileset::TransformationFlags transformations;
if (atts.value(QLatin1String("hflip")).toInt())
transformations |= Tileset::AllowFlipHorizontally;
if (atts.value(QLatin1String("vflip")).toInt())
transformations |= Tileset::AllowFlipVertically;
if (atts.value(QLatin1String("rotate")).toInt())
transformations |= Tileset::AllowRotate;
if (atts.value(QLatin1String("preferuntransformed")).toInt())
transformations |= Tileset::PreferUntransformed;

tileset.setTransformationFlags(transformations);

xml.skipCurrentElement();
}

void MapReaderPrivate::readTilesetImage(Tileset &tileset)
{
Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("image"));
Expand Down Expand Up @@ -713,9 +739,9 @@ void MapReaderPrivate::readTilesetWangSets(Tileset &tileset)
const QXmlStreamAttributes atts = xml.attributes();
const QString name = atts.value(QLatin1String("name")).toString();
const WangSet::Type type = wangSetTypeFromString(atts.value(QLatin1String("type")).toString());
const int tile = atts.value(QLatin1String("tile")).toInt();
const int tileId = atts.value(QLatin1String("tile")).toInt();

auto wangSet = std::make_unique<WangSet>(&tileset, name, type, tile);
auto wangSet = std::make_unique<WangSet>(&tileset, name, type, tileId);

// For backwards-compatibility
QVector<int> cornerColors;
Expand Down Expand Up @@ -761,18 +787,7 @@ void MapReaderPrivate::readTilesetWangSets(Tileset &tileset)
return;
}

const bool fH = tileAtts.value(QLatin1String("hflip")).toInt();
const bool fV = tileAtts.value(QLatin1String("vflip")).toInt();
const bool fA = tileAtts.value(QLatin1String("dflip")).toInt();

Tile *tile = tileset.findOrCreateTile(tileId);

WangTile wangTile(tile, wangId);
wangTile.setFlippedHorizontally(fH);
wangTile.setFlippedVertically(fV);
wangTile.setFlippedAntiDiagonally(fA);

wangSet->addWangTile(wangTile);
wangSet->setWangId(tileId, wangId);

xml.skipCurrentElement();
} else if (xml.name() == QLatin1String("wangcolor") || isCorner || isEdge) {
Expand Down
15 changes: 11 additions & 4 deletions src/libtiled/maptovariantconverter.cpp
Expand Up @@ -228,6 +228,16 @@ QVariant MapToVariantConverter::toVariant(const Tileset &tileset,
tilesetVariant[QStringLiteral("imageheight")] = tileset.imageHeight();
}

const auto transformationFlags = tileset.transformationFlags();
if (transformationFlags) {
tilesetVariant[QStringLiteral("transformations")] = QVariantMap {
{ QStringLiteral("hflip"), transformationFlags.testFlag(Tileset::AllowFlipHorizontally) },
{ QStringLiteral("vflip"), transformationFlags.testFlag(Tileset::AllowFlipVertically) },
{ QStringLiteral("rotate"), transformationFlags.testFlag(Tileset::AllowRotate) },
{ QStringLiteral("preferuntransformed"), transformationFlags.testFlag(Tileset::PreferUntransformed) },
};
}

// Write the properties, terrain, external image, object group and
// animation for those tiles that have them.

Expand Down Expand Up @@ -383,10 +393,7 @@ QVariant MapToVariantConverter::toVariant(const WangSet &wangSet) const
wangIdVariant.append(QVariant(wangTile.wangId().indexColor(i)));

wangTileVariant[QStringLiteral("wangid")] = wangIdVariant;
wangTileVariant[QStringLiteral("tileid")] = wangTile.tile()->id();
wangTileVariant[QStringLiteral("hflip")] = wangTile.flippedHorizontally();
wangTileVariant[QStringLiteral("vflip")] = wangTile.flippedVertically();
wangTileVariant[QStringLiteral("dflip")] = wangTile.flippedAntiDiagonally();
wangTileVariant[QStringLiteral("tileid")] = wangTile.tileId();

wangTileVariants.append(wangTileVariant);
}
Expand Down
26 changes: 15 additions & 11 deletions src/libtiled/mapwriter.cpp
Expand Up @@ -383,6 +383,20 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset &tileset,
w.writeEndElement();
}

const auto transformationFlags = tileset.transformationFlags();
if (transformationFlags) {
w.writeStartElement(QStringLiteral("transformations"));
w.writeAttribute(QStringLiteral("hflip"),
QString::number(transformationFlags.testFlag(Tileset::AllowFlipHorizontally)));
w.writeAttribute(QStringLiteral("vflip"),
QString::number(transformationFlags.testFlag(Tileset::AllowFlipVertically)));
w.writeAttribute(QStringLiteral("rotate"),
QString::number(transformationFlags.testFlag(Tileset::AllowRotate)));
w.writeAttribute(QStringLiteral("preferuntransformed"),
QString::number(transformationFlags.testFlag(Tileset::PreferUntransformed)));
w.writeEndElement();
}

// Write the tileset properties
writeProperties(w, tileset.properties());

Expand Down Expand Up @@ -521,18 +535,8 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset &tileset,
const auto wangTiles = ws->sortedWangTiles();
for (const WangTile &wangTile : wangTiles) {
w.writeStartElement(QStringLiteral("wangtile"));
w.writeAttribute(QStringLiteral("tileid"), QString::number(wangTile.tile()->id()));
w.writeAttribute(QStringLiteral("tileid"), QString::number(wangTile.tileId()));
w.writeAttribute(QStringLiteral("wangid"), wangTile.wangId().toString());

if (wangTile.flippedHorizontally())
w.writeAttribute(QStringLiteral("hflip"), QString::number(1));

if (wangTile.flippedVertically())
w.writeAttribute(QStringLiteral("vflip"), QString::number(1));

if (wangTile.flippedAntiDiagonally())
w.writeAttribute(QStringLiteral("dflip"), QString::number(1));

w.writeEndElement(); // </wangtile>
}

Expand Down

0 comments on commit e65120d

Please sign in to comment.