1 /*
2 * stampbrush.cpp
3 * Copyright 2009-2010, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
4 * Copyright 2010 Stefan Beller <stefanbeller@googlemail.com>
5 *
6 * This file is part of Tiled.
7 *
8 * This program is free software; you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License as published by the Free
10 * Software Foundation; either version 2 of the License, or (at your option)
11 * any later version.
12 *
13 * This program is distributed in the hope that it will be useful, but WITHOUT
14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
16 * more details.
17 *
18 * You should have received a copy of the GNU General Public License along with
19 * this program. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 #include "stampbrush.h"
23
24 #include "addremovelayer.h"
25 #include "addremovetileset.h"
26 #include "brushitem.h"
27 #include "geometry.h"
28 #include "map.h"
29 #include "mapdocument.h"
30 #include "mapscene.h"
31 #include "painttilelayer.h"
32 #include "staggeredrenderer.h"
33 #include "stampactions.h"
34 #include "tile.h"
35 #include "tilestamp.h"
36 #include "wangset.h"
37 #include "wangfiller.h"
38
39 #include <QAction>
40 #include <QToolBar>
41 #include <QVector>
42
43 #include <memory>
44
45 using namespace Tiled;
46
StampBrush(QObject * parent)47 StampBrush::StampBrush(QObject *parent)
48 : AbstractTileTool("StampTool",
49 tr("Stamp Brush"),
50 QIcon(QLatin1String(
51 ":images/22/stock-tool-clone.png")),
52 QKeySequence(Qt::Key_B),
53 nullptr,
54 parent)
55 , mBrushBehavior(Free)
56 , mIsRandom(false)
57 , mIsWangFill(false)
58 , mWangSet(nullptr)
59 , mRandomCacheValid(true)
60 , mStampActions(new StampActions(this))
61 {
62 connect(mStampActions->random(), &QAction::toggled, this, &StampBrush::randomChanged);
63 connect(mStampActions->wangFill(), &QAction::toggled, this, &StampBrush::wangFillChanged);
64
65 connect(mStampActions->flipHorizontal(), &QAction::triggered,
66 [this] { emit stampChanged(mStamp.flipped(FlipHorizontally)); });
67 connect(mStampActions->flipVertical(), &QAction::triggered,
68 [this] { emit stampChanged(mStamp.flipped(FlipVertically)); });
69 connect(mStampActions->rotateLeft(), &QAction::triggered,
70 [this] { emit stampChanged(mStamp.rotated(RotateLeft)); });
71 connect(mStampActions->rotateRight(), &QAction::triggered,
72 [this] { emit stampChanged(mStamp.rotated(RotateRight)); });
73 }
74
~StampBrush()75 StampBrush::~StampBrush()
76 {
77 }
78
deactivate(MapScene * scene)79 void StampBrush::deactivate(MapScene *scene)
80 {
81 mCaptureStampHelper.reset();
82 AbstractTileTool::deactivate(scene);
83 }
84
tilePositionChanged(QPoint pos)85 void StampBrush::tilePositionChanged(QPoint pos)
86 {
87 if (mBrushBehavior == Paint) {
88 // Draw a line from the previous point to avoid gaps, skipping the
89 // first point, since it was painted when the mouse was pressed, or the
90 // last time the mouse was moved.
91 QVector<QPoint> points = pointsOnLine(mPrevTilePosition, pos);
92 QHash<TileLayer*, QRegion> paintedRegions;
93
94 for (int i = 1; i < points.size(); ++i) {
95 drawPreviewLayer(QVector<QPoint>() << points.at(i));
96
97 // Only update the brush item for the last drawn piece
98 if (i == points.size() - 1)
99 brushItem()->setMap(mPreviewMap);
100
101 doPaint(Mergeable, &paintedRegions);
102 }
103
104 QHashIterator<TileLayer*, QRegion> ri(paintedRegions);
105 while (ri.hasNext()) {
106 ri.next();
107 emit mapDocument()->regionEdited(ri.value(), ri.key());
108 }
109 } else {
110 updatePreview();
111 }
112 mPrevTilePosition = pos;
113 }
114
mousePressed(QGraphicsSceneMouseEvent * event)115 void StampBrush::mousePressed(QGraphicsSceneMouseEvent *event)
116 {
117 if (brushItem()->isVisible()) {
118 if (event->button() == Qt::LeftButton) {
119 switch (mBrushBehavior) {
120 case Line:
121 mStampReference = tilePosition();
122 mBrushBehavior = LineStartSet;
123 break;
124 case Circle:
125 mStampReference = tilePosition();
126 mBrushBehavior = CircleMidSet;
127 break;
128 case LineStartSet:
129 doPaint();
130 mStampReference = tilePosition();
131 break;
132 case CircleMidSet:
133 doPaint();
134 break;
135 case Paint:
136 beginPaint();
137 break;
138 case Free:
139 beginPaint();
140 mBrushBehavior = Paint;
141 break;
142 case Capture:
143 break;
144 }
145 return;
146 } else if (event->button() == Qt::RightButton && event->modifiers() == Qt::NoModifier) {
147 beginCapture();
148 return;
149 }
150 }
151
152 AbstractTileTool::mousePressed(event);
153 }
154
mouseReleased(QGraphicsSceneMouseEvent * event)155 void StampBrush::mouseReleased(QGraphicsSceneMouseEvent *event)
156 {
157 switch (mBrushBehavior) {
158 case LineStartSet:
159 if (event->button() == Qt::LeftButton) {
160 if (mStampReference != tilePosition()) {
161 doPaint();
162 mBrushBehavior = Line;
163 }
164 }
165 break;
166 case CircleMidSet:
167 if (event->button() == Qt::LeftButton) {
168 if (mStampReference != tilePosition()) {
169 doPaint();
170 updateBrushBehavior();
171 }
172 }
173 break;
174 case Capture:
175 if (event->button() == Qt::RightButton) {
176 endCapture();
177 mBrushBehavior = Free;
178 }
179 break;
180 case Paint:
181 if (event->button() == Qt::LeftButton) {
182 mBrushBehavior = Free;
183
184 // allow going over different variations by repeatedly clicking
185 updatePreview();
186 }
187 break;
188 default:
189 // do nothing?
190 break;
191 }
192 }
193
modifiersChanged(Qt::KeyboardModifiers modifiers)194 void StampBrush::modifiersChanged(Qt::KeyboardModifiers modifiers)
195 {
196 mModifiers = modifiers;
197
198 if (!mStamp.isEmpty() || mIsWangFill)
199 updateBrushBehavior();
200 }
201
updateBrushBehavior()202 void StampBrush::updateBrushBehavior()
203 {
204 BrushBehavior brushBehavior = mBrushBehavior;
205
206 if (mModifiers & Qt::ShiftModifier) {
207 if (mModifiers & Qt::ControlModifier) {
208 if (brushBehavior == LineStartSet) {
209 brushBehavior = CircleMidSet;
210 } else if (brushBehavior != CircleMidSet) {
211 brushBehavior = Circle;
212 }
213 } else {
214 if (brushBehavior == CircleMidSet) {
215 brushBehavior = LineStartSet;
216 } else if (brushBehavior != LineStartSet) {
217 brushBehavior = Line;
218 }
219 }
220 } else if (brushBehavior != Paint && brushBehavior != Capture) {
221 brushBehavior = Free;
222 }
223
224 if (mBrushBehavior != brushBehavior) {
225 mBrushBehavior = brushBehavior;
226 updatePreview();
227 }
228 }
229
languageChanged()230 void StampBrush::languageChanged()
231 {
232 setName(tr("Stamp Brush"));
233
234 mStampActions->languageChanged();
235 }
236
mapDocumentChanged(MapDocument * oldDocument,MapDocument * newDocument)237 void StampBrush::mapDocumentChanged(MapDocument *oldDocument,
238 MapDocument *newDocument)
239 {
240 AbstractTileTool::mapDocumentChanged(oldDocument, newDocument);
241
242 if (oldDocument) {
243 disconnect(oldDocument, &MapDocument::tileProbabilityChanged,
244 this, &StampBrush::invalidateRandomCache);
245 }
246
247 if (newDocument) {
248 invalidateRandomCache();
249 updatePreview();
250 connect(newDocument, &MapDocument::tileProbabilityChanged,
251 this, &StampBrush::invalidateRandomCache);
252 }
253 }
254
findTileLayerByName(const Map & map,const QString & name)255 static TileLayer *findTileLayerByName(const Map &map, const QString &name)
256 {
257 return static_cast<TileLayer*>(map.findLayer(name, Layer::TileLayerType));
258 }
259
targetLayers() const260 QList<Layer *> StampBrush::targetLayers() const
261 {
262 if (mIsRandom || mIsWangFill || mStamp.isEmpty())
263 return AbstractTileTool::targetLayers();
264
265 return targetLayersForStamp(mStamp);
266 }
267
268 /**
269 * Updates the list used random stamps.
270 * This is done by taking all non-null tiles from the original stamp mStamp.
271 */
updateRandomList()272 void StampBrush::updateRandomList()
273 {
274 mRandomCellPicker.clear();
275
276 if (!mIsRandom)
277 return;
278
279 mMissingTilesets.clear();
280
281 for (const TileStampVariation &variation : mStamp.variations()) {
282 mapDocument()->unifyTilesets(variation.map, mMissingTilesets);
283
284 for (auto layer : variation.map->tileLayers())
285 for (const Cell &cell : *static_cast<TileLayer*>(layer))
286 if (const Tile *tile = cell.tile())
287 mRandomCellPicker.add(cell, tile->probability());
288 }
289 }
290
setStamp(const TileStamp & stamp)291 void StampBrush::setStamp(const TileStamp &stamp)
292 {
293 if (mStamp == stamp)
294 return;
295
296 mStamp = stamp;
297
298 invalidateRandomCache();
299 updatePreview();
300 }
301
populateToolBar(QToolBar * toolBar)302 void StampBrush::populateToolBar(QToolBar *toolBar)
303 {
304 mStampActions->populateToolBar(toolBar, mIsRandom, mIsWangFill);
305 }
306
setWangSet(WangSet * wangSet)307 void StampBrush::setWangSet(WangSet *wangSet)
308 {
309 mWangSet = wangSet;
310
311 mMissingTilesets.clear();
312
313 if (!wangSet)
314 return;
315
316 const SharedTileset &tileset = wangSet->tileset()->sharedPointer();
317
318 if (!mapDocument() || !mapDocument()->map()->tilesets().contains(tileset))
319 mMissingTilesets.append(tileset);
320 }
321
beginPaint()322 void StampBrush::beginPaint()
323 {
324 if (mBrushBehavior != Free)
325 return;
326
327 mBrushBehavior = Paint;
328 doPaint();
329 }
330
beginCapture()331 void StampBrush::beginCapture()
332 {
333 if (mBrushBehavior != Free)
334 return;
335
336 mBrushBehavior = Capture;
337 mCaptureStampHelper.beginCapture(tilePosition());
338
339 setStamp(TileStamp());
340 }
341
endCapture()342 void StampBrush::endCapture()
343 {
344 if (mBrushBehavior != Capture)
345 return;
346
347 mBrushBehavior = Free;
348
349 TileStamp stamp = mCaptureStampHelper.endCapture(*mapDocument(), tilePosition());
350 if (!stamp.isEmpty())
351 emit stampChanged(TileStamp(stamp));
352 else
353 updatePreview();
354 }
355
356 /**
357 * Merges the tile layer of its brush item into the current map.
358 *
359 * \a flags can be set to Mergeable, which applies to the undo command.
360 *
361 * \a paintedRegions is an optional argument that can be used to accumilate
362 * the regions touched for each tile layer.
363 */
doPaint(int flags,QHash<TileLayer *,QRegion> * paintedRegions)364 void StampBrush::doPaint(int flags, QHash<TileLayer*, QRegion> *paintedRegions)
365 {
366 // local reference to avoid issues when member gets cleared
367 SharedMap preview = mPreviewMap;
368 if (!preview)
369 return;
370
371 mapDocument()->paintTileLayers(preview.data(),
372 (flags & Mergeable) == Mergeable,
373 &mMissingTilesets,
374 paintedRegions);
375 }
376
377 struct PaintOperation
378 {
379 QPoint pos;
380 Map *stamp;
381 };
382
shiftRows(TileLayer * tileLayer,Map::StaggerIndex staggerIndex)383 static void shiftRows(TileLayer *tileLayer, Map::StaggerIndex staggerIndex)
384 {
385 tileLayer->resize(QSize(tileLayer->width() + 1, tileLayer->height()), QPoint());
386
387 for (int y = (staggerIndex + 1) & 1; y < tileLayer->height(); y += 2) {
388 for (int x = tileLayer->width() - 2; x >= 0; --x)
389 tileLayer->setCell(x + 1, y, tileLayer->cellAt(x, y));
390 tileLayer->setCell(0, y, Cell());
391 }
392 }
393
shiftColumns(TileLayer * tileLayer,Map::StaggerIndex staggerIndex)394 static void shiftColumns(TileLayer *tileLayer, Map::StaggerIndex staggerIndex)
395 {
396 tileLayer->resize(QSize(tileLayer->width(), tileLayer->height() + 1), QPoint());
397
398 for (int x = (staggerIndex + 1) & 1; x < tileLayer->width(); x += 2) {
399 for (int y = tileLayer->height() - 2; y >= 0; --y)
400 tileLayer->setCell(x, y + 1, tileLayer->cellAt(x, y));
401 tileLayer->setCell(x, 0, Cell());
402 }
403 }
404
drawPreviewLayer(const QVector<QPoint> & points)405 void StampBrush::drawPreviewLayer(const QVector<QPoint> &points)
406 {
407 mPreviewMap.clear();
408
409 if (mStamp.isEmpty() && !mIsWangFill)
410 return;
411
412 if (mIsRandom) {
413 if (!mRandomCacheValid) {
414 updateRandomList();
415 mRandomCacheValid = true;
416 }
417
418 if (mRandomCellPicker.isEmpty())
419 return;
420
421 QRect bounds;
422 for (const QPoint &p : points)
423 bounds |= QRect(p, p);
424
425 Map::Parameters mapParameters = mapDocument()->map()->parameters();
426 mapParameters.width = bounds.width();
427 mapParameters.height = bounds.height();
428 mapParameters.infinite = false;
429 SharedMap preview = SharedMap::create(mapParameters);
430
431 std::unique_ptr<TileLayer> previewLayer {
432 new TileLayer(QString(), bounds.topLeft(), bounds.size())
433 };
434
435 for (const QPoint &p : points) {
436 const Cell &cell = mRandomCellPicker.pick();
437 previewLayer->setCell(p.x() - bounds.left(),
438 p.y() - bounds.top(),
439 cell);
440 }
441
442 preview->addLayer(std::move(previewLayer));
443 preview->addTilesets(preview->usedTilesets());
444 mPreviewMap = preview;
445 } else if (mIsWangFill) {
446 if (!mWangSet)
447 return;
448
449 const TileLayer *tileLayer = currentTileLayer();
450 if (!tileLayer)
451 return;
452
453 QRegion paintedRegion;
454 for (const QPoint &p : points)
455 paintedRegion += QRect(p, p);
456
457 const QRect bounds = paintedRegion.boundingRect();
458
459 Map::Parameters mapParameters = mapDocument()->map()->parameters();
460 mapParameters.width = bounds.width();
461 mapParameters.height = bounds.height();
462 mapParameters.infinite = false;
463 SharedMap preview = SharedMap::create(mapParameters);
464
465 std::unique_ptr<TileLayer> previewLayer {
466 new TileLayer(QString(), bounds.topLeft(), bounds.size())
467 };
468
469 WangFiller wangFiller(*mWangSet, mapDocument()->renderer());
470
471 wangFiller.fillRegion(*previewLayer, *tileLayer, paintedRegion);
472
473 preview->addLayer(std::move(previewLayer));
474 preview->addTileset(mWangSet->tileset()->sharedPointer());
475 mPreviewMap = preview;
476 } else {
477 QRegion paintedRegion;
478 QVector<PaintOperation> operations;
479 QHash<const Map *, QRegion> regionCache;
480 QHash<const Map *, Map *> shiftedCopies;
481
482 mMissingTilesets.clear();
483
484 for (const QPoint &p : points) {
485 Map *map = mStamp.randomVariation().map;
486 mapDocument()->unifyTilesets(map, mMissingTilesets);
487
488 Map::StaggerAxis mapStaggerAxis = mapDocument()->map()->staggerAxis();
489
490 // if staggered map, makes sure stamp stays the same
491 if (mapDocument()->map()->isStaggered()
492 && ((mapStaggerAxis == Map::StaggerY) ? map->height() > 1 : map->width() > 1)) {
493
494 Map::StaggerIndex mapStaggerIndex = mapDocument()->map()->staggerIndex();
495 Map::StaggerIndex stampStaggerIndex = map->staggerIndex();
496
497 if (mapStaggerAxis == Map::StaggerY) {
498 bool topIsOdd = (p.y() - map->height() / 2) & 1;
499
500 if ((stampStaggerIndex == mapStaggerIndex) == topIsOdd) {
501 Map *shiftedMap = shiftedCopies.value(map);
502 if (!shiftedMap) {
503 shiftedMap = map->clone().release();
504 shiftedCopies.insert(map, shiftedMap);
505
506 LayerIterator it(shiftedMap, Layer::TileLayerType);
507 while (auto tileLayer = static_cast<TileLayer*>(it.next()))
508 shiftRows(tileLayer, stampStaggerIndex);
509 }
510 map = shiftedMap;
511 }
512 } else {
513 bool leftIsOdd = (p.x() - map->width() / 2) & 1;
514
515 if ((stampStaggerIndex == mapStaggerIndex) == leftIsOdd) {
516 Map *shiftedMap = shiftedCopies.value(map);
517 if (!shiftedMap) {
518 shiftedMap = map->clone().release();
519 shiftedCopies.insert(map, shiftedMap);
520
521 LayerIterator it(shiftedMap, Layer::TileLayerType);
522 while (auto tileLayer = static_cast<TileLayer*>(it.next()))
523 shiftColumns(tileLayer, stampStaggerIndex);
524 }
525 map = shiftedMap;
526 }
527 }
528 }
529
530 QRegion stampRegion;
531 if (regionCache.contains(map)) {
532 stampRegion = regionCache.value(map);
533 } else {
534 stampRegion = map->tileRegion();
535 regionCache.insert(map, stampRegion);
536 }
537
538 QPoint centered(p.x() - map->width() / 2,
539 p.y() - map->height() / 2);
540
541 const QRegion region = stampRegion.translated(centered.x(),
542 centered.y());
543 if (!paintedRegion.intersects(region)) {
544 paintedRegion += region;
545
546 PaintOperation op = { centered, map };
547 operations.append(op);
548 }
549 }
550
551 const QRect bounds = paintedRegion.boundingRect();
552
553 Map::Parameters mapParameters = mapDocument()->map()->parameters();
554 mapParameters.width = bounds.width();
555 mapParameters.height = bounds.height();
556 mapParameters.infinite = false;
557 SharedMap preview = SharedMap::create(mapParameters);
558
559 for (const PaintOperation &op : operations) {
560 LayerIterator layerIterator(op.stamp, Layer::TileLayerType);
561 while (auto tileLayer = static_cast<TileLayer*>(layerIterator.next())) {
562 TileLayer *target = findTileLayerByName(*preview, tileLayer->name());
563 if (!target) {
564 target = new TileLayer(tileLayer->name(), bounds.topLeft(), bounds.size());
565 preview->addLayer(target);
566 }
567 target->merge(op.pos - bounds.topLeft() + tileLayer->position(), tileLayer);
568 }
569 }
570
571 qDeleteAll(shiftedCopies);
572
573 preview->addTilesets(preview->usedTilesets());
574 mPreviewMap = preview;
575 }
576 }
577
578 /**
579 * Updates the position of the brush item based on the mouse position.
580 */
updatePreview()581 void StampBrush::updatePreview()
582 {
583 updatePreview(tilePosition());
584 }
585
updatePreview(QPoint tilePos)586 void StampBrush::updatePreview(QPoint tilePos)
587 {
588 if (!mapDocument())
589 return;
590
591 QRegion tileRegion;
592
593 if (mBrushBehavior == Capture) {
594 mPreviewMap.clear();
595 tileRegion = mCaptureStampHelper.capturedArea(tilePos);
596 } else {
597 switch (mBrushBehavior) {
598 case LineStartSet:
599 drawPreviewLayer(pointsOnLine(mStampReference, tilePos));
600 break;
601 case CircleMidSet:
602 drawPreviewLayer(pointsOnEllipse(mStampReference, tilePos));
603 break;
604 case Capture:
605 // already handled above
606 break;
607 case Circle:
608 // while finding the mid point, there is no need to show
609 // the (maybe bigger than 1x1) stamp
610 mPreviewMap.clear();
611 break;
612 case Line:
613 case Free:
614 case Paint:
615 drawPreviewLayer(QVector<QPoint>() << tilePos);
616 break;
617 }
618
619 if (mPreviewMap)
620 tileRegion = mPreviewMap->tileRegion();
621
622 if (tileRegion.isEmpty())
623 tileRegion = QRect(tilePos, tilePos);
624 }
625
626 brushItem()->setMap(mPreviewMap, tileRegion);
627 }
628
setRandom(bool value)629 void StampBrush::setRandom(bool value)
630 {
631 if (mIsRandom == value)
632 return;
633
634 mIsRandom = value;
635
636 if (mIsRandom) {
637 mIsWangFill = false;
638 mStampActions->wangFill()->setChecked(false);
639 }
640
641 invalidateRandomCache();
642 updatePreview();
643 }
644
setWangFill(bool value)645 void StampBrush::setWangFill(bool value)
646 {
647 if (mIsWangFill == value)
648 return;
649
650 mIsWangFill = value;
651
652 if (mIsWangFill) {
653 mIsRandom = false;
654 mStampActions->random()->setChecked(false);
655 }
656
657 updatePreview();
658 }
659
invalidateRandomCache()660 void StampBrush::invalidateRandomCache()
661 {
662 mRandomCacheValid = false;
663 }
664
665 #include "moc_stampbrush.cpp"
666