Permalink
Browse files

Automapping: Added a "StrictEmpty" flag to input layers

In "StrictEmpty" mode, empty tiles in the input region match empty tiles
in the set layer. So when an "input" layer contains an empty tile within
the input region, this means an empty tile is allowed at that location.
And when an "inputnot" layer contains an empty tile within the input
region, it means an empty tile is not allowed at that location.

Without the "StrictEmpty" option, when no "input" layer specifies any
tile for a given location, and no "inputnot" layers exist, any tile that
isn't used elsewhere on any input layer is considered valid. Also, when
an "inputnot" layer contains an empty tile, it is simply ignored. This
behavior can be convenient, but it is not always desired.

The "StrictEmpty" mode can be enabled per input layer by adding a custom
boolean property named "StrictEmpty" with the value "true".

See also the following discussion on the forum:
http://discourse.mapeditor.org/t/automapping-check-for-empty-tile/965
  • Loading branch information...
bjorn committed Dec 31, 2017
1 parent aea9fa1 commit eb1e85044f309d1cd9132a15baf7eaab1be6d4a5
Showing with 104 additions and 52 deletions.
  1. +17 −0 docs/manual/automapping.rst
  2. +78 −50 src/tiled/automapper.cpp
  3. +9 −2 src/tiled/automapper.h
