diff --git a/CHANGELOG.md b/CHANGELOG.md index a5dea998..f3124e92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d ### Added - Support for [pokefirered](https://github.com/pret/pokefirered). Kanto fans rejoice! At long last porymap supports the FRLG decompilation project. +- Add ability to export map stitches with `File -> Export Map Stitch Image...`. ### Changed - Porymap now saves map and encounter json data in an order consistent with the upstream repos. This will provide more comprehensible diffs, among other things. @@ -20,6 +21,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix bug where pressing TAB key did not navigate through widgets in the wild encounter tables. - Fix bug that allowed selecting an invalid metatile in the metatile selector. + ## [3.0.1] - 2020-03-04 ### Fixed - Fix bug on Mac where tileset images were corrupted when saving. diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index d92666d5..d9b926bb 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2890,6 +2890,7 @@ + @@ -3172,6 +3173,11 @@ Themes... + + + Export Map Stitch Image... + + diff --git a/include/editor.h b/include/editor.h index a0ab0e7e..54a310df 100644 --- a/include/editor.h +++ b/include/editor.h @@ -131,6 +131,8 @@ public: void objectsView_onMouseMove(QMouseEvent *event); void objectsView_onMouseRelease(QMouseEvent *event); + int getBorderDrawDistance(int dimension); + private: void setConnectionItemsVisible(bool); void setBorderItemsVisible(bool, qreal = 1); @@ -147,7 +149,6 @@ private: void updateMirroredConnectionMap(MapConnection*, QString); void updateMirroredConnection(MapConnection*, QString, QString, bool isDelete = false); void updateEncounterFields(EncounterFields newFields); - int getBorderDrawDistance(int dimension); Event* createNewObjectEvent(); Event* createNewWarpEvent(); Event* createNewHealLocationEvent(); diff --git a/include/lib/orderedjson.h b/include/lib/orderedjson.h index 645b4336..54452314 100644 --- a/include/lib/orderedjson.h +++ b/include/lib/orderedjson.h @@ -62,6 +62,10 @@ #include #include +#include +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 1) + #include "qstringhash.h" +#endif #include "orderedmap.h" #ifdef _MSC_VER diff --git a/include/lib/orderedmap.h b/include/lib/orderedmap.h index df94d861..40fe08b8 100644 --- a/include/lib/orderedmap.h +++ b/include/lib/orderedmap.h @@ -1640,7 +1640,7 @@ private: * the erased element and all the ones after the erased element (including end()). * Otherwise all the iterators are invalidated if an erase occurs. */ -template, class KeyEqual = std::equal_to, @@ -2031,7 +2031,7 @@ public: - T& operator[](const Key& key) { return m_ht[key]; } + T& operator[](const Key& key) { return m_ht[key]; } T& operator[](Key&& key) { return m_ht[std::move(key)]; } @@ -2042,7 +2042,7 @@ public: * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. */ - size_type count(const Key& key, std::size_t precalculated_hash) const { + size_type count(const Key& key, std::size_t precalculated_hash) const { return m_ht.count(key, precalculated_hash); } @@ -2079,7 +2079,7 @@ public: /** * @copydoc find(const Key& key, std::size_t precalculated_hash) */ - const_iterator find(const Key& key, std::size_t precalculated_hash) const { + const_iterator find(const Key& key, std::size_t precalculated_hash) const { return m_ht.find(key, precalculated_hash); } @@ -2124,7 +2124,7 @@ public: * Use the hash value 'precalculated_hash' instead of hashing the key. The hash value should be the same * as hash_function()(key). Usefull to speed-up the lookup if you already have the hash. */ - std::pair equal_range(const Key& key, std::size_t precalculated_hash) { + std::pair equal_range(const Key& key, std::size_t precalculated_hash) { return m_ht.equal_range(key, precalculated_hash); } @@ -2133,7 +2133,7 @@ public: /** * @copydoc equal_range(const Key& key, std::size_t precalculated_hash) */ - std::pair equal_range(const Key& key, std::size_t precalculated_hash) const { + std::pair equal_range(const Key& key, std::size_t precalculated_hash) const { return m_ht.equal_range(key, precalculated_hash); } diff --git a/include/lib/qstringhash.h b/include/lib/qstringhash.h new file mode 100644 index 00000000..10947d6f --- /dev/null +++ b/include/lib/qstringhash.h @@ -0,0 +1,19 @@ +#ifndef QSTRINGHASH_H +#define QSTRINGHASH_H + +#include +#include +#include + +// This is a custom hash function for QString so it can be used as +// a key in a std::hash structure. Qt 5.14 added this function, so +// this file should only be included in earlier versions. +namespace std { + template<> struct hash { + std::size_t operator()(const QString& s) const noexcept { + return static_cast(qHash(s)); + } + }; +} + +#endif // QSTRINGHASH_H diff --git a/include/mainwindow.h b/include/mainwindow.h index 27fe541c..619718b8 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -109,6 +109,7 @@ private slots: void currentMetatilesSelectionChanged(); void on_action_Export_Map_Image_triggered(); + void on_actionExport_Stitched_Map_Image_triggered(); void on_comboBox_ConnectionDirection_currentIndexChanged(const QString &arg1); void on_spinBox_ConnectionOffset_valueChanged(int offset); @@ -219,6 +220,7 @@ private: void closeSupplementaryWindows(); bool isProjectOpen(); + void showExportMapImageWindow(bool stitchMode); }; enum MapListUserRoles { diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index a6772ed1..0dedb204 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -15,7 +15,7 @@ class MapImageExporter : public QDialog Q_OBJECT public: - explicit MapImageExporter(QWidget *parent, Editor *editor); + explicit MapImageExporter(QWidget *parent, Editor *editor, bool stitchMode); ~MapImageExporter(); private: @@ -39,9 +39,12 @@ private: bool showGrid = false; bool showBorder = false; bool showCollision = false; + bool stitchMode = false; void updatePreview(); void saveImage(); + QPixmap getStitchedImage(QProgressDialog *progress, bool includeBorder); + QPixmap getFormattedMapPixmap(Map *map, bool ignoreBorder); private slots: void on_checkBox_Objects_stateChanged(int state); diff --git a/porymap.pro b/porymap.pro index ad12b9f0..5e686c78 100644 --- a/porymap.pro +++ b/porymap.pro @@ -134,7 +134,8 @@ HEADERS += include/core/block.h \ include/project.h \ include/settings.h \ include/log.h \ - include/ui/newtilesetdialog.h + include/ui/newtilesetdialog.h \ + include/lib/qstringhash.h FORMS += forms/mainwindow.ui \ forms/eventpropertiesframe.ui \ diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e0e64da0..a0f86678 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -28,6 +28,7 @@ #include #include #include +#include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), @@ -2080,13 +2081,21 @@ void MainWindow::onTilesetsSaved(QString primaryTilesetLabel, QString secondaryT this->editor->updateSecondaryTileset(secondaryTilesetLabel, true); } -void MainWindow::on_action_Export_Map_Image_triggered() -{ - if (!this->mapImageExporter) { - this->mapImageExporter = new MapImageExporter(this, this->editor); - connect(this->mapImageExporter, &QObject::destroyed, [=](QObject *) { this->mapImageExporter = nullptr; }); - this->mapImageExporter->setAttribute(Qt::WA_DeleteOnClose); - } +void MainWindow::on_action_Export_Map_Image_triggered() { + showExportMapImageWindow(false); +} + +void MainWindow::on_actionExport_Stitched_Map_Image_triggered() { + showExportMapImageWindow(true); +} + +void MainWindow::showExportMapImageWindow(bool stitchMode) { + if (this->mapImageExporter) + delete this->mapImageExporter; + + this->mapImageExporter = new MapImageExporter(this, this->editor, stitchMode); + connect(this->mapImageExporter, &QObject::destroyed, [=](QObject *) { this->mapImageExporter = nullptr; }); + this->mapImageExporter->setAttribute(Qt::WA_DeleteOnClose); if (!this->mapImageExporter->isVisible()) { this->mapImageExporter->show(); diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 2cdd45e7..f820e54e 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -7,13 +7,19 @@ #include #include -MapImageExporter::MapImageExporter(QWidget *parent_, Editor *editor_) : +#define STITCH_MODE_BORDER_DISTANCE 2 + +MapImageExporter::MapImageExporter(QWidget *parent_, Editor *editor_, bool stitchMode) : QDialog(parent_), ui(new Ui::MapImageExporter) { ui->setupUi(this); this->map = editor_->map; this->editor = editor_; + this->stitchMode = stitchMode; + + this->setWindowTitle(this->stitchMode ? "Export Map Stitch Image" : "Export Map Image"); + this->ui->groupBox_Connections->setVisible(!this->stitchMode); this->ui->comboBox_MapSelection->addItems(*editor->project->mapNames); this->ui->comboBox_MapSelection->setCurrentText(map->name); @@ -28,32 +34,192 @@ MapImageExporter::~MapImageExporter() { } void MapImageExporter::saveImage() { - QString defaultFilepath = QString("%1/%2.png").arg(editor->project->root).arg(map->name); - QString filepath = QFileDialog::getSaveFileName(this, "Export Map Image", defaultFilepath, + QString title = this->stitchMode ? "Export Map Stitch Image" : "Export Map Image"; + QString defaultFilename = this->stitchMode ? QString("Stitch_From_%1").arg(map->name) : map->name; + QString defaultFilepath = QString("%1/%2.png").arg(editor->project->root).arg(defaultFilename); + QString filepath = QFileDialog::getSaveFileName(this, title, defaultFilepath, "Image Files (*.png *.jpg *.bmp)"); if (!filepath.isEmpty()) { - this->preview.save(filepath); + if (this->stitchMode) { + QProgressDialog progress("Building map stitch...", "Cancel", 0, 1, this); + progress.setAutoClose(true); + progress.setWindowModality(Qt::WindowModal); + progress.setModal(true); + QPixmap pixmap = this->getStitchedImage(&progress, this->showBorder); + if (progress.wasCanceled()) { + progress.close(); + return; + } + pixmap.save(filepath); + progress.close(); + } else { + this->preview.save(filepath); + } this->close(); } } +struct StitchedMap { + int x; + int y; + Map* map; +}; + +QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool includeBorder) { + // Do a bread-first search to gather a collection of + // all reachable maps with their relative offsets. + QSet visited; + QList stitchedMaps; + QList unvisited; + unvisited.append(StitchedMap{0, 0, this->editor->map}); + + progress->setLabelText("Gathering stitched maps..."); + while (!unvisited.isEmpty()) { + if (progress->wasCanceled()) { + return QPixmap(); + } + progress->setMaximum(visited.size() + unvisited.size()); + progress->setValue(visited.size()); + + StitchedMap cur = unvisited.takeFirst(); + if (visited.contains(cur.map->name)) + continue; + visited.insert(cur.map->name); + stitchedMaps.append(cur); + + for (MapConnection *connection : cur.map->connections) { + if (connection->direction == "dive" || connection->direction == "emerge") + continue; + int x = cur.x; + int y = cur.y; + int offset = connection->offset.toInt(nullptr, 0); + Map *connectionMap = this->editor->project->loadMap(connection->map_name); + if (connection->direction == "up") { + x += offset; + y -= connectionMap->getHeight(); + } else if (connection->direction == "down") { + x += offset; + y += cur.map->getHeight(); + } else if (connection->direction == "left") { + x -= connectionMap->getWidth(); + y += offset; + } else if (connection->direction == "right") { + x += cur.map->getWidth(); + y += offset; + } + unvisited.append(StitchedMap{x, y, connectionMap}); + } + } + + // Determine the overall dimensions of the stitched maps. + int maxX = INT_MIN; + int minX = INT_MAX; + int maxY = INT_MIN; + int minY = INT_MAX; + for (StitchedMap map : stitchedMaps) { + int left = map.x; + int right = map.x + map.map->getWidth(); + int top = map.y; + int bottom = map.y + map.map->getHeight(); + if (left < minX) + minX = left; + if (right > maxX) + maxX = right; + if (top < minY) + minY = top; + if (bottom > maxY) + maxY = bottom; + } + + if (includeBorder) { + minX -= STITCH_MODE_BORDER_DISTANCE; + maxX += STITCH_MODE_BORDER_DISTANCE; + minY -= STITCH_MODE_BORDER_DISTANCE; + maxY += STITCH_MODE_BORDER_DISTANCE; + } + + // Draw the maps on the full canvas, while taking + // their respective offsets into account. + progress->setLabelText("Drawing stitched maps..."); + progress->setValue(0); + progress->setMaximum(stitchedMaps.size()); + int numDrawn = 0; + QPixmap stitchedPixmap((maxX - minX) * 16, (maxY - minY) * 16); + QPainter painter(&stitchedPixmap); + for (StitchedMap map : stitchedMaps) { + if (progress->wasCanceled()) { + return QPixmap(); + } + progress->setValue(numDrawn); + numDrawn++; + + int pixelX = (map.x - minX) * 16; + int pixelY = (map.y - minY) * 16; + if (includeBorder) { + pixelX -= STITCH_MODE_BORDER_DISTANCE * 16; + pixelY -= STITCH_MODE_BORDER_DISTANCE * 16; + } + QPixmap pixmap = this->getFormattedMapPixmap(map.map, false); + painter.drawPixmap(pixelX, pixelY, pixmap); + } + + // When including the borders, we simply draw all the maps again + // without their borders, since the first pass results in maps + // being occluded by other map borders. + if (includeBorder) { + progress->setLabelText("Drawing stitched maps without borders..."); + progress->setValue(0); + progress->setMaximum(stitchedMaps.size()); + numDrawn = 0; + for (StitchedMap map : stitchedMaps) { + if (progress->wasCanceled()) { + return QPixmap(); + } + progress->setValue(numDrawn); + numDrawn++; + + int pixelX = (map.x - minX) * 16; + int pixelY = (map.y - minY) * 16; + QPixmap pixmapWithoutBorders = this->getFormattedMapPixmap(map.map, true); + painter.drawPixmap(pixelX, pixelY, pixmapWithoutBorders); + } + } + + return stitchedPixmap; +} + void MapImageExporter::updatePreview() { if (scene) { delete scene; scene = nullptr; } + + preview = getFormattedMapPixmap(this->map, false); scene = new QGraphicsScene; + scene->addPixmap(preview); + this->scene->setSceneRect(this->scene->itemsBoundingRect()); + + this->ui->graphicsView_Preview->setScene(scene); + this->ui->graphicsView_Preview->setFixedSize(scene->itemsBoundingRect().width() + 2, + scene->itemsBoundingRect().height() + 2); +} + +QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { + QPixmap pixmap; // draw background layer / base image + map->render(true); if (showCollision) { - preview = map->collision_pixmap; + map->renderCollision(editor->collisionOpacity, true); + pixmap = map->collision_pixmap; } else { - preview = map->pixmap; + pixmap = map->pixmap; } // draw events - QPainter eventPainter(&preview); + QPainter eventPainter(&pixmap); QList events = map->getAllEvents(); + editor->project->loadEventPixmaps(events); for (Event *event : events) { QString group = event->get("event_group_type"); if ((showObjects && group == "object_event_group") @@ -69,31 +235,40 @@ void MapImageExporter::updatePreview() { // note: this will break when allowing map to be selected from drop down maybe int borderHeight = 0, borderWidth = 0; bool forceDrawBorder = showUpConnections || showDownConnections || showLeftConnections || showRightConnections; - if (showBorder || forceDrawBorder) { - borderHeight = BORDER_DISTANCE * 16, borderWidth = BORDER_DISTANCE * 16; - QPixmap newPreview = QPixmap(map->pixmap.width() + borderWidth * 2, map->pixmap.height() + borderHeight * 2); - QPainter borderPainter(&newPreview); - for (auto borderItem : editor->borderItems) { - borderPainter.drawImage(QPoint(borderItem->x() + borderWidth, borderItem->y() + borderHeight), - borderItem->pixmap().toImage()); + if (!ignoreBorder && (showBorder || forceDrawBorder)) { + int borderDistance = this->stitchMode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; + map->renderBorder(); + int borderHorzDist = editor->getBorderDrawDistance(map->getBorderWidth()); + int borderVertDist = editor->getBorderDrawDistance(map->getBorderHeight()); + borderWidth = borderDistance * 16; + borderHeight = borderDistance * 16; + QPixmap newPixmap = QPixmap(map->pixmap.width() + borderWidth * 2, map->pixmap.height() + borderHeight * 2); + QPainter borderPainter(&newPixmap); + for (int y = borderDistance - borderVertDist; y < map->getHeight() + borderVertDist * 2; y += map->getBorderHeight()) { + for (int x = borderDistance - borderHorzDist; x < map->getWidth() + borderHorzDist * 2; x += map->getBorderWidth()) { + borderPainter.drawPixmap(x * 16, y * 16, map->layout->border_pixmap); + } } - borderPainter.drawImage(QPoint(borderWidth, borderHeight), preview.toImage()); + + borderPainter.drawImage(borderWidth, borderHeight, pixmap.toImage()); borderPainter.end(); - preview = newPreview; + pixmap = newPixmap; } - // if showing connections, draw on outside of image - QPainter connectionPainter(&preview); - for (auto connectionItem : editor->connection_edit_items) { - QString direction = connectionItem->connection->direction; - if ((showUpConnections && direction == "up") - || (showDownConnections && direction == "down") - || (showLeftConnections && direction == "left") - || (showRightConnections && direction == "right")) - connectionPainter.drawImage(connectionItem->initialX + borderWidth, connectionItem->initialY + borderHeight, - connectionItem->basePixmap.toImage()); + if (!this->stitchMode) { + // if showing connections, draw on outside of image + QPainter connectionPainter(&pixmap); + for (auto connectionItem : editor->connection_edit_items) { + QString direction = connectionItem->connection->direction; + if ((showUpConnections && direction == "up") + || (showDownConnections && direction == "down") + || (showLeftConnections && direction == "left") + || (showRightConnections && direction == "right")) + connectionPainter.drawImage(connectionItem->initialX + borderWidth, connectionItem->initialY + borderHeight, + connectionItem->basePixmap.toImage()); + } + connectionPainter.end(); } - connectionPainter.end(); // draw grid directly onto the pixmap // since the last grid lines are outside of the pixmap, add a pixel to the bottom and right @@ -102,24 +277,20 @@ void MapImageExporter::updatePreview() { if (borderHeight) addY = 0; if (borderWidth) addX = 0; - QPixmap newPreview = QPixmap(preview.width() + addX, preview.height() + addY); - QPainter gridPainter(&newPreview); - gridPainter.drawImage(QPoint(0, 0), preview.toImage()); - for (auto lineItem : editor->gridLines) { - QPointF addPos(borderWidth, borderHeight); - gridPainter.drawLine(lineItem->line().p1() + addPos, lineItem->line().p2() + addPos); + QPixmap newPixmap= QPixmap(pixmap.width() + addX, pixmap.height() + addY); + QPainter gridPainter(&newPixmap); + gridPainter.drawImage(QPoint(0, 0), pixmap.toImage()); + for (int x = 0; x < newPixmap.width(); x += 16) { + gridPainter.drawLine(x, 0, x, newPixmap.height()); + } + for (int y = 0; y < newPixmap.height(); y += 16) { + gridPainter.drawLine(0, y, newPixmap.width(), y); } gridPainter.end(); - preview = newPreview; + pixmap = newPixmap; } - scene->addPixmap(preview); - - this->scene->setSceneRect(this->scene->itemsBoundingRect()); - - this->ui->graphicsView_Preview->setScene(scene); - this->ui->graphicsView_Preview->setFixedSize(scene->itemsBoundingRect().width() + 2, - scene->itemsBoundingRect().height() + 2); + return pixmap; } void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) {