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 ®ion) 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 ®ion) 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