@@ -183,6 +183,23 @@ These properties are map wide, meaning it applies to all rules which are
part of the rulemap. If you need rules with different properties, you
can use multiple rulemaps.
Layer Properties
----------------
The following properties are supported on a per-layer basis:
StrictEmpty
This layer property is a boolean property. It can be added to
**input** and **inputnot** layers to customize the behavior for
empty tiles within the input region.
In "StrictEmpty" mode, empty tiles in the input region match empty tiles
in the set layer. So when an "input" layer contains an empty tile within
the input region, this means an empty tile is allowed at that location.
And when an "inputnot" layer contains an empty tile within the input region,
it means an empty tile is not allowed at that location.
Converting Rules From 0.8 and Below
===================================
View
@@ -127,6 +127,30 @@ bool AutoMapper::setupRuleMapProperties()
return true;
}
void AutoMapper::setupInputLayerProperties(InputLayer &inputLayer)
{
inputLayer.strictEmpty = false;
QMapIterator<QString, QVariant> it(inputLayer.tileLayer->properties());
while (it.hasNext()) {
it.next();
const QString &name = it.key();
const QVariant &value = it.value();
if (name.compare(QLatin1String("strictempty"), Qt::CaseInsensitive) == 0) {
if (value.canConvert(QVariant::Bool)) {
inputLayer.strictEmpty = value.toBool();
continue;
}
}
mWarning += tr("'%1': Property '%2' = '%3' on layer '%4' does not make sense. "
"Ignoring this property.")
.arg(mRulePath, name, value.toString(), inputLayer.tileLayer->name()) + QLatin1Char('\n');
}
}
bool AutoMapper::setupRuleMapTileLayers()
{
Q_ASSERT(mLayerList.isEmpty());
@@ -212,10 +236,15 @@ bool AutoMapper::setupRuleMapTileLayers()
if (!mInputRules[index].contains(name))
mInputRules[index].insert(name, InputConditions());
InputLayer inputLayer;
inputLayer.tileLayer = tileLayer;
setupInputLayerProperties(inputLayer);
InputConditions &conditions = mInputRules[index][name];
if (isNotList)
mInputRules[index][name].listNo.append(tileLayer);
conditions.listNo.append(inputLayer);
else
mInputRules[index][name].listYes.append(tileLayer);
conditions.listYes.append(inputLayer);
continue;
}
@@ -478,19 +507,19 @@ QRegion AutoMapper::computeSetLayersRegion() const
* Fills \a cells with the list of all cells which can be found within all
* tile layers within the given region.
*/
static void collectCellsInRegion(const QVector<TileLayer*> &list,
static void collectCellsInRegion(const QVector<InputLayer> &list,
const QRegion &r,
QVarLengthArray<Cell, 8> &cells)
{
for (const TileLayer *tilelayer : list) {
for (const InputLayer &inputLayer : list) {
#if QT_VERSION < 0x050800
foreach (const QRect &rect, r.rects()) {
#else
for (const QRect &rect : r) {
#endif
for (int x = rect.left(); x <= rect.right(); ++x) {
for (int y = rect.top(); y <= rect.bottom(); ++y) {
const Cell &cell = tilelayer->cellAt(x, y);
const Cell &cell = inputLayer.tileLayer->cellAt(x, y);
if (!cells.contains(cell))
cells.append(cell);
}
@@ -555,18 +584,21 @@ static void collectCellsInRegion(const QVector<TileLayer*> &list,
* (need of less layers.)
* It was not added to the case, when having only listNo layers to
* avoid total symmetry between those lists.
* It can be turned off by setting the StrictEmpty property on the input
* layer.
*
* If all positions are considered good, return true.
* return false otherwise.
*
* @return bool, if the tile layer matches the given list of layers.
*/
static bool compareLayerTo(const TileLayer *setLayer,
const InputConditions &conditions,
const QRegion &ruleRegion, const QPoint &offset)
static bool layerMatchesConditions(const TileLayer &setLayer,
const InputConditions &conditions,
const QRegion &ruleRegion,
const QPoint &offset)
{
const QVector<TileLayer*> &listYes = conditions.listYes;
const QVector<TileLayer*> &listNo = conditions.listNo;
const auto &listYes = conditions.listYes;
const auto &listNo = conditions.listNo;
if (listYes.isEmpty() && listNo.isEmpty())
return false;
@@ -581,14 +613,14 @@ static bool compareLayerTo(const TileLayer *setLayer,
#endif
for (int x = rect.left(); x <= rect.right(); ++x) {
for (int y = rect.top(); y <= rect.bottom(); ++y) {
const Cell &setCell = setLayer->cellAt(x + offset.x(),
y + offset.y());
const Cell &setCell = setLayer.cellAt(x + offset.x(),
y + offset.y());
// First check listNo. If any tile matches there, we can
// immediately know there is no match.
for (const TileLayer *comparedTileLayer : listNo) {
const Cell &noCell = comparedTileLayer->cellAt(x, y);
if (!noCell.isEmpty() && setCell == noCell)
for (const InputLayer &inputNotLayer : listNo) {
const Cell &noCell = inputNotLayer.tileLayer->cellAt(x, y);
if ((inputNotLayer.strictEmpty || !noCell.isEmpty()) && setCell == noCell)
return false;
}
@@ -600,9 +632,9 @@ static bool compareLayerTo(const TileLayer *setLayer,
bool ruleDefinedListYes = false;
bool matchListYes = false;
for (const TileLayer *comparedTileLayer : listYes) {
const Cell &yesCell = comparedTileLayer->cellAt(x, y);
if (!yesCell.isEmpty()) {
for (const InputLayer &inputLayer : listYes) {
const Cell &yesCell = inputLayer.tileLayer->cellAt(x, y);
if (inputLayer.strictEmpty || !yesCell.isEmpty()) {
ruleDefinedListYes = true;
if (setCell == yesCell) {
matchListYes = true;
@@ -670,11 +702,9 @@ QRect AutoMapper::applyRule(int ruleIndex, const QRect &where)
const InputConditions &conditions = inputIndexIterator.value();
const int i = mMapWork->indexOfLayer(name, Layer::TileLayerType);
const TileLayer *setLayer = (i >= 0) ? mMapWork->layerAt(i)->asTileLayer() : &dummy;
if (!compareLayerTo(setLayer,
conditions,
ruleInputRegion,
QPoint(x, y))) {
const TileLayer &setLayer = (i >= 0) ? *mMapWork->layerAt(i)->asTileLayer() : dummy;
if (!layerMatchesConditions(setLayer, conditions, ruleInputRegion, QPoint(x, y))) {
allLayerNamesMatch = false;
break;
}
@@ -691,43 +721,41 @@ QRect AutoMapper::applyRule(int ruleIndex, const QRect &where)
const int r = qrand() % mLayerList.size();
const RuleOutput &translationTable = mLayerList.at(r);
if (!mNoOverlappingRules) {
copyMapRegion(ruleOutputRegion, QPoint(x, y), translationTable);
ret = ret.united(rbr.translated(QPoint(x, y)));
continue;
}
bool missmatch = false;
const QList<Layer*> layers = translationTable.keys();
if (mNoOverlappingRules) {
bool overlap = false;
const QList<Layer*> layers = translationTable.keys();
// check if there are no overlaps within this rule.
QVector<QRegion> ruleRegionInLayer;
for (int i = 0; i < layers.size(); ++i) {
Layer *layer = layers.at(i);
// check if there are no overlaps within this rule.
QVector<QRegion> ruleRegionInLayer;
for (int i = 0; i < layers.size(); ++i) {
Layer *layer = layers.at(i);
QRegion appliedPlace;
QRegion appliedPlace;
if (TileLayer *tileLayer = layer->asTileLayer())
appliedPlace = tileLayer->region();
else if (ObjectGroup *objectGroup = layer->asObjectGroup())
appliedPlace = tileRegionOfObjectGroup(objectGroup);
else
continue;
if (TileLayer *tileLayer = layer->asTileLayer())
appliedPlace = tileLayer->region();
else if (ObjectGroup *objectGroup = layer->asObjectGroup())
appliedPlace = tileRegionOfObjectGroup(objectGroup);
else
continue;
ruleRegionInLayer.append(appliedPlace.intersected(ruleOutputRegion));
ruleRegionInLayer.append(appliedPlace.intersected(ruleOutputRegion));
if (appliedRegions.at(i).intersects(ruleRegionInLayer.at(i).translated(x, y))) {
missmatch = true;
break;
if (appliedRegions.at(i).intersects(ruleRegionInLayer.at(i).translated(x, y))) {
overlap = true;
break;
}
}
if (overlap)
continue;
for (int i = 0; i < translationTable.size(); ++i)
appliedRegions[i] += ruleRegionInLayer.at(i).translated(x, y);
}
if (missmatch)
continue;
copyMapRegion(ruleOutputRegion, QPoint(x, y), translationTable);
ret = ret.united(rbr.translated(QPoint(x, y)));
for (int i = 0; i < translationTable.size(); ++i)
appliedRegions[i] += ruleRegionInLayer[i].translated(x, y);
}
}
View
@@ -41,11 +41,17 @@ namespace Internal {
class MapDocument;
struct InputLayer
{
TileLayer *tileLayer;
bool strictEmpty;
};
class InputConditions
{
public:
QVector<TileLayer*> listYes; // "input"
QVector<TileLayer*> listNo; // "inputnot"
QVector<InputLayer> listYes; // "input"
QVector<InputLayer> listNo; // "inputnot"
};
// Maps layer names to their conditions
@@ -143,6 +149,7 @@ class AutoMapper : public QObject
* @return returns true when anything is ok, false when errors occurred.
*/
bool setupRuleMapProperties();
void setupInputLayerProperties(InputLayer &inputLayer);
void cleanUpRulesMap();

0 comments on commit eb1e850

Please sign in to comment.