From 5ed684e533edcd1ee31e2cf2ddb282737a64e49f Mon Sep 17 00:00:00 2001 From: Manu Evans Date: Thu, 17 May 2012 15:27:50 +0300 Subject: [PATCH] Added initial terrain painting tool. --- src/libtiled/mapreader.cpp | 35 +++ src/libtiled/mapwriter.cpp | 35 ++- src/libtiled/tile.h | 36 ++- src/libtiled/tileset.h | 36 +++ src/tiled/mainwindow.cpp | 3 + src/tiled/mainwindow.h | 2 + src/tiled/terrainbrush.cpp | 461 +++++++++++++++++++++++++++++++++++++ src/tiled/terrainbrush.h | 138 +++++++++++ src/tiled/tiled.pro | 6 +- 9 files changed, 745 insertions(+), 7 deletions(-) create mode 100644 src/tiled/terrainbrush.cpp create mode 100644 src/tiled/terrainbrush.h diff --git a/src/libtiled/mapreader.cpp b/src/libtiled/mapreader.cpp index 24c330fb1b..712795811d 100644 --- a/src/libtiled/mapreader.cpp +++ b/src/libtiled/mapreader.cpp @@ -78,6 +78,7 @@ class MapReaderPrivate Tileset *readTileset(); void readTilesetTile(Tileset *tileset); void readTilesetImage(Tileset *tileset); + void readTilesetTerrainTypes(Tileset *tileset); TileLayer *readLayer(); void readLayerData(TileLayer *tileLayer); @@ -284,6 +285,8 @@ Tileset *MapReaderPrivate::readTileset() tileset->mergeProperties(readProperties()); } else if (xml.name() == "image") { readTilesetImage(tileset); + } else if (xml.name() == "terraintypes") { + readTilesetTerrainTypes(tileset); } else { readUnknownElement(); } @@ -322,6 +325,19 @@ void MapReaderPrivate::readTilesetTile(Tileset *tileset) // TODO: Add support for individual tiles (then it needs to be added here) + // Read tile quadrant terrain ids + QString terrain = atts.value(QLatin1String("terrain")).toString(); + if (!terrain.isEmpty()) { + Tile *tile = tileset->tileAt(id); + QStringList quadrants = terrain.split(QLatin1String(",")); + if (quadrants.size() == 4) { + for (int i = 0; i < 4; ++i) { + int t = quadrants[i].isEmpty() ? -1 : quadrants[i].toInt(); + tile->setCornerTerrainType(i, t); + } + } + } + while (xml.readNextStartElement()) { if (xml.name() == "properties") { Tile *tile = tileset->tileAt(id); @@ -359,6 +375,25 @@ void MapReaderPrivate::readTilesetImage(Tileset *tileset) xml.skipCurrentElement(); } +void MapReaderPrivate::readTilesetTerrainTypes(Tileset *tileset) +{ + Q_ASSERT(xml.isStartElement() && xml.name() == "terraintypes"); + + while (xml.readNextStartElement()) { + if (xml.name() == "terrain") { + const QXmlStreamAttributes atts = xml.attributes(); + QString name = atts.value(QLatin1String("name")).toString(); + int tile = atts.value(QLatin1String("tile")).toString().toInt(); + + tileset->addTerrainType(name, tile); + + xml.skipCurrentElement(); + } + else + readUnknownElement(); + } +} + static void readLayerAttributes(Layer *layer, const QXmlStreamAttributes &atts) { diff --git a/src/libtiled/mapwriter.cpp b/src/libtiled/mapwriter.cpp index a374cceb9a..de057ffa76 100644 --- a/src/libtiled/mapwriter.cpp +++ b/src/libtiled/mapwriter.cpp @@ -193,6 +193,19 @@ void MapWriterPrivate::writeMap(QXmlStreamWriter &w, const Map *map) w.writeEndElement(); } +static QString makeTerrainAttribute(const Tile *tile) +{ + QString terrain; + for (int i = 0; i < 4; ++i ) { + if (i > 0) + terrain += QLatin1String(","); + int t = tile->cornerTerrainType(i); + if (t > -1) + terrain += QString::number(t); + } + return terrain; +} + void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset *tileset, uint firstGid) { @@ -259,14 +272,32 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset *tileset, w.writeEndElement(); } + // Write the terrain types + if (tileset->terrainTypeCount() > 0) { + w.writeStartElement(QLatin1String("terraintypes")); + for (int i = 0; i < tileset->terrainTypeCount(); ++i) { + TerrainType* tt = tileset->terrainType(i); + w.writeStartElement(QLatin1String("terrain")); + w.writeAttribute(QLatin1String("name"), tt->name()); +// w.writeAttribute(QLatin1String("color"), tt->color()); + w.writeAttribute(QLatin1String("tile"), QString::number(tt->paletteImageTile())); + w.writeEndElement(); + } + w.writeEndElement(); + } + // Write the properties for those tiles that have them for (int i = 0; i < tileset->tileCount(); ++i) { const Tile *tile = tileset->tileAt(i); const Properties properties = tile->properties(); - if (!properties.isEmpty()) { + unsigned int terrain = tile->terrain(); + if (!properties.isEmpty() || terrain != 0xFFFFFFFF) { w.writeStartElement(QLatin1String("tile")); w.writeAttribute(QLatin1String("id"), QString::number(i)); - writeProperties(w, properties); + if (terrain != 0xFFFFFFFF) + w.writeAttribute(QLatin1String("terrain"), makeTerrainAttribute(tile)); + if (!properties.isEmpty()) + writeProperties(w, properties); w.writeEndElement(); } } diff --git a/src/libtiled/tile.h b/src/libtiled/tile.h index b512622a64..3e2fac2cc5 100644 --- a/src/libtiled/tile.h +++ b/src/libtiled/tile.h @@ -31,20 +31,20 @@ #define TILE_H #include "object.h" +#include "tileset.h" #include namespace Tiled { -class Tileset; - class TILEDSHARED_EXPORT Tile : public Object { public: Tile(const QPixmap &image, int id, Tileset *tileset): mId(id), mTileset(tileset), - mImage(image) + mImage(image), + mTerrain(-1) {} /** @@ -82,10 +82,40 @@ class TILEDSHARED_EXPORT Tile : public Object */ QSize size() const { return mImage.size(); } + /** + * Returns the TerrainType of a given corner. + */ + TerrainType *cornerTerrain(int corner) const { return mTileset->terrainType(cornerTerrainType(corner)); } + + /** + * Returns the terrain at a given corner. + */ + int cornerTerrainType(int corner) const { unsigned int t = (terrain() >> (3 - corner)*8) & 0xFF; return t == 0xFF ? -1 : (int)t; } + + /** + * Set the terrain type of a given corner. + */ + void setCornerTerrainType(int corner, int terrain) + { + unsigned int mask = 0xFF << (3 - corner)*8; + unsigned int insert = terrain << (3 - corner)*8; + mTerrain = (mTerrain & ~mask) | (insert & mask); + } + + /** + * Functions to get various terrain type information from tiles. + */ + unsigned short topEdge() const { return terrain() >> 16; } + unsigned short bottomEdge() const { return terrain() & 0xFFFF; } + unsigned short leftEdge() const { return((terrain() >> 16) & 0xFF00) | ((terrain() >> 8) & 0xFF); } + unsigned short rightEdge() const { return ((terrain() >> 8) & 0xFF00) | (terrain() & 0xFF); } + unsigned int terrain() const { return this == NULL ? 0xFFFFFFFF : mTerrain; } // HACK: NULL Tile has 'none' terrain type. + private: int mId; Tileset *mTileset; QPixmap mImage; + unsigned int mTerrain; }; } // namespace Tiled diff --git a/src/libtiled/tileset.h b/src/libtiled/tileset.h index 99c7ba67db..1ead0b5ca1 100644 --- a/src/libtiled/tileset.h +++ b/src/libtiled/tileset.h @@ -43,6 +43,26 @@ namespace Tiled { class Tile; +class TerrainType +{ +public: + TerrainType(int id, QString name, int imageTile): + mId(id), + mName(name), + mImageTile(imageTile) + { + } + + int id() const { return mId; } + QString name() const { return mName; } + int paletteImageTile() const { return mImageTile; } + +private: + int mId; + QString mName; + int mImageTile; +}; + /** * A tileset, representing a set of tiles. * @@ -213,6 +233,21 @@ class TILEDSHARED_EXPORT Tileset : public Object */ int columnCountForWidth(int width) const; + /** + * Returns the number of terrain types in this tileset. + */ + int terrainTypeCount() const { return mTerrainTypes.size(); } + + /** + * Returns the number of tiles in this tileset. + */ + TerrainType *terrainType(int terrain) const { return terrain >= 0 ? mTerrainTypes[terrain] : NULL; } + + /** + * Add a new terrain type. + */ + void addTerrainType(QString name, int imageTile); + private: QString mName; QString mFileName; @@ -227,6 +262,7 @@ class TILEDSHARED_EXPORT Tileset : public Object int mImageHeight; int mColumnCount; QList mTiles; + QList mTerrainTypes; }; } // namespace Tiled diff --git a/src/tiled/mainwindow.cpp b/src/tiled/mainwindow.cpp index 6e31547e12..d602375f10 100644 --- a/src/tiled/mainwindow.cpp +++ b/src/tiled/mainwindow.cpp @@ -62,6 +62,7 @@ #include "quickstampmanager.h" #include "saveasimagedialog.h" #include "stampbrush.h" +#include "terrainbrush.h" #include "tilelayer.h" #include "tileselectiontool.h" #include "tileset.h" @@ -317,6 +318,7 @@ MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags) setThemeIcon(mUi->actionAbout, "help-about"); mStampBrush = new StampBrush(this); + mTerrainBrush = new TerrainBrush(this); mBucketFillTool = new BucketFillTool(this); CreateObjectTool *tileObjectsTool = new CreateObjectTool( CreateObjectTool::CreateTile, this); @@ -341,6 +343,7 @@ MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags) ToolManager *toolManager = ToolManager::instance(); toolManager->registerTool(mStampBrush); + toolManager->registerTool(mTerrainBrush); toolManager->registerTool(mBucketFillTool); toolManager->registerTool(new Eraser(this)); toolManager->registerTool(new TileSelectionTool(this)); diff --git a/src/tiled/mainwindow.h b/src/tiled/mainwindow.h index b45de118e0..c85e73c8aa 100644 --- a/src/tiled/mainwindow.h +++ b/src/tiled/mainwindow.h @@ -51,6 +51,7 @@ class MapDocumentActionHandler; class MapScene; class StampBrush; class BucketFillTool; +class TerrainBrush; class TilesetDock; class MapView; class CommandButton; @@ -210,6 +211,7 @@ public slots: StampBrush *mStampBrush; BucketFillTool *mBucketFillTool; + TerrainBrush *mTerrainBrush; ClipboardManager *mClipboardManager; diff --git a/src/tiled/terrainbrush.cpp b/src/tiled/terrainbrush.cpp new file mode 100644 index 0000000000..576f089d69 --- /dev/null +++ b/src/tiled/terrainbrush.cpp @@ -0,0 +1,461 @@ +/* + * terrainbrush.cpp + * Copyright 2009-2010, Thorbjørn Lindeijer + * Copyright 2010, Stefan Beller + * Copyright 2012, Manu Evans + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "terrainbrush.h" + +#include "brushitem.h" +#include "map.h" +#include "mapdocument.h" +#include "mapscene.h" +#include "painttilelayer.h" +#include "tilelayer.h" +#include "tileset.h" +#include "tile.h" + +#include +#include + +using namespace Tiled; +using namespace Tiled::Internal; + +TerrainBrush::TerrainBrush(QObject *parent) + : AbstractTileTool(tr("Terrain Brush"), + QIcon(QLatin1String( + ":images/22x22/stock-tool-clone.png")), + QKeySequence(tr("T")), + parent) + , mTerrain(NULL) + , mPaintX(0), mPaintY(0) + , mOffsetX(0), mOffsetY(0) + , mBrushBehavior(Free) + , mLineReferenceX(0) + , mLineReferenceY(0) +{ +} + +TerrainBrush::~TerrainBrush() +{ +} + + +/** + * Returns the lists of points on a line from (x0,y0) to (x1,y1). + * + * This is an implementation of bresenhams line algorithm, initially copied + * from http://en.wikipedia.org/wiki/Bresenham's_line_algorithm#Optimization + * changed to C++ syntax. + */ +static QVector calculateLine(int x0, int y0, int x1, int y1) +{ + QVector ret; + + bool steep = qAbs(y1 - y0) > qAbs(x1 - x0); + if (steep) { + qSwap(x0, y0); + qSwap(x1, y1); + } + if (x0 > x1) { + qSwap(x0, x1); + qSwap(y0, y1); + } + const int deltax = x1 - x0; + const int deltay = qAbs(y1 - y0); + int error = deltax / 2; + int ystep; + int y = y0; + + if (y0 < y1) + ystep = 1; + else + ystep = -1; + + for (int x = x0; x < x1 + 1 ; x++) { + if (steep) + ret += QPoint(y, x); + else + ret += QPoint(x, y); + error = error - deltay; + if (error < 0) { + y = y + ystep; + error = error + deltax; + } + } + + return ret; +} + +void TerrainBrush::tilePositionChanged(const QPoint &pos) +{ + switch (mBrushBehavior) { + case Paint: + { + int x = mPaintX; + int y = mPaintY; + foreach (const QPoint &p, calculateLine(x, y, pos.x(), pos.y())) { + updateBrush(p); + doPaint(true, p.x(), p.y()); + } + // HACK-ish: because the line may traverse in the reverse direction, updateBrush() leaves these at the line start point + mPaintX = pos.x(); + mPaintY = pos.y(); + break; + } + case LineStartSet: + // TODO... +// calculateLine(mLineReferenceX, mLineReferenceY, pos.x(), pos.y()) + break; + case Line: + case Free: + updateBrush(pos); + break; + } +} + +void TerrainBrush::mousePressed(QGraphicsSceneMouseEvent *event) +{ + if (!brushItem()->isVisible()) + return; + + if (event->button() == Qt::LeftButton) { + switch (mBrushBehavior) { + case Line: + mLineReferenceX = mPaintX; + mLineReferenceY = mPaintY; + mBrushBehavior = LineStartSet; + break; + case LineStartSet: + doPaint(false, mPaintX, mPaintY); + mLineReferenceX = mPaintX; + mLineReferenceY = mPaintY; + break; + case Paint: + beginPaint(); + break; + case Free: + beginPaint(); + mBrushBehavior = Paint; + break; + } + } else { + if (event->button() == Qt::RightButton) + capture(); + } +} + +void TerrainBrush::mouseReleased(QGraphicsSceneMouseEvent *event) +{ + switch (mBrushBehavior) { + case Paint: + if (event->button() == Qt::LeftButton) + mBrushBehavior = Free; + default: + // do nothing? + break; + } +} + +void TerrainBrush::modifiersChanged(Qt::KeyboardModifiers modifiers) +{ + if (modifiers & Qt::ShiftModifier) { + mBrushBehavior = Line; + } else { + mBrushBehavior = Free; + } + + updateBrush(tilePosition()); +} + +void TerrainBrush::languageChanged() +{ + setName(tr("Terain Brush")); + setShortcut(QKeySequence(tr("T"))); +} + +void TerrainBrush::mapDocumentChanged(MapDocument *oldDocument, + MapDocument *newDocument) +{ + AbstractTileTool::mapDocumentChanged(oldDocument, newDocument); + + // Reset the brush, since it probably became invalid + brushItem()->setTileRegion(QRegion()); + setTerrain(NULL); + + // hack + Tileset *t = newDocument->map()->tilesets().at(0); + setTerrain(t->terrainType(2)); +} + +void TerrainBrush::setTerrain(TerrainType *terrain) +{ + if (mTerrain == terrain) + return; + + mTerrain = terrain; + + updateBrush(tilePosition()); +} + +void TerrainBrush::beginPaint() +{ + if (mBrushBehavior != Free) + return; + + mBrushBehavior = Paint; + doPaint(false, mPaintX, mPaintY); +} + +void TerrainBrush::capture() +{ + TileLayer *tileLayer = currentTileLayer(); + Q_ASSERT(tileLayer); + + // TODO: we need to know which corner the mouse is closest to... + + const Cell &cell = tileLayer->cellAt(tilePosition()); + TerrainType *t = cell.tile->cornerTerrain(0); + setTerrain(t); +} + +void TerrainBrush::doPaint(bool mergeable, int whereX, int whereY) +{ + TileLayer *stamp = brushItem()->tileLayer(); + + if (!stamp) + return; + + // This method shouldn't be called when current layer is not a tile layer + TileLayer *tileLayer = currentTileLayer(); + Q_ASSERT(tileLayer); + + whereX -= mOffsetX; + whereY -= mOffsetY; + + if (!tileLayer->bounds().intersects(QRect(whereX, whereY, stamp->width(), stamp->height()))) + return; + + PaintTileLayer *paint = new PaintTileLayer(mapDocument(), tileLayer, whereX, whereY, stamp); + paint->setMergeable(mergeable); + mapDocument()->undoStack()->push(paint); + mapDocument()->emitRegionEdited(brushItem()->tileRegion(), tileLayer); +} + +static inline unsigned int makeTerrain(int t) +{ + t &= 0xFF; + return t << 24 | t << 16 | t << 8 | t; +} + +static inline unsigned int makeTerrain(int tl, int tr, int bl, int br) +{ + return (tl & 0xFF) << 24 | (tr & 0xFF) << 16 | (bl & 0xFF) << 8 | (br & 0xFF); +} + +Tile *TerrainBrush::findBestTile(unsigned int terrain, unsigned int considerationMask) +{ + // if all quadrants are set to 'no terrain', then the 'empty' tile is the only choice we can deduce + if (terrain == 0xFFFFFFFF) + return NULL; + + QList matches; + int confidence = 0; + + // TODO: should we scan each tileset? we really need to use gId's for terrains aswell as tiles i guess... + Tileset *tileset = mapDocument()->map()->tilesets().at(0); + + int tileCount = tileset->tileCount(); + for (int i = 0; i < tileCount; ++i) { + Tile *t = tileset->tileAt(i); + if ((t->terrain() & considerationMask) != (terrain & considerationMask)) + continue; + + // prefer tiles with the most possible matches to the requested terrain + int matchingQuadrants = 0; + if ((t->terrain() & 0xFF000000) == (terrain & 0xFF000000)) + ++matchingQuadrants; + if ((t->terrain() & 0xFF0000) == (terrain & 0xFF0000)) + ++matchingQuadrants; + if ((t->terrain() & 0xFF00) == (terrain & 0xFF00)) + ++matchingQuadrants; + if ((t->terrain() & 0xFF) == (terrain & 0xFF)) + ++matchingQuadrants; + + if (matchingQuadrants >= confidence) { + if (matchingQuadrants > confidence) + matches.clear(); + confidence = matchingQuadrants; + + matches.push_back(t); + } + } + + // choose a candidate at random, with consideration for terrain probability + if (!matches.isEmpty()) + return matches[0]; + + return NULL; +} + +void TerrainBrush::updateBrush(const QPoint &cursorPos) +{ + // get the current tile layer (TODO: what if the current layer isn't a tile layer?) + TileLayer *currentLayer = currentTileLayer(); + Q_ASSERT(currentLayer); + + int layerWidth = currentLayer->width(); + int layerHeight = currentLayer->height(); + int numTiles = layerWidth * layerHeight; + + if (!currentLayer->bounds().contains(cursorPos)) + return; + + // allocate a buffer to build the terrain tilemap (TODO: this could be retained per layer to save regular allocation) + Tile **newTerrain = new Tile*[numTiles]; + + // allocate a buffer of flags for each tile that may be considered (TODO: this could be retained per layer to save regular allocation) + char *checked = new char[numTiles]; + memset(checked, 0, numTiles); + + // create a consideration list, and push the starting point + QList transitionList; + transitionList.push_back(cursorPos); + bool first = true; + + QRect brushRect(cursorPos, cursorPos); + + // produce terrain with transitions using a simple, relative naive approach (considers each tile once, and doesn't allow re-consideration if selection was bad) + while (!transitionList.isEmpty()) { + // get the next point in the consideration list + QPoint p = transitionList.front(); + transitionList.pop_front(); + int x = p.x(), y = p.y(); + int i = y*layerWidth + x; + + // if we have already considered this point, skip to the next + if (checked[i]) + continue; + + // get the relevant tiles + const Tile *tile = currentLayer->cellAt(p).tile; + Tile *paste = NULL; + + // find a tile that best suits this position + if (first) { + // the first tile is special, we will just paste the selected terrain and add the surroundings for consideration + + // TODO: if we're painting quadrants rather than full tiles, we need to set the appropriate mask + paste = mTerrain ? findBestTile(makeTerrain(mTerrain->id()), 0xFFFFFFFF) : NULL; + newTerrain[y*layerWidth + x] = paste; + first = false; + } else { + // following tiles each need consideration against their surroundings + unsigned int preferredTerrain = tile->terrain(); + unsigned int mask = 0; + + // depending which connections have been set, we update the preferred terrain of the tile accordingly + if (y > 0 && checked[i - layerWidth]) { + preferredTerrain = (newTerrain[i - layerWidth]->terrain() << 16) | (preferredTerrain & 0x0000FFFF); + mask |= 0xFFFF0000; + } + if (y < layerHeight - 1 && checked[i + layerWidth]) { + preferredTerrain = (newTerrain[i + layerWidth]->terrain() >> 16) | (preferredTerrain & 0xFFFF0000); + mask |= 0x0000FFFF; + } + if (x > 0 && checked[i - 1]) { + preferredTerrain = ((newTerrain[i - 1]->terrain() << 8) & 0xFF00FF00) | (preferredTerrain & 0x00FF00FF); + mask |= 0xFF00FF00; + } + if (x < layerWidth - 1 && checked[i + 1]) { + preferredTerrain = ((newTerrain[i + 1]->terrain() >> 8) & 0x00FF00FF) | (preferredTerrain & 0xFF00FF00); + mask |= 0x00FF00FF; + } + + paste = findBestTile(preferredTerrain, mask); + newTerrain[i] = paste; + } + + // update the brush rect and consideration map + brushRect |= QRect(p, p); + checked[i] = true; + + // consider surrounding tiles + if (y > 0 && !checked[i - layerWidth]) { + const Tile *above = currentLayer->cellAt(x, y - 1).tile; + if (paste->topEdge() != above->bottomEdge()) + transitionList.push_back(QPoint(x, y - 1)); + } + if (y < layerHeight - 1 && !checked[i + layerWidth]) { + const Tile *below = currentLayer->cellAt(x, y + 1).tile; + if (paste->bottomEdge() != below->topEdge()) + transitionList.push_back(QPoint(x, y + 1)); + } + if (x > 0 && !checked[i - 1]) { + const Tile *left = currentLayer->cellAt(x - 1, y).tile; + if (paste->leftEdge() != left->rightEdge()) + transitionList.push_back(QPoint(x - 1, y)); + } + if (x < layerWidth - 1 && !checked[i + 1]) { + const Tile *right = currentLayer->cellAt(x + 1, y).tile; + if (paste->rightEdge() != right->leftEdge()) + transitionList.push_back(QPoint(x + 1, y)); + } + } + + // create a stamp for the terrain block + TileLayer *stamp = new TileLayer(QString(), 0, 0, brushRect.width(), brushRect.height()); + + for (int y = brushRect.top(); y <= brushRect.bottom(); ++y) { + for (int x = brushRect.left(); x <= brushRect.right(); ++x) { + int i = y*layerWidth + x; + if (!checked[i]) + continue; + + Tile *tile = newTerrain[i]; + if (tile) + stamp->setCell(x - brushRect.left(), y - brushRect.top(), Cell(tile)); + else { + // TODO: we need to do something to erase tiles where checked[i] is true, and newTerrain[i] is NULL + // is there an eraser stamp? investigate how the eraser works... + } + } + } + + // set the new tile layer as the brush + brushItem()->setTileLayer(stamp); + + delete[] checked; + delete[] newTerrain; + +/* + const QPoint tilePos = tilePosition(); + + if (!brushItem()->tileLayer()) { + brushItem()->setTileRegion(QRect(tilePos, QSize(1, 1))); + } +*/ + + brushItem()->setTileLayerPosition(QPoint(brushRect.left(), brushRect.top())); + + mPaintX = cursorPos.x(); + mPaintY = cursorPos.y(); + mOffsetX = cursorPos.x() - brushRect.left(); + mOffsetY = cursorPos.y() - brushRect.top(); +} diff --git a/src/tiled/terrainbrush.h b/src/tiled/terrainbrush.h new file mode 100644 index 0000000000..35ca73a7ab --- /dev/null +++ b/src/tiled/terrainbrush.h @@ -0,0 +1,138 @@ +/* + * terrainbrush.h + * Copyright 2009-2010, Thorbjørn Lindeijer + * Copyright 2010, Stefan Beller + * Copyright 2012, Manu Evans + * + * This file is part of Tiled. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#ifndef TERRAINBRUSH_H +#define TERRAINBRUSH_H + +#include "abstracttiletool.h" +#include "tilelayer.h" + +namespace Tiled { + +class Tile; +class TerrainType; + +namespace Internal { + +class MapDocument; + +/** + * Implements a tile brush that paints terrain with automatic transitions. + */ +class TerrainBrush : public AbstractTileTool +{ + Q_OBJECT + +public: + TerrainBrush(QObject *parent = 0); + ~TerrainBrush(); + + void mousePressed(QGraphicsSceneMouseEvent *event); + void mouseReleased(QGraphicsSceneMouseEvent *event); + + void modifiersChanged(Qt::KeyboardModifiers modifiers); + + void languageChanged(); + + /** + * Sets the stamp that is drawn when painting. The stamp brush takes + * ownership over the stamp layer. + */ + void setTerrain(TerrainType *terrain); + + /** + * This returns the actual tile layer which is used to define the current + * state. + */ + TerrainType *terrain() const { return mTerrain; } + +signals: + /** + * Emitted when the currently selected tiles changed. The stamp brush emits + * this signal instead of setting its stamp directly so that the fill tool + * also gets the new stamp. + */ + void currentTilesChanged(const TileLayer *tiles); + +protected: + void tilePositionChanged(const QPoint &tilePos); + + void mapDocumentChanged(MapDocument *oldDocument, + MapDocument *newDocument); + +private: + void beginPaint(); + + /** + * Merges the tile layer of its brush item into the current map. + * mergeable determines if this can be merged with similar actions for undo. + * whereX and whereY give an offset where to merge the brush items tilelayer + * into the current map. + */ + void doPaint(bool mergeable, int whereX, int whereY); + + void capture(); + + Tile *findBestTile(unsigned int terrain, unsigned int considerationMask); + + /** + * updates the brush given new coordinates. + */ + void updateBrush(const QPoint &cursorPos); + + /** + * mTerrain is the terrain we are currently painting + */ + TerrainType *mTerrain; + int mPaintX, mPaintY; + int mOffsetX, mOffsetY; + + /** + * There are several options how the stamp utility can be used. + * It must be one of the following: + */ + enum BrushBehavior { + Free, // nothing special: you can move the mouse, + // preview of the selection + Paint, // left mouse pressed: free painting + Line, // hold shift: a line + LineStartSet // when you have defined a starting point, + // cancel with right click + }; + + /** + * This stores the current behavior. + */ + BrushBehavior mBrushBehavior; + + /** + * The starting position needed for drawing lines and circles. + * When drawing lines, this point will be one end. + * When drawing circles this will be the midpoint. + */ + int mLineReferenceX, mLineReferenceY; +}; + +} // namespace Internal +} // namespace Tiled + +#endif // STAMPBRUSH_H diff --git a/src/tiled/tiled.pro b/src/tiled/tiled.pro index 3477cbfb54..247e7d9795 100644 --- a/src/tiled/tiled.pro +++ b/src/tiled/tiled.pro @@ -128,7 +128,8 @@ SOURCES += aboutdialog.cpp \ toolmanager.cpp \ undodock.cpp \ utils.cpp \ - zoomable.cpp + zoomable.cpp \ + terrainbrush.cpp HEADERS += aboutdialog.h \ abstractobjecttool.h \ @@ -224,7 +225,8 @@ HEADERS += aboutdialog.h \ undocommands.h \ undodock.h \ utils.h \ - zoomable.h + zoomable.h \ + terrainbrush.h macx { OBJECTIVE_SOURCES += macsupport.mm