1 /*
2  * abstracttilefilltool.cpp
3  * Copyright 2017, Benjamin Trotter <bdtrotte@ucsc.edu>
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 "abstracttilefilltool.h"
22 #include "brushitem.h"
23 #include "mapdocument.h"
24 #include "staggeredrenderer.h"
25 #include "stampactions.h"
26 #include "wangfiller.h"
27 
28 #include <QAction>
29 
30 using namespace Tiled;
31 
AbstractTileFillTool(Id id,const QString & name,const QIcon & icon,const QKeySequence & shortcut,BrushItem * brushItem,QObject * parent)32 AbstractTileFillTool::AbstractTileFillTool(Id id,
33                                            const QString &name,
34                                            const QIcon &icon,
35                                            const QKeySequence &shortcut,
36                                            BrushItem *brushItem,
37                                            QObject *parent)
38     : AbstractTileTool(id, name, icon, shortcut, brushItem, parent)
39     , mFillMethod(TileFill)
40     , mStampActions(new StampActions(this))
41     , mWangSet(nullptr)
42     , mRandomAndMissingCacheValid(true)
43 {
44     connect(mStampActions->random(), &QAction::toggled, this, &AbstractTileFillTool::randomChanged);
45     connect(mStampActions->wangFill(), &QAction::toggled, this, &AbstractTileFillTool::wangFillChanged);
46 
47     connect(mStampActions->flipHorizontal(), &QAction::triggered,
48             [this] { emit stampChanged(mStamp.flipped(FlipHorizontally)); });
49     connect(mStampActions->flipVertical(), &QAction::triggered,
50             [this] { emit stampChanged(mStamp.flipped(FlipVertically)); });
51     connect(mStampActions->rotateLeft(), &QAction::triggered,
52             [this] { emit stampChanged(mStamp.rotated(RotateLeft)); });
53     connect(mStampActions->rotateRight(), &QAction::triggered,
54             [this] { emit stampChanged(mStamp.rotated(RotateRight)); });
55 }
56 
~AbstractTileFillTool()57 AbstractTileFillTool::~AbstractTileFillTool()
58 {
59 }
60 
deactivate(MapScene * scene)61 void AbstractTileFillTool::deactivate(MapScene *scene)
62 {
63     mCaptureStampHelper.reset();
64     AbstractTileTool::deactivate(scene);
65 }
66 
mousePressed(QGraphicsSceneMouseEvent * event)67 void AbstractTileFillTool::mousePressed(QGraphicsSceneMouseEvent *event)
68 {
69     if (event->button() == Qt::RightButton && event->modifiers() == Qt::NoModifier) {
70         mCaptureStampHelper.beginCapture(tilePosition());
71         return;
72     }
73 
74     AbstractTileTool::mousePressed(event);
75 }
76 
mouseReleased(QGraphicsSceneMouseEvent * event)77 void AbstractTileFillTool::mouseReleased(QGraphicsSceneMouseEvent *event)
78 {
79     if (event->button() == Qt::RightButton && mCaptureStampHelper.isActive()) {
80         clearOverlay();
81 
82         TileStamp stamp = mCaptureStampHelper.endCapture(*mapDocument(), tilePosition());
83         if (!stamp.isEmpty())
84             emit stampChanged(stamp);
85 
86         return;
87     }
88 
89     event->ignore();
90 }
91 
setStamp(const TileStamp & stamp)92 void AbstractTileFillTool::setStamp(const TileStamp &stamp)
93 {
94     // Clear any overlay that we presently have with an old stamp
95     clearOverlay();
96 
97     mStamp = stamp;
98 
99     invalidateRandomAndMissingCache();
100 
101     if (brushItem()->isVisible())
102         tilePositionChanged(tilePosition());
103 }
104 
populateToolBar(QToolBar * toolBar)105 void AbstractTileFillTool::populateToolBar(QToolBar *toolBar)
106 {
107     mStampActions->populateToolBar(toolBar,
108                                    mFillMethod == RandomFill,
109                                    mFillMethod == WangFill);
110 }
111 
setFillMethod(FillMethod fillMethod)112 void AbstractTileFillTool::setFillMethod(FillMethod fillMethod)
113 {
114     if (mFillMethod == fillMethod)
115         return;
116 
117     mFillMethod = fillMethod;
118 
119     mStampActions->random()->setChecked(mFillMethod == RandomFill);
120     mStampActions->wangFill()->setChecked(mFillMethod == WangFill);
121 
122     if (mFillMethod == RandomFill || mFillMethod == WangFill)
123         invalidateRandomAndMissingCache();
124 
125     // Don't need to recalculate fill region if there was no preview
126     if (!mPreviewMap)
127         return;
128 
129     tilePositionChanged(tilePosition());
130 }
131 
setWangSet(WangSet * wangSet)132 void AbstractTileFillTool::setWangSet(WangSet *wangSet)
133 {
134     mWangSet = wangSet;
135 
136     invalidateRandomAndMissingCache();
137 }
138 
mapDocumentChanged(MapDocument * oldDocument,MapDocument * newDocument)139 void AbstractTileFillTool::mapDocumentChanged(MapDocument *oldDocument,
140                                               MapDocument *newDocument)
141 {
142     AbstractTileTool::mapDocumentChanged(oldDocument, newDocument);
143 
144     clearConnections(oldDocument);
145 
146     if (oldDocument) {
147         disconnect(oldDocument, &MapDocument::tileProbabilityChanged,
148                    this, &AbstractTileFillTool::invalidateRandomAndMissingCache);
149     }
150 
151     if (newDocument) {
152         invalidateRandomAndMissingCache();
153         connect(newDocument, &MapDocument::tileProbabilityChanged,
154                 this, &AbstractTileFillTool::invalidateRandomAndMissingCache);
155     }
156 
157     clearOverlay();
158 }
159 
tilePositionChanged(QPoint tilePos)160 void AbstractTileFillTool::tilePositionChanged(QPoint tilePos)
161 {
162     if (mCaptureStampHelper.isActive()) {
163         clearOverlay();
164 
165         QRegion capturedArea = mCaptureStampHelper.capturedArea(tilePos);
166         if (!capturedArea.isEmpty())
167             brushItem()->setTileRegion(capturedArea);
168     }
169 }
170 
targetLayers() const171 QList<Layer *> AbstractTileFillTool::targetLayers() const
172 {
173     if (mFillMethod == TileFill && !mStamp.isEmpty())
174         return targetLayersForStamp(mStamp);
175 
176     return AbstractTileTool::targetLayers();
177 }
178 
updatePreview(const QRegion & fillRegion)179 void AbstractTileFillTool::updatePreview(const QRegion &fillRegion)
180 {
181     if (!mRandomAndMissingCacheValid) {
182         updateRandomListAndMissingTilesets();
183         mRandomAndMissingCacheValid = true;
184     }
185 
186     mFillBounds = fillRegion.boundingRect();
187     auto preview = SharedMap::create(mapDocument()->map()->parameters());
188 
189     switch (mFillMethod) {
190     case TileFill:
191         fillWithStamp(*preview, mStamp, fillRegion);
192         break;
193     case RandomFill: {
194         std::unique_ptr<TileLayer> previewLayer {
195             new TileLayer(QString(), mFillBounds.topLeft(), mFillBounds.size())
196         };
197         randomFill(*previewLayer, fillRegion);
198         preview->addLayer(previewLayer.release());
199         break;
200     }
201     case WangFill: {
202         TileLayer *tileLayer = currentTileLayer();
203         if (!tileLayer)
204             return;
205 
206         std::unique_ptr<TileLayer> previewLayer {
207             new TileLayer(QString(), mFillBounds.topLeft(), mFillBounds.size())
208         };
209 
210         wangFill(*previewLayer, *tileLayer, fillRegion);
211         preview->addLayer(previewLayer.release());
212         break;
213     }
214     }
215 
216     preview->addTilesets(preview->usedTilesets());
217 
218     brushItem()->setMap(preview);
219     mPreviewMap = preview;
220 }
221 
clearOverlay()222 void AbstractTileFillTool::clearOverlay()
223 {
224     brushItem()->clear();
225     mPreviewMap.clear();
226 }
227 
updateRandomListAndMissingTilesets()228 void AbstractTileFillTool::updateRandomListAndMissingTilesets()
229 {
230     mRandomCellPicker.clear();
231     mMissingTilesets.clear();
232 
233     if (!mapDocument())
234         return;
235 
236     if (mFillMethod == WangFill) {
237         if (mWangSet) {
238             const SharedTileset &tileset = mWangSet->tileset()->sharedPointer();
239             if (!mapDocument()->map()->tilesets().contains(tileset))
240                 mMissingTilesets.append(tileset);
241         }
242     } else {
243         for (const TileStampVariation &variation : mStamp.variations()) {
244             mapDocument()->unifyTilesets(variation.map, mMissingTilesets);
245             if (mFillMethod == RandomFill) {
246                 for (auto layer : variation.map->tileLayers()) {
247                     for (const Cell &cell : *static_cast<TileLayer*>(layer)) {
248                         if (const Tile *tile = cell.tile())
249                             mRandomCellPicker.add(cell, tile->probability());
250                     }
251                 }
252             }
253         }
254     }
255 }
256 
randomFill(TileLayer & tileLayer,const QRegion & region) const257 void AbstractTileFillTool::randomFill(TileLayer &tileLayer, const QRegion &region) const
258 {
259     if (region.isEmpty() || mRandomCellPicker.isEmpty())
260         return;
261 
262     const auto localRegion = region.translated(-tileLayer.position());
263 
264 #if QT_VERSION < 0x050800
265     const auto rects = localRegion.rects();
266     for (const QRect &rect : rects) {
267 #else
268     for (const QRect &rect : localRegion) {
269 #endif
270         for (int y = rect.top(); y <= rect.bottom(); ++y) {
271             for (int x = rect.left(); x <= rect.right(); ++x) {
272                 tileLayer.setCell(x, y,
273                                   mRandomCellPicker.pick());
274             }
275         }
276     }
277 }
278 
279 void AbstractTileFillTool::wangFill(TileLayer &tileLayerToFill,
280                                     const TileLayer &backgroundTileLayer,
281                                     const QRegion &region) const
282 {
283     if (!mWangSet)
284         return;
285 
286     WangFiller wangFiller(*mWangSet, mapDocument()->renderer());
287 
288     wangFiller.fillRegion(tileLayerToFill, backgroundTileLayer, region);
289 }
290 
291 void AbstractTileFillTool::fillWithStamp(Map &map,
292                                          const TileStamp &stamp,
293                                          const QRegion &mask)
294 {
295     if (stamp.isEmpty())
296         return;
297 
298     const QSize size = stamp.maxSize();
299     if (size.isEmpty())
300         return;
301 
302     const QRect bounds = mask.boundingRect();
303 
304     // Fill the entire map with random variations of the stamp
305     for (int y = 0; y < bounds.height(); y += size.height()) {
306         for (int x = 0; x < bounds.width(); x += size.width()) {
307             const Map *stampMap = stamp.randomVariation().map;
308 
309             for (Layer *layer : stampMap->tileLayers()) {
310                 TileLayer *target = static_cast<TileLayer*>(map.findLayer(layer->name(), Layer::TileLayerType));
311                 if (!target) {
312                     target = new TileLayer(layer->name(), bounds.topLeft(), bounds.size());
313                     map.addLayer(target);
314                 }
315                 target->setCells(x, y, static_cast<TileLayer*>(layer));
316             }
317         }
318     }
319 
320     // Erase tiles outside of the masked region. This can easily be faster than
321     // avoiding to place tiles outside of the region in the first place.
322     for (Layer *layer : map.tileLayers()) {
323         auto tileLayer = static_cast<TileLayer*>(layer);
324         tileLayer->erase((QRegion(tileLayer->bounds()) - mask).translated(-tileLayer->position()));
325     }
326 }
327 
328 void AbstractTileFillTool::invalidateRandomAndMissingCache()
329 {
330     mRandomAndMissingCacheValid = false;
331 }
332 
333 #include "moc_abstracttilefilltool.cpp"
334