1 /*
2  * adjusttileindexes.cpp
3  * Copyright 2015, Thorbjørn Lindeijer <bjorn@lindeijer.nl>
4  *
5  * This file is part of Tiled.
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the Free
9  * Software Foundation; either version 2 of the License, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15  * more details.
16  *
17  * You should have received a copy of the GNU General Public License along with
18  * this program. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "adjusttileindexes.h"
22 
23 #include "changemapobject.h"
24 #include "changeproperties.h"
25 #include "changetileanimation.h"
26 #include "changetileobjectgroup.h"
27 #include "changetileprobability.h"
28 #include "changetilewangid.h"
29 #include "changewangcolordata.h"
30 #include "changewangsetdata.h"
31 #include "map.h"
32 #include "mapdocument.h"
33 #include "mapobject.h"
34 #include "objectgroup.h"
35 #include "painttilelayer.h"
36 #include "tile.h"
37 #include "tilelayer.h"
38 #include "tilesetdocument.h"
39 #include "wangset.h"
40 
41 #include <QCoreApplication>
42 
43 namespace Tiled {
44 
AdjustTileIndexes(MapDocument * mapDocument,const Tileset & tileset)45 AdjustTileIndexes::AdjustTileIndexes(MapDocument *mapDocument,
46                                      const Tileset &tileset)
47     : QUndoCommand(QCoreApplication::translate("Undo Commands",
48                                                "Adjust Tile Indexes"))
49 {
50     int oldColumnCount = tileset.expectedColumnCount();
51     int newColumnCount = tileset.columnCount();
52 
53     auto isFromTileset = [&](const Cell &cell) -> bool {
54         return cell.tileset() == &tileset;
55     };
56 
57     auto adjustCell = [&](Cell cell) -> Cell {
58         int tileIndex = cell.tileId();
59         int row = tileIndex / oldColumnCount;
60         int column = tileIndex % oldColumnCount;
61 
62         if (column < newColumnCount) {
63             int newTileIndex = row * newColumnCount + column;
64             cell.setTile(cell.tileset(), newTileIndex);
65         } else {
66             cell.setTile(nullptr);
67         }
68 
69         return cell;
70     };
71 
72     QVector<MapObjectCell> objectChanges;
73 
74     // Adjust tile references from map layers
75     LayerIterator iterator(mapDocument->map());
76     while (Layer *layer = iterator.next()) {
77         switch (layer->layerType()) {
78         case Layer::TileLayerType: {
79             TileLayer *tileLayer = static_cast<TileLayer*>(layer);
80             const QRegion region = tileLayer->region(isFromTileset).translated(-layer->position());
81 
82             if (!region.isEmpty()) {
83                 const QRect boundingRect(region.boundingRect());
84                 auto changedLayer = new TileLayer(QString(), 0, 0,
85                                                   boundingRect.width(),
86                                                   boundingRect.height());
87 
88 #if QT_VERSION < 0x050800
89                 const auto rects = region.rects();
90                 for (const QRect &rect : rects) {
91 #else
92                 for (const QRect &rect : region) {
93 #endif
94                     for (int x = rect.left(); x <= rect.right(); ++x) {
95                         for (int y = rect.top(); y <= rect.bottom(); ++y) {
96                             Cell cell = adjustCell(tileLayer->cellAt(x, y));
97                             changedLayer->setCell(x - boundingRect.x(),
98                                                   y - boundingRect.y(),
99                                                   cell);
100                         }
101                     }
102                 }
103 
104                 new PaintTileLayer(mapDocument, tileLayer,
105                                    boundingRect.x() + tileLayer->x(),
106                                    boundingRect.y() + tileLayer->y(),
107                                    changedLayer,
108                                    this);
109 
110                 delete changedLayer;
111             }
112 
113             break;
114         }
115 
116         case Layer::ObjectGroupType:
117             for (MapObject *mapObject : *static_cast<ObjectGroup*>(layer)) {
118                 if ((!mapObject->isTemplateInstance() || mapObject->propertyChanged(MapObject::CellProperty))
119                         && isFromTileset(mapObject->cell())) {
120                     MapObjectCell change;
121                     change.object = mapObject;
122                     change.cell = adjustCell(mapObject->cell());
123                     objectChanges.append(change);
124                 }
125             }
126             break;
127 
128         case Layer::ImageLayerType:
129         case Layer::GroupLayerType:
130             break;
131         }
132     }
133 
134     if (!objectChanges.isEmpty())
135         new ChangeMapObjectCells(mapDocument, objectChanges, this);
136 }
137 
138 AdjustTileMetaData::AdjustTileMetaData(TilesetDocument *tilesetDocument)
139     : QUndoCommand(QCoreApplication::translate("Undo Commands",
140                                                "Adjust Tile Indexes"))
141 {
142     const Tileset &tileset = *tilesetDocument->tileset();
143 
144     int oldColumnCount = tileset.expectedColumnCount();
145     int newColumnCount = tileset.columnCount();
146 
147     auto adjustTile = [&](const Tile *tile) -> Tile* {
148         int tileIndex = tile->id();
149         int row = tileIndex / oldColumnCount;
150         int column = tileIndex % oldColumnCount;
151 
152         if (column < newColumnCount) {
153             int newTileIndex = row * newColumnCount + column;
154             return tileset.findTile(newTileIndex);
155         }
156 
157         return nullptr;
158     };
159 
160     // Adjust tile meta data
161     QList<Tile*> tilesChangingProbability;
162     QList<qreal> tileProbabilities;
163     QSet<Tile*> tilesToReset;
164 
165     auto adjustAnimationFrames = [&](const QVector<Frame> &frames) -> QVector<Frame> {
166         QVector<Frame> newFrames;
167         for (const Frame &frame : frames) {
168             if (Tile *tile = tileset.findTile(frame.tileId)) {
169                 if (Tile *newTile = adjustTile(tile))
170                     newFrames.append(Frame { newTile->id(), frame.duration });
171             }
172         }
173         return newFrames;
174     };
175 
176     auto applyMetaData = [&](Tile *toTile,
177                              const Properties &properties,
178                              qreal probability,
179                              std::unique_ptr<ObjectGroup> objectGroup,
180                              const QVector<Frame> &frames)
181     {
182         if (properties != toTile->properties()) {
183             new ChangeProperties(tilesetDocument,
184                                  QCoreApplication::translate("MapDocument", "Tile"),
185                                  toTile,
186                                  properties,
187                                  this);
188         }
189 
190         if (probability != toTile->probability()) {
191             tilesChangingProbability.append(toTile);
192             tileProbabilities.append(probability);
193         }
194 
195         if (objectGroup.get() != toTile->objectGroup())
196             new ChangeTileObjectGroup(tilesetDocument, toTile, std::move(objectGroup), this);
197 
198         if (frames != toTile->frames())
199             new ChangeTileAnimation(tilesetDocument, toTile, frames, this);
200     };
201 
202     auto moveMetaData = [&](Tile *fromTile) {
203         Tile *toTile = adjustTile(fromTile);
204         if (toTile == fromTile)
205             return;
206 
207         tilesToReset.insert(fromTile);  // We may still need to reset "fromTile"
208 
209         if (!toTile)
210             return;
211 
212         tilesToReset.remove(toTile);    // "toTile" no longer needs to get reset
213 
214         // Copy meta data from "fromTile" to "toTile"
215 
216         std::unique_ptr<ObjectGroup> objectGroup;
217         if (fromTile->objectGroup())
218             objectGroup.reset(fromTile->objectGroup()->clone());
219 
220         applyMetaData(toTile,
221                       fromTile->properties(),
222                       fromTile->probability(),
223                       std::move(objectGroup),
224                       adjustAnimationFrames(fromTile->frames()));
225     };
226 
227     QMapIterator<int, Tile *> iterator { tileset.tilesById() };
228 
229     if (newColumnCount > oldColumnCount) {
230         // Increasing column count means information is copied to higher tiles,
231         // so we need to iterate backwards.
232         iterator.toBack();
233         while (iterator.hasPrevious())
234             moveMetaData(iterator.previous().value());
235     } else {
236         while (iterator.hasNext())
237             moveMetaData(iterator.next().value());
238     }
239 
240     // Reset meta data on tiles that nothing was copied to
241     QSetIterator<Tile*> resetIterator(tilesToReset);
242     while (resetIterator.hasNext()) {
243         applyMetaData(resetIterator.next(),
244                       Properties(), 1.0, nullptr, QVector<Frame>());
245     }
246 
247     // Translate tile references in Wang sets and Wang colors
248     for (WangSet *wangSet : tileset.wangSets()) {
249         // WangSet tile image
250         if (Tile *fromTile = tileset.findTile(wangSet->imageTileId())) {
251             if (Tile *newTile = adjustTile(fromTile))
252                 if (fromTile != newTile)
253                     new SetWangSetImage(tilesetDocument, wangSet, newTile->id(), this);
254         }
255 
256         // WangColor tile images
257         for (const QSharedPointer<WangColor> &wangColor : wangSet->colors()) {
258             if (Tile *fromTile = tileset.findTile(wangColor->imageId()))
259                 if (Tile *newTile = adjustTile(fromTile))
260                     if (fromTile != newTile)
261                         new ChangeWangColorImage(tilesetDocument, wangColor.data(), newTile->id(), this);
262         }
263 
264         QVector<ChangeTileWangId::WangIdChange> changes;
265 
266         // Move all WangIds to their new tiles
267         QHashIterator<int, WangId> it(wangSet->wangIdByTileId());
268         while (it.hasNext()) {
269             it.next();
270 
271             if (Tile *fromTile = tileset.findTile(it.key())) {
272                 if (Tile *newTile = adjustTile(fromTile)) {
273                     const WangId fromWangId = wangSet->wangIdOfTile(newTile);
274                     const WangId toWangId = it.value();
275                     changes.append(ChangeTileWangId::WangIdChange(fromWangId, toWangId, newTile->id()));
276                 }
277             }
278         }
279 
280         // Clear WangIds from other tiles
281         it.toFront();
282         while (it.hasNext()) {
283             it.next();
284 
285             if (Tile *fromTile = tileset.findTile(it.key())) {
286                 auto matchesTile = [fromTileId = it.key()](const ChangeTileWangId::WangIdChange &change) {
287                     return change.tileId == fromTileId;
288                 };
289                 if (!std::any_of(changes.begin(), changes.end(), matchesTile)) {
290                     const WangId fromWangId = it.value();
291                     changes.append(ChangeTileWangId::WangIdChange(fromWangId, WangId(), fromTile->id()));
292                 }
293             }
294         }
295 
296         if (!changes.isEmpty())
297             new ChangeTileWangId(tilesetDocument, wangSet, changes, this);
298     }
299 
300     if (!tilesChangingProbability.isEmpty()) {
301         new ChangeTileProbability(tilesetDocument,
302                                   tilesChangingProbability,
303                                   tileProbabilities,
304                                   this);
305     }
306 }
307 
308 } // namespace Tiled
309