From e65120d759741fd4c3a0b50bc4dce1206851c110 Mon Sep 17 00:00:00 2001 From: Christof Petig <33882057+cpetig@users.noreply.github.com> Date: Wed, 25 Nov 2020 15:09:26 +0100 Subject: [PATCH] Added support for rotating and flipping Wang tiles (#2912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/reference/json-map-format.rst | 29 ++- docs/reference/tmx-changelog.rst | 5 + docs/reference/tmx-map-format.rst | 28 ++- src/libtiled/mapreader.cpp | 125 ++++++----- src/libtiled/maptovariantconverter.cpp | 15 +- src/libtiled/mapwriter.cpp | 26 ++- src/libtiled/tilelayer.cpp | 19 ++ src/libtiled/tilelayer.h | 2 + src/libtiled/tileset.cpp | 1 + src/libtiled/tileset.h | 25 +++ src/libtiled/varianttomapconverter.cpp | 35 +-- src/libtiled/wangset.cpp | 298 ++++++++++++++----------- src/libtiled/wangset.h | 103 +++------ src/plugins/lua/luaplugin.cpp | 8 +- src/tiled/adjusttileindexes.cpp | 30 ++- src/tiled/changetilewangid.cpp | 39 ++-- src/tiled/changetilewangid.h | 15 +- src/tiled/editablewangset.cpp | 2 +- src/tiled/propertybrowser.cpp | 57 +++++ src/tiled/propertybrowser.h | 6 +- src/tiled/tilesetchanges.cpp | 19 ++ src/tiled/tilesetchanges.h | 15 ++ src/tiled/tilesetdocument.cpp | 6 + src/tiled/tilesetdocument.h | 1 + src/tiled/variantpropertymanager.cpp | 10 +- src/tiled/variantpropertymanager.h | 4 + src/tiled/wangbrush.cpp | 4 +- src/tiled/wangfiller.cpp | 37 +-- src/tiled/wangfiller.h | 2 +- 29 files changed, 596 insertions(+), 370 deletions(-) diff --git a/docs/reference/json-map-format.rst b/docs/reference/json-map-format.rst index a75337b358..cc7d51ee92 100644 --- a/docs/reference/json-map-format.rst +++ b/docs/reference/json-map-format.rst @@ -441,6 +441,7 @@ Tileset tileoffset, :ref:`json-tileset-tileoffset`, "(optional)" tiles, array, "Array of :ref:`Tiles ` (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" @@ -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 ~~~~~~~~~~~~~~~ @@ -667,10 +684,7 @@ 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: @@ -678,10 +692,7 @@ Example: .. code:: json { - "dflip": false, - "hflip": false, "tileid": 0, - "vflip": false, "wangid": [2, 0, 1, 0, 1, 0, 2, 0] } @@ -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 ~~~~~~~~~ diff --git a/docs/reference/tmx-changelog.rst b/docs/reference/tmx-changelog.rst index 4a47621bb0..36e5ebedfc 100644 --- a/docs/reference/tmx-changelog.rst +++ b/docs/reference/tmx-changelog.rst @@ -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 --------- diff --git a/docs/reference/tmx-map-format.rst b/docs/reference/tmx-map-format.rst index 8bb0557c3d..7e9dcd790c 100644 --- a/docs/reference/tmx-map-format.rst +++ b/docs/reference/tmx-map-format.rst @@ -169,7 +169,7 @@ an ```` 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` @@ -245,6 +245,20 @@ Can contain any number: :ref:`tmx-terrain` Can contain at most one: :ref:`tmx-properties` +.. _tmx-tileset-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: @@ -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 ` 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 ` 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 ` 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: diff --git a/src/libtiled/mapreader.cpp b/src/libtiled/mapreader.cpp index 128c4a0aab..0650129ce1 100644 --- a/src/libtiled/mapreader.cpp +++ b/src/libtiled/mapreader.cpp @@ -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); @@ -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 @@ -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 @@ -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")); @@ -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(&tileset, name, type, tile); + auto wangSet = std::make_unique(&tileset, name, type, tileId); // For backwards-compatibility QVector cornerColors; @@ -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) { diff --git a/src/libtiled/maptovariantconverter.cpp b/src/libtiled/maptovariantconverter.cpp index c80212bb24..45a5b136e8 100644 --- a/src/libtiled/maptovariantconverter.cpp +++ b/src/libtiled/maptovariantconverter.cpp @@ -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. @@ -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); } diff --git a/src/libtiled/mapwriter.cpp b/src/libtiled/mapwriter.cpp index a1628b0283..bdf77a15ae 100644 --- a/src/libtiled/mapwriter.cpp +++ b/src/libtiled/mapwriter.cpp @@ -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()); @@ -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(); // } diff --git a/src/libtiled/tilelayer.cpp b/src/libtiled/tilelayer.cpp index 23772a6b80..ca1a314254 100644 --- a/src/libtiled/tilelayer.cpp +++ b/src/libtiled/tilelayer.cpp @@ -41,6 +41,25 @@ using namespace Tiled; Cell Cell::empty; +void Cell::rotate(RotateDirection direction) +{ + constexpr unsigned char rotateMasks[2][8] = { + { 3, 2, 7, 6, 1, 0, 5, 4 }, + { 5, 4, 1, 0, 7, 6, 3, 2 }, + }; + + unsigned char mask = + (flippedHorizontally() << 2) | + (flippedVertically() << 1) | + (flippedAntiDiagonally() << 0); + + mask = rotateMasks[direction][mask]; + + setFlippedHorizontally((mask & 4) != 0); + setFlippedVertically((mask & 2) != 0); + setFlippedAntiDiagonally((mask & 1) != 0); +} + QRegion Chunk::region(std::function condition) const { QRegion region; diff --git a/src/libtiled/tilelayer.h b/src/libtiled/tilelayer.h index ae06f9c408..897df803ff 100644 --- a/src/libtiled/tilelayer.h +++ b/src/libtiled/tilelayer.h @@ -120,6 +120,8 @@ class TILEDSHARED_EXPORT Cell void setFlippedAntiDiagonally(bool v) { v ? _flags |= FlippedAntiDiagonally : _flags &= ~FlippedAntiDiagonally; } void setRotatedHexagonal120(bool v) { v ? _flags |= RotatedHexagonal120 : _flags &= ~RotatedHexagonal120; } + void rotate(RotateDirection direction); + bool checked() const { return _flags & Checked; } void setChecked(bool checked) { checked ? _flags |= Checked : _flags &= ~Checked; } diff --git a/src/libtiled/tileset.cpp b/src/libtiled/tileset.cpp index 57c7dacab3..d6d12f741c 100644 --- a/src/libtiled/tileset.cpp +++ b/src/libtiled/tileset.cpp @@ -863,6 +863,7 @@ SharedTileset Tileset::clone() const c->mStatus = mStatus; c->mBackgroundColor = mBackgroundColor; c->mFormat = mFormat; + c->mTransformationFlags = mTransformationFlags; QMapIterator tileIterator(mTiles); while (tileIterator.hasNext()) { diff --git a/src/libtiled/tileset.h b/src/libtiled/tileset.h index d8a76969bc..5b1298208f 100644 --- a/src/libtiled/tileset.h +++ b/src/libtiled/tileset.h @@ -224,6 +224,18 @@ class TILEDSHARED_EXPORT Tileset : public Object LoadingStatus status() const; LoadingStatus imageStatus() const; + enum TransformationFlag { + NoTransformation = 0, + AllowFlipHorizontally = 1 << 0, + AllowFlipVertically = 1 << 1, + AllowRotate = 1 << 2, + PreferUntransformed = 1 << 3, + }; + Q_DECLARE_FLAGS(TransformationFlags, TransformationFlag) + + TransformationFlags transformationFlags() const; + void setTransformationFlags(TransformationFlags flags); + void swap(Tileset &other); SharedTileset clone() const; @@ -272,6 +284,7 @@ class TILEDSHARED_EXPORT Tileset : public Object LoadingStatus mStatus; QColor mBackgroundColor; QString mFormat; + TransformationFlags mTransformationFlags; QWeakPointer mWeakPointer; QWeakPointer mOriginalTileset; @@ -682,7 +695,19 @@ inline LoadingStatus Tileset::imageStatus() const return mImageReference.status; } +inline Tileset::TransformationFlags Tileset::transformationFlags() const +{ + return mTransformationFlags; +} + +inline void Tileset::setTransformationFlags(TransformationFlags flags) +{ + mTransformationFlags = flags; +} + } // namespace Tiled Q_DECLARE_METATYPE(Tiled::Tileset*) Q_DECLARE_METATYPE(Tiled::SharedTileset) + +Q_DECLARE_OPERATORS_FOR_FLAGS(Tiled::Tileset::TransformationFlags) diff --git a/src/libtiled/varianttomapconverter.cpp b/src/libtiled/varianttomapconverter.cpp index 452962b801..69fe5e8ac7 100644 --- a/src/libtiled/varianttomapconverter.cpp +++ b/src/libtiled/varianttomapconverter.cpp @@ -220,6 +220,7 @@ SharedTileset VariantToMapConverter::toTileset(const QVariant &variant) const int columns = variantMap[QStringLiteral("columns")].toInt(); const QString backgroundColor = variantMap[QStringLiteral("backgroundcolor")].toString(); const QString objectAlignment = variantMap[QStringLiteral("objectalignment")].toString(); + const QVariantMap transformations = variantMap[QStringLiteral("transformations")].toMap(); if (tileWidth <= 0 || tileHeight <= 0 || (firstGid == 0 && !mReadingExternalTileset)) { @@ -235,6 +236,21 @@ SharedTileset VariantToMapConverter::toTileset(const QVariant &variant) tileset->setTileOffset(QPoint(tileOffsetX, tileOffsetY)); tileset->setColumnCount(columns); + if (!transformations.isEmpty()) { + Tileset::TransformationFlags transformationFlags; + + if (transformations[QStringLiteral("hflip")].toBool()) + transformationFlags |= Tileset::AllowFlipHorizontally; + if (transformations[QStringLiteral("vflip")].toBool()) + transformationFlags |= Tileset::AllowFlipVertically; + if (transformations[QStringLiteral("rotate")].toBool()) + transformationFlags |= Tileset::AllowRotate; + if (transformations[QStringLiteral("preferuntransformed")].toBool()) + transformationFlags |= Tileset::PreferUntransformed; + + tileset->setTransformationFlags(transformationFlags); + } + readTilesetEditorSettings(*tileset, variantMap[QStringLiteral("editorsettings")].toMap()); if (!grid.isEmpty()) { @@ -312,7 +328,7 @@ SharedTileset VariantToMapConverter::toTileset(const QVariant &variant) } if (terrainWangSet->wangIdIsValid(wangId) && ok) - terrainWangSet->addTile(tile, wangId); + terrainWangSet->setWangId(tile->id(), wangId); } qreal probability = tileVar[QStringLiteral("probability")].toDouble(&ok); @@ -420,9 +436,9 @@ std::unique_ptr VariantToMapConverter::toWangSet(const QVariantMap &var { const QString name = variantMap[QStringLiteral("name")].toString(); const WangSet::Type type = wangSetTypeFromString(variantMap[QStringLiteral("type")].toString()); - const int tile = variantMap[QStringLiteral("tile")].toInt(); + const int tileId = variantMap[QStringLiteral("tile")].toInt(); - std::unique_ptr wangSet { new WangSet(tileset, name, type, tile) }; + std::unique_ptr wangSet { new WangSet(tileset, name, type, tileId) }; wangSet->setProperties(extractProperties(variantMap)); @@ -480,18 +496,7 @@ std::unique_ptr VariantToMapConverter::toWangSet(const QVariantMap &var return nullptr; } - const bool fH = wangTileVariantMap[QStringLiteral("hflip")].toBool(); - const bool fV = wangTileVariantMap[QStringLiteral("vflip")].toBool(); - const bool fA = wangTileVariantMap[QStringLiteral("dflip")].toBool(); - - 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); } diff --git a/src/libtiled/wangset.cpp b/src/libtiled/wangset.cpp index 83e69ea007..b180414883 100644 --- a/src/libtiled/wangset.cpp +++ b/src/libtiled/wangset.cpp @@ -36,22 +36,6 @@ namespace Tiled { -static unsigned cellToTileInfo(const Cell &cell) -{ - return cell.tileId() - | (cell.flippedHorizontally() << 29) - | (cell.flippedVertically() << 28) - | (cell.flippedAntiDiagonally() << 27); -} - -static unsigned wangTileToTileInfo(const WangTile &wangTile) -{ - return wangTile.tile()->id() - | (wangTile.flippedHorizontally() << 29) - | (wangTile.flippedVertically() << 28) - | (wangTile.flippedAntiDiagonally() << 27); -} - /** * These return the color of the edge of the WangId. * 0 being the top edge: @@ -263,6 +247,20 @@ WangId WangId::rotated(int rotations) const * Flips the wang Id horizontally. */ void WangId::flipHorizontally() +{ + *this = flippedHorizontally(); +} + +/** + * Flips the wang Id vertically. + */ +void WangId::flipVertically() +{ + flipHorizontally(); + rotate(2); +} + +WangId WangId::flippedHorizontally() const { WangId newWangId = mId; @@ -272,16 +270,14 @@ void WangId::flipHorizontally() for (int i = 0; i < NumCorners; ++i) newWangId.setCornerColor(i, cornerColor(NumCorners - 1 - i)); - mId = newWangId; + return newWangId; } -/** - * Flips the wang Id vertically. - */ -void WangId::flipVertically() +WangId WangId::flippedVertically() const { - flipHorizontally(); - rotate(2); + WangId newWangId = mId; + newWangId.flipVertically(); + return newWangId; } WangId::Index WangId::indexByGrid(int x, int y) @@ -364,72 +360,18 @@ QString WangId::toString() const } -// performs a translation (either flipping or rotating) based on a one to -// one map of size 8 (from 0 - 7) -void WangTile::translate(const int map[]) -{ - int mask = (mFlippedHorizontally << 2) - | (mFlippedVertically << 1) - | (mFlippedAntiDiagonally << 0); - - mask = map[mask]; - - mFlippedHorizontally = mask & 4; - mFlippedVertically = mask & 2; - mFlippedAntiDiagonally = mask & 1; -} - -void WangTile::rotateRight() -{ - const int map[] = { 5, 4, 1, 0, 7, 6, 3, 2 }; - mWangId.rotate(1); - - translate(map); -} - -void WangTile::rotateLeft() -{ - const int map[] = { 3, 2, 7, 6, 1, 0, 5, 4 }; - mWangId.rotate(3); - - translate(map); -} - -void WangTile::flipHorizontally() -{ - const int map[] = { 4, 3, 6, 1, 0, 7, 2, 5 }; - mWangId.flipHorizontally(); - - translate(map); -} - -void WangTile::flipVertically() -{ - const int map[] = { 2, 5, 0, 7, 6, 1, 4, 3 }; - mWangId.flipVertically(); - - translate(map); -} - -Cell WangTile::makeCell() const +QDebug operator<<(QDebug debug, WangId wangId) { - if (!mTile) - return Cell(); - - Cell cell(mTile); - cell.setFlippedHorizontally(mFlippedHorizontally); - cell.setFlippedVertically(mFlippedVertically); - cell.setFlippedAntiDiagonally(mFlippedAntiDiagonally); - - return cell; + QDebugStateSaver state(debug); + debug.nospace().noquote() << "WangId(" << wangId.toString() << ')'; + return debug; } -QDebug operator<<(QDebug debug, WangId wangId) +QDebug operator<<(QDebug debug, const WangTile &wangTile) { - const bool oldSetting = debug.autoInsertSpaces(); - debug.nospace() << "WangId(" << wangId.toString() << ')'; - debug.setAutoInsertSpaces(oldSetting); - return debug.maybeSpace(); + QDebugStateSaver state(debug); + debug.nospace() << "WangTile(" << wangTile.tileId() << ", " << wangTile.wangId() << ')'; + return debug; } @@ -556,53 +498,119 @@ void WangSet::removeWangColorAt(int color) } /** - * Adds a \a wangTile to the wang set. + * Associates the given \a wangId with the given \a tileId. * * If the given WangTile is already in the set with a different wangId, then * that reference is removed, and replaced with the new wangId. If the wangId - * provided is zero then the wangTile is removed if already in the set. Updates - * the UniqueFullWangIdCount. + * provided is zero then the wangTile is removed if already in the set. */ -void WangSet::addWangTile(const WangTile &wangTile) +void WangSet::setWangId(int tileId, WangId wangId) { - Q_ASSERT(wangTile.tile()->tileset() == mTileset); - Q_ASSERT(wangIdIsValid(wangTile.wangId())); + Q_ASSERT(wangIdIsValid(wangId)); - if (WangId previousWangId = mTileInfoToWangId.value(wangTileToTileInfo(wangTile))) { + if (WangId previousWangId = mTileIdToWangId.value(tileId)) { // return when the same tile is already part of this set with the same WangId - if (previousWangId == wangTile.wangId()) + if (previousWangId == wangId) return; - removeWangTile(wangTile); + removeTileId(tileId); } - if (wangTile.wangId() == 0) + if (wangId == 0) return; - if (!wangTile.wangId().hasWildCards() && !mWangIdToWangTile.contains(wangTile.wangId())) - ++mUniqueFullWangIdCount; - - mWangIdToWangTile.insert(wangTile.wangId(), wangTile); - mTileInfoToWangId.insert(wangTileToTileInfo(wangTile), wangTile.wangId()); + mTileIdToWangId.insert(tileId, wangId); + mColorDistancesDirty = true; + mCellsDirty = true; +} +void WangSet::removeTileId(int tileId) +{ + mTileIdToWangId.remove(tileId); mColorDistancesDirty = true; + mCellsDirty = true; +} + +const QVector &WangSet::wangIdsAndCells() const +{ + if (cellsDirty()) + const_cast(this)->recalculateCells(); + return mWangIdAndCells; } -void WangSet::removeWangTile(const WangTile &wangTile) +void WangSet::recalculateCells() { - WangId wangId = mTileInfoToWangId.take(wangTileToTileInfo(wangTile)); + mWangIdAndCells.clear(); + mCellsDirty = false; + mUniqueFullWangIdCount = 0; + + QSet addedWangIds; - WangTile w = wangTile; - w.setWangId(wangId); + // First insert all available tiles + QHashIterator it(mTileIdToWangId); + while (it.hasNext()) { + it.next(); + mUniqueFullWangIdCount += !it.value().hasWildCards() && !addedWangIds.contains(it.value()); + addedWangIds.insert(it.value()); + mWangIdAndCells.append({it.value(), Cell(mTileset, it.key())}); + } - mWangIdToWangTile.remove(wangId, w); + const auto transformationFlags = tileset()->transformationFlags(); + mLastSeenTranslationFlags = transformationFlags; - if (wangId - && !mWangIdToWangTile.contains(wangId) - && !wangId.hasWildCards()) - --mUniqueFullWangIdCount; + if (!(transformationFlags & ~Tileset::PreferUntransformed)) + return; - mColorDistancesDirty = true; + // Then insert variations based on flipping + it.toFront(); + while (it.hasNext()) { + it.next(); + + Cell cells[8] = { Cell(mTileset, it.key()) }; + WangId wangIds[8] = { it.value() }; + int count = 1; + const bool hasWildCards = it.value().hasWildCards(); + + // Add 90, 180 and 270 degree rotations if enabled + if (transformationFlags.testFlag(Tileset::AllowRotate)) { + for (int i = 0; i < 3; ++i) { + cells[count + i] = cells[i]; + cells[count + i].rotate(RotateRight); + wangIds[count + i] = wangIds[i].rotated(1); + } + + count = 4; + } + + if (transformationFlags.testFlag(Tileset::AllowFlipHorizontally)) { + for (int i = 0; i < count; ++i) { + cells[count + i] = cells[i]; + cells[count + i].setFlippedHorizontally(!cells[count + i].flippedHorizontally()); + wangIds[count + i] = wangIds[i].flippedHorizontally(); + } + + count *= 2; + } + + if (count <= 4 && transformationFlags.testFlag(Tileset::AllowFlipVertically)) { + for (int i = 0; i < count; ++i) { + cells[count + i] = cells[i]; + cells[count + i].setFlippedVertically(!cells[count + i].flippedVertically()); + wangIds[count + i] = wangIds[i].flippedVertically(); + } + + count *= 2; + } + + for (int i = 1; i < count; ++i) { + const bool exists = addedWangIds.contains(wangIds[i]); + if (transformationFlags.testFlag(Tileset::PreferUntransformed) && exists) + continue; + mUniqueFullWangIdCount += !hasWildCards && !exists; + addedWangIds.insert(wangIds[i]); + mWangIdAndCells.append({wangIds[i], cells[i]}); + } + } } /** @@ -621,20 +629,20 @@ void WangSet::recalculateColorDistances() QVector distance(colorCount() + 1, -1); // Check all tiles for transitions to other Wang colors - for (const WangTile &tile : qAsConst(mWangIdToWangTile)) { + for (const WangId wangId : qAsConst(mTileIdToWangId)) { // Don't consider edges and corners to be connected. This helps // avoid seeing transitions to "no color" for edge or corner // based sets. - if (tile.wangId().hasCornerWithColor(i)) { + if (wangId.hasCornerWithColor(i)) { for (int index = 0; index < 4; ++index) - distance[tile.wangId().cornerColor(index)] = 1; + distance[wangId.cornerColor(index)] = 1; } - if (tile.wangId().hasEdgeWithColor(i)) { + if (wangId.hasEdgeWithColor(i)) { for (int index = 0; index < 4; ++index) - distance[tile.wangId().edgeColor(index)] = 1; + distance[wangId.edgeColor(index)] = 1; } } @@ -693,12 +701,19 @@ void WangSet::recalculateColorDistances() } /** - * Returns a sorted list of the wangTiles in this set. - * Sorted by tileId. + * Returns a list of the wangTiles in this set, sorted by tileId. */ QList WangSet::sortedWangTiles() const { - QList wangTiles = mWangIdToWangTile.values(); + QList wangTiles; + wangTiles.reserve(mTileIdToWangId.size()); + + QHashIterator it(mTileIdToWangId); + while (it.hasNext()) { + it.next(); + wangTiles.append(WangTile(it.key(), it.value())); + } + std::stable_sort(wangTiles.begin(), wangTiles.end()); return wangTiles; } @@ -765,7 +780,7 @@ WangId WangSet::wangIdFromSurrounding(const Cell surroundingCells[]) const WangId WangSet::wangIdOfTile(const Tile *tile) const { Q_ASSERT(tile->tileset() == mTileset); - return mTileInfoToWangId.value(tile->id()); + return mTileIdToWangId.value(tile->id()); } /** @@ -776,27 +791,36 @@ WangId WangSet::wangIdOfTile(const Tile *tile) const */ WangId WangSet::wangIdOfCell(const Cell &cell) const { - if (cell.tileset() == mTileset) - return mTileInfoToWangId.value(cellToTileInfo(cell)); - return WangId(); + WangId wangId; + + if (cell.tileset() == mTileset) { + wangId = mTileIdToWangId.value(cell.tileId()); + + if (cell.flippedAntiDiagonally()) { + wangId.rotate(1); + wangId.flipHorizontally(); + } + if (cell.flippedHorizontally()) + wangId.flipHorizontally(); + if (cell.flippedVertically()) + wangId.flipVertically(); + } + + return wangId; } /** - * The probability of a given wang tile of being selected. + * The probability of a given WangId of being selected. */ -qreal WangSet::wangTileProbability(const WangTile &wangTile) const +qreal WangSet::wangIdProbability(WangId wangId) const { qreal probability = 1.0; - WangId wangId = wangTile.wangId(); for (int i = 0; i < WangId::NumIndexes; ++i) { if (int color = wangId.indexColor(i)) probability *= colorAt(color)->probability(); } - if (Tile *tile = wangTile.tile()) - probability *= tile->probability(); - return probability; } @@ -826,13 +850,10 @@ bool WangSet::wangIdIsValid(WangId wangId, int colorCount) */ bool WangSet::wangIdIsUsed(WangId wangId, WangId mask) const { - if (mask == WangId::FULL_MASK) - return mWangIdToWangTile.contains(wangId); - const quint64 maskedWangId = wangId & mask; - for (const WangTile &wangTile : mWangIdToWangTile) - if ((wangTile.wangId() & mask) == maskedWangId) + for (const auto &wangIdAndCell : wangIdsAndCells()) + if ((wangIdAndCell.wangId & mask) == maskedWangId) return true; return false; @@ -843,7 +864,7 @@ int WangSet::transitionPenalty(int colorA, int colorB) const if (mColorDistancesDirty) const_cast(this)->recalculateColorDistances(); - // Do some magic, since we don't have a transition array for no-terrain + // Do some magic, since we don't have a transition array for no-color if (colorA == 0 && colorB == 0) return 0; @@ -866,6 +887,9 @@ int WangSet::maximumColorDistance() const */ bool WangSet::isComplete() const { + if (cellsDirty()) + const_cast(this)->recalculateCells(); + return mUniqueFullWangIdCount == completeSetSize(); } @@ -943,10 +967,12 @@ WangSet *WangSet::clone(Tileset *tileset) const c->mUniqueFullWangIdCount = mUniqueFullWangIdCount; c->mColors = mColors; - c->mWangIdToWangTile = mWangIdToWangTile; - c->mTileInfoToWangId = mTileInfoToWangId; + c->mTileIdToWangId = mTileIdToWangId; + c->mWangIdAndCells = mWangIdAndCells; c->mMaximumColorDistance = mMaximumColorDistance; c->mColorDistancesDirty = mColorDistancesDirty; + c->mCellsDirty = mCellsDirty; + c->mLastSeenTranslationFlags = mLastSeenTranslationFlags; c->setProperties(properties()); // Avoid sharing Wang colors diff --git a/src/libtiled/wangset.h b/src/libtiled/wangset.h index a24f83d430..030dc734f7 100644 --- a/src/libtiled/wangset.h +++ b/src/libtiled/wangset.h @@ -108,6 +108,8 @@ class TILEDSHARED_EXPORT WangId WangId rotated(int rotations) const; void flipHorizontally(); void flipVertically(); + WangId flippedHorizontally() const; + WangId flippedVertically() const; static Index indexByGrid(int x, int y); static Index oppositeIndex(int index); @@ -149,70 +151,30 @@ TILEDSHARED_EXPORT QDebug operator<<(QDebug debug, WangId wangId); /** - * Class for holding info about rotation and flipping. + * Class for holding a tile and its WangId. */ class TILEDSHARED_EXPORT WangTile { public: - WangTile() : WangTile(nullptr, WangId()) + WangTile(int tileId, WangId wangId) + : mTileId(tileId) + , mWangId(wangId) {} - WangTile(Tile *tile, WangId wangId): - mTile(tile), - mWangId(wangId), - mFlippedHorizontally(false), - mFlippedVertically(false), - mFlippedAntiDiagonally(false) - {} - - WangTile(const Cell &cell, WangId wangId): - mTile(cell.tile()), - mWangId(wangId), - mFlippedHorizontally(cell.flippedHorizontally()), - mFlippedVertically(cell.flippedVertically()), - mFlippedAntiDiagonally(cell.flippedAntiDiagonally()) - {} - - Tile *tile() const { return mTile; } - + int tileId() const { return mTileId; } WangId wangId() const { return mWangId; } - void setWangId(WangId wangId) { mWangId = wangId; } - - bool flippedHorizontally() const { return mFlippedHorizontally; } - bool flippedVertically() const { return mFlippedVertically; } - bool flippedAntiDiagonally() const { return mFlippedAntiDiagonally; } - - void setFlippedHorizontally(bool b) { mFlippedHorizontally = b; } - void setFlippedVertically(bool b) { mFlippedVertically = b; } - void setFlippedAntiDiagonally(bool b) { mFlippedAntiDiagonally = b; } - - void rotateRight(); - void rotateLeft(); - void flipHorizontally(); - void flipVertically(); - - Cell makeCell() const; - - bool operator== (const WangTile &other) const - { return mTile == other.mTile - && mWangId == other.mWangId - && mFlippedHorizontally == other.mFlippedHorizontally - && mFlippedVertically == other.mFlippedVertically - && mFlippedAntiDiagonally == other.mFlippedAntiDiagonally; } bool operator< (const WangTile &other) const - { return mTile->id() < other.mTile->id(); } + { return mTileId < other.mTileId; } private: - void translate(const int map[]); - - Tile *mTile; + int mTileId; WangId mWangId; - bool mFlippedHorizontally; - bool mFlippedVertically; - bool mFlippedAntiDiagonally; }; +TILEDSHARED_EXPORT QDebug operator<<(QDebug debug, const WangTile &wangTile); + + class TILEDSHARED_EXPORT WangColor : public Object { public: @@ -301,11 +263,17 @@ class TILEDSHARED_EXPORT WangSet : public Object const QSharedPointer &colorAt(int index) const; const QVector> &colors() const { return mColors; } - void addTile(Tile *tile, WangId wangId); - void addCell(const Cell &cell, WangId wangId); - void addWangTile(const WangTile &wangTile); + void setWangId(int tileId, WangId wangId); + + const QHash &wangIdByTileId() const { return mTileIdToWangId; } + + struct WangIdAndCell + { + WangId wangId; + Cell cell; + }; - const QMultiHash &wangTilesByWangId() const { return mWangIdToWangTile; } + const QVector &wangIdsAndCells() const; QList sortedWangTiles() const; @@ -315,7 +283,7 @@ class TILEDSHARED_EXPORT WangSet : public Object WangId wangIdOfTile(const Tile *tile) const; WangId wangIdOfCell(const Cell &cell) const; - qreal wangTileProbability(const WangTile &wangTile) const; + qreal wangIdProbability(WangId wangId) const; bool wangIdIsValid(WangId wangId) const; @@ -335,8 +303,10 @@ class TILEDSHARED_EXPORT WangSet : public Object WangSet *clone(Tileset *tileset) const; private: - void removeWangTile(const WangTile &wangTile); + void removeTileId(int tileId); + bool cellsDirty() const; + void recalculateCells(); void recalculateColorDistances(); Tileset *mTileset; @@ -349,14 +319,14 @@ class TILEDSHARED_EXPORT WangSet : public Object quint64 mUniqueFullWangIdCount = 0; QVector> mColors; - QMultiHash mWangIdToWangTile; + QHash mTileIdToWangId; - // Tile info being the tileId, with the last three bits (32, 31, 30) - // being info on flip (horizontal, vertical, and antidiagonal) - QHash mTileInfoToWangId; + QVector mWangIdAndCells; int mMaximumColorDistance = 0; bool mColorDistancesDirty = true; + bool mCellsDirty = true; + Tileset::TransformationFlags mLastSeenTranslationFlags; }; @@ -421,19 +391,14 @@ inline const QSharedPointer &WangSet::colorAt(int index) const return mColors.at(index - 1); } -inline void WangSet::addTile(Tile *tile, WangId wangId) -{ - addWangTile(WangTile(tile, wangId)); -} - -inline void WangSet::addCell(const Cell &cell, WangId wangId) +inline bool WangSet::isEmpty() const { - addWangTile(WangTile(cell, wangId)); + return mTileIdToWangId.isEmpty(); } -inline bool WangSet::isEmpty() const +inline bool WangSet::cellsDirty() const { - return mWangIdToWangTile.isEmpty(); + return mCellsDirty || mLastSeenTranslationFlags != mTileset->transformationFlags(); } TILEDSHARED_EXPORT QString wangSetTypeToString(WangSet::Type type); diff --git a/src/plugins/lua/luaplugin.cpp b/src/plugins/lua/luaplugin.cpp index fce35a11ee..a9d81b7bd7 100644 --- a/src/plugins/lua/luaplugin.cpp +++ b/src/plugins/lua/luaplugin.cpp @@ -486,13 +486,7 @@ void LuaWriter::writeWangSet(const WangSet &wangSet) mWriter.writeEndTable(); mWriter.setSuppressNewlines(false); - mWriter.writeKeyAndValue("tileid", wangTile.tile()->id()); - if (wangTile.flippedHorizontally()) - mWriter.writeKeyAndValue("hflip", wangTile.flippedHorizontally()); - if (wangTile.flippedVertically()) - mWriter.writeKeyAndValue("vflip", wangTile.flippedVertically()); - if (wangTile.flippedAntiDiagonally()) - mWriter.writeKeyAndValue("dflip", wangTile.flippedAntiDiagonally()); + mWriter.writeKeyAndValue("tileid", wangTile.tileId()); mWriter.writeEndTable(); } diff --git a/src/tiled/adjusttileindexes.cpp b/src/tiled/adjusttileindexes.cpp index 18561aae81..f141b9cd26 100644 --- a/src/tiled/adjusttileindexes.cpp +++ b/src/tiled/adjusttileindexes.cpp @@ -264,24 +264,32 @@ AdjustTileMetaData::AdjustTileMetaData(TilesetDocument *tilesetDocument) QVector changes; // Move all WangIds to their new tiles - for (const WangTile &wangTile : wangSet->wangTilesByWangId()) { - if (Tile *fromTile = wangTile.tile()) { + QHashIterator it(wangSet->wangIdByTileId()); + while (it.hasNext()) { + it.next(); + + if (Tile *fromTile = tileset.findTile(it.key())) { if (Tile *newTile = adjustTile(fromTile)) { - WangId fromWangId = wangSet->wangIdOfTile(newTile); - WangId toWangId = wangTile.wangId(); - changes.append(ChangeTileWangId::WangIdChange(fromWangId, toWangId, newTile)); + const WangId fromWangId = wangSet->wangIdOfTile(newTile); + const WangId toWangId = it.value(); + changes.append(ChangeTileWangId::WangIdChange(fromWangId, toWangId, newTile->id())); } } } // Clear WangIds from other tiles - for (const WangTile &wangTile : wangSet->wangTilesByWangId()) { - if (Tile *fromTile = wangTile.tile()) { - auto matchesTile = [fromTile](const ChangeTileWangId::WangIdChange &change) { - return change.tile == fromTile; + it.toFront(); + while (it.hasNext()) { + it.next(); + + if (Tile *fromTile = tileset.findTile(it.key())) { + auto matchesTile = [fromTileId = it.key()](const ChangeTileWangId::WangIdChange &change) { + return change.tileId == fromTileId; }; - if (!std::any_of(changes.begin(), changes.end(), matchesTile)) - changes.append(ChangeTileWangId::WangIdChange(wangTile.wangId(), WangId(), fromTile)); + if (!std::any_of(changes.begin(), changes.end(), matchesTile)) { + const WangId fromWangId = it.value(); + changes.append(ChangeTileWangId::WangIdChange(fromWangId, WangId(), fromTile->id())); + } } } diff --git a/src/tiled/changetilewangid.cpp b/src/tiled/changetilewangid.cpp index 38d6070872..a0bcab216d 100644 --- a/src/tiled/changetilewangid.cpp +++ b/src/tiled/changetilewangid.cpp @@ -47,7 +47,7 @@ ChangeTileWangId::ChangeTileWangId(TilesetDocument *tilesetDocument, { Q_ASSERT(mWangSet); setText(QCoreApplication::translate("Undo Commands", "Change Tile WangId")); - mChanges.append(WangIdChange(mWangSet->wangIdOfTile(tile), wangId, tile)); + mChanges.append(WangIdChange(mWangSet->wangIdOfTile(tile), wangId, tile->id())); } ChangeTileWangId::ChangeTileWangId(TilesetDocument *tilesetDocument, @@ -76,8 +76,9 @@ void ChangeTileWangId::undo() while (changes.hasPrevious()) { const WangIdChange &wangIdChange = changes.previous(); - changedTiles.append(wangIdChange.tile); - mWangSet->addTile(wangIdChange.tile, wangIdChange.from); + if (Tile *tile = findTile(wangIdChange.tileId)) + changedTiles.append(tile); + mWangSet->setWangId(wangIdChange.tileId, wangIdChange.from); } emit mTilesetDocument->tileWangSetChanged(changedTiles); @@ -91,8 +92,9 @@ void ChangeTileWangId::redo() QList changedTiles; for (const WangIdChange &wangIdChange : qAsConst(mChanges)) { - changedTiles.append(wangIdChange.tile); - mWangSet->addTile(wangIdChange.tile, wangIdChange.to); + if (Tile *tile = findTile(wangIdChange.tileId)) + changedTiles.append(tile); + mWangSet->setWangId(wangIdChange.tileId, wangIdChange.to); } emit mTilesetDocument->tileWangSetChanged(changedTiles); @@ -122,15 +124,17 @@ QVector ChangeTileWangId::changesOnSetColorCount { QVector changes; - for (const WangTile &wangTile : wangSet->wangTilesByWangId()) { - WangId newWangId = wangTile.wangId(); + QHashIterator it(wangSet->wangIdByTileId()); + while (it.hasNext()) { + it.next(); + WangId newWangId = it.value(); for (int i = 0; i < WangId::NumIndexes; ++i) if (newWangId.indexColor(i) > colorCount) newWangId.setIndexColor(i, 0); - if (wangTile.wangId() != newWangId) - changes.append(WangIdChange(wangTile.wangId(), newWangId, wangTile.tile())); + if (it.value() != newWangId) + changes.append(WangIdChange(it.value(), newWangId, it.key())); } return changes; @@ -141,8 +145,10 @@ QVector ChangeTileWangId::changesOnRemoveColor( { QVector changes; - for (const WangTile &wangTile : wangSet->wangTilesByWangId()) { - WangId newWangId = wangTile.wangId(); + QHashIterator it(wangSet->wangIdByTileId()); + while (it.hasNext()) { + it.next(); + WangId newWangId = it.value(); for (int i = 0; i < WangId::NumIndexes; ++i) { const int color = newWangId.indexColor(i); @@ -152,8 +158,8 @@ QVector ChangeTileWangId::changesOnRemoveColor( newWangId.setIndexColor(i, color - 1); } - if (wangTile.wangId() != newWangId) - changes.append(WangIdChange(wangTile.wangId(), newWangId, wangTile.tile())); + if (it.value() != newWangId) + changes.append(WangIdChange(it.value(), newWangId, it.key())); } return changes; @@ -162,5 +168,10 @@ QVector ChangeTileWangId::changesOnRemoveColor( void ChangeTileWangId::applyChanges(WangSet *wangSet, const QVector &changes) { for (const WangIdChange &change : changes) - wangSet->addTile(change.tile, change.to); + wangSet->setWangId(change.tileId, change.to); +} + +Tile *ChangeTileWangId::findTile(int tileId) const +{ + return mTilesetDocument->tileset()->findTile(tileId); } diff --git a/src/tiled/changetilewangid.h b/src/tiled/changetilewangid.h index 817b1e3722..00e7d32497 100644 --- a/src/tiled/changetilewangid.h +++ b/src/tiled/changetilewangid.h @@ -29,29 +29,22 @@ namespace Tiled { class TilesetDocument; -// NOTE: This class does not take into account that in fact a single tile can -// have multiple WangIds assigned to it, up to 8 due to flipping and rotation. -// However, currently the UI does not support assigning WangIds to flipped or -// rotated versions of tiles (and it might be preferable to support that as -// an option on the WangSet). - class ChangeTileWangId : public QUndoCommand { public: struct WangIdChange { - WangIdChange(WangId from, WangId to, Tile *tile) + WangIdChange(WangId from, WangId to, int tileId) : from(from) , to(to) - , tile(tile) + , tileId(tileId) {} WangIdChange() - : tile(nullptr) {} WangId from; WangId to; - Tile *tile; + int tileId = 0; }; ChangeTileWangId(); @@ -80,6 +73,8 @@ class ChangeTileWangId : public QUndoCommand static void applyChanges(WangSet *wangSet, const QVector &changes); private: + Tile *findTile(int tileId) const; + TilesetDocument *mTilesetDocument; WangSet *mWangSet; QVector mChanges; diff --git a/src/tiled/editablewangset.cpp b/src/tiled/editablewangset.cpp index 3d0eb6262f..78eb7d8567 100644 --- a/src/tiled/editablewangset.cpp +++ b/src/tiled/editablewangset.cpp @@ -100,7 +100,7 @@ void EditableWangSet::setWangId(EditableTile *editableTile, QJSValue value) if (auto doc = tilesetDocument()) asset()->push(new ChangeTileWangId(doc, wangSet(), editableTile->tile(), wangId)); else if (!checkReadOnly()) - wangSet()->addTile(editableTile->tile(), wangId); + wangSet()->setWangId(editableTile->id(), wangId); } void EditableWangSet::setName(const QString &name) diff --git a/src/tiled/propertybrowser.cpp b/src/tiled/propertybrowser.cpp index 44ae9a30ee..8bf895839a 100644 --- a/src/tiled/propertybrowser.cpp +++ b/src/tiled/propertybrowser.cpp @@ -829,6 +829,19 @@ void PropertyBrowser::addTilesetProperties() QtVariantProperty *columnsProperty = addProperty(ColumnCountProperty, QVariant::Int, tr("Columns"), groupProperty); columnsProperty->setAttribute(QLatin1String("minimum"), 1); + QtVariantProperty *transformationsGroupProperty = mVariantManager->addProperty(VariantPropertyManager::unstyledGroupTypeId(), tr("Allowed Transformations")); + + QtVariantProperty *flipHorizontallyProperty = addProperty(AllowFlipHorizontallyProperty, QVariant::Bool, tr("Flip Horizontally"), transformationsGroupProperty); + QtVariantProperty *flipVerticallyProperty = addProperty(AllowFlipVerticallyProperty, QVariant::Bool, tr("Flip Vertically"), transformationsGroupProperty); + QtVariantProperty *rotateProperty = addProperty(AllowRotateProperty, QVariant::Bool, tr("Rotate"), transformationsGroupProperty); + QtVariantProperty *randomProperty = addProperty(PreferUntransformedProperty, QVariant::Bool, tr("Prefer Untransformed Tiles"), transformationsGroupProperty); + flipHorizontallyProperty->setEnabled(mTilesetDocument); + flipVerticallyProperty->setEnabled(mTilesetDocument); + rotateProperty->setEnabled(mTilesetDocument); + randomProperty->setEnabled(mTilesetDocument); + + groupProperty->addSubProperty(transformationsGroupProperty); + // Next properties we should add only for non 'Collection of Images' tilesets if (!tileset->isCollection()) { QtVariantProperty *parametersProperty = @@ -1349,6 +1362,44 @@ void PropertyBrowser::applyTilesetValue(PropertyId id, const QVariant &val) undoStack->push(new ChangeTilesetBackgroundColor(mTilesetDocument, val.value())); break; + case AllowFlipHorizontallyProperty: + case AllowFlipVerticallyProperty: + case AllowRotateProperty: + case PreferUntransformedProperty: { + Q_ASSERT(mTilesetDocument); + + Tileset::TransformationFlag flag = Tileset::NoTransformation; + switch (id) { + case AllowFlipHorizontallyProperty: + flag = Tileset::AllowFlipHorizontally; + break; + case AllowFlipVerticallyProperty: + flag = Tileset::AllowFlipVertically; + break; + case AllowRotateProperty: + flag = Tileset::AllowRotate; + break; + case PreferUntransformedProperty: + flag = Tileset::PreferUntransformed; + break; + default: + return; + } + + auto flags = tileset->transformationFlags(); + +#if QT_VERSION >= 0x050700 + flags.setFlag(flag, val.toBool()); +#else + if (val.toBool()) + flags |= flag; + else + flags &= ~flag; +#endif + + undoStack->push(new ChangeTilesetTransformationFlags(mTilesetDocument, flags)); + break; + } default: break; } @@ -1742,6 +1793,12 @@ void PropertyBrowser::updateProperties() mIdToProperty[SpacingProperty]->setValue(tileset->tileSpacing()); mIdToProperty[ColorProperty]->setValue(tileset->transparentColor()); } + + const auto flags = tileset->transformationFlags(); + mIdToProperty[AllowFlipHorizontallyProperty]->setValue(flags.testFlag(Tileset::AllowFlipHorizontally)); + mIdToProperty[AllowFlipVerticallyProperty]->setValue(flags.testFlag(Tileset::AllowFlipVertically)); + mIdToProperty[AllowRotateProperty]->setValue(flags.testFlag(Tileset::AllowRotate)); + mIdToProperty[PreferUntransformedProperty]->setValue(flags.testFlag(Tileset::PreferUntransformed)); break; } case Object::TileType: { diff --git a/src/tiled/propertybrowser.h b/src/tiled/propertybrowser.h index 28c14c11e2..5a921873b5 100644 --- a/src/tiled/propertybrowser.h +++ b/src/tiled/propertybrowser.h @@ -145,7 +145,11 @@ class PropertyBrowser : public QtTreePropertyBrowser CompressionLevelProperty, ChunkWidthProperty, ChunkHeightProperty, - TintColorProperty + TintColorProperty, + AllowFlipHorizontallyProperty, + AllowFlipVerticallyProperty, + AllowRotateProperty, + PreferUntransformedProperty, }; void addMapProperties(); diff --git a/src/tiled/tilesetchanges.cpp b/src/tiled/tilesetchanges.cpp index cd172424b6..7ad1850c26 100644 --- a/src/tiled/tilesetchanges.cpp +++ b/src/tiled/tilesetchanges.cpp @@ -228,4 +228,23 @@ void ChangeTilesetGridSize::swap() emit mTilesetDocument->tilesetChanged(&tileset); } + +ChangeTilesetTransformationFlags::ChangeTilesetTransformationFlags(TilesetDocument *tilesetDocument, + Tileset::TransformationFlags newValue) + : QUndoCommand(QCoreApplication::translate("Undo Commands", "Change Tileset")) + , mTilesetDocument(tilesetDocument) + , mOldValue(tilesetDocument->tileset()->transformationFlags()) + , mNewValue(newValue) +{ +} + +void ChangeTilesetTransformationFlags::undo() +{ + mTilesetDocument->setTilesetTransformationFlags(mOldValue); +} +void ChangeTilesetTransformationFlags::redo() +{ + mTilesetDocument->setTilesetTransformationFlags(mNewValue); +} + } // namespace Tiled diff --git a/src/tiled/tilesetchanges.h b/src/tiled/tilesetchanges.h index 875ee37a2a..65ef328d92 100644 --- a/src/tiled/tilesetchanges.h +++ b/src/tiled/tilesetchanges.h @@ -190,4 +190,19 @@ class ChangeTilesetGridSize : public QUndoCommand QSize mGridSize; }; +class ChangeTilesetTransformationFlags : public QUndoCommand +{ +public: + ChangeTilesetTransformationFlags(TilesetDocument *TilesetDocument, + Tileset::TransformationFlags newValue); + + void undo() override; + void redo() override; + +private: + TilesetDocument *mTilesetDocument; + const Tileset::TransformationFlags mOldValue; + const Tileset::TransformationFlags mNewValue; +}; + } // namespace Tiled diff --git a/src/tiled/tilesetdocument.cpp b/src/tiled/tilesetdocument.cpp index f3e6205939..f9f7c9666f 100644 --- a/src/tiled/tilesetdocument.cpp +++ b/src/tiled/tilesetdocument.cpp @@ -319,6 +319,12 @@ void TilesetDocument::setTilesetObjectAlignment(Alignment objectAlignment) emit mapDocument->tilesetTilePositioningChanged(mTileset.data()); } +void TilesetDocument::setTilesetTransformationFlags(Tileset::TransformationFlags flags) +{ + tileset()->setTransformationFlags(flags); + emit tilesetChanged(mTileset.data()); +} + void TilesetDocument::addTiles(const QList &tiles) { mTileset->addTiles(tiles); diff --git a/src/tiled/tilesetdocument.h b/src/tiled/tilesetdocument.h index 0c341cda26..707fc187b0 100644 --- a/src/tiled/tilesetdocument.h +++ b/src/tiled/tilesetdocument.h @@ -95,6 +95,7 @@ class TilesetDocument : public Document void setTilesetName(const QString &name); void setTilesetTileOffset(QPoint tileOffset); void setTilesetObjectAlignment(Alignment objectAlignment); + void setTilesetTransformationFlags(Tileset::TransformationFlags flags); void addTiles(const QList &tiles); void removeTiles(const QList &tiles); diff --git a/src/tiled/variantpropertymanager.cpp b/src/tiled/variantpropertymanager.cpp index 8761ad3fca..ac85007b9e 100644 --- a/src/tiled/variantpropertymanager.cpp +++ b/src/tiled/variantpropertymanager.cpp @@ -81,7 +81,8 @@ bool VariantPropertyManager::isPropertyTypeSupported(int propertyType) const if (propertyType == filePathTypeId() || propertyType == displayObjectRefTypeId() || propertyType == tilesetParametersTypeId() - || propertyType == alignmentTypeId()) + || propertyType == alignmentTypeId() + || propertyType == unstyledGroupTypeId()) return true; return QtVariantPropertyManager::isPropertyTypeSupported(propertyType); } @@ -96,6 +97,8 @@ int VariantPropertyManager::valueType(int propertyType) const return qMetaTypeId(); if (propertyType == alignmentTypeId()) return propertyType; + if (propertyType == unstyledGroupTypeId()) + return propertyType; return QtVariantPropertyManager::valueType(propertyType); } @@ -158,6 +161,11 @@ int VariantPropertyManager::displayObjectRefTypeId() return qMetaTypeId(); } +int VariantPropertyManager::unstyledGroupTypeId() +{ + return qMetaTypeId(); +} + QString VariantPropertyManager::objectRefLabel(const MapObject *object) const { QString label = tr("%1: ").arg(object->id()); diff --git a/src/tiled/variantpropertymanager.h b/src/tiled/variantpropertymanager.h index 2c800e143f..d3b1ff06da 100644 --- a/src/tiled/variantpropertymanager.h +++ b/src/tiled/variantpropertymanager.h @@ -50,6 +50,8 @@ class DisplayObjectRef { MapDocument *mapDocument; }; +class UnstyledGroup {}; + /** * Extension of the QtVariantPropertyManager that adds support for a filePath * data type. @@ -73,6 +75,7 @@ class VariantPropertyManager : public QtVariantPropertyManager static int tilesetParametersTypeId(); static int alignmentTypeId(); static int displayObjectRefTypeId(); + static int unstyledGroupTypeId(); public slots: void setValue(QtProperty *property, const QVariant &val) override; @@ -129,3 +132,4 @@ public slots: } // namespace Tiled Q_DECLARE_METATYPE(Tiled::DisplayObjectRef) +Q_DECLARE_METATYPE(Tiled::UnstyledGroup) diff --git a/src/tiled/wangbrush.cpp b/src/tiled/wangbrush.cpp index 7f12c2c2e4..1d9fb130e1 100644 --- a/src/tiled/wangbrush.cpp +++ b/src/tiled/wangbrush.cpp @@ -223,9 +223,9 @@ void WangBrush::setColor(int color) bool usedAsEdge = false; if (mWangSet && color > 0 && color <= mWangSet->colorCount()) { - for (const auto &wangTile : mWangSet->wangTilesByWangId()) { + for (const WangId wangId : mWangSet->wangIdByTileId()) { for (int i = 0; i < WangId::NumIndexes; ++i) { - if (wangTile.wangId().indexColor(i) == color) { + if (wangId.indexColor(i) == color) { const bool isCorner = WangId::isCorner(i); usedAsCorner |= isCorner; usedAsEdge |= !isCorner; diff --git a/src/tiled/wangfiller.cpp b/src/tiled/wangfiller.cpp index 1e44f011fd..c6bc1ed03d 100644 --- a/src/tiled/wangfiller.cpp +++ b/src/tiled/wangfiller.cpp @@ -178,17 +178,17 @@ void WangFiller::fillRegion(TileLayer &target, if (target.cellAt(targetPos).checked()) return; - WangTile wangTile; - if (!findBestMatch(target, grid, QPoint(x, y), wangTile)) { + Cell cell; + if (!findBestMatch(target, grid, QPoint(x, y), cell)) { // TODO: error feedback return; } - auto cell = wangTile.makeCell(); cell.setChecked(true); - target.setCell(targetPos.x(), targetPos.y(), cell); + const WangId cellWangId = mWangSet.wangIdOfCell(cell); + // Adjust the desired WangIds for the surrounding tiles based on the placed one QPoint adjacentPoints[WangId::NumIndexes]; getSurroundingPoints(QPoint(x, y), mStaggeredRenderer, adjacentPoints); @@ -199,7 +199,7 @@ void WangFiller::fillRegion(TileLayer &target, continue; CellInfo adjacentInfo = grid.get(p); - updateToAdjacent(adjacentInfo, wangTile.wangId(), WangId::oppositeIndex(i)); + updateToAdjacent(adjacentInfo, cellWangId, WangId::oppositeIndex(i)); // Check if we may need to reconsider a tile outside of our starting region if (!WangId::isCorner(i) && mCorrectionsEnabled && bounds.contains(p) && !region.contains(p)) { @@ -261,23 +261,23 @@ WangId WangFiller::wangIdFromSurroundings(const TileLayer &back, bool WangFiller::findBestMatch(const TileLayer &target, const Grid &grid, QPoint position, - WangTile &result) const + Cell &result) const { const CellInfo info = grid.get(position); const quint64 maskedWangId = info.desired & info.mask; - RandomPicker matches; + RandomPicker matches; int lowestPenalty = INT_MAX; - auto processCandidate = [&] (const WangTile &wangTile) { - if ((wangTile.wangId() & info.mask) != maskedWangId) + auto processCandidate = [&] (WangId wangId, const Cell &cell) { + if ((wangId & info.mask) != maskedWangId) return; int totalPenalty = 0; for (int i = 0; i < WangId::NumIndexes; ++i) { const int desiredColor = info.desired.indexColor(i); - const int candidateColor = wangTile.wangId().indexColor(i); + const int candidateColor = wangId.indexColor(i); if (candidateColor != desiredColor) { int penalty = mWangSet.transitionPenalty(desiredColor, candidateColor); @@ -305,16 +305,20 @@ bool WangFiller::findBestMatch(const TileLayer &target, lowestPenalty = totalPenalty; } - matches.add(wangTile, mWangSet.wangTileProbability(wangTile)); + qreal probability = mWangSet.wangIdProbability(wangId); + if (Tile *tile = cell.tile()) + probability *= tile->probability(); + + matches.add(cell, probability); } }; - // TODO: this is a slow linear search, perhaps we could use a better find algorithm... - for (const WangTile &wangTile : mWangSet.wangTilesByWangId()) - processCandidate(wangTile); + const auto &wangIdsAndCells = mWangSet.wangIdsAndCells(); + for (int i = 0, i_end = wangIdsAndCells.size(); i < i_end; ++i) + processCandidate(wangIdsAndCells[i].wangId, wangIdsAndCells[i].cell); if (mErasingEnabled) - processCandidate(WangTile()); + processCandidate(WangId(), Cell()); // Choose a candidate at random, with consideration for probability while (!matches.isEmpty()) { @@ -326,6 +330,7 @@ bool WangFiller::findBestMatch(const TileLayer &target, // complete. if (!mCorrectionsEnabled && !mWangSet.isComplete()) { bool discard = false; + WangId resultWangId = mWangSet.wangIdOfCell(result); // Adjust the desired WangIds for the surrounding tiles based on // the to be placed one. @@ -338,7 +343,7 @@ bool WangFiller::findBestMatch(const TileLayer &target, continue; CellInfo adjacentInfo = grid.get(p); - updateToAdjacent(adjacentInfo, result.wangId(), WangId::oppositeIndex(i)); + updateToAdjacent(adjacentInfo, resultWangId, WangId::oppositeIndex(i)); if (!mWangSet.wangIdIsUsed(adjacentInfo.desired, adjacentInfo.mask)) { discard = true; diff --git a/src/tiled/wangfiller.h b/src/tiled/wangfiller.h index 4e46e689a9..2c86815a7b 100644 --- a/src/tiled/wangfiller.h +++ b/src/tiled/wangfiller.h @@ -84,7 +84,7 @@ class WangFiller bool findBestMatch(const TileLayer &target, const Grid &grid, QPoint position, - WangTile &result) const; + Cell &result) const; const WangSet &mWangSet; const MapRenderer * const mMapRenderer;