diff --git a/src/tiled/automapper.cpp b/src/tiled/automapper.cpp index cea76e0899..8a37b55da1 100644 --- a/src/tiled/automapper.cpp +++ b/src/tiled/automapper.cpp @@ -20,11 +20,7 @@ #include "automapper.h" -#include "addremovelayer.h" -#include "addremovemapobject.h" -#include "addremovetileset.h" #include "automappingutils.h" -#include "changeproperties.h" #include "geometry.h" #include "layermodel.h" #include "logginginterface.h" @@ -116,10 +112,8 @@ struct CompileContext struct ApplyContext { - ApplyContext(QList *touchedTileLayers, - QRegion *appliedRegion) - : touchedTileLayers(touchedTileLayers) - , appliedRegion(appliedRegion) + ApplyContext(QRegion *appliedRegion) + : appliedRegion(appliedRegion) {} // These regions store which parts or the map have already been altered by @@ -129,11 +123,16 @@ struct ApplyContext QRandomGenerator *randomGenerator = QRandomGenerator::global(); - QList *touchedTileLayers; QRegion *appliedRegion; }; +AutoMappingContext::AutoMappingContext(MapDocument *mapDocument) + : targetDocument(mapDocument) + , targetMap(targetDocument->map()) +{ +} + /* * About the order of the methods in this file. * The AutoMapper class has 3 bigger public functions, that is @@ -142,12 +141,9 @@ struct ApplyContext * are put directly below each of these functions. */ -AutoMapper::AutoMapper(MapDocument *mapDocument, - std::unique_ptr rulesMap, +AutoMapper::AutoMapper(std::unique_ptr rulesMap, const QString &rulesMapFileName) - : mTargetDocument(mapDocument) - , mTargetMap(mapDocument->map()) - , mRulesMap(std::move(rulesMap)) + : mRulesMap(std::move(rulesMap)) , mRulesMapFileName(rulesMapFileName) { Q_ASSERT(mRulesMap); @@ -164,9 +160,6 @@ AutoMapper::AutoMapper(MapDocument *mapDocument, AutoMapper::~AutoMapper() { - // These should no longer be around - Q_ASSERT(mAddedLayers.isEmpty()); - Q_ASSERT(mAddedTilesets.isEmpty()); } bool AutoMapper::ruleLayerNameUsed(const QString &ruleLayerName) const @@ -227,15 +220,6 @@ bool AutoMapper::setupRuleMapProperties() SelectCustomProperty { mRulesMapFileName, name, mRulesMap.get() }); } - // OverflowBorder and WrapBorder make no sense for infinite maps - if (mTargetMap->infinite()) { - mOptions.overflowBorder = false; - mOptions.wrapBorder = false; - - // Infinite maps have no size, so we always match outside the map - mOptions.matchOutsideMap = true; - } - // Each of the border options imply MatchOutsideMap if (mOptions.overflowBorder || mOptions.wrapBorder) mOptions.matchOutsideMap = true; @@ -381,9 +365,9 @@ bool AutoMapper::setupRuleMapLayers() if (ruleMapLayerName.startsWith(QLatin1String("output"), Qt::CaseInsensitive)) { if (layer->isTileLayer()) - mOutputTileLayers.insert(layerName, nullptr); + setup.mOutputTileLayerNames.insert(layerName); else if (layer->isObjectGroup()) - mOutputObjectGroups.insert(layerName, nullptr); + setup.mOutputObjectGroupNames.insert(layerName); OutputSet &outputSet = find_or_emplace(setup.mOutputSets, [&setName] (const OutputSet &set) { return set.name == setName; @@ -493,81 +477,57 @@ bool AutoMapper::setupRuleList() return true; } -void AutoMapper::prepareAutoMap() +void AutoMapper::prepareAutoMap(AutoMappingContext &context) { - mWarning.clear(); - mError.clear(); + setupWorkMapLayers(context); - setupWorkMapLayers(); - setupTilesets(); + context.targetDocument->unifyTilesets(*mRulesMap, context.newTilesets); } /** * Makes sure all needed output layers are present in the working map. */ -void AutoMapper::setupWorkMapLayers() +void AutoMapper::setupWorkMapLayers(AutoMappingContext &context) const { - QUndoStack *undoStack = mTargetDocument->undoStack(); - - // Set up pointers to output tile layers in mTargetMap. + // Set up pointers to output tile layers in the target map. // They are created when they are not present. - QMutableHashIterator itTileLayers(mOutputTileLayers); - while (itTileLayers.hasNext()) { - itTileLayers.next(); + for (const QString &name : qAsConst(mRuleMapSetup.mOutputTileLayerNames)) { + auto tileLayer = context.outputTileLayers.value(name); + if (tileLayer) + continue; - const QString &name = itTileLayers.key(); - auto tileLayer = static_cast(mTargetMap->findLayer(name, Layer::TileLayerType)); + tileLayer = static_cast(context.targetMap->findLayer(name, Layer::TileLayerType)); if (!tileLayer) { - const int index = mTargetMap->layerCount(); - tileLayer = new TileLayer(name, 0, 0, - mTargetMap->width(), - mTargetMap->height()); - undoStack->push(new AddLayer(mTargetDocument, index, tileLayer, nullptr)); - mAddedLayers.append(tileLayer); + tileLayer = new TileLayer(name, QPoint(), context.targetMap->size()); + context.newLayers.append(tileLayer); } - itTileLayers.setValue(tileLayer); + context.outputTileLayers.insert(name, tileLayer); } - // Set up pointers to output object layers in mTargetMap. + // Set up pointers to output object layers in the target map. // They are created when they are not present. - QMutableHashIterator itObjectGroups(mOutputObjectGroups); - while (itObjectGroups.hasNext()) { - itObjectGroups.next(); + for (const QString &name : qAsConst(mRuleMapSetup.mOutputObjectGroupNames)) { + auto objectGroup = context.outputObjectGroups.value(name); + if (objectGroup) + continue; - const QString &name = itObjectGroups.key(); - auto objectGroup = static_cast(mTargetMap->findLayer(name, Layer::ObjectGroupType)); + objectGroup = static_cast(context.targetMap->findLayer(name, Layer::ObjectGroupType)); if (!objectGroup) { - const int index = mTargetMap->layerCount(); objectGroup = new ObjectGroup(name, 0, 0); - undoStack->push(new AddLayer(mTargetDocument, index, objectGroup, nullptr)); - mAddedLayers.append(objectGroup); + context.newLayers.append(objectGroup); } - itObjectGroups.setValue(objectGroup); + context.outputObjectGroups.insert(name, objectGroup); } - // Set up pointers to "set" layers (input layers in mTargetMap). They don't - // need to be created if not present. + // Set up pointers to "set" layers (input layers in the target map). They + // don't need to be created if not present. for (const QString &name : qAsConst(mRuleMapSetup.mInputLayerNames)) - if (auto tileLayer = static_cast(mTargetMap->findLayer(name, Layer::TileLayerType))) - mSetLayers.insert(name, tileLayer); -} - -/** - * Makes sure the tilesets present in the rules map are present in the - * target map. - */ -void AutoMapper::setupTilesets() -{ - Q_ASSERT(mAddedTilesets.isEmpty()); - - mTargetDocument->unifyTilesets(*mRulesMap, mAddedTilesets); - - for (const SharedTileset &tileset : qAsConst(mAddedTilesets)) - mTargetDocument->undoStack()->push(new AddTileset(mTargetDocument, tileset)); + if (auto tileLayer = static_cast(context.targetMap->findLayer(name, Layer::TileLayerType))) + context.inputLayers.insert(name, tileLayer); } /** @@ -596,14 +556,14 @@ static void collectCellsInRegion(const QVector &list, /** * Sets up a small data structure for this rule that is optimized for matching. */ -void AutoMapper::compileRule(Rule &rule) const +void AutoMapper::compileRule(Rule &rule, const AutoMappingContext &context) const { - CompileContext context; + CompileContext compileContext; rule.inputSets.clear(); for (const InputSet &inputSet : qAsConst(mRuleMapSetup.mInputSets)) { RuleInputSet index; - if (compileInputSet(index, inputSet, rule.inputRegion, context)) + if (compileInputSet(index, inputSet, rule.inputRegion, compileContext, context)) rule.inputSets.append(std::move(index)); } } @@ -617,19 +577,20 @@ void AutoMapper::compileRule(Rule &rule) const bool AutoMapper::compileInputSet(RuleInputSet &index, const InputSet &inputSet, const QRegion &inputRegion, - CompileContext &context) const + CompileContext &compileContext, + const AutoMappingContext &context) const { const QPoint topLeft = inputRegion.boundingRect().topLeft(); - QVector &anyOf = context.anyOf; - QVector &noneOf = context.noneOf; - QVector &inputCells = context.inputCells; + QVector &anyOf = compileContext.anyOf; + QVector &noneOf = compileContext.noneOf; + QVector &inputCells = compileContext.inputCells; for (const InputConditions &conditions : inputSet.layers) { inputCells.clear(); RuleInputLayer layer; - layer.targetLayer = mSetLayers.value(conditions.layerName, &mDummy); + layer.targetLayer = context.inputLayers.value(conditions.layerName, &context.dummy); for (const QRect &rect : inputRegion) { for (int x = rect.left(); x <= rect.right(); ++x) { @@ -712,7 +673,7 @@ bool AutoMapper::compileInputSet(RuleInputSet &index, // When the input layer is missing, it is considered empty. // In this case, we can drop this input set when empty // tiles are not allowed here. - if (layer.targetLayer == &mDummy) { + if (layer.targetLayer == &context.dummy) { const bool emptyAllowed = (anyOf.isEmpty() || std::any_of(anyOf.cbegin(), anyOf.cend(), @@ -750,8 +711,8 @@ bool AutoMapper::compileInputSet(RuleInputSet &index, } void AutoMapper::autoMap(const QRegion &where, - QList *touchedTileLayers, - QRegion *appliedRegion) + QRegion *appliedRegion, + AutoMappingContext &context) { QRegion applyRegion; @@ -773,8 +734,8 @@ void AutoMapper::autoMap(const QRegion &where, // none of the input layers have any contents. QRegion inputLayersRegion; for (const QString &name : qAsConst(mRuleMapSetup.mInputLayerNames)) { - if (const TileLayer *setLayer = mSetLayers.value(name)) - inputLayersRegion |= setLayer->region(); + if (const TileLayer *inputLayer = context.inputLayers.value(name)) + inputLayersRegion |= inputLayer->region(); } const QRegion regionToErase = inputLayersRegion.intersected(applyRegion); @@ -787,11 +748,11 @@ void AutoMapper::autoMap(const QRegion &where, switch (it.key()->layerType()) { case Layer::TileLayerType: - mOutputTileLayers.value(name)->erase(regionToErase); + context.outputTileLayers.value(name)->erase(regionToErase); break; case Layer::ObjectGroupType: - eraseRegionObjectGroup(mTargetDocument, - mOutputObjectGroups.value(name), + eraseRegionObjectGroup(context.targetDocument, + context.outputObjectGroups.value(name), regionToErase); break; case Layer::ImageLayerType: @@ -805,60 +766,52 @@ void AutoMapper::autoMap(const QRegion &where, // Those two options are guaranteed to be false if the map is infinite, // so no "invalid" width/height accessing here. - GetCell get = mOptions.wrapBorder ? &getWrappedCell : - mOptions.overflowBorder ? &getBoundCell - : &getCell; + GetCell get = &getCell; + if (!context.targetMap->infinite()) { + if (mOptions.wrapBorder) + get = &getWrappedCell; + else if (mOptions.overflowBorder) + get = &getBoundCell; + } - ApplyContext context(touchedTileLayers, appliedRegion); + ApplyContext applyContext(appliedRegion); if (mOptions.matchInOrder) { for (const Rule &rule : mRules) { matchRule(rule, applyRegion, get, [&] (QPoint pos) { - applyRule(rule, pos, context); - }); - context.appliedRegions.clear(); + applyRule(rule, pos, applyContext, context); + }, context); + applyContext.appliedRegions.clear(); } } else { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - auto result = QtConcurrent::blockingMapped(mRules, [&] (const Rule &rule) { + auto collectMatches = [&] (const Rule &rule) { QVector positions; - matchRule(rule, applyRegion, get, [&] (QPoint pos) { positions.append(pos); }); + matchRule(rule, applyRegion, get, [&] (QPoint pos) { positions.append(pos); }, context); return positions; - }); + }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto result = QtConcurrent::blockingMapped(mRules, collectMatches); #else struct MatchRule { - MatchRule(const AutoMapper &autoMapper, - const QRegion &applyRegion, - GetCell getCell) - : autoMapper(autoMapper) - , applyRegion(applyRegion) - , getCell(getCell) - {} - using result_type = QVector; - QVector operator()(const Rule &rule) + std::function collectMatches; + + result_type operator()(const Rule &rule) { - QVector result; - autoMapper.matchRule(rule, applyRegion, getCell, - [&] (QPoint pos) { result.append(pos); }); - return result; + return collectMatches(rule); } - - const AutoMapper &autoMapper; - const QRegion &applyRegion; - const GetCell getCell; }; - const auto result = QtConcurrent::mapped(mRules, - MatchRule(*this, applyRegion, get)).results(); + const auto result = QtConcurrent::blockingMapped>>(mRules, + MatchRule { collectMatches }); #endif for (size_t i = 0; i < mRules.size(); ++i) { for (const QPoint pos : result[i]) - applyRule(mRules[i], pos, context); - context.appliedRegions.clear(); + applyRule(mRules[i], pos, applyContext, context); + applyContext.appliedRegions.clear(); } } } @@ -903,7 +856,7 @@ static bool matchInputIndex(const RuleInputSet &inputSet, QPoint offset, AutoMap return true; } -static bool matchRule(const Rule &rule, QPoint offset, AutoMapper::GetCell getCell) +static bool matchRuleAtOffset(const Rule &rule, QPoint offset, AutoMapper::GetCell getCell) { return std::any_of(rule.inputSets.begin(), rule.inputSets.end(), @@ -913,12 +866,13 @@ static bool matchRule(const Rule &rule, QPoint offset, AutoMapper::GetCell getCe void AutoMapper::matchRule(const Rule &rule, const QRegion &matchRegion, GetCell getCell, - const std::function &matched) const + const std::function &matched, + const AutoMappingContext &context) const { // Small hack to compile rules concurrently. Should be fine since // compilation only alters the rule, and each rule is only accessed by a // single thread. - compileRule(const_cast(rule)); + compileRule(const_cast(rule), context); const QRect inputBounds = rule.inputRegion.boundingRect(); @@ -937,21 +891,23 @@ void AutoMapper::matchRule(const Rule &rule, // When we're not matching a rule outside the map, we reduce the region in // in which it is applied accordingly. - if (!mOptions.matchOutsideMap) { + if (!mOptions.matchOutsideMap && !context.targetMap->infinite()) { ruleMatchRegion &= QRect(0, 0, - mTargetMap->width() - ruleWidth, - mTargetMap->height() - ruleHeight); + context.targetMap->width() - ruleWidth, + context.targetMap->height() - ruleHeight); } for (const QRect &rect : ruleMatchRegion) { for (int y = rect.top(); y <= rect.bottom(); ++y) for (int x = rect.left(); x <= rect.right(); ++x) - if (Tiled::matchRule(rule, QPoint(x, y), getCell)) + if (matchRuleAtOffset(rule, QPoint(x, y), getCell)) matched(QPoint(x, y)); } } -void AutoMapper::applyRule(const Rule &rule, QPoint pos, ApplyContext &context) +void AutoMapper::applyRule(const Rule &rule, QPoint pos, + ApplyContext &applyContext, + AutoMappingContext &context) { Q_ASSERT(!mRuleMapSetup.mOutputSets.empty()); @@ -959,7 +915,7 @@ void AutoMapper::applyRule(const Rule &rule, QPoint pos, ApplyContext &context) pos -= rule.inputRegion.boundingRect().topLeft(); // choose by chance which group of rule_layers should be used: - const int r = context.randomGenerator->generate() % mRuleMapSetup.mOutputSets.size(); + const int r = applyContext.randomGenerator->generate() % mRuleMapSetup.mOutputSets.size(); const OutputSet &ruleOutput = mRuleMapSetup.mOutputSets.at(r); if (mOptions.noOverlappingRules) { @@ -991,7 +947,7 @@ void AutoMapper::applyRule(const Rule &rule, QPoint pos, ApplyContext &context) ruleRegionInLayer[layer] = outputLayerRegion; - return context.appliedRegions[layer].intersects(outputLayerRegion); + return applyContext.appliedRegions[layer].intersects(outputLayerRegion); }); if (overlap) @@ -1001,19 +957,19 @@ void AutoMapper::applyRule(const Rule &rule, QPoint pos, ApplyContext &context) std::for_each(ruleOutput.layers.keyBegin(), ruleOutput.layers.keyEnd(), [&] (const Layer *layer) { - context.appliedRegions[layer] |= ruleRegionInLayer[layer]; + applyContext.appliedRegions[layer] |= ruleRegionInLayer[layer]; }); } copyMapRegion(rule.outputRegion, pos, ruleOutput, context); - if (context.appliedRegion) - *context.appliedRegion |= rule.outputRegion.translated(pos.x(), pos.y()); + if (applyContext.appliedRegion) + *applyContext.appliedRegion |= rule.outputRegion.translated(pos.x(), pos.y()); } void AutoMapper::copyMapRegion(const QRegion ®ion, QPoint offset, const OutputSet &ruleOutput, - ApplyContext &context) + AutoMappingContext &context) { for (auto it = ruleOutput.layers.begin(), end = ruleOutput.layers.end(); it != end; ++it) { const Layer *from = it.key(); @@ -1024,25 +980,28 @@ void AutoMapper::copyMapRegion(const QRegion ®ion, QPoint offset, switch (from->layerType()) { case Layer::TileLayerType: { auto fromTileLayer = static_cast(from); - auto toTileLayer = mOutputTileLayers.value(targetName); + auto toTileLayer = context.outputTileLayers.value(targetName); - if (context.touchedTileLayers && !context.touchedTileLayers->contains(toTileLayer)) - context.touchedTileLayers->append(toTileLayer); + if (!context.touchedTileLayers.isEmpty()) + if (!context.touchedTileLayers.contains(toTileLayer)) + context.touchedTileLayers.append(toTileLayer); to = toTileLayer; for (const QRect &rect : region) { copyTileRegion(fromTileLayer, rect, toTileLayer, - rect.x() + offset.x(), rect.y() + offset.y()); + rect.x() + offset.x(), rect.y() + offset.y(), + context); } break; } case Layer::ObjectGroupType: { auto fromObjectGroup = static_cast(from); - auto toObjectGroup = mOutputObjectGroups.value(targetName); + auto toObjectGroup = context.outputObjectGroups.value(targetName); to = toObjectGroup; for (const QRect &rect : region) { copyObjectRegion(fromObjectGroup, rect, toObjectGroup, - rect.x() + offset.x(), rect.y() + offset.y()); + rect.x() + offset.x(), rect.y() + offset.y(), + context); } break; } @@ -1055,20 +1014,22 @@ void AutoMapper::copyMapRegion(const QRegion ®ion, QPoint offset, // Copy any custom properties set on the output layer if (!from->properties().isEmpty()) { - Properties mergedProperties = to->properties(); + Properties mergedProperties = context.changedProperties.value(to, to->properties()); mergeProperties(mergedProperties, from->properties()); if (mergedProperties != to->properties()) { - QUndoStack *undoStack = mTargetDocument->undoStack(); - undoStack->push(new ChangeProperties(mTargetDocument, QString(), - to, mergedProperties)); + if (context.newLayers.contains(to)) + to->setProperties(mergedProperties); + else + context.changedProperties.insert(to, mergedProperties); } } } } void AutoMapper::copyTileRegion(const TileLayer *srcLayer, QRect rect, - TileLayer *dstLayer, int dstX, int dstY) + TileLayer *dstLayer, int dstX, int dstY, + const AutoMappingContext &context) { int startX = dstX; int startY = dstY; @@ -1079,7 +1040,8 @@ void AutoMapper::copyTileRegion(const TileLayer *srcLayer, QRect rect, const int dwidth = dstLayer->width(); const int dheight = dstLayer->height(); - if (!mOptions.wrapBorder && !mTargetMap->infinite()) { + const bool wrapBorder = mOptions.wrapBorder && !context.targetMap->infinite(); + if (!wrapBorder) { startX = qMax(0, startX); startY = qMax(0, startY); endX = qMin(dwidth, endX); @@ -1097,8 +1059,7 @@ void AutoMapper::copyTileRegion(const TileLayer *srcLayer, QRect rect, int xd = x; int yd = y; - // WrapBorder is only true on finite maps - if (mOptions.wrapBorder) { + if (wrapBorder) { xd = wrap(xd, dwidth); yd = wrap(yd, dheight); } @@ -1121,67 +1082,24 @@ void AutoMapper::copyTileRegion(const TileLayer *srcLayer, QRect rect, } void AutoMapper::copyObjectRegion(const ObjectGroup *srcLayer, const QRectF &rect, - ObjectGroup *dstLayer, int dstX, int dstY) + ObjectGroup *dstLayer, int dstX, int dstY, + AutoMappingContext &context) { - const QRectF pixelRect = mTargetDocument->renderer()->tileToPixelCoords(rect); + const QRectF pixelRect = context.targetDocument->renderer()->tileToPixelCoords(rect); const QList objects = objectsInRegion(srcLayer, pixelRect.toAlignedRect()); - QPointF pixelOffset = mTargetDocument->renderer()->tileToPixelCoords(dstX, dstY); + QPointF pixelOffset = context.targetDocument->renderer()->tileToPixelCoords(dstX, dstY); pixelOffset -= pixelRect.topLeft(); - QVector objectsToAdd; - objectsToAdd.reserve(objects.size()); + context.newMapObjects.reserve(context.newMapObjects.size() + objects.size()); for (MapObject *obj : objects) { MapObject *clone = obj->clone(); clone->resetId(); clone->setX(clone->x() + pixelOffset.x()); clone->setY(clone->y() + pixelOffset.y()); - objectsToAdd.append(AddMapObjects::Entry { clone, dstLayer }); - } - - mTargetDocument->undoStack()->push(new AddMapObjects(mTargetDocument, objectsToAdd)); -} - -void AutoMapper::finalizeAutoMap() -{ - cleanTilesets(); - cleanEmptyLayers(); -} - -void AutoMapper::cleanTilesets() -{ - QUndoStack *undoStack = mTargetDocument->undoStack(); - - for (const SharedTileset &tileset : qAsConst(mAddedTilesets)) { - if (mTargetMap->isTilesetUsed(tileset.data())) - continue; - - const int index = mTargetMap->indexOfTileset(tileset); - if (index == -1) - continue; - - undoStack->push(new RemoveTileset(mTargetDocument, index)); - } - - mAddedTilesets.clear(); -} - -void AutoMapper::cleanEmptyLayers() -{ - QUndoStack *undoStack = mTargetDocument->undoStack(); - - for (Layer *layer : qAsConst(mAddedLayers)) { - if (!layer->isEmpty()) - continue; - - const int index = layer->siblingIndex(); - GroupLayer *parentLayer = layer->parentLayer(); - - undoStack->push(new RemoveLayer(mTargetDocument, index, parentLayer)); + context.newMapObjects.append(AddMapObjects::Entry { clone, dstLayer }); } - - mAddedLayers.clear(); } void AutoMapper::addWarning(const QString &message, std::function callback) diff --git a/src/tiled/automapper.h b/src/tiled/automapper.h index 9204d918c3..4bbf196cac 100644 --- a/src/tiled/automapper.h +++ b/src/tiled/automapper.h @@ -20,6 +20,7 @@ #pragma once +#include "addremovemapobject.h" #include "tilededitor_global.h" #include "tilelayer.h" #include "tileset.h" @@ -107,10 +108,9 @@ struct RuleMapSetup */ std::vector mOutputSets; - /** - * All input layer names. - */ QSet mInputLayerNames; + QSet mOutputTileLayerNames; + QSet mOutputObjectGroupNames; }; struct RuleInputLayer @@ -148,6 +148,30 @@ struct Rule struct CompileContext; struct ApplyContext; +/** + * A single context is used for running all active AutoMapper instances on a + * specific target map. + */ +struct TILED_EDITOR_EXPORT AutoMappingContext +{ + AutoMappingContext(MapDocument *mapDocument); + + MapDocument *targetDocument; + Map *targetMap; + + QVector newTilesets; + QVector newLayers; + QVector newMapObjects; + QHash changedProperties; + + QHash inputLayers; + QHash outputTileLayers; + QHash outputObjectGroups; + + QList touchedTileLayers; // only used when initially non-empty + + const TileLayer dummy; // used in case input layers are missing +}; /** * This class does all the work for the automapping feature. @@ -220,8 +244,7 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject * AutoMapper takes ownership of this map. * @param rulesMapFileName: The filepath to the rule map. */ - AutoMapper(MapDocument *mapDocument, - std::unique_ptr rulesMap, + AutoMapper(std::unique_ptr rulesMap, const QString &rulesMapFileName); ~AutoMapper() override; @@ -233,19 +256,13 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject */ bool ruleLayerNameUsed(const QString &ruleLayerName) const; - /** - * Returns a map of name to target layer, which could be touched - * considering the output layers of the rule map. - */ - const QHash &outputTileLayers() const; - /** * This needs to be called directly before the autoMap call. * It sets up some data structures which change rapidly, so it is quite * painful to keep these data structures up to date all time. (indices of * layers of the working map) */ - void prepareAutoMap(); + void prepareAutoMap(AutoMappingContext &context); /** * Here is done all the automapping. @@ -253,13 +270,9 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject * When an \a appliedRegion is provided, it is set to the region where * rule outputs have been applied. */ - void autoMap(const QRegion &where, QList *touchedTileLayers, QRegion *appliedRegion); - - /** - * This cleans all data structures, which are setup via prepareAutoMap, - * so the auto mapper becomes ready for its next automatic mapping. - */ - void finalizeAutoMap(); + void autoMap(const QRegion &where, + QRegion *appliedRegion, + AutoMappingContext &context); /** * Contains any errors which occurred while interpreting the rules map. @@ -279,12 +292,6 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject bool setupRuleMapProperties(); void setupInputLayerProperties(InputLayer &inputLayer); - /** - * Searches the rules layer for regions and stores these in \a rules. - * @return returns true when anything is ok, false when errors occurred. - */ - bool setupRuleList(); - /** * Sets up the layers in the rules map, which are used for automapping. * The layers are detected and put in the internal data structures. @@ -292,13 +299,18 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject */ bool setupRuleMapLayers(); - void setupWorkMapLayers(); - void setupTilesets(); - void compileRule(Rule &rule) const; + /** + * Searches the rules layer for regions and stores these in \a rules. + * @return returns true when anything is ok, false when errors occurred. + */ + bool setupRuleList(); + + void setupWorkMapLayers(AutoMappingContext &context) const; + void compileRule(Rule &rule, const AutoMappingContext &context) const; bool compileInputSet(RuleInputSet &index, const InputSet &inputSet, - const QRegion &inputRegion, - CompileContext &context) const; + const QRegion &inputRegion, CompileContext &compileContext, + const AutoMappingContext &context) const; /** * This copies all tiles from TileLayer \a srcLayer to TileLayer @@ -311,7 +323,7 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject * so the maybe existing tile in dst will not be overwritten. */ void copyTileRegion(const TileLayer *srcLayer, QRect rect, TileLayer *dstLayer, - int dstX, int dstY); + int dstX, int dstY, const AutoMappingContext &context); /** * This copies all objects from the \a src_lr ObjectGroup to the \a dst_lr @@ -321,7 +333,8 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject * destination object group. */ void copyObjectRegion(const ObjectGroup *srcLayer, const QRectF &rect, - ObjectGroup *dstLayer, int dstX, int dstY); + ObjectGroup *dstLayer, int dstX, int dstY, + AutoMappingContext &context); /** @@ -333,7 +346,7 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject */ void copyMapRegion(const QRegion ®ion, QPoint Offset, const OutputSet &ruleOutput, - ApplyContext &context); + AutoMappingContext &context); /** * This goes through all the positions in \a matchRegion and checks if the @@ -344,7 +357,8 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject void matchRule(const Rule &rule, const QRegion &matchRegion, GetCell getCell, - const std::function &matched) const; + const std::function &matched, + const AutoMappingContext &context) const; /** * Applies the given \a rule at each of the given \a positions. @@ -352,61 +366,17 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject * Might skip some of the positions to satisfy the NoOverlappingRules * option. */ - void applyRule(const Rule &rule, QPoint pos, ApplyContext &context); - - /** - * Cleans up the data structures filled by setupTilesets(), - * so the next rule can be processed. - */ - void cleanTilesets(); - - /** - * Cleans up the added tile layers setup by setupMissingLayers(), - * so we have a minimal addition of tile layers by the automapping. - */ - void cleanEmptyLayers(); + void applyRule(const Rule &rule, QPoint pos, ApplyContext &applyContext, + AutoMappingContext &context); void addWarning(const QString &text, std::function callback = std::function()); - /** - * where to work in - */ - MapDocument *mTargetDocument; - - /** - * the same as mMapDocument->map() - */ - Map *mTargetMap; - /** * map containing the rules, usually different than mTargetMap */ std::unique_ptr mRulesMap; - /** - * Contains the tilesets that have been added to mTargetMap. - * - * Any tilesets used by the rules map, which are not in the mTargetMap, are - * added before applying the rules. Afterwards, we remove the added - * tilesets that didn't end up getting used. - * - * @see setupTilesets() - * @see cleanTilesets() - */ - QVector mAddedTilesets; - - /** - * Contains the layers that have been added to mTargetMap. - * - * Any output layers that were not found in mTargetMap are added. If they - * remain empty after applying the rules, they are removed again. - * - * @see setupMissingLayers() - * @see cleanEmptyLayers() - */ - QVector mAddedLayers; - RuleMapSetup mRuleMapSetup; /** @@ -422,18 +392,6 @@ class TILED_EDITOR_EXPORT AutoMapper : public QObject Options mOptions; - /* - * These map layer names to the target layers in mTargetMap. To ensure no - * roaming pointers are used, this mapping is updated right before each - * automapping. - * - * @see setupWorkMapLayers() - */ - QHash mSetLayers; - QHash mOutputTileLayers; - QHash mOutputObjectGroups; - - const TileLayer mDummy; // used in case input layers are missing QString mError; QString mWarning; }; @@ -443,9 +401,4 @@ inline QString AutoMapper::rulesMapFileName() const return mRulesMapFileName; } -inline const QHash &AutoMapper::outputTileLayers() const -{ - return mOutputTileLayers; -} - } // namespace Tiled diff --git a/src/tiled/automapperwrapper.cpp b/src/tiled/automapperwrapper.cpp index a392a15baf..6c29ffb83f 100644 --- a/src/tiled/automapperwrapper.cpp +++ b/src/tiled/automapperwrapper.cpp @@ -21,6 +21,11 @@ #include "automapperwrapper.h" +#include "addremovelayer.h" +#include "addremovemapobject.h" +#include "addremovetileset.h" +#include "changeproperties.h" +#include "containerhelpers.h" #include "map.h" #include "mapdocument.h" #include "tile.h" @@ -29,32 +34,32 @@ using namespace Tiled; AutoMapperWrapper::AutoMapperWrapper(MapDocument *mapDocument, - const std::vector > &autoMappers, + const std::vector> &autoMappers, const QRegion &where, const TileLayer *touchedLayer) : mMapDocument(mapDocument) { - for (const auto &autoMapper : autoMappers) { - autoMapper->prepareAutoMap(); + AutoMappingContext context(mapDocument); - // Store a copy of each output tile layer before AutoMapping. - for (TileLayer *layer : autoMapper->outputTileLayers()) { - if (mOutputTileLayers.find(layer) != mOutputTileLayers.end()) - continue; + for (const auto &autoMapper : autoMappers) + autoMapper->prepareAutoMap(context); - OutputLayerData &data = mOutputTileLayers[layer]; - data.before.reset(layer->clone()); - } + // Store a copy of each output tile layer before AutoMapping. + for (TileLayer *layer : qAsConst(context.outputTileLayers)) { + if (contains(context.newLayers, layer)) + continue; // Don't store diff for new layers + + if (mExistingOutputTileLayers.find(layer) != mExistingOutputTileLayers.end()) + continue; + + OutputLayerData &data = mExistingOutputTileLayers[layer]; + data.before.reset(layer->clone()); } // During "AutoMap while drawing", keep track of the touched layers, so we // can skip any rule maps that don't have these layers as input entirely. - QList touchedTileLayers; - QList *touchedTileLayersPtr = nullptr; - if (touchedLayer) { - touchedTileLayers.append(touchedLayer); - touchedTileLayersPtr = &touchedTileLayers; - } + if (touchedLayer) + context.touchedTileLayers.append(touchedLayer); // use a copy of the region, so each AutoMapper can manipulate it and the // following AutoMappers do see the impact @@ -69,14 +74,14 @@ AutoMapperWrapper::AutoMapperWrapper(MapDocument *mapDocument, if (appliedRegionPtr && (!map->infinite() && (mapRect - region).isEmpty())) appliedRegionPtr = nullptr; - if (!touchedTileLayers.isEmpty()) { - if (std::none_of(touchedTileLayers.cbegin(), - touchedTileLayers.cend(), + if (!context.touchedTileLayers.isEmpty()) { + if (std::none_of(context.touchedTileLayers.cbegin(), + context.touchedTileLayers.cend(), [&] (const TileLayer *tileLayer) { return autoMapper->ruleLayerNameUsed(tileLayer->name()); })) continue; } - autoMapper->autoMap(region, touchedTileLayersPtr, appliedRegionPtr); + autoMapper->autoMap(region, appliedRegionPtr, context); if (appliedRegionPtr) { // expand where with modified area @@ -87,9 +92,8 @@ AutoMapperWrapper::AutoMapperWrapper(MapDocument *mapDocument, } } - for (std::pair &pair : mOutputTileLayers) { - auto target = pair.first; - auto &before = pair.second.before; + for (auto& [target, data] : mExistingOutputTileLayers) { + auto &before = data.before; MapDocument::TileLayerChangeFlags flags; @@ -102,23 +106,57 @@ AutoMapperWrapper::AutoMapperWrapper(MapDocument *mapDocument, emit mMapDocument->tileLayerChanged(target, flags); // reduce memory usage by saving only diffs - pair.second.region = before->computeDiffRegion(target); - const QRect diffRect = pair.second.region.boundingRect(); + data.region = before->computeDiffRegion(target); + const QRect diffRect = data.region.boundingRect(); - auto beforeDiff = before->copy(pair.second.region); + auto beforeDiff = before->copy(data.region); beforeDiff->setPosition(diffRect.topLeft()); beforeDiff->setName(before->name()); - auto afterDiff = target->copy(pair.second.region); + auto afterDiff = target->copy(data.region); afterDiff->setPosition(diffRect.topLeft()); afterDiff->setName(target->name()); - pair.second.before = std::move(beforeDiff); - pair.second.after = std::move(afterDiff); + data.before = std::move(beforeDiff); + data.after = std::move(afterDiff); } - for (const auto &autoMapper : autoMappers) - autoMapper->finalizeAutoMap(); + // Make sure to add any newly used tilesets to the map + for (const SharedTileset &tileset : qAsConst(context.newTilesets)) + if (context.targetMap->isTilesetUsed(tileset.data())) + new AddTileset(mapDocument, tileset, this); + + // Apply any property changes to existing layers + QHashIterator changedPropertiesIt(context.changedProperties); + while (changedPropertiesIt.hasNext()) { + const auto item = changedPropertiesIt.next(); + new ChangeProperties(mapDocument, QString(), item.key(), item.value(), this); + } + + // Make sure to add any new layers to the map, deleting the ones that + // didn't get any output + for (Layer *layer : qAsConst(context.newLayers)) { + if (layer->isTileLayer() && layer->isEmpty()) { + delete layer; + continue; + } + + if (ObjectGroup *objectGroup = layer->asObjectGroup()) { + if (std::none_of(context.newMapObjects.cbegin(), + context.newMapObjects.cend(), + [=] (const AddMapObjects::Entry &entry) { return entry.objectGroup == objectGroup; })) { + delete layer; + continue; + } + } + + new AddLayer(mapDocument, + context.targetMap->layerCount(), + layer, nullptr, this); + } + + // Add any newly placed objects + new AddMapObjects(mapDocument, context.newMapObjects, this); } AutoMapperWrapper::~AutoMapperWrapper() @@ -127,18 +165,25 @@ AutoMapperWrapper::~AutoMapperWrapper() void AutoMapperWrapper::undo() { - for (std::pair &pair : mOutputTileLayers) - patchLayer(pair.first, *pair.second.before, pair.second.region); + for (const auto& [target, data] : mExistingOutputTileLayers) + patchLayer(target, *data.before, data.region); + + QUndoCommand::undo(); // undo child commands } void AutoMapperWrapper::redo() { - for (std::pair &pair : mOutputTileLayers) - patchLayer(pair.first, *pair.second.after, pair.second.region); + QUndoCommand::redo(); // redo child commands + + for (const auto& [target, data] : mExistingOutputTileLayers) + patchLayer(target, *data.after, data.region); } void AutoMapperWrapper::patchLayer(TileLayer *target, const TileLayer &layer, const QRegion ®ion) { + // Performing the same logic as in TilePainter::setCells manually, since + // emitting the tileLayerChanged signal already happens in the constructor, + // due to AutoMapper::autoMap changing the map in-place. target->setCells(layer.x() - target->x(), layer.y() - target->y(), &layer, diff --git a/src/tiled/automapperwrapper.h b/src/tiled/automapperwrapper.h index e5ee8b1cda..ba3d8b4383 100644 --- a/src/tiled/automapperwrapper.h +++ b/src/tiled/automapperwrapper.h @@ -64,7 +64,7 @@ class AutoMapperWrapper : public QUndoCommand }; MapDocument *mMapDocument; - std::unordered_map mOutputTileLayers; + std::unordered_map mExistingOutputTileLayers; }; } // namespace Tiled diff --git a/src/tiled/automappingmanager.cpp b/src/tiled/automappingmanager.cpp index e365d04cdb..931676cc18 100644 --- a/src/tiled/automappingmanager.cpp +++ b/src/tiled/automappingmanager.cpp @@ -232,7 +232,7 @@ bool AutomappingManager::loadRuleMap(const QString &filePath) return false; } - std::unique_ptr autoMapper { new AutoMapper(mMapDocument, std::move(rules), filePath) }; + std::unique_ptr autoMapper { new AutoMapper(std::move(rules), filePath) }; mWarning += autoMapper->warningString(); const QString error = autoMapper->errorString(); @@ -267,10 +267,6 @@ void AutomappingManager::setMapDocument(MapDocument *mapDocument, const QString connect(mMapDocument, &MapDocument::regionEdited, this, &AutomappingManager::onRegionEdited); } - - // Cleanup needed because AutoMapper instances hold a pointer to the - // MapDocument they apply to. - cleanUp(); } refreshRulesFile(rulesFile); diff --git a/src/tiled/tilepainter.cpp b/src/tiled/tilepainter.cpp index caec1857d2..08d80bead5 100644 --- a/src/tiled/tilepainter.cpp +++ b/src/tiled/tilepainter.cpp @@ -99,7 +99,7 @@ void TilePainter::setCell(int x, int y, const Cell &cell) } void TilePainter::setCells(int x, int y, - TileLayer *tileLayer, + const TileLayer *tileLayer, const QRegion &mask) { QRegion region = paintableRegion(mask); diff --git a/src/tiled/tilepainter.h b/src/tiled/tilepainter.h index c6489006d4..aa5e61a588 100644 --- a/src/tiled/tilepainter.h +++ b/src/tiled/tilepainter.h @@ -69,7 +69,7 @@ class TilePainter * Only cells that fall within this mask are set. The mask is applied in * map coordinates. */ - void setCells(int x, int y, TileLayer *tileLayer, const QRegion &mask); + void setCells(int x, int y, const TileLayer *tileLayer, const QRegion &mask); /** * Draws the cells in the given tile layer at the given coordinates. The diff --git a/tests/automapping/test_automapping.cpp b/tests/automapping/test_automapping.cpp index 5cb3bb5bb0..cc76dc4e52 100644 --- a/tests/automapping/test_automapping.cpp +++ b/tests/automapping/test_automapping.cpp @@ -48,14 +48,17 @@ void test_AutoMapping::autoMap() QVERIFY(rulesMap.get()); MapDocument mapDocument(std::move(inputMap)); - AutoMapper autoMapper(&mapDocument, std::move(rulesMap), QStringLiteral("rules.tmx")); + AutoMapper autoMapper(std::move(rulesMap), QStringLiteral("rules.tmx")); + AutoMappingContext context(&mapDocument); const QSize mapSize = mapDocument.map()->size(); - autoMapper.prepareAutoMap(); + autoMapper.prepareAutoMap(context); QBENCHMARK { - autoMapper.autoMap(QRect(QPoint(), mapSize), nullptr, nullptr); // todo: test appliedRegion as well + autoMapper.autoMap(QRect(QPoint(), mapSize), nullptr, context); // todo: test appliedRegion as well } - autoMapper.finalizeAutoMap(); + for (Layer *layer : qAsConst(context.newLayers)) + if (!layer->isEmpty()) + context.targetMap->addLayer(layer); QCOMPARE(mapDocument.map()->layerCount(), resultMap->layerCount());