From 52a06d5b32c392396ffdb7cac91cda35b993f3ae Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 20 Mar 2025 14:10:58 -0400 Subject: [PATCH 01/12] Separate MapImageExporter from Editor --- forms/mapimageexporter.ui | 3 + include/core/map.h | 2 +- include/core/mapconnection.h | 8 +- include/core/maplayout.h | 4 + include/editor.h | 2 - include/ui/mapimageexporter.h | 24 +++--- src/core/map.cpp | 6 +- src/core/mapconnection.cpp | 24 +++++- src/core/maplayout.cpp | 19 +++++ src/editor.cpp | 19 +---- src/mainwindow.cpp | 15 +++- src/ui/connectionpixmapitem.cpp | 33 +++----- src/ui/divingmappixmapitem.cpp | 2 +- src/ui/mapimageexporter.cpp | 139 +++++++++++++++++--------------- 14 files changed, 176 insertions(+), 124 deletions(-) diff --git a/forms/mapimageexporter.ui b/forms/mapimageexporter.ui index 322793c9..1496a581 100644 --- a/forms/mapimageexporter.ui +++ b/forms/mapimageexporter.ui @@ -40,6 +40,9 @@ QComboBox::SizeAdjustPolicy::AdjustToContents + + QComboBox::InsertPolicy::NoInsert + diff --git a/include/core/map.h b/include/core/map.h index db81fc93..d643acbc 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -89,7 +89,7 @@ public: bool hasEvent(Event *) const; void deleteConnections(); - QList getConnections() const; + QList getConnections() const { return m_connections; } void removeConnection(MapConnection *); void addConnection(MapConnection *); void loadConnection(MapConnection *); diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 21c00ac6..496e884f 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -26,13 +26,19 @@ public: QString direction() const { return m_direction; } void setDirection(const QString &direction, bool mirror = true); + bool isCardinal() const { return isCardinal(m_direction); } + bool isHorizontal() const { return isHorizontal(m_direction); } + bool isVertical() const { return isVertical(m_direction); } + bool isDiving() const { return isDiving(m_direction); } + int offset() const { return m_offset; } void setOffset(int offset, bool mirror = true); MapConnection* findMirror(); MapConnection* createMirror(); - QPixmap getPixmap(); + QPixmap render() const; + QPoint relativePos(bool clipped = false) const; static QPointer project; static const QMap oppositeDirections; diff --git a/include/core/maplayout.h b/include/core/maplayout.h index cbe719b7..f2eccd3c 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -95,6 +95,8 @@ public: int getHeight() const { return height; } int getBorderWidth() const { return border_width; } int getBorderHeight() const { return border_height; } + int getBorderDrawWidth() const; + int getBorderDrawHeight() const; bool isWithinBounds(int x, int y); bool isWithinBorderBounds(int x, int y); @@ -140,6 +142,8 @@ private: void setNewDimensionsBlockdata(int newWidth, int newHeight); void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); + static int getBorderDrawDistance(int dimension, qreal minimum); + signals: void layoutChanged(Layout *layout); //void modified(); diff --git a/include/editor.h b/include/editor.h index 4c7bff02..48fdfc0d 100644 --- a/include/editor.h +++ b/include/editor.h @@ -175,8 +175,6 @@ public: void eventsView_onMousePress(QMouseEvent *event); - int getBorderDrawDistance(int dimension); - bool selectingEvent = false; void deleteSelectedEvents(); diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 88a00447..9b249dcc 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -1,10 +1,7 @@ #ifndef MAPIMAGEEXPORTER_H #define MAPIMAGEEXPORTER_H -#include "map.h" -#include "editor.h" - -#include +#include "project.h" namespace Ui { class MapImageExporter; @@ -39,15 +36,21 @@ class MapImageExporter : public QDialog Q_OBJECT public: - explicit MapImageExporter(QWidget *parent, Editor *editor, ImageExporterMode mode); + explicit MapImageExporter(QWidget *parent, Project *project, Layout *layout, ImageExporterMode mode = ImageExporterMode::Normal) + : MapImageExporter(parent, project, nullptr, layout, mode) {}; + explicit MapImageExporter(QWidget *parent, Project *project, Map *map, ImageExporterMode mode = ImageExporterMode::Normal) + : MapImageExporter(parent, project, map, map->layout(), mode) {}; ~MapImageExporter(); -private: - Ui::MapImageExporter *ui; + ImageExporterMode mode() const { return m_mode; } - Layout *m_layout = nullptr; +private: + explicit MapImageExporter(QWidget *parent, Project *project, Map *map, Layout *layout, ImageExporterMode mode); + + Ui::MapImageExporter *ui; + Project *m_project = nullptr; Map *m_map = nullptr; - Editor *m_editor = nullptr; + Layout *m_layout = nullptr; QGraphicsScene *m_scene = nullptr; QPixmap m_preview; @@ -55,6 +58,8 @@ private: ImageExporterSettings m_settings; ImageExporterMode m_mode = ImageExporterMode::Normal; + QString getTitle(ImageExporterMode mode); + QString getDescription(ImageExporterMode mode); void updatePreview(); void scalePreview(); void updateShowBorderState(); @@ -65,6 +70,7 @@ private: QPixmap getFormattedLayoutPixmap(Layout *layout, bool ignoreBorder = false, bool ignoreGrid = false); void paintGrid(QPixmap *pixmap, bool ignoreBorder = false); bool historyItemAppliesToFrame(const QUndoCommand *command); + void updateMapSelection(const QString &text); protected: virtual void showEvent(QShowEvent *) override; diff --git a/src/core/map.cpp b/src/core/map.cpp index 7335c5bb..d130e44f 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -233,10 +233,6 @@ void Map::deleteConnections() { m_connections.clear(); } -QList Map::getConnections() const { - return m_connections; -} - void Map::addConnection(MapConnection *connection) { if (!connection || m_connections.contains(connection)) return; @@ -244,7 +240,7 @@ void Map::addConnection(MapConnection *connection) { // Maps should only have one Dive/Emerge connection at a time. // (Users can technically have more by editing their data manually, but we will only display one at a time) // Any additional connections being added (this can happen via mirroring) are tracked for deleting but otherwise ignored. - if (MapConnection::isDiving(connection->direction())) { + if (connection->isDiving()) { for (const auto &i : m_connections) { if (i->direction() == connection->direction()) { trackConnection(connection); diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index db2755e9..188b0c8e 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -60,7 +60,7 @@ Map* MapConnection::targetMap() const { return getMap(m_targetMapName); } -QPixmap MapConnection::getPixmap() { +QPixmap MapConnection::render() const { auto map = targetMap(); if (!map) return QPixmap(); @@ -68,6 +68,28 @@ QPixmap MapConnection::getPixmap() { return map->renderConnection(m_direction, m_parentMap ? m_parentMap->layout() : nullptr); } +// Get the position of the target map relative to its parent map. +// For right/down connections this is offset by the dimensions of the parent map. +// For left/up connections this is offset by the dimensions of the target map. +// If 'clipped' is true, only the rendered dimensions of the target map will be used, rather than its full dimensions. +QPoint MapConnection::relativePos(bool clipped) const { + int x = 0, y = 0; + if (m_direction == "right") { + if (m_parentMap) x = m_parentMap->getWidth(); + y = m_offset; + } else if (m_direction == "down") { + x = m_offset; + if (m_parentMap) y = m_parentMap->getHeight(); + } else if (m_direction == "left") { + if (targetMap()) x = !clipped ? -targetMap()->getWidth() : -targetMap()->getConnectionRect(m_direction).width(); + y = m_offset; + } else if (m_direction == "up") { + x = m_offset; + if (targetMap()) y = !clipped ? -targetMap()->getHeight() : -targetMap()->getConnectionRect(m_direction).height(); + } + return QPoint(x, y); +} + void MapConnection::setParentMap(Map* map, bool mirror) { if (map == m_parentMap) return; diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index d2083203..ceeb6db5 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -58,6 +58,25 @@ bool Layout::isWithinBorderBounds(int x, int y) { return (x >= 0 && x < this->getBorderWidth() && y >= 0 && y < this->getBorderHeight()); } +int Layout::getBorderDrawWidth() const { + return getBorderDrawDistance(border_width, BORDER_DISTANCE); +} + +int Layout::getBorderDrawHeight() const { + return getBorderDrawDistance(border_height, BORDER_DISTANCE); +} + +// We need to draw sufficient border blocks to fill the area that gets loaded around the player in-game (BORDER_DISTANCE). +// Note that this is not the same as the player's view distance. +// The result will be some multiple of the input dimension, because we only draw the border in increments of its full width/height. +int Layout::getBorderDrawDistance(int dimension, qreal minimum) { + if (dimension >= minimum) + return dimension; + + // Get first multiple of dimension >= the minimum + return dimension * qCeil(minimum / qMax(dimension, 1)); +} + bool Layout::getBlock(int x, int y, Block *out) { if (isWithinBounds(x, y)) { int i = y * getWidth() + x; diff --git a/src/editor.cpp b/src/editor.cpp index b6c810a9..abee6684 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -761,7 +761,7 @@ void Editor::displayConnection(MapConnection *connection) { if (!connection) return; - if (MapConnection::isDiving(connection->direction())) { + if (connection->isDiving()) { displayDivingConnection(connection); return; } @@ -826,7 +826,7 @@ void Editor::removeConnectionPixmap(MapConnection *connection) { if (!connection) return; - if (MapConnection::isDiving(connection->direction())) { + if (connection->isDiving()) { removeDivingMapPixmap(connection); return; } @@ -1790,8 +1790,8 @@ void Editor::displayMapBorder() { int borderWidth = this->layout->getBorderWidth(); int borderHeight = this->layout->getBorderHeight(); - int borderHorzDist = getBorderDrawDistance(borderWidth); - int borderVertDist = getBorderDrawDistance(borderHeight); + int borderHorzDist = this->layout->getBorderDrawWidth(); + int borderVertDist = this->layout->getBorderDrawHeight(); QPixmap pixmap = this->layout->renderBorder(); for (int y = -borderVertDist; y < this->layout->getHeight() + borderVertDist; y += borderHeight) for (int x = -borderHorzDist; x < this->layout->getWidth() + borderHorzDist; x += borderWidth) { @@ -1816,17 +1816,6 @@ void Editor::updateMapConnections() { item->render(true); } -int Editor::getBorderDrawDistance(int dimension) { - // Draw sufficient border blocks to fill the player's view (BORDER_DISTANCE) - if (dimension >= BORDER_DISTANCE) { - return dimension; - } else if (dimension) { - return dimension * (BORDER_DISTANCE / dimension + (BORDER_DISTANCE % dimension ? 1 : 0)); - } else { - return BORDER_DISTANCE; - } -} - void Editor::toggleGrid(bool checked) { if (porymapConfig.showGrid == checked) return; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ed7e7d77..28dabc33 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2492,12 +2492,19 @@ void MainWindow::on_actionImport_Map_from_Advance_Map_1_92_triggered() { void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { if (!editor->project) return; - // If the user is requesting this window again we assume it's for a new - // window (the map/mode may have changed), so delete the old window. - if (this->mapImageExporter) + // If the user is requesting this window again with a different mode + // then we'll recreate the window with the new mode. + if (this->mapImageExporter && this->mapImageExporter->mode() != mode) delete this->mapImageExporter; - this->mapImageExporter = new MapImageExporter(this, this->editor, mode); + if (!this->mapImageExporter) { + // Open new image export window + if (this->editor->map){ + this->mapImageExporter = new MapImageExporter(this, this->editor->project, this->editor->map, mode); + } else if (this->editor->layout) { + this->mapImageExporter = new MapImageExporter(this, this->editor->project, this->editor->layout, mode); + } + } openSubWindow(this->mapImageExporter); } diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 93bab7c5..f1bceac5 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -5,7 +5,7 @@ #include ConnectionPixmapItem::ConnectionPixmapItem(MapConnection* connection) - : QGraphicsPixmapItem(connection->getPixmap()), + : QGraphicsPixmapItem(connection->render()), connection(connection) { this->setEditable(true); @@ -28,7 +28,7 @@ void ConnectionPixmapItem::refresh() { // Render additional visual effects on top of the base map image. void ConnectionPixmapItem::render(bool ignoreCache) { if (ignoreCache) - this->basePixmap = this->connection->getPixmap(); + this->basePixmap = this->connection->render(); QPixmap pixmap = this->basePixmap.copy(0, 0, this->basePixmap.width(), this->basePixmap.height()); this->setZValue(-1); @@ -63,10 +63,10 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari int newOffset = this->connection->offset(); // Restrict movement to the metatile grid and perpendicular to the connection direction. - if (MapConnection::isVertical(this->connection->direction())) { + if (this->connection->isVertical()) { x = (round(newPos.x() / this->mWidth) * this->mWidth) - this->originX; newOffset = x / this->mWidth; - } else if (MapConnection::isHorizontal(this->connection->direction())) { + } else if (this->connection->isHorizontal()) { y = (round(newPos.y() / this->mHeight) * this->mHeight) - this->originY; newOffset = y / this->mHeight; } @@ -87,9 +87,9 @@ void ConnectionPixmapItem::updatePos() { qreal x = this->originX; qreal y = this->originY; - if (MapConnection::isVertical(this->connection->direction())) { + if (this->connection->isVertical()) { x += this->connection->offset() * this->mWidth; - } else if (MapConnection::isHorizontal(this->connection->direction())) { + } else if (this->connection->isHorizontal()) { y += this->connection->offset() * this->mHeight; } @@ -98,22 +98,13 @@ void ConnectionPixmapItem::updatePos() { } void ConnectionPixmapItem::updateOrigin() { - const Map *parentMap = connection->parentMap(); - const Map *targetMap = connection->targetMap(); - const QString direction = connection->direction(); - int x = 0, y = 0; - - if (direction == "right") { - if (parentMap) x = parentMap->getWidth(); - } else if (direction == "down") { - if (parentMap) y = parentMap->getHeight(); - } else if (direction == "left") { - if (targetMap) x = -targetMap->getConnectionRect(direction).width(); - } else if (direction == "up") { - if (targetMap) y = -targetMap->getConnectionRect(direction).height(); + if (this->connection->isVertical()) { + this->originX = 0; + this->originY = this->connection->relativePos(true).y() * this->mHeight; + } else if (this->connection->isHorizontal()) { + this->originX = this->connection->relativePos(true).x() * this->mWidth; + this->originY = 0; } - this->originX = x * this->mWidth; - this->originY = y * this->mHeight; updatePos(); } diff --git a/src/ui/divingmappixmapitem.cpp b/src/ui/divingmappixmapitem.cpp index 70a611fa..e20b8f25 100644 --- a/src/ui/divingmappixmapitem.cpp +++ b/src/ui/divingmappixmapitem.cpp @@ -25,7 +25,7 @@ QPixmap DivingMapPixmapItem::getBasePixmap(MapConnection* connection) { return QPixmap(); // Save some rendering time if it won't be displayed if (connection->targetMapName() == connection->parentMapName()) return QPixmap(); // If the map is connected to itself then rendering is pointless. - return connection->getPixmap(); + return connection->render(); } void DivingMapPixmapItem::updatePixmap() { diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 4b11ea95..cc3d3f25 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -10,61 +10,71 @@ #define STITCH_MODE_BORDER_DISTANCE 2 -QString getTitle(ImageExporterMode mode) { +QString MapImageExporter::getTitle(ImageExporterMode mode) { switch (mode) { case ImageExporterMode::Normal: - return "Export Map Image"; + return QString("Export %1 Image").arg(m_map ? "Map" : "Layout"); case ImageExporterMode::Stitch: - return "Export Map Stitch Image"; + return QStringLiteral("Export Map Stitch Image"); case ImageExporterMode::Timelapse: - return "Export Map Timelapse Image"; + return QString("Export %1 Timelapse Image").arg(m_map ? "Map" : "Layout"); } return ""; } -QString getDescription(ImageExporterMode mode) { +QString MapImageExporter::getDescription(ImageExporterMode mode) { switch (mode) { case ImageExporterMode::Normal: - return "Exports an image of the selected map."; + return QString("Exports an image of the selected %1.").arg(m_map ? "Map" : "Layout"); case ImageExporterMode::Stitch: return "Exports a combined image of all the maps connected to the selected map."; case ImageExporterMode::Timelapse: - return "Exports a GIF of the edit history for the selected map."; + return QString("Exports a GIF of the edit history for the selected %1.").arg(m_map ? "Map" : "Layout"); } return ""; } -MapImageExporter::MapImageExporter(QWidget *parent, Editor *editor, ImageExporterMode mode) : +MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, Layout *layout, ImageExporterMode mode) : QDialog(parent), - ui(new Ui::MapImageExporter) + ui(new Ui::MapImageExporter), + m_project(project), + m_map(map), + m_layout(layout), + m_mode(mode) { setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); - m_map = editor->map; - m_layout = editor->layout; - m_editor = editor; - m_mode = mode; setWindowTitle(getTitle(m_mode)); ui->label_Description->setText(getDescription(m_mode)); - ui->groupBox_Connections->setVisible(m_mode != ImageExporterMode::Stitch); + ui->groupBox_Connections->setVisible(m_map && m_mode != ImageExporterMode::Stitch); ui->groupBox_Timelapse->setVisible(m_mode == ImageExporterMode::Timelapse); + ui->groupBox_Events->setVisible(m_map != nullptr); - if (m_map) { - ui->comboBox_MapSelection->addItems(editor->project->mapNames); - ui->comboBox_MapSelection->setCurrentText(m_map->name()); - ui->comboBox_MapSelection->setEnabled(false);// TODO: allow selecting map from drop-down + // Initialize map selector. + if (m_mode != ImageExporterMode::Timelapse) { + if (m_map) { + ui->comboBox_MapSelection->addItems(m_project->mapNames); + ui->comboBox_MapSelection->setCurrentText(m_map->name()); + ui->label_MapSelection->setText(m_mode == ImageExporterMode::Stitch ? QStringLiteral("Starting Map") : QStringLiteral("Map")); + } else if (m_layout) { + ui->comboBox_MapSelection->addItems(m_project->layoutIds); + ui->comboBox_MapSelection->setCurrentText(m_layout->id); + ui->label_MapSelection->setText(QStringLiteral("Layout")); + } } else { - // Some settings only apply to maps. When exporting an image in layout-only mode we hide them. + // At the moment edit history for events (and the DraggablePixmapItem class) + // depend on the editor and assume their map is the current map. + // Until this is resolved the selected map cannot be changed in Timelapse mode. ui->comboBox_MapSelection->setVisible(false); ui->label_MapSelection->setVisible(false); - ui->groupBox_Events->setVisible(false); - ui->groupBox_Connections->setVisible(false); } + ui->graphicsView_Preview->setFocus(); connect(ui->pushButton_Save, &QPushButton::pressed, this, &MapImageExporter::saveImage); connect(ui->pushButton_Cancel, &QPushButton::pressed, this, &MapImageExporter::close); + connect(ui->comboBox_MapSelection, &QComboBox::currentTextChanged, this, &MapImageExporter::updateMapSelection); } MapImageExporter::~MapImageExporter() { @@ -84,6 +94,25 @@ void MapImageExporter::resizeEvent(QResizeEvent *event) { scalePreview(); } +void MapImageExporter::updateMapSelection(const QString &text) { + if (m_map) { + if (!m_project->mapNames.contains(text)) + return; + Map *newMap = m_project->loadMap(text); + if (newMap == m_map) + return; + m_map = newMap; + } else { + if (!m_project->layoutIds.contains(text)) + return; + Layout *newLayout = m_project->loadLayout(text); + if (newLayout == m_layout) + return; + m_layout = newLayout; + } + updatePreview(); +} + void MapImageExporter::saveImage() { // Make sure preview is up-to-date before we save. if (m_preview.isNull()) @@ -91,7 +120,6 @@ void MapImageExporter::saveImage() { if (m_preview.isNull()) return; - const QString title = getTitle(m_mode); const QString itemName = m_map ? m_map->name() : m_layout->name; QString defaultFilename; switch (m_mode) @@ -112,7 +140,7 @@ void MapImageExporter::saveImage() { .arg(defaultFilename) .arg(m_mode == ImageExporterMode::Timelapse ? "gif" : "png"); QString filter = m_mode == ImageExporterMode::Timelapse ? "Image Files (*.gif)" : "Image Files (*.png *.jpg *.bmp)"; - QString filepath = FileDialog::getSaveFileName(this, title, defaultFilepath, filter); + QString filepath = FileDialog::getSaveFileName(this, windowTitle(), defaultFilepath, filter); if (!filepath.isEmpty()) { switch (m_mode) { case ImageExporterMode::Normal: @@ -275,7 +303,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu QSet visited; QList stitchedMaps; QList unvisited; - unvisited.append(StitchedMap{0, 0, m_editor->map}); + unvisited.append(StitchedMap{0, 0, m_map}); progress->setLabelText("Gathering stitched maps..."); while (!unvisited.isEmpty()) { @@ -291,31 +319,10 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu visited.insert(cur.map->name()); stitchedMaps.append(cur); - for (MapConnection *connection : cur.map->getConnections()) { - const QString direction = connection->direction(); - int x = cur.x; - int y = cur.y; - int offset = connection->offset(); - Map *connectionMap = connection->targetMap(); - if (!connectionMap) - continue; - if (direction == "up") { - x += offset; - y -= connectionMap->getHeight(); - } else if (direction == "down") { - x += offset; - y += cur.map->getHeight(); - } else if (direction == "left") { - x -= connectionMap->getWidth(); - y += offset; - } else if (direction == "right") { - x += cur.map->getWidth(); - y += offset; - } else { - // Ignore Dive/Emerge connections and unrecognized directions - continue; - } - unvisited.append(StitchedMap{x, y, connectionMap}); + for (const auto &connection : cur.map->getConnections()) { + if (!connection->isCardinal()) continue; + QPoint pos = connection->relativePos(); + unvisited.append(StitchedMap{cur.x + pos.x(), cur.y + pos.y(), connection->targetMap()}); } } @@ -441,7 +448,7 @@ QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout, bool ignoreBo if (m_settings.showCollision) { QPainter collisionPainter(&pixmap); layout->renderCollision(true); - collisionPainter.setOpacity(m_editor->collisionOpacity); + collisionPainter.setOpacity(static_cast(porymapConfig.collisionOpacity) / 100); collisionPainter.drawPixmap(0, 0, layout->collision_pixmap); collisionPainter.end(); } @@ -452,8 +459,8 @@ QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout, bool ignoreBo if (!ignoreBorder && m_settings.showBorder) { int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; layout->renderBorder(); - int borderHorzDist = m_editor->getBorderDrawDistance(layout->getBorderWidth()); - int borderVertDist = m_editor->getBorderDrawDistance(layout->getBorderHeight()); + int borderHorzDist = layout->getBorderDrawWidth(); + int borderVertDist = layout->getBorderDrawHeight(); borderWidth = borderDistance * 16; borderHeight = borderDistance * 16; QPixmap newPixmap = QPixmap(layout->pixmap.width() + borderWidth * 2, layout->pixmap.height() + borderHeight * 2); @@ -487,17 +494,20 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { QPainter connectionPainter(&pixmap); int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; - int borderWidth = borderDistance * 16; - int borderHeight = borderDistance * 16; - // TODO: Reading the connections from the editor and not 'map' is incorrect. - for (auto connectionItem : m_editor->connection_items) { - const QString direction = connectionItem->connection->direction(); - if ((m_settings.showUpConnections && direction == "up") - || (m_settings.showDownConnections && direction == "down") - || (m_settings.showLeftConnections && direction == "left") - || (m_settings.showRightConnections && direction == "right")) - connectionPainter.drawImage(connectionItem->x() + borderWidth, connectionItem->y() + borderHeight, - connectionItem->connection->getPixmap().toImage()); + for (const auto &connection : m_map->getConnections()) { + const QString direction = connection->direction(); + if (direction == "up") { + if (!m_settings.showUpConnections) continue; + } else if (direction == "down") { + if (!m_settings.showDownConnections) continue; + } else if (direction == "left") { + if (!m_settings.showLeftConnections) continue; + } else if (direction == "right") { + if (!m_settings.showRightConnections) continue; + } else continue; // Ignore any other directions + + QPoint pos = connection->relativePos(true); + connectionPainter.drawImage((pos.x() + borderDistance) * 16, (pos.y() + borderDistance) * 16, connection->render().toImage()); } connectionPainter.end(); } @@ -517,7 +527,8 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { || (m_settings.showBGs && group == Event::Group::Bg) || (m_settings.showTriggers && group == Event::Group::Coord) || (m_settings.showHealLocations && group == Event::Group::Heal)) { - m_editor->project->loadEventPixmap(event); + m_project->loadEventPixmap(event); + eventPainter.setOpacity(event->getUsesDefaultPixmap() ? 0.7 : 1.0); eventPainter.drawImage(QPoint(event->getPixelX() + pixelOffset, event->getPixelY() + pixelOffset), event->getPixmap().toImage()); } } From d1eaf6cfd2ad90c8e83eca855ee72a72ad0b8900 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 21 Mar 2025 12:56:22 -0400 Subject: [PATCH 02/12] Refactor image export settings, fix event occlusion --- include/core/events.h | 1 + include/ui/mapimageexporter.h | 24 ++-- src/core/events.cpp | 20 ++- src/ui/mapimageexporter.cpp | 240 ++++++++++++++++++++-------------- 4 files changed, 170 insertions(+), 115 deletions(-) diff --git a/include/core/events.h b/include/core/events.h index fc0b90d8..322f09e8 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -170,6 +170,7 @@ public: static QString typeToJsonKey(Event::Type type); static Event::Type typeFromJsonKey(QString type); static QList types(); + static QList groups(); // protected attributes protected: diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 9b249dcc..29cefe81 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -14,15 +14,8 @@ enum ImageExporterMode { }; struct ImageExporterSettings { - bool showObjects = false; - bool showWarps = false; - bool showBGs = false; - bool showTriggers = false; - bool showHealLocations = false; - bool showUpConnections = false; - bool showDownConnections = false; - bool showLeftConnections = false; - bool showRightConnections = false; + QSet showEvents; + QSet showConnections; bool showGrid = false; bool showBorder = false; bool showCollision = false; @@ -62,13 +55,18 @@ private: QString getDescription(ImageExporterMode mode); void updatePreview(); void scalePreview(); + bool eventsEnabled(); + void setEventGroupEnabled(Event::Group group, bool enable); + bool connectionsEnabled(); + void setConnectionDirectionEnabled(const QString &dir, bool enable); void updateShowBorderState(); void saveImage(); - QPixmap getStitchedImage(QProgressDialog *progress, bool includeBorder); + QPixmap getStitchedImage(QProgressDialog *progress); QPixmap getFormattedMapPixmap(); - QPixmap getFormattedMapPixmap(Map *map, bool ignoreBorder = false); - QPixmap getFormattedLayoutPixmap(Layout *layout, bool ignoreBorder = false, bool ignoreGrid = false); - void paintGrid(QPixmap *pixmap, bool ignoreBorder = false); + QPixmap getFormattedMapPixmap(Map *map); + QPixmap getFormattedLayoutPixmap(Layout *layout); + void paintEvents(QPixmap *pixmap, const Map *map, const QPoint &pixelOffset); + void paintGrid(QPixmap *pixmap); bool historyItemAppliesToFrame(const QUndoCommand *command); void updateMapSelection(const QString &text); diff --git a/src/core/events.cpp b/src/core/events.cpp index ac197bc7..7ceeae58 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -73,17 +73,23 @@ void Event::modify() { this->map->modify(); } +const QMap groupToStringMap = { + {Event::Group::Object, "Object"}, + {Event::Group::Warp, "Warp"}, + {Event::Group::Coord, "Trigger"}, + {Event::Group::Bg, "BG"}, + {Event::Group::Heal, "Heal Location"}, +}; + QString Event::groupToString(Event::Group group) { - static const QMap groupToStringMap = { - {Event::Group::Object, "Object"}, - {Event::Group::Warp, "Warp"}, - {Event::Group::Coord, "Trigger"}, - {Event::Group::Bg, "BG"}, - {Event::Group::Heal, "Heal Location"}, - }; return groupToStringMap.value(group); } +QList Event::groups() { + static QList groupList = groupToStringMap.keys(); + return groupList; +} + // These are the expected key names used in the map.json files. // We re-use them for key names in the copy/paste JSON data, const QMap typeToJsonKeyMap = { diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index cc3d3f25..c2858a0f 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -272,19 +272,18 @@ bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { case CommandId::ID_MapConnectionChangeMap: case CommandId::ID_MapConnectionAdd: case CommandId::ID_MapConnectionRemove: - return m_settings.showUpConnections || m_settings.showDownConnections || m_settings.showLeftConnections || m_settings.showRightConnections; + return connectionsEnabled(); case CommandId::ID_EventMove: case CommandId::ID_EventShift: case CommandId::ID_EventCreate: case CommandId::ID_EventDelete: case CommandId::ID_EventDuplicate: { - bool eventTypeIsApplicable = - (m_settings.showObjects && (command->id() & IDMask_EventType_Object) != 0) - || (m_settings.showWarps && (command->id() & IDMask_EventType_Warp) != 0) - || (m_settings.showBGs && (command->id() & IDMask_EventType_BG) != 0) - || (m_settings.showTriggers && (command->id() & IDMask_EventType_Trigger) != 0) - || (m_settings.showHealLocations && (command->id() & IDMask_EventType_Heal) != 0); - return eventTypeIsApplicable; + if (command->id() & IDMask_EventType_Object) return m_settings.showEvents.contains(Event::Group::Object); + if (command->id() & IDMask_EventType_Warp) return m_settings.showEvents.contains(Event::Group::Warp); + if (command->id() & IDMask_EventType_BG) return m_settings.showEvents.contains(Event::Group::Bg); + if (command->id() & IDMask_EventType_Trigger) return m_settings.showEvents.contains(Event::Group::Coord); + if (command->id() & IDMask_EventType_Heal) return m_settings.showEvents.contains(Event::Group::Heal); + return false; } default: return false; @@ -297,7 +296,7 @@ struct StitchedMap { Map* map; }; -QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool includeBorder) { +QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { // Do a breadth-first search to gather a collection of // all reachable maps with their relative offsets. QSet visited; @@ -346,7 +345,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu maxY = bottom; } - if (includeBorder) { + if (m_settings.showBorder) { minX -= STITCH_MODE_BORDER_DISTANCE; maxX += STITCH_MODE_BORDER_DISTANCE; minY -= STITCH_MODE_BORDER_DISTANCE; @@ -359,8 +358,16 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu progress->setValue(0); progress->setMaximum(stitchedMaps.size()); int numDrawn = 0; + QPixmap stitchedPixmap((maxX - minX) * 16, (maxY - minY) * 16); stitchedPixmap.fill(Qt::black); + + // Temporarily disable settings that have separate passes. + auto showEvents = m_settings.showEvents; + m_settings.showEvents.clear(); + bool showGrid = m_settings.showGrid; + m_settings.showGrid = false; + QPainter painter(&stitchedPixmap); for (StitchedMap map : stitchedMaps) { if (progress->wasCanceled()) { @@ -371,7 +378,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu int pixelX = (map.x - minX) * 16; int pixelY = (map.y - minY) * 16; - if (includeBorder) { + if (m_settings.showBorder) { pixelX -= STITCH_MODE_BORDER_DISTANCE * 16; pixelY -= STITCH_MODE_BORDER_DISTANCE * 16; } @@ -382,7 +389,8 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu // 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) { + if (m_settings.showBorder) { + m_settings.showBorder = false; progress->setLabelText("Drawing stitched maps without borders..."); progress->setValue(0); progress->setMaximum(stitchedMaps.size()); @@ -396,10 +404,36 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu int pixelX = (map.x - minX) * 16; int pixelY = (map.y - minY) * 16; - QPixmap pixmapWithoutBorders = getFormattedMapPixmap(map.map, true); + QPixmap pixmapWithoutBorders = getFormattedMapPixmap(map.map); painter.drawPixmap(pixelX, pixelY, pixmapWithoutBorders); } + m_settings.showBorder = true; } + painter.end(); + + // Events can be occluded by neighboring maps if they are positioned near or outside the map's edge. + // Now that all the maps have been rendered we can render the events (if enabled). + m_settings.showEvents = showEvents; + if (eventsEnabled()) { + progress->setLabelText("Drawing stitched map events..."); + 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; + paintEvents(&stitchedPixmap, map.map, QPoint(pixelX, pixelY)); + } + } + + m_settings.showGrid = showGrid; + paintGrid(&stitchedPixmap); return stitchedPixmap; } @@ -417,7 +451,7 @@ void MapImageExporter::updatePreview() { progress.setWindowModality(Qt::WindowModal); progress.setModal(true); progress.setMinimumDuration(1000); - m_preview = getStitchedImage(&progress, m_settings.showBorder); + m_preview = getStitchedImage(&progress); progress.close(); } else { // Timelapse mode doesn't currently have a real preview. It just displays the current map as in Normal mode. @@ -438,7 +472,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap() { return m_map ? getFormattedMapPixmap(m_map) : getFormattedLayoutPixmap(m_layout); } -QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout, bool ignoreBorder, bool ignoreGrid) { +QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout) { if (!layout) return QPixmap(); @@ -454,9 +488,8 @@ QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout, bool ignoreBo } // draw map border - // note: this will break when allowing map to be selected from drop down maybe int borderHeight = 0, borderWidth = 0; - if (!ignoreBorder && m_settings.showBorder) { + if (m_settings.showBorder) { int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; layout->renderBorder(); int borderHorzDist = layout->getBorderDrawWidth(); @@ -475,96 +508,113 @@ QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout, bool ignoreBo pixmap = newPixmap; } - // The grid should be painted last, so if this layout pixmap is being painted - // as part of a map (which has more to paint after this) then don't paint the grid yet. - if (!ignoreGrid) - paintGrid(&pixmap, ignoreBorder); + paintGrid(&pixmap); return pixmap; } -QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { +QPixmap MapImageExporter::getFormattedMapPixmap(Map *map) { if (!map) return QPixmap(); - QPixmap pixmap = getFormattedLayoutPixmap(map->layout(), ignoreBorder, true); + // Temporarily disable the grid so that it doesn't get painted when we render the layout. + auto showGrid = m_settings.showGrid; + m_settings.showGrid = false; + QPixmap pixmap = getFormattedLayoutPixmap(map->layout()); + m_settings.showGrid = showGrid; - if (!ignoreBorder && (m_settings.showUpConnections || m_settings.showDownConnections || m_settings.showLeftConnections || m_settings.showRightConnections)) { - // if showing connections, draw on outside of image + // Paint connections + if (m_settings.showBorder && connectionsEnabled()) { QPainter connectionPainter(&pixmap); int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; for (const auto &connection : m_map->getConnections()) { - const QString direction = connection->direction(); - if (direction == "up") { - if (!m_settings.showUpConnections) continue; - } else if (direction == "down") { - if (!m_settings.showDownConnections) continue; - } else if (direction == "left") { - if (!m_settings.showLeftConnections) continue; - } else if (direction == "right") { - if (!m_settings.showRightConnections) continue; - } else continue; // Ignore any other directions - + if (!m_settings.showConnections.contains(connection->direction())) + continue; QPoint pos = connection->relativePos(true); connectionPainter.drawImage((pos.x() + borderDistance) * 16, (pos.y() + borderDistance) * 16, connection->render().toImage()); } connectionPainter.end(); } - // draw events - if (m_settings.showObjects || m_settings.showWarps || m_settings.showBGs || m_settings.showTriggers || m_settings.showHealLocations) { - QPainter eventPainter(&pixmap); - int pixelOffset = 0; - if (!ignoreBorder && m_settings.showBorder) { - pixelOffset = m_mode == ImageExporterMode::Normal ? BORDER_DISTANCE * 16 : STITCH_MODE_BORDER_DISTANCE * 16; - } - const QList events = map->getEvents(); - for (const auto &event : events) { - Event::Group group = event->getEventGroup(); - if ((m_settings.showObjects && group == Event::Group::Object) - || (m_settings.showWarps && group == Event::Group::Warp) - || (m_settings.showBGs && group == Event::Group::Bg) - || (m_settings.showTriggers && group == Event::Group::Coord) - || (m_settings.showHealLocations && group == Event::Group::Heal)) { - m_project->loadEventPixmap(event); - eventPainter.setOpacity(event->getUsesDefaultPixmap() ? 0.7 : 1.0); - eventPainter.drawImage(QPoint(event->getPixelX() + pixelOffset, event->getPixelY() + pixelOffset), event->getPixmap().toImage()); - } - } - eventPainter.end(); + int eventPixelOffset = 0; + if (m_settings.showBorder) { + eventPixelOffset = (m_mode == ImageExporterMode::Normal) ? BORDER_DISTANCE * 16 : STITCH_MODE_BORDER_DISTANCE * 16; } - paintGrid(&pixmap, ignoreBorder); + paintEvents(&pixmap, map, QPoint(eventPixelOffset, eventPixelOffset)); + paintGrid(&pixmap); return pixmap; } -void MapImageExporter::paintGrid(QPixmap *pixmap, bool ignoreBorder) { - // draw grid directly onto the pixmap - // since the last grid lines are outside of the pixmap, add a pixel to the bottom and right - if (m_settings.showGrid) { - bool hasBorder = !ignoreBorder && m_settings.showBorder; - int addX = 1, addY = 1; - if (hasBorder) addY = 0; - if (hasBorder) addX = 0; +void MapImageExporter::paintEvents(QPixmap *pixmap, const Map *map, const QPoint &pixelOffset) { + if (!eventsEnabled()) + return; - 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()); + QPainter painter(pixmap); + painter.translate(pixelOffset); + for (const auto &group : Event::groups()) { + if (!m_settings.showEvents.contains(group)) + continue; + for (const auto &event : map->getEvents(group)) { + m_project->loadEventPixmap(event); + painter.setOpacity(event->getUsesDefaultPixmap() ? 0.7 : 1.0); + painter.drawImage(QPoint(event->getPixelX(), event->getPixelY()), event->getPixmap().toImage()); } - for (int y = 0; y < newPixmap.height(); y += 16) { - gridPainter.drawLine(0, y, newPixmap.width(), y); - } - gridPainter.end(); - *pixmap = newPixmap; + } + painter.end(); +} + +void MapImageExporter::paintGrid(QPixmap *pixmap) { + if (!m_settings.showGrid) + return; + + int addX = 0, addY = 0; + if (!m_settings.showBorder) { + // since the last grid lines are outside of the pixmap, add a pixel to the bottom and right + addX = 1, addY = 1; + } + + 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(); + *pixmap = newPixmap; +} + +bool MapImageExporter::eventsEnabled() { + return !m_settings.showEvents.isEmpty(); +} + +void MapImageExporter::setEventGroupEnabled(Event::Group group, bool enable) { + if (enable) { + m_settings.showEvents.insert(group); + } else { + m_settings.showEvents.remove(group); + } +} + +bool MapImageExporter::connectionsEnabled() { + return !m_settings.showConnections.isEmpty(); +} + +void MapImageExporter::setConnectionDirectionEnabled(const QString &dir, bool enable) { + if (enable) { + m_settings.showConnections.insert(dir); + } else { + m_settings.showConnections.remove(dir); } } void MapImageExporter::updateShowBorderState() { // If any of the Connections settings are enabled then this setting is locked (it's implicitly enabled) - bool on = (m_settings.showUpConnections || m_settings.showDownConnections || m_settings.showLeftConnections || m_settings.showRightConnections); + bool on = connectionsEnabled(); const QSignalBlocker blocker(ui->checkBox_Border); ui->checkBox_Border->setChecked(on); ui->checkBox_Border->setDisabled(on); @@ -587,27 +637,27 @@ void MapImageExporter::on_checkBox_Border_stateChanged(int state) { } void MapImageExporter::on_checkBox_Objects_stateChanged(int state) { - m_settings.showObjects = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Object, state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Warps_stateChanged(int state) { - m_settings.showWarps = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Warp, state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_BGs_stateChanged(int state) { - m_settings.showBGs = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Bg, state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Triggers_stateChanged(int state) { - m_settings.showTriggers = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Coord, state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_HealLocations_stateChanged(int state) { - m_settings.showHealLocations = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Heal, state == Qt::Checked); updatePreview(); } @@ -618,51 +668,51 @@ void MapImageExporter::on_checkBox_AllEvents_stateChanged(int state) { const QSignalBlocker b_Objects(ui->checkBox_Objects); ui->checkBox_Objects->setChecked(on); ui->checkBox_Objects->setDisabled(on); - m_settings.showObjects = on; + setEventGroupEnabled(Event::Group::Object, on); const QSignalBlocker b_Warps(ui->checkBox_Warps); ui->checkBox_Warps->setChecked(on); ui->checkBox_Warps->setDisabled(on); - m_settings.showWarps = on; + setEventGroupEnabled(Event::Group::Warp, on); const QSignalBlocker b_BGs(ui->checkBox_BGs); ui->checkBox_BGs->setChecked(on); ui->checkBox_BGs->setDisabled(on); - m_settings.showBGs = on; + setEventGroupEnabled(Event::Group::Bg, on); const QSignalBlocker b_Triggers(ui->checkBox_Triggers); ui->checkBox_Triggers->setChecked(on); ui->checkBox_Triggers->setDisabled(on); - m_settings.showTriggers = on; + setEventGroupEnabled(Event::Group::Coord, on); const QSignalBlocker b_HealLocations(ui->checkBox_HealLocations); ui->checkBox_HealLocations->setChecked(on); ui->checkBox_HealLocations->setDisabled(on); - m_settings.showHealLocations = on; + setEventGroupEnabled(Event::Group::Heal, on); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionUp_stateChanged(int state) { - m_settings.showUpConnections = (state == Qt::Checked); + setConnectionDirectionEnabled("up", state == Qt::Checked); updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionDown_stateChanged(int state) { - m_settings.showDownConnections = (state == Qt::Checked); + setConnectionDirectionEnabled("down", state == Qt::Checked); updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionLeft_stateChanged(int state) { - m_settings.showLeftConnections = (state == Qt::Checked); + setConnectionDirectionEnabled("left", state == Qt::Checked); updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionRight_stateChanged(int state) { - m_settings.showRightConnections = (state == Qt::Checked); + setConnectionDirectionEnabled("right", state == Qt::Checked); updateShowBorderState(); updatePreview(); } @@ -674,22 +724,22 @@ void MapImageExporter::on_checkBox_AllConnections_stateChanged(int state) { const QSignalBlocker b_Up(ui->checkBox_ConnectionUp); ui->checkBox_ConnectionUp->setChecked(on); ui->checkBox_ConnectionUp->setDisabled(on); - m_settings.showUpConnections = on; + setConnectionDirectionEnabled("up", on); const QSignalBlocker b_Down(ui->checkBox_ConnectionDown); ui->checkBox_ConnectionDown->setChecked(on); ui->checkBox_ConnectionDown->setDisabled(on); - m_settings.showDownConnections = on; + setConnectionDirectionEnabled("down", on); const QSignalBlocker b_Left(ui->checkBox_ConnectionLeft); ui->checkBox_ConnectionLeft->setChecked(on); ui->checkBox_ConnectionLeft->setDisabled(on); - m_settings.showLeftConnections = on; + setConnectionDirectionEnabled("left", on); const QSignalBlocker b_Right(ui->checkBox_ConnectionRight); ui->checkBox_ConnectionRight->setChecked(on); ui->checkBox_ConnectionRight->setDisabled(on); - m_settings.showRightConnections = on; + setConnectionDirectionEnabled("right", on); updateShowBorderState(); updatePreview(); From f7781ca5dff44dc28c3b678eb3013bad3490af6f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 21 Mar 2025 13:47:13 -0400 Subject: [PATCH 03/12] Render full border in image export --- src/ui/mapimageexporter.cpp | 40 ++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index c2858a0f..e665651b 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -8,8 +8,6 @@ #include #include -#define STITCH_MODE_BORDER_DISTANCE 2 - QString MapImageExporter::getTitle(ImageExporterMode mode) { switch (mode) { @@ -167,8 +165,8 @@ void MapImageExporter::saveImage() { int maxWidth = m_layout->getWidth() * 16; int maxHeight = m_layout->getHeight() * 16; if (m_settings.showBorder) { - maxWidth += 2 * STITCH_MODE_BORDER_DISTANCE * 16; - maxHeight += 2 * STITCH_MODE_BORDER_DISTANCE * 16; + maxWidth += 2 * BORDER_DISTANCE * 16; + maxHeight += 2 * BORDER_DISTANCE * 16; } // Rewind to the specified start of the map edit history. int i = 0; @@ -178,8 +176,8 @@ void MapImageExporter::saveImage() { int width = m_layout->getWidth() * 16; int height = m_layout->getHeight() * 16; if (m_settings.showBorder) { - width += 2 * STITCH_MODE_BORDER_DISTANCE * 16; - height += 2 * STITCH_MODE_BORDER_DISTANCE * 16; + width += 2 * BORDER_DISTANCE * 16; + height += 2 * BORDER_DISTANCE * 16; } if (width > maxWidth) { maxWidth = width; @@ -346,10 +344,10 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { } if (m_settings.showBorder) { - minX -= STITCH_MODE_BORDER_DISTANCE; - maxX += STITCH_MODE_BORDER_DISTANCE; - minY -= STITCH_MODE_BORDER_DISTANCE; - maxY += STITCH_MODE_BORDER_DISTANCE; + minX -= BORDER_DISTANCE; + maxX += BORDER_DISTANCE; + minY -= BORDER_DISTANCE; + maxY += BORDER_DISTANCE; } // Draw the maps on the full canvas, while taking @@ -379,8 +377,8 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { int pixelX = (map.x - minX) * 16; int pixelY = (map.y - minY) * 16; if (m_settings.showBorder) { - pixelX -= STITCH_MODE_BORDER_DISTANCE * 16; - pixelY -= STITCH_MODE_BORDER_DISTANCE * 16; + pixelX -= BORDER_DISTANCE * 16; + pixelY -= BORDER_DISTANCE * 16; } QPixmap pixmap = getFormattedMapPixmap(map.map); painter.drawPixmap(pixelX, pixelY, pixmap); @@ -490,16 +488,15 @@ QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout) { // draw map border int borderHeight = 0, borderWidth = 0; if (m_settings.showBorder) { - int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; layout->renderBorder(); int borderHorzDist = layout->getBorderDrawWidth(); int borderVertDist = layout->getBorderDrawHeight(); - borderWidth = borderDistance * 16; - borderHeight = borderDistance * 16; + borderWidth = BORDER_DISTANCE * 16; + borderHeight = BORDER_DISTANCE * 16; QPixmap newPixmap = QPixmap(layout->pixmap.width() + borderWidth * 2, layout->pixmap.height() + borderHeight * 2); QPainter borderPainter(&newPixmap); - for (int y = borderDistance - borderVertDist; y < layout->getHeight() + borderVertDist * 2; y += layout->getBorderHeight()) { - for (int x = borderDistance - borderHorzDist; x < layout->getWidth() + borderHorzDist * 2; x += layout->getBorderWidth()) { + for (int y = BORDER_DISTANCE - borderVertDist; y < layout->getHeight() + borderVertDist * 2; y += layout->getBorderHeight()) { + for (int x = BORDER_DISTANCE - borderHorzDist; x < layout->getWidth() + borderHorzDist * 2; x += layout->getBorderWidth()) { borderPainter.drawPixmap(x * 16, y * 16, layout->border_pixmap); } } @@ -527,21 +524,18 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map) { if (m_settings.showBorder && connectionsEnabled()) { QPainter connectionPainter(&pixmap); - int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; for (const auto &connection : m_map->getConnections()) { if (!m_settings.showConnections.contains(connection->direction())) continue; QPoint pos = connection->relativePos(true); - connectionPainter.drawImage((pos.x() + borderDistance) * 16, (pos.y() + borderDistance) * 16, connection->render().toImage()); + connectionPainter.drawImage((pos.x() + BORDER_DISTANCE) * 16, (pos.y() + BORDER_DISTANCE) * 16, connection->render().toImage()); } connectionPainter.end(); } - int eventPixelOffset = 0; - if (m_settings.showBorder) { - eventPixelOffset = (m_mode == ImageExporterMode::Normal) ? BORDER_DISTANCE * 16 : STITCH_MODE_BORDER_DISTANCE * 16; - } + int eventPixelOffset = m_settings.showBorder ? BORDER_DISTANCE * 16 : 0; paintEvents(&pixmap, map, QPoint(eventPixelOffset, eventPixelOffset)); + paintGrid(&pixmap); return pixmap; From ee1d6ac4815fb11d8e32c3509be0b6c383397c7b Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 21 Mar 2025 15:03:37 -0400 Subject: [PATCH 04/12] Allow exporting connections with no border --- include/ui/mapimageexporter.h | 6 +- src/ui/mapimageexporter.cpp | 164 ++++++++++++++++++---------------- 2 files changed, 92 insertions(+), 78 deletions(-) diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 29cefe81..dc6ae46b 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -59,12 +59,14 @@ private: void setEventGroupEnabled(Event::Group group, bool enable); bool connectionsEnabled(); void setConnectionDirectionEnabled(const QString &dir, bool enable); - void updateShowBorderState(); void saveImage(); QPixmap getStitchedImage(QProgressDialog *progress); QPixmap getFormattedMapPixmap(); QPixmap getFormattedMapPixmap(Map *map); - QPixmap getFormattedLayoutPixmap(Layout *layout); + QPixmap getFormattedLayoutPixmap(Layout *layout, bool ignoreGrid = false); + void paintBorder(QPixmap *pixmap, Layout *layout); + void paintCollision(QPixmap *pixmap, Layout *layout); + void paintConnections(QPixmap *pixmap, const Map *map); void paintEvents(QPixmap *pixmap, const Map *map, const QPoint &pixelOffset); void paintGrid(QPixmap *pixmap); bool historyItemAppliesToFrame(const QUndoCommand *command); diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index e665651b..159a9d30 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -360,12 +360,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { QPixmap stitchedPixmap((maxX - minX) * 16, (maxY - minY) * 16); stitchedPixmap.fill(Qt::black); - // Temporarily disable settings that have separate passes. - auto showEvents = m_settings.showEvents; - m_settings.showEvents.clear(); - bool showGrid = m_settings.showGrid; - m_settings.showGrid = false; - + // First pass, render the layouts, borders, and collision (if enabled) QPainter painter(&stitchedPixmap); for (StitchedMap map : stitchedMaps) { if (progress->wasCanceled()) { @@ -380,8 +375,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { pixelX -= BORDER_DISTANCE * 16; pixelY -= BORDER_DISTANCE * 16; } - QPixmap pixmap = getFormattedMapPixmap(map.map); - painter.drawPixmap(pixelX, pixelY, pixmap); + painter.drawPixmap(pixelX, pixelY, getFormattedLayoutPixmap(map.map->layout(), true)); } // When including the borders, we simply draw all the maps again @@ -402,8 +396,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { int pixelX = (map.x - minX) * 16; int pixelY = (map.y - minY) * 16; - QPixmap pixmapWithoutBorders = getFormattedMapPixmap(map.map); - painter.drawPixmap(pixelX, pixelY, pixmapWithoutBorders); + painter.drawPixmap(pixelX, pixelY, getFormattedLayoutPixmap(map.map->layout(), true)); } m_settings.showBorder = true; } @@ -411,7 +404,6 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { // Events can be occluded by neighboring maps if they are positioned near or outside the map's edge. // Now that all the maps have been rendered we can render the events (if enabled). - m_settings.showEvents = showEvents; if (eventsEnabled()) { progress->setLabelText("Drawing stitched map events..."); progress->setValue(0); @@ -430,7 +422,6 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { } } - m_settings.showGrid = showGrid; paintGrid(&stitchedPixmap); return stitchedPixmap; @@ -470,42 +461,16 @@ QPixmap MapImageExporter::getFormattedMapPixmap() { return m_map ? getFormattedMapPixmap(m_map) : getFormattedLayoutPixmap(m_layout); } -QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout) { +QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout, bool ignoreGrid) { if (!layout) return QPixmap(); layout->render(true); QPixmap pixmap = layout->pixmap; - if (m_settings.showCollision) { - QPainter collisionPainter(&pixmap); - layout->renderCollision(true); - collisionPainter.setOpacity(static_cast(porymapConfig.collisionOpacity) / 100); - collisionPainter.drawPixmap(0, 0, layout->collision_pixmap); - collisionPainter.end(); - } - - // draw map border - int borderHeight = 0, borderWidth = 0; - if (m_settings.showBorder) { - layout->renderBorder(); - int borderHorzDist = layout->getBorderDrawWidth(); - int borderVertDist = layout->getBorderDrawHeight(); - borderWidth = BORDER_DISTANCE * 16; - borderHeight = BORDER_DISTANCE * 16; - QPixmap newPixmap = QPixmap(layout->pixmap.width() + borderWidth * 2, layout->pixmap.height() + borderHeight * 2); - QPainter borderPainter(&newPixmap); - for (int y = BORDER_DISTANCE - borderVertDist; y < layout->getHeight() + borderVertDist * 2; y += layout->getBorderHeight()) { - for (int x = BORDER_DISTANCE - borderHorzDist; x < layout->getWidth() + borderHorzDist * 2; x += layout->getBorderWidth()) { - borderPainter.drawPixmap(x * 16, y * 16, layout->border_pixmap); - } - } - borderPainter.drawImage(borderWidth, borderHeight, pixmap.toImage()); - borderPainter.end(); - pixmap = newPixmap; - } - - paintGrid(&pixmap); + paintCollision(&pixmap, layout); + paintBorder(&pixmap, layout); + if (!ignoreGrid) paintGrid(&pixmap); return pixmap; } @@ -514,33 +479,94 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map) { if (!map) return QPixmap(); - // Temporarily disable the grid so that it doesn't get painted when we render the layout. - auto showGrid = m_settings.showGrid; - m_settings.showGrid = false; - QPixmap pixmap = getFormattedLayoutPixmap(map->layout()); - m_settings.showGrid = showGrid; + QPixmap pixmap = getFormattedLayoutPixmap(map->layout(), true); + paintConnections(&pixmap, map); - // Paint connections - if (m_settings.showBorder && connectionsEnabled()) { - QPainter connectionPainter(&pixmap); - - for (const auto &connection : m_map->getConnections()) { - if (!m_settings.showConnections.contains(connection->direction())) - continue; - QPoint pos = connection->relativePos(true); - connectionPainter.drawImage((pos.x() + BORDER_DISTANCE) * 16, (pos.y() + BORDER_DISTANCE) * 16, connection->render().toImage()); - } - connectionPainter.end(); - } - - int eventPixelOffset = m_settings.showBorder ? BORDER_DISTANCE * 16 : 0; - paintEvents(&pixmap, map, QPoint(eventPixelOffset, eventPixelOffset)); + int eventPixelOffsetX = (m_settings.showBorder || m_settings.showConnections.contains("left")) ? BORDER_DISTANCE * 16 : 0; + int eventPixelOffsetY = (m_settings.showBorder || m_settings.showConnections.contains("up")) ? BORDER_DISTANCE * 16 : 0; + paintEvents(&pixmap, map, QPoint(eventPixelOffsetX, eventPixelOffsetY)); paintGrid(&pixmap); return pixmap; } +void MapImageExporter::paintCollision(QPixmap *pixmap, Layout *layout) { + if (!m_settings.showCollision) + return; + + layout->renderCollision(true); + + QPainter painter(pixmap); + painter.setOpacity(static_cast(porymapConfig.collisionOpacity) / 100); + painter.drawPixmap(0, 0, layout->collision_pixmap); + painter.end(); +} + +void MapImageExporter::paintBorder(QPixmap *pixmap, Layout *layout) { + if (!m_settings.showBorder) + return; + + layout->renderBorder(); + + int borderHorzDist = layout->getBorderDrawWidth(); + int borderVertDist = layout->getBorderDrawHeight(); + int borderWidth = BORDER_DISTANCE * 16; + int borderHeight = BORDER_DISTANCE * 16; + QPixmap newPixmap = QPixmap(layout->pixmap.width() + borderWidth * 2, layout->pixmap.height() + borderHeight * 2); + QPainter painter(&newPixmap); + for (int y = BORDER_DISTANCE - borderVertDist; y < layout->getHeight() + borderVertDist * 2; y += layout->getBorderHeight()) { + for (int x = BORDER_DISTANCE - borderHorzDist; x < layout->getWidth() + borderHorzDist * 2; x += layout->getBorderWidth()) { + painter.drawPixmap(x * 16, y * 16, layout->border_pixmap); + } + } + painter.drawImage(borderWidth, borderHeight, pixmap->toImage()); + painter.end(); + *pixmap = newPixmap; +} + +void MapImageExporter::paintConnections(QPixmap *pixmap, const Map *map) { + if (!connectionsEnabled()) + return; + + QMargins margins; + if (m_settings.showBorder) { + // Adjust for the border having resized the pixmap + margins.setLeft(BORDER_DISTANCE * 16); + margins.setTop(BORDER_DISTANCE * 16); + } else { + // We haven't rendered the map border, so we will need to resize the canvas to fit any map connections. + // We do this minimally so that the final image will not contain unnecessary space for unoccupied connections. + for (const auto &connection : map->getConnections()) { + const QString dir = connection->direction(); + if (!m_settings.showConnections.contains(dir)) + continue; + if (dir == "up") margins.setTop(BORDER_DISTANCE * 16); + else if (dir == "down") margins.setBottom(BORDER_DISTANCE * 16); + else if (dir == "left") margins.setLeft(BORDER_DISTANCE * 16); + else if (dir == "right") margins.setRight(BORDER_DISTANCE * 16); + } + if (!margins.isNull()) { + QPixmap resizedPixmap = QPixmap(map->layout()->pixmap.width() + margins.left() + margins.right(), + map->layout()->pixmap.height() + margins.top() + margins.bottom()); + resizedPixmap.fill(Qt::black); + QPainter resizePainter(&resizedPixmap); + resizePainter.drawPixmap(margins.left(), margins.top(), *pixmap); + resizePainter.end(); + *pixmap = resizedPixmap; + } + } + + QPainter painter(pixmap); + for (const auto &connection : map->getConnections()) { + if (!m_settings.showConnections.contains(connection->direction())) + continue; + QPoint pos = connection->relativePos(true) * 16; + painter.drawImage(pos.x() + margins.left(), pos.y() + margins.top(), connection->render().toImage()); + } + painter.end(); +} + void MapImageExporter::paintEvents(QPixmap *pixmap, const Map *map, const QPoint &pixelOffset) { if (!eventsEnabled()) return; @@ -606,15 +632,6 @@ void MapImageExporter::setConnectionDirectionEnabled(const QString &dir, bool en } } -void MapImageExporter::updateShowBorderState() { - // If any of the Connections settings are enabled then this setting is locked (it's implicitly enabled) - bool on = connectionsEnabled(); - const QSignalBlocker blocker(ui->checkBox_Border); - ui->checkBox_Border->setChecked(on); - ui->checkBox_Border->setDisabled(on); - m_settings.showBorder = on; -} - void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) { m_settings.showCollision = (state == Qt::Checked); updatePreview(); @@ -689,25 +706,21 @@ void MapImageExporter::on_checkBox_AllEvents_stateChanged(int state) { void MapImageExporter::on_checkBox_ConnectionUp_stateChanged(int state) { setConnectionDirectionEnabled("up", state == Qt::Checked); - updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionDown_stateChanged(int state) { setConnectionDirectionEnabled("down", state == Qt::Checked); - updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionLeft_stateChanged(int state) { setConnectionDirectionEnabled("left", state == Qt::Checked); - updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionRight_stateChanged(int state) { setConnectionDirectionEnabled("right", state == Qt::Checked); - updateShowBorderState(); updatePreview(); } @@ -735,7 +748,6 @@ void MapImageExporter::on_checkBox_AllConnections_stateChanged(int state) { ui->checkBox_ConnectionRight->setDisabled(on); setConnectionDirectionEnabled("right", on); - updateShowBorderState(); updatePreview(); } From c8dc7d0eb964e53d429c906ec674f758cf0bb373 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 21 Mar 2025 19:39:26 -0400 Subject: [PATCH 05/12] Prevent crash in Timelapse mode, sync editor with exporter --- include/mainwindow.h | 4 ++ include/ui/mapimageexporter.h | 10 +++- src/mainwindow.cpp | 7 +++ src/ui/mapimageexporter.cpp | 101 +++++++++++++++++++++++----------- 4 files changed, 87 insertions(+), 35 deletions(-) diff --git a/include/mainwindow.h b/include/mainwindow.h index 69e56b3c..446838e4 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -301,6 +301,10 @@ public: Ui::MainWindow *ui; QPointer editor = nullptr; +signals: + void mapOpened(Map*); + void layoutOpened(Layout*); + private: QLabel *label_MapRulerStatus = nullptr; QPointer tilesetEditor = nullptr; diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index dc6ae46b..5b3650ea 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -29,14 +29,17 @@ class MapImageExporter : public QDialog Q_OBJECT public: - explicit MapImageExporter(QWidget *parent, Project *project, Layout *layout, ImageExporterMode mode = ImageExporterMode::Normal) - : MapImageExporter(parent, project, nullptr, layout, mode) {}; explicit MapImageExporter(QWidget *parent, Project *project, Map *map, ImageExporterMode mode = ImageExporterMode::Normal) : MapImageExporter(parent, project, map, map->layout(), mode) {}; + explicit MapImageExporter(QWidget *parent, Project *project, Layout *layout, ImageExporterMode mode = ImageExporterMode::Normal) + : MapImageExporter(parent, project, nullptr, layout, mode) {}; ~MapImageExporter(); ImageExporterMode mode() const { return m_mode; } + void setMap(Map *map); + void setLayout(Layout *layout); + private: explicit MapImageExporter(QWidget *parent, Project *project, Map *map, Layout *layout, ImageExporterMode mode); @@ -51,6 +54,8 @@ private: ImageExporterSettings m_settings; ImageExporterMode m_mode = ImageExporterMode::Normal; + void setModeSpecificUi(); + void updateMapSelection(); QString getTitle(ImageExporterMode mode); QString getDescription(ImageExporterMode mode); void updatePreview(); @@ -70,7 +75,6 @@ private: void paintEvents(QPixmap *pixmap, const Map *map, const QPoint &pixelOffset); void paintGrid(QPixmap *pixmap); bool historyItemAppliesToFrame(const QUndoCommand *command); - void updateMapSelection(const QString &text); protected: virtual void showEvent(QShowEvent *) override; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 28dabc33..30347c14 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -934,6 +934,9 @@ bool MainWindow::setMap(QString map_name) { Scripting::cb_MapOpened(map_name); prefab.updatePrefabUi(editor->layout); updateTilesetEditor(); + + emit mapOpened(editor->map); + return true; } @@ -988,6 +991,8 @@ bool MainWindow::setLayout(QString layoutId) { userConfig.recentMapOrLayout = layoutId; + emit layoutOpened(editor->layout); + return true; } @@ -2501,8 +2506,10 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { // Open new image export window if (this->editor->map){ this->mapImageExporter = new MapImageExporter(this, this->editor->project, this->editor->map, mode); + connect(this, &MainWindow::mapOpened, this->mapImageExporter, &MapImageExporter::setMap); } else if (this->editor->layout) { this->mapImageExporter = new MapImageExporter(this, this->editor->project, this->editor->layout, mode); + connect(this, &MainWindow::layoutOpened, this->mapImageExporter, &MapImageExporter::setLayout); } } diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 159a9d30..259bb0ee 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -44,14 +44,38 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, { setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); + setModeSpecificUi(); + + connect(ui->pushButton_Save, &QPushButton::pressed, this, &MapImageExporter::saveImage); + connect(ui->pushButton_Cancel, &QPushButton::pressed, this, &MapImageExporter::close); + connect(ui->comboBox_MapSelection, &QComboBox::currentIndexChanged, this, &MapImageExporter::updateMapSelection); + connect(ui->comboBox_MapSelection->lineEdit(), &QLineEdit::editingFinished, this, &MapImageExporter::updateMapSelection); + + ui->graphicsView_Preview->setFocus(); +} + +MapImageExporter::~MapImageExporter() { + delete m_scene; + delete ui; +} + +void MapImageExporter::setModeSpecificUi() { setWindowTitle(getTitle(m_mode)); ui->label_Description->setText(getDescription(m_mode)); ui->groupBox_Connections->setVisible(m_map && m_mode != ImageExporterMode::Stitch); ui->groupBox_Timelapse->setVisible(m_mode == ImageExporterMode::Timelapse); ui->groupBox_Events->setVisible(m_map != nullptr); - // Initialize map selector. - if (m_mode != ImageExporterMode::Timelapse) { + // Initialize map selector + const QSignalBlocker b(ui->comboBox_MapSelection); + ui->comboBox_MapSelection->clear(); + if (m_mode == ImageExporterMode::Timelapse) { + // At the moment edit history for events (and the DraggablePixmapItem class) + // depend on the editor and assume their map is the current map. + // Until this is resolved, the selected map and the editor's map must be the same. + ui->comboBox_MapSelection->setEnabled(false); + ui->label_MapSelection->setEnabled(false); + } else { if (m_map) { ui->comboBox_MapSelection->addItems(m_project->mapNames); ui->comboBox_MapSelection->setCurrentText(m_map->name()); @@ -61,23 +85,23 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, ui->comboBox_MapSelection->setCurrentText(m_layout->id); ui->label_MapSelection->setText(QStringLiteral("Layout")); } - } else { - // At the moment edit history for events (and the DraggablePixmapItem class) - // depend on the editor and assume their map is the current map. - // Until this is resolved the selected map cannot be changed in Timelapse mode. - ui->comboBox_MapSelection->setVisible(false); - ui->label_MapSelection->setVisible(false); } - ui->graphicsView_Preview->setFocus(); - - connect(ui->pushButton_Save, &QPushButton::pressed, this, &MapImageExporter::saveImage); - connect(ui->pushButton_Cancel, &QPushButton::pressed, this, &MapImageExporter::close); - connect(ui->comboBox_MapSelection, &QComboBox::currentTextChanged, this, &MapImageExporter::updateMapSelection); } -MapImageExporter::~MapImageExporter() { - delete m_scene; - delete ui; +void MapImageExporter::setMap(Map *map) { + if (!map) return; + + const QSignalBlocker b(ui->comboBox_MapSelection); + ui->comboBox_MapSelection->setCurrentText(map->name()); + updateMapSelection(); +} + +void MapImageExporter::setLayout(Layout *layout) { + if (!layout) return; + + const QSignalBlocker b(ui->comboBox_MapSelection); + ui->comboBox_MapSelection->setCurrentText(layout->id); + updateMapSelection(); } // Allow the window to open before displaying the preview. @@ -92,23 +116,36 @@ void MapImageExporter::resizeEvent(QResizeEvent *event) { scalePreview(); } -void MapImageExporter::updateMapSelection(const QString &text) { - if (m_map) { - if (!m_project->mapNames.contains(text)) - return; - Map *newMap = m_project->loadMap(text); - if (newMap == m_map) - return; - m_map = newMap; - } else { - if (!m_project->layoutIds.contains(text)) - return; - Layout *newLayout = m_project->loadLayout(text); - if (newLayout == m_layout) - return; - m_layout = newLayout; +void MapImageExporter::updateMapSelection() { + auto oldMap = m_map; + auto oldLayout = m_layout; + + const QString text = ui->comboBox_MapSelection->currentText(); + if (m_project->mapNames.contains(text)) { + auto newMap = m_project->loadMap(text); + if (newMap) { + m_map = newMap; + m_layout = newMap->layout(); + } + } else if (m_project->layoutIds.contains(text) && m_mode != ImageExporterMode::Stitch) { + auto newLayout = m_project->loadLayout(text); + if (newLayout) { + m_map = nullptr; + m_layout = newLayout; + } + } + + // Ensure text in the combo box remains valid + const QSignalBlocker b(ui->comboBox_MapSelection); + ui->comboBox_MapSelection->setCurrentText(m_map ? m_map->name() : m_layout->id); + + if (m_map != oldMap && (!m_map || !oldMap)) { + // Switching to or from layout-only mode + setModeSpecificUi(); + } + if (m_map != oldMap || m_layout != oldLayout){ + updatePreview(); } - updatePreview(); } void MapImageExporter::saveImage() { From f28088af3c93e46e5cb04785dd9a7700555d2478 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sat, 22 Mar 2025 00:37:03 -0400 Subject: [PATCH 06/12] Add live preview of gif export --- include/ui/mapimageexporter.h | 8 +- src/ui/mapimageexporter.cpp | 287 +++++++++++++++++++--------------- 2 files changed, 171 insertions(+), 124 deletions(-) diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 5b3650ea..cf200c46 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -3,6 +3,8 @@ #include "project.h" +class QGifImage; + namespace Ui { class MapImageExporter; } @@ -48,8 +50,11 @@ private: Map *m_map = nullptr; Layout *m_layout = nullptr; QGraphicsScene *m_scene = nullptr; + QGifImage *m_timelapseImage = nullptr; + QBuffer *m_timelapseBuffer = nullptr; + QMovie *m_timelapseMovie = nullptr; + QGraphicsPixmapItem *m_preview = nullptr; - QPixmap m_preview; ImageExporterSettings m_settings; ImageExporterMode m_mode = ImageExporterMode::Normal; @@ -65,6 +70,7 @@ private: bool connectionsEnabled(); void setConnectionDirectionEnabled(const QString &dir, bool enable); void saveImage(); + QGifImage* createTimelapseImage(); QPixmap getStitchedImage(QProgressDialog *progress); QPixmap getFormattedMapPixmap(); QPixmap getFormattedMapPixmap(Map *map); diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 259bb0ee..eb31ed8a 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -55,6 +55,7 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, } MapImageExporter::~MapImageExporter() { + delete m_timelapseImage; delete m_scene; delete ui; } @@ -69,22 +70,22 @@ void MapImageExporter::setModeSpecificUi() { // Initialize map selector const QSignalBlocker b(ui->comboBox_MapSelection); ui->comboBox_MapSelection->clear(); + if (m_map) { + ui->comboBox_MapSelection->addItems(m_project->mapNames); + ui->comboBox_MapSelection->setCurrentText(m_map->name()); + ui->label_MapSelection->setText(m_mode == ImageExporterMode::Stitch ? QStringLiteral("Starting Map") : QStringLiteral("Map")); + } else if (m_layout) { + ui->comboBox_MapSelection->addItems(m_project->layoutIds); + ui->comboBox_MapSelection->setCurrentText(m_layout->id); + ui->label_MapSelection->setText(QStringLiteral("Layout")); + } + if (m_mode == ImageExporterMode::Timelapse) { // At the moment edit history for events (and the DraggablePixmapItem class) // depend on the editor and assume their map is the current map. - // Until this is resolved, the selected map and the editor's map must be the same. + // Until this is resolved, the selected map and the editor's map must remain the same. ui->comboBox_MapSelection->setEnabled(false); ui->label_MapSelection->setEnabled(false); - } else { - if (m_map) { - ui->comboBox_MapSelection->addItems(m_project->mapNames); - ui->comboBox_MapSelection->setCurrentText(m_map->name()); - ui->label_MapSelection->setText(m_mode == ImageExporterMode::Stitch ? QStringLiteral("Starting Map") : QStringLiteral("Map")); - } else if (m_layout) { - ui->comboBox_MapSelection->addItems(m_project->layoutIds); - ui->comboBox_MapSelection->setCurrentText(m_layout->id); - ui->label_MapSelection->setText(QStringLiteral("Layout")); - } } } @@ -149,12 +150,6 @@ void MapImageExporter::updateMapSelection() { } void MapImageExporter::saveImage() { - // Make sure preview is up-to-date before we save. - if (m_preview.isNull()) - updatePreview(); - if (m_preview.isNull()) - return; - const QString itemName = m_map ? m_map->name() : m_layout->name; QString defaultFilename; switch (m_mode) @@ -181,103 +176,20 @@ void MapImageExporter::saveImage() { case ImageExporterMode::Normal: case ImageExporterMode::Stitch: // Normal and Stitch modes already have the image ready to go in the preview. - m_preview.save(filepath); + if (m_preview->pixmap().isNull()) { + updatePreview(); + if (m_preview->pixmap().isNull()) + return; // Canceled + } + m_preview->pixmap().save(filepath); break; case ImageExporterMode::Timelapse: - // Timelapse will play in order of layout changes then map changes (events) - // TODO: potentially update in the future? - QGifImage timelapseImg; - timelapseImg.setDefaultDelay(m_settings.timelapseDelayMs); - timelapseImg.setDefaultTransparentColor(QColor(0, 0, 0)); - - // lambda to avoid redundancy - auto generateTimelapseFromHistory = [this, &timelapseImg](QString progressText, QUndoStack *historyStack){ - QProgressDialog progress(progressText, "Cancel", 0, 1, this); - progress.setAutoClose(true); - progress.setWindowModality(Qt::WindowModal); - progress.setModal(true); - progress.setMaximum(1); - progress.setValue(0); - - int maxWidth = m_layout->getWidth() * 16; - int maxHeight = m_layout->getHeight() * 16; - if (m_settings.showBorder) { - maxWidth += 2 * BORDER_DISTANCE * 16; - maxHeight += 2 * BORDER_DISTANCE * 16; - } - // Rewind to the specified start of the map edit history. - int i = 0; - while (historyStack->canUndo()) { - progress.setValue(i); - historyStack->undo(); - int width = m_layout->getWidth() * 16; - int height = m_layout->getHeight() * 16; - if (m_settings.showBorder) { - width += 2 * BORDER_DISTANCE * 16; - height += 2 * BORDER_DISTANCE * 16; - } - if (width > maxWidth) { - maxWidth = width; - } - if (height > maxHeight) { - maxHeight = height; - } - i++; - } - - // Draw each frame, skpping the specified number of map edits in - // the undo history. - progress.setMaximum(i); - while (i > 0) { - if (progress.wasCanceled()) { - progress.close(); - while (i > 0 && historyStack->canRedo()) { - i--; - historyStack->redo(); - } - return; - } - while (historyStack->canRedo() && - !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { - i--; - historyStack->redo(); - } - progress.setValue(progress.maximum() - i); - QPixmap pixmap = getFormattedMapPixmap(); - if (pixmap.width() < maxWidth || pixmap.height() < maxHeight) { - QPixmap pixmap2 = QPixmap(maxWidth, maxHeight); - QPainter painter(&pixmap2); - pixmap2.fill(QColor(0, 0, 0)); - painter.drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap); - painter.end(); - pixmap = pixmap2; - } - timelapseImg.addFrame(pixmap.toImage()); - for (int j = 0; j < m_settings.timelapseSkipAmount; j++) { - if (i > 0) { - i--; - historyStack->redo(); - while (historyStack->canRedo() && - !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { - i--; - historyStack->redo(); - } - } - } - } - // The latest map state is the last animated frame. - QPixmap pixmap = getFormattedMapPixmap(); - timelapseImg.addFrame(pixmap.toImage()); - progress.close(); - }; - - if (m_layout) - generateTimelapseFromHistory("Building layout timelapse...", &m_layout->editHistory); - - if (m_map) - generateTimelapseFromHistory("Building map timelapse...", m_map->editHistory()); - - timelapseImg.save(filepath); + if (!m_timelapseImage || m_timelapseImage->frameCount() == 0) { + m_timelapseImage = createTimelapseImage(); + if (!m_timelapseImage || m_timelapseImage->frameCount() == 0) + return; // Canceled + } + m_timelapseImage->save(filepath); break; } close(); @@ -464,26 +376,153 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { return stitchedPixmap; } -void MapImageExporter::updatePreview() { - if (m_scene) { - delete m_scene; - m_scene = nullptr; - } - m_scene = new QGraphicsScene; +QGifImage* MapImageExporter::createTimelapseImage() { + // Timelapse will play in order of layout changes then map changes (events) + // TODO: potentially update in the future? + auto timelapseImg = new QGifImage(); + timelapseImg->setDefaultDelay(m_settings.timelapseDelayMs); + timelapseImg->setDefaultTransparentColor(QColor(0, 0, 0)); + // lambda to avoid redundancy + auto generateTimelapseFromHistory = [this, timelapseImg](QString progressText, QUndoStack *historyStack){ + QProgressDialog progress(progressText, "Cancel", 0, 1, this); + progress.setAutoClose(true); + progress.setWindowModality(Qt::WindowModal); + progress.setModal(true); + progress.setMaximum(1); + progress.setValue(0); + + int maxWidth = m_layout->getWidth() * 16; + int maxHeight = m_layout->getHeight() * 16; + if (m_settings.showBorder) { + // TODO: We probably need to account for connections with no border here + maxWidth += 2 * BORDER_DISTANCE * 16; + maxHeight += 2 * BORDER_DISTANCE * 16; + } + // Rewind to the specified start of the map edit history. + int i = 0; + while (historyStack->canUndo()) { + progress.setValue(i); + historyStack->undo(); + int width = m_layout->getWidth() * 16; + int height = m_layout->getHeight() * 16; + if (m_settings.showBorder) { + width += 2 * BORDER_DISTANCE * 16; + height += 2 * BORDER_DISTANCE * 16; + } + if (width > maxWidth) { + maxWidth = width; + } + if (height > maxHeight) { + maxHeight = height; + } + i++; + } + + // Draw each frame, skipping the specified number of map edits in + // the undo history. + progress.setMaximum(i); + while (i > 0) { + if (progress.wasCanceled()) { + progress.close(); + while (i > 0 && historyStack->canRedo()) { + i--; + historyStack->redo(); + } + return false; + } + while (historyStack->canRedo() && + !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { + i--; + historyStack->redo(); + } + progress.setValue(progress.maximum() - i); + // TODO: Painting events is doing something funky to the quality of the timelapse image + QPixmap pixmap = getFormattedMapPixmap(); + if (pixmap.width() < maxWidth || pixmap.height() < maxHeight) { + QPixmap resizedPixmap = QPixmap(maxWidth, maxHeight); + QPainter painter(&resizedPixmap); + resizedPixmap.fill(QColor(0, 0, 0)); + painter.drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap); + painter.end(); + pixmap = resizedPixmap; + } + timelapseImg->addFrame(pixmap.toImage()); + for (int j = 0; j < m_settings.timelapseSkipAmount; j++) { + if (i > 0) { + i--; + historyStack->redo(); + while (historyStack->canRedo() && + !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { + i--; + historyStack->redo(); + } + } + } + } + // The latest map state is the last animated frame. + QPixmap pixmap = getFormattedMapPixmap(); + timelapseImg->addFrame(pixmap.toImage()); + progress.close(); + return true; + }; + + if (m_layout && !generateTimelapseFromHistory("Building layout timelapse...", &m_layout->editHistory)) { + delete timelapseImg; + return nullptr; + } + + if (m_map && !generateTimelapseFromHistory("Building map timelapse...", m_map->editHistory())) { + delete timelapseImg; + return nullptr; + } + + return timelapseImg; +} + +void MapImageExporter::updatePreview() { + QPixmap previewPixmap; if (m_mode == ImageExporterMode::Stitch) { QProgressDialog progress("Building map stitch...", "Cancel", 0, 1, this); progress.setAutoClose(true); progress.setWindowModality(Qt::WindowModal); progress.setModal(true); progress.setMinimumDuration(1000); - m_preview = getStitchedImage(&progress); + previewPixmap = getStitchedImage(&progress); progress.close(); - } else { - // Timelapse mode doesn't currently have a real preview. It just displays the current map as in Normal mode. - m_preview = getFormattedMapPixmap(); + } else if (m_mode == ImageExporterMode::Timelapse) { + delete m_timelapseImage; + m_timelapseImage = createTimelapseImage(); + + // We want to convert the QGifImage data into a QMovie for the preview display. + // Both support input/output with a QIODevice, so we use a QBuffer to translate the data. + delete m_timelapseBuffer; + m_timelapseBuffer = new QBuffer(this); + m_timelapseBuffer->open(QBuffer::ReadWrite); + m_timelapseImage->save(m_timelapseBuffer); + m_timelapseBuffer->close(); + + delete m_timelapseMovie; + m_timelapseMovie = new QMovie(m_timelapseBuffer, "gif", this); + m_timelapseMovie->setCacheMode(QMovie::CacheAll); + connect(m_timelapseMovie, &QMovie::frameChanged, [this](int) { + if (m_preview) m_preview->setPixmap(m_timelapseMovie->currentPixmap()); + }); + m_timelapseMovie->start(); + previewPixmap = m_timelapseMovie->currentPixmap(); + + } else if (m_mode == ImageExporterMode::Normal) { + previewPixmap = getFormattedMapPixmap(); } - m_scene->addPixmap(m_preview); + + if (m_preview) { + if (m_preview->scene()) + m_preview->scene()->removeItem(m_preview); + delete m_preview; + } + delete m_scene; + m_scene = new QGraphicsScene; + m_preview = m_scene->addPixmap(previewPixmap); ui->graphicsView_Preview->setScene(m_scene); scalePreview(); } @@ -810,8 +849,10 @@ void MapImageExporter::on_pushButton_Reset_pressed() { void MapImageExporter::on_spinBox_TimelapseDelay_valueChanged(int delayMs) { m_settings.timelapseDelayMs = delayMs; + updatePreview(); } void MapImageExporter::on_spinBox_FrameSkip_valueChanged(int skip) { m_settings.timelapseSkipAmount = skip; + updatePreview(); } From d437dffcd25569ab1944fbe385c84e2de53dc64a Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sat, 22 Mar 2025 01:12:52 -0400 Subject: [PATCH 07/12] Fix transparency when rendering events in timelapse mode --- src/ui/mapimageexporter.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index eb31ed8a..eb2e5ddb 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -437,7 +437,6 @@ QGifImage* MapImageExporter::createTimelapseImage() { historyStack->redo(); } progress.setValue(progress.maximum() - i); - // TODO: Painting events is doing something funky to the quality of the timelapse image QPixmap pixmap = getFormattedMapPixmap(); if (pixmap.width() < maxWidth || pixmap.height() < maxHeight) { QPixmap resizedPixmap = QPixmap(maxWidth, maxHeight); @@ -654,7 +653,10 @@ void MapImageExporter::paintEvents(QPixmap *pixmap, const Map *map, const QPoint continue; for (const auto &event : map->getEvents(group)) { m_project->loadEventPixmap(event); - painter.setOpacity(event->getUsesDefaultPixmap() ? 0.7 : 1.0); + if (m_mode != ImageExporterMode::Timelapse) { + // GIF format doesn't support partial transparency, so we can't do this in Timelapse mode. + painter.setOpacity(event->getUsesDefaultPixmap() ? 0.7 : 1.0); + } painter.drawImage(QPoint(event->getPixelX(), event->getPixelY()), event->getPixmap().toImage()); } } From 77330be555c40f6f68006beec0f97cf77014dec8 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sat, 22 Mar 2025 01:58:55 -0400 Subject: [PATCH 08/12] Fix pixmap resizing, speed up stitch rendering --- forms/mapimageexporter.ui | 19 +- include/ui/mapimageexporter.h | 16 +- src/ui/mapimageexporter.cpp | 399 ++++++++++++++++------------------ 3 files changed, 216 insertions(+), 218 deletions(-) diff --git a/forms/mapimageexporter.ui b/forms/mapimageexporter.ui index 1496a581..7130f032 100644 --- a/forms/mapimageexporter.ui +++ b/forms/mapimageexporter.ui @@ -7,7 +7,7 @@ 0 0 817 - 535 + 556 @@ -37,12 +37,12 @@ - - QComboBox::SizeAdjustPolicy::AdjustToContents - QComboBox::InsertPolicy::NoInsert + + QComboBox::SizeAdjustPolicy::AdjustToContents + @@ -326,16 +326,22 @@ + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents + true + + Qt::AlignmentFlag::AlignCenter + 0 0 469 - 464 + 485 @@ -368,6 +374,9 @@ QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored + + QPainter::RenderHint::LosslessImageRendering|QPainter::RenderHint::TextAntialiasing + QGraphicsView::DragMode::NoDrag diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index cf200c46..f57dffb6 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -55,11 +55,11 @@ private: QMovie *m_timelapseMovie = nullptr; QGraphicsPixmapItem *m_preview = nullptr; - ImageExporterSettings m_settings; ImageExporterMode m_mode = ImageExporterMode::Normal; void setModeSpecificUi(); + void setSelectionText(const QString &text); void updateMapSelection(); QString getTitle(ImageExporterMode mode); QString getDescription(ImageExporterMode mode); @@ -74,12 +74,14 @@ private: QPixmap getStitchedImage(QProgressDialog *progress); QPixmap getFormattedMapPixmap(); QPixmap getFormattedMapPixmap(Map *map); - QPixmap getFormattedLayoutPixmap(Layout *layout, bool ignoreGrid = false); - void paintBorder(QPixmap *pixmap, Layout *layout); - void paintCollision(QPixmap *pixmap, Layout *layout); - void paintConnections(QPixmap *pixmap, const Map *map); - void paintEvents(QPixmap *pixmap, const Map *map, const QPoint &pixelOffset); - void paintGrid(QPixmap *pixmap); + QPixmap getFormattedLayoutPixmap(Layout *layout); + void paintBorder(QPainter *painter, Layout *layout); + void paintCollision(QPainter *painter, Layout *layout); + void paintConnections(QPainter *painter, const Map *map); + void paintEvents(QPainter *painter, const Map *map); + void paintGrid(QPainter *painter, const Layout *layout = nullptr); + QPixmap getResizedPixmap(const QPixmap &pixmap, const QMargins &margins); + QMargins getMargins(const Map *map = nullptr); bool historyItemAppliesToFrame(const QUndoCommand *command); protected: diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index eb2e5ddb..7ec3948b 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -44,10 +44,17 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, { setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); + + m_scene = new QGraphicsScene(this); + ui->graphicsView_Preview->setScene(m_scene); + setModeSpecificUi(); connect(ui->pushButton_Save, &QPushButton::pressed, this, &MapImageExporter::saveImage); connect(ui->pushButton_Cancel, &QPushButton::pressed, this, &MapImageExporter::close); + + // Update the map selector when the text changes. + // We don't use QComboBox::currentTextChanged to avoid unnecessary re-rendering. connect(ui->comboBox_MapSelection, &QComboBox::currentIndexChanged, this, &MapImageExporter::updateMapSelection); connect(ui->comboBox_MapSelection->lineEdit(), &QLineEdit::editingFinished, this, &MapImageExporter::updateMapSelection); @@ -56,7 +63,6 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, MapImageExporter::~MapImageExporter() { delete m_timelapseImage; - delete m_scene; delete ui; } @@ -89,22 +95,6 @@ void MapImageExporter::setModeSpecificUi() { } } -void MapImageExporter::setMap(Map *map) { - if (!map) return; - - const QSignalBlocker b(ui->comboBox_MapSelection); - ui->comboBox_MapSelection->setCurrentText(map->name()); - updateMapSelection(); -} - -void MapImageExporter::setLayout(Layout *layout) { - if (!layout) return; - - const QSignalBlocker b(ui->comboBox_MapSelection); - ui->comboBox_MapSelection->setCurrentText(layout->id); - updateMapSelection(); -} - // Allow the window to open before displaying the preview. void MapImageExporter::showEvent(QShowEvent *event) { QWidget::showEvent(event); @@ -117,6 +107,20 @@ void MapImageExporter::resizeEvent(QResizeEvent *event) { scalePreview(); } +void MapImageExporter::setMap(Map *map) { + if (map) setSelectionText(map->name()); +} + +void MapImageExporter::setLayout(Layout *layout) { + if (layout) setSelectionText(layout->id); +} + +void MapImageExporter::setSelectionText(const QString &text) { + const QSignalBlocker b(ui->comboBox_MapSelection); + ui->comboBox_MapSelection->setCurrentText(text); + updateMapSelection(); +} + void MapImageExporter::updateMapSelection() { auto oldMap = m_map; auto oldLayout = m_layout; @@ -267,111 +271,105 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { for (const auto &connection : cur.map->getConnections()) { if (!connection->isCardinal()) continue; + Map *connectedMap = connection->targetMap(); + if (!connectedMap) continue; QPoint pos = connection->relativePos(); - unvisited.append(StitchedMap{cur.x + pos.x(), cur.y + pos.y(), connection->targetMap()}); + unvisited.append(StitchedMap{cur.x + (pos.x() * 16), cur.y + (pos.y() * 16), connectedMap}); } } + if (stitchedMaps.isEmpty()) + return QPixmap(); - // 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 (m_settings.showBorder) { - minX -= BORDER_DISTANCE; - maxX += BORDER_DISTANCE; - minY -= BORDER_DISTANCE; - maxY += 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); + // Determine the overall dimensions of the stitched maps. + QRect dimensions(0,0, m_map->getWidth(), m_map->getHeight()); + for (const StitchedMap &map : stitchedMaps) { + dimensions.setLeft(qMin(dimensions.left(), map.x)); + dimensions.setTop(qMin(dimensions.top(), map.y)); + dimensions.setRight(qMax(dimensions.right(), map.x + (map.map->getWidth()) * 16)); + dimensions.setBottom(qMax(dimensions.bottom(), map.y + (map.map->getHeight()) * 16)); + } + + // Adjust overall dimensions to account for elements at the edge (the border, grid, etc.) + QMargins margins = getMargins(); + dimensions += margins; + + QPixmap stitchedPixmap(dimensions.width(), dimensions.height()); stitchedPixmap.fill(Qt::black); - // First pass, render the layouts, borders, and collision (if enabled) QPainter painter(&stitchedPixmap); - for (StitchedMap map : stitchedMaps) { + painter.translate(-dimensions.left(), -dimensions.top()); + + // Borders can occlude neighboring maps, so we draw all the borders before drawing any maps. + if (m_settings.showBorder) { + progress->setLabelText("Drawing borders..."); + progress->setValue(0); + numDrawn = 0; + for (const StitchedMap &map : stitchedMaps) { + if (progress->wasCanceled()) { + return QPixmap(); + } + painter.translate(map.x, map.y); + paintBorder(&painter, map.map->layout()); + painter.translate(-map.x, -map.y); + + progress->setValue(numDrawn++); + } + } + + // Draw the layouts and collision images. + // There's no chance these will overlap with neighbors, so we can do both in a single pass. + progress->setLabelText("Drawing maps..."); + progress->setValue(0); + numDrawn = 0; + for (const StitchedMap &map : stitchedMaps) { if (progress->wasCanceled()) { return QPixmap(); } - progress->setValue(numDrawn); - numDrawn++; + painter.translate(map.x, map.y); + painter.drawPixmap(0, 0, map.map->layout()->render(true)); + paintCollision(&painter, map.map->layout()); + painter.translate(-map.x, -map.y); - int pixelX = (map.x - minX) * 16; - int pixelY = (map.y - minY) * 16; - if (m_settings.showBorder) { - pixelX -= BORDER_DISTANCE * 16; - pixelY -= BORDER_DISTANCE * 16; - } - painter.drawPixmap(pixelX, pixelY, getFormattedLayoutPixmap(map.map->layout(), true)); + progress->setValue(numDrawn++); } - // 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 (m_settings.showBorder) { - m_settings.showBorder = false; - 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; - painter.drawPixmap(pixelX, pixelY, getFormattedLayoutPixmap(map.map->layout(), true)); - } - m_settings.showBorder = true; - } - painter.end(); - - // Events can be occluded by neighboring maps if they are positioned near or outside the map's edge. - // Now that all the maps have been rendered we can render the events (if enabled). + // Events can be occluded by neighboring maps if they are positioned + // near or outside the map's edge, so we draw them after all the maps. if (eventsEnabled()) { - progress->setLabelText("Drawing stitched map events..."); + progress->setLabelText("Drawing map events..."); progress->setValue(0); - progress->setMaximum(stitchedMaps.size()); numDrawn = 0; - for (StitchedMap map : stitchedMaps) { + for (const StitchedMap &map : stitchedMaps) { if (progress->wasCanceled()) { return QPixmap(); } - progress->setValue(numDrawn); - numDrawn++; + painter.translate(map.x, map.y); + paintEvents(&painter, map.map); + painter.translate(-map.x, -map.y); - int pixelX = (map.x - minX) * 16; - int pixelY = (map.y - minY) * 16; - paintEvents(&stitchedPixmap, map.map, QPoint(pixelX, pixelY)); + progress->setValue(numDrawn++); } } - paintGrid(&stitchedPixmap); + // Nothing should be on top of the grid, so it's drawn last. + if (m_settings.showGrid) { + progress->setLabelText("Drawing map grids..."); + progress->setValue(0); + numDrawn = 0; + for (const StitchedMap &map : stitchedMaps) { + if (progress->wasCanceled()) { + return QPixmap(); + } + painter.translate(map.x, map.y); + paintGrid(&painter, map.map->layout()); + painter.translate(-map.x, -map.y); + + progress->setValue(numDrawn++); + } + } return stitchedPixmap; } @@ -392,30 +390,17 @@ QGifImage* MapImageExporter::createTimelapseImage() { progress.setMaximum(1); progress.setValue(0); - int maxWidth = m_layout->getWidth() * 16; - int maxHeight = m_layout->getHeight() * 16; - if (m_settings.showBorder) { - // TODO: We probably need to account for connections with no border here - maxWidth += 2 * BORDER_DISTANCE * 16; - maxHeight += 2 * BORDER_DISTANCE * 16; - } + QMargins margins = getMargins(m_map); // m_map may be nullptr here, that's ok. + int maxWidth = m_layout->getWidth() * 16 + margins.left() + margins.right(); + int maxHeight = m_layout->getHeight() * 16 + margins.top() + margins.bottom(); + // Rewind to the specified start of the map edit history. int i = 0; while (historyStack->canUndo()) { - progress.setValue(i); historyStack->undo(); - int width = m_layout->getWidth() * 16; - int height = m_layout->getHeight() * 16; - if (m_settings.showBorder) { - width += 2 * BORDER_DISTANCE * 16; - height += 2 * BORDER_DISTANCE * 16; - } - if (width > maxWidth) { - maxWidth = width; - } - if (height > maxHeight) { - maxHeight = height; - } + margins = getMargins(m_map); + maxWidth = qMax(maxWidth, m_layout->getWidth() * 16 + margins.left() + margins.right()); + maxHeight = qMax(maxHeight, m_layout->getHeight() * 16 + margins.top() + margins.bottom()); i++; } @@ -519,33 +504,34 @@ void MapImageExporter::updatePreview() { m_preview->scene()->removeItem(m_preview); delete m_preview; } - delete m_scene; - m_scene = new QGraphicsScene; m_preview = m_scene->addPixmap(previewPixmap); - ui->graphicsView_Preview->setScene(m_scene); + m_scene->setSceneRect(m_scene->itemsBoundingRect()); scalePreview(); } void MapImageExporter::scalePreview() { - if (m_scene && !m_settings.previewActualSize){ - ui->graphicsView_Preview->fitInView(m_scene->sceneRect(), Qt::KeepAspectRatioByExpanding); - } + if (!m_preview || m_settings.previewActualSize) + return; + ui->graphicsView_Preview->fitInView(m_preview, Qt::KeepAspectRatioByExpanding); } QPixmap MapImageExporter::getFormattedMapPixmap() { return m_map ? getFormattedMapPixmap(m_map) : getFormattedLayoutPixmap(m_layout); } -QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout, bool ignoreGrid) { +QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout) { if (!layout) return QPixmap(); - layout->render(true); - QPixmap pixmap = layout->pixmap; + QMargins margins = getMargins(); + QPixmap pixmap = getResizedPixmap(layout->render(true), margins); - paintCollision(&pixmap, layout); - paintBorder(&pixmap, layout); - if (!ignoreGrid) paintGrid(&pixmap); + QPainter painter(&pixmap); + painter.translate(margins.left(), margins.top()); + + paintCollision(&painter, layout); + paintBorder(&painter, layout); + paintGrid(&painter, layout); return pixmap; } @@ -554,31 +540,74 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map) { if (!map) return QPixmap(); - QPixmap pixmap = getFormattedLayoutPixmap(map->layout(), true); - paintConnections(&pixmap, map); + QMargins margins = getMargins(map); + QPixmap pixmap = getResizedPixmap(map->layout()->render(true), margins); - int eventPixelOffsetX = (m_settings.showBorder || m_settings.showConnections.contains("left")) ? BORDER_DISTANCE * 16 : 0; - int eventPixelOffsetY = (m_settings.showBorder || m_settings.showConnections.contains("up")) ? BORDER_DISTANCE * 16 : 0; - paintEvents(&pixmap, map, QPoint(eventPixelOffsetX, eventPixelOffsetY)); + QPainter painter(&pixmap); + painter.translate(margins.left(), margins.top()); - paintGrid(&pixmap); + paintCollision(&painter, map->layout()); + paintBorder(&painter, map->layout()); + paintConnections(&painter, map); + paintEvents(&painter, map); + paintGrid(&painter, map->layout()); return pixmap; } -void MapImageExporter::paintCollision(QPixmap *pixmap, Layout *layout) { +QMargins MapImageExporter::getMargins(const Map *map) { + QMargins margins; + + const int borderPixelWidth = BORDER_DISTANCE * 16; + const int borderPixelHeight = BORDER_DISTANCE * 16; + if (m_settings.showBorder) { + margins = QMargins(borderPixelWidth, borderPixelHeight, borderPixelWidth, borderPixelHeight); + } else if (map && connectionsEnabled()) { + for (const auto &connection : map->getConnections()) { + const QString dir = connection->direction(); + if (!m_settings.showConnections.contains(dir)) + continue; + if (dir == "up") margins.setTop(borderPixelHeight); + else if (dir == "down") margins.setBottom(borderPixelHeight); + else if (dir == "left") margins.setLeft(borderPixelWidth); + else if (dir == "right") margins.setRight(borderPixelWidth); + } + } + + if (m_settings.showGrid) { + margins += QMargins(0, 0, 1, 1); + } + return margins; +} + +QPixmap MapImageExporter::getResizedPixmap(const QPixmap &pixmap, const QMargins &margins) { + if (margins.isNull()) + return pixmap; + + QPixmap resizedPixmap = QPixmap(pixmap.width() + margins.left() + margins.right(), + pixmap.height() + margins.top() + margins.bottom()); + resizedPixmap.fill(Qt::black); + + QPainter painter(&resizedPixmap); + painter.drawPixmap(margins.left(), margins.top(), pixmap); + painter.end(); + return resizedPixmap; +} + +void MapImageExporter::paintCollision(QPainter *painter, Layout *layout) { if (!m_settings.showCollision) return; layout->renderCollision(true); - QPainter painter(pixmap); - painter.setOpacity(static_cast(porymapConfig.collisionOpacity) / 100); - painter.drawPixmap(0, 0, layout->collision_pixmap); - painter.end(); + auto savedOpacity = painter->opacity(); + painter->setOpacity(static_cast(porymapConfig.collisionOpacity) / 100); + painter->drawPixmap(0, 0, layout->collision_pixmap); + painter->setOpacity(savedOpacity); } -void MapImageExporter::paintBorder(QPixmap *pixmap, Layout *layout) { +// TODO: Route109 map border has an empty row? +void MapImageExporter::paintBorder(QPainter *painter, Layout *layout) { if (!m_settings.showBorder) return; @@ -586,68 +615,29 @@ void MapImageExporter::paintBorder(QPixmap *pixmap, Layout *layout) { int borderHorzDist = layout->getBorderDrawWidth(); int borderVertDist = layout->getBorderDrawHeight(); - int borderWidth = BORDER_DISTANCE * 16; - int borderHeight = BORDER_DISTANCE * 16; - QPixmap newPixmap = QPixmap(layout->pixmap.width() + borderWidth * 2, layout->pixmap.height() + borderHeight * 2); - QPainter painter(&newPixmap); - for (int y = BORDER_DISTANCE - borderVertDist; y < layout->getHeight() + borderVertDist * 2; y += layout->getBorderHeight()) { - for (int x = BORDER_DISTANCE - borderHorzDist; x < layout->getWidth() + borderHorzDist * 2; x += layout->getBorderWidth()) { - painter.drawPixmap(x * 16, y * 16, layout->border_pixmap); - } + for (int y = -borderVertDist; y < layout->getHeight() + borderVertDist; y += layout->getBorderHeight()) + for (int x = -borderHorzDist; x < layout->getWidth() + borderHorzDist; x += layout->getBorderWidth()) { + if (layout->isWithinBounds(x, y)) continue; // Skip border painting if it would be covered by the rest of the map. + painter->drawPixmap(x * 16, y * 16, layout->border_pixmap); } - painter.drawImage(borderWidth, borderHeight, pixmap->toImage()); - painter.end(); - *pixmap = newPixmap; } -void MapImageExporter::paintConnections(QPixmap *pixmap, const Map *map) { +void MapImageExporter::paintConnections(QPainter *painter, const Map *map) { if (!connectionsEnabled()) return; - QMargins margins; - if (m_settings.showBorder) { - // Adjust for the border having resized the pixmap - margins.setLeft(BORDER_DISTANCE * 16); - margins.setTop(BORDER_DISTANCE * 16); - } else { - // We haven't rendered the map border, so we will need to resize the canvas to fit any map connections. - // We do this minimally so that the final image will not contain unnecessary space for unoccupied connections. - for (const auto &connection : map->getConnections()) { - const QString dir = connection->direction(); - if (!m_settings.showConnections.contains(dir)) - continue; - if (dir == "up") margins.setTop(BORDER_DISTANCE * 16); - else if (dir == "down") margins.setBottom(BORDER_DISTANCE * 16); - else if (dir == "left") margins.setLeft(BORDER_DISTANCE * 16); - else if (dir == "right") margins.setRight(BORDER_DISTANCE * 16); - } - if (!margins.isNull()) { - QPixmap resizedPixmap = QPixmap(map->layout()->pixmap.width() + margins.left() + margins.right(), - map->layout()->pixmap.height() + margins.top() + margins.bottom()); - resizedPixmap.fill(Qt::black); - QPainter resizePainter(&resizedPixmap); - resizePainter.drawPixmap(margins.left(), margins.top(), *pixmap); - resizePainter.end(); - *pixmap = resizedPixmap; - } - } - - QPainter painter(pixmap); for (const auto &connection : map->getConnections()) { if (!m_settings.showConnections.contains(connection->direction())) continue; - QPoint pos = connection->relativePos(true) * 16; - painter.drawImage(pos.x() + margins.left(), pos.y() + margins.top(), connection->render().toImage()); + painter->drawImage(connection->relativePos(true) * 16, connection->render().toImage()); } - painter.end(); } -void MapImageExporter::paintEvents(QPixmap *pixmap, const Map *map, const QPoint &pixelOffset) { +void MapImageExporter::paintEvents(QPainter *painter, const Map *map) { if (!eventsEnabled()) return; - QPainter painter(pixmap); - painter.translate(pixelOffset); + auto savedOpacity = painter->opacity(); for (const auto &group : Event::groups()) { if (!m_settings.showEvents.contains(group)) continue; @@ -655,35 +645,26 @@ void MapImageExporter::paintEvents(QPixmap *pixmap, const Map *map, const QPoint m_project->loadEventPixmap(event); if (m_mode != ImageExporterMode::Timelapse) { // GIF format doesn't support partial transparency, so we can't do this in Timelapse mode. - painter.setOpacity(event->getUsesDefaultPixmap() ? 0.7 : 1.0); + painter->setOpacity(event->getUsesDefaultPixmap() ? 0.7 : 1.0); } - painter.drawImage(QPoint(event->getPixelX(), event->getPixelY()), event->getPixmap().toImage()); + painter->drawImage(QPoint(event->getPixelX(), event->getPixelY()), event->getPixmap().toImage()); } } - painter.end(); + painter->setOpacity(savedOpacity); } -void MapImageExporter::paintGrid(QPixmap *pixmap) { +void MapImageExporter::paintGrid(QPainter *painter, const Layout *layout) { if (!m_settings.showGrid) return; - - int addX = 0, addY = 0; - if (!m_settings.showBorder) { - // since the last grid lines are outside of the pixmap, add a pixel to the bottom and right - addX = 1, addY = 1; - } - 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()); + int w = layout->getWidth() * 16; + int h = layout->getHeight() * 16; + for (int x = 0; x <= w; x += 16) { + painter->drawLine(x, 0, x, h); } - for (int y = 0; y < newPixmap.height(); y += 16) { - gridPainter.drawLine(0, y, newPixmap.width(), y); + for (int y = 0; y <= h; y += 16) { + painter->drawLine(0, y, w, y); } - gridPainter.end(); - *pixmap = newPixmap; } bool MapImageExporter::eventsEnabled() { @@ -840,12 +821,18 @@ void MapImageExporter::on_checkBox_ActualSize_stateChanged(int state) { void MapImageExporter::on_pushButton_Reset_pressed() { m_settings = {}; + for (auto widget : this->findChildren()) { const QSignalBlocker b(widget); // Prevent calls to updatePreview widget->setChecked(false); } + + const QSignalBlocker b_TimelapseDelay(ui->spinBox_TimelapseDelay); ui->spinBox_TimelapseDelay->setValue(m_settings.timelapseDelayMs); + + const QSignalBlocker b_FrameSkip(ui->spinBox_FrameSkip); ui->spinBox_FrameSkip->setValue(m_settings.timelapseSkipAmount); + updatePreview(); } From 243a6064c2e9adf7f70a2a751a8d115872e584a4 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 24 Mar 2025 16:39:46 -0400 Subject: [PATCH 09/12] Fix more timelapse bugs, clean up --- forms/mapimageexporter.ui | 3 - include/core/maplayout.h | 5 +- include/ui/mapimageexporter.h | 14 +- src/core/maplayout.cpp | 8 +- src/mainwindow.cpp | 4 +- src/ui/mapimageexporter.cpp | 471 ++++++++++--------- src/vendor/QtGifImage/gifimage/qgifimage.cpp | 9 + src/vendor/QtGifImage/gifimage/qgifimage.h | 2 + 8 files changed, 270 insertions(+), 246 deletions(-) diff --git a/forms/mapimageexporter.ui b/forms/mapimageexporter.ui index 7130f032..805ca6f2 100644 --- a/forms/mapimageexporter.ui +++ b/forms/mapimageexporter.ui @@ -374,9 +374,6 @@ QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored - - QPainter::RenderHint::LosslessImageRendering|QPainter::RenderHint::TextAntialiasing - QGraphicsView::DragMode::NoDrag diff --git a/include/core/maplayout.h b/include/core/maplayout.h index f2eccd3c..696dc28c 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -98,8 +98,9 @@ public: int getBorderDrawWidth() const; int getBorderDrawHeight() const; - bool isWithinBounds(int x, int y); - bool isWithinBorderBounds(int x, int y); + bool isWithinBounds(int x, int y) const; + bool isWithinBounds(const QRect &rect) const; + bool isWithinBorderBounds(int x, int y) const; bool getBlock(int x, int y, Block *out); void setBlock(int x, int y, Block block, bool enableScriptCallback = false); diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index f57dffb6..ffd893e5 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -24,6 +24,7 @@ struct ImageExporterSettings { bool previewActualSize = false; int timelapseSkipAmount = 1; int timelapseDelayMs = 200; + QColor fillColor = Qt::transparent; // Not exposed as a setting in the UI atm. }; class MapImageExporter : public QDialog @@ -50,13 +51,14 @@ private: Map *m_map = nullptr; Layout *m_layout = nullptr; QGraphicsScene *m_scene = nullptr; - QGifImage *m_timelapseImage = nullptr; + QGifImage *m_timelapseGifImage = nullptr; QBuffer *m_timelapseBuffer = nullptr; QMovie *m_timelapseMovie = nullptr; QGraphicsPixmapItem *m_preview = nullptr; ImageExporterSettings m_settings; ImageExporterMode m_mode = ImageExporterMode::Normal; + ImageExporterMode m_originalMode; void setModeSpecificUi(); void setSelectionText(const QString &text); @@ -70,19 +72,17 @@ private: bool connectionsEnabled(); void setConnectionDirectionEnabled(const QString &dir, bool enable); void saveImage(); - QGifImage* createTimelapseImage(); + QGifImage* createTimelapseGifImage(QProgressDialog *progress); QPixmap getStitchedImage(QProgressDialog *progress); QPixmap getFormattedMapPixmap(); - QPixmap getFormattedMapPixmap(Map *map); - QPixmap getFormattedLayoutPixmap(Layout *layout); void paintBorder(QPainter *painter, Layout *layout); void paintCollision(QPainter *painter, Layout *layout); void paintConnections(QPainter *painter, const Map *map); void paintEvents(QPainter *painter, const Map *map); void paintGrid(QPainter *painter, const Layout *layout = nullptr); - QPixmap getResizedPixmap(const QPixmap &pixmap, const QMargins &margins); - QMargins getMargins(const Map *map = nullptr); - bool historyItemAppliesToFrame(const QUndoCommand *command); + QMargins getMargins(const Map *map); + QPixmap getExpandedPixmap(const QPixmap &pixmap, const QSize &minSize, const QColor &fillColor); + bool currentHistoryAppliesToFrame(QUndoStack *historyStack); protected: virtual void showEvent(QShowEvent *) override; diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index ceeb6db5..35b5f5b5 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -50,11 +50,15 @@ Layout::Settings Layout::settings() const { return settings; } -bool Layout::isWithinBounds(int x, int y) { +bool Layout::isWithinBounds(int x, int y) const { return (x >= 0 && x < this->getWidth() && y >= 0 && y < this->getHeight()); } -bool Layout::isWithinBorderBounds(int x, int y) { +bool Layout::isWithinBounds(const QRect &rect) const { + return rect.left() >= 0 && rect.right() < this->getWidth() && rect.top() >= 0 && rect.bottom() < this->getHeight(); +} + +bool Layout::isWithinBorderBounds(int x, int y) const { return (x >= 0 && x < this->getBorderWidth() && y >= 0 && y < this->getBorderHeight()); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 30347c14..86ec1aec 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2506,9 +2506,11 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { // Open new image export window if (this->editor->map){ this->mapImageExporter = new MapImageExporter(this, this->editor->project, this->editor->map, mode); - connect(this, &MainWindow::mapOpened, this->mapImageExporter, &MapImageExporter::setMap); } else if (this->editor->layout) { this->mapImageExporter = new MapImageExporter(this, this->editor->project, this->editor->layout, mode); + } + if (this->mapImageExporter) { + connect(this, &MainWindow::mapOpened, this->mapImageExporter, &MapImageExporter::setMap); connect(this, &MainWindow::layoutOpened, this->mapImageExporter, &MapImageExporter::setLayout); } } diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 7ec3948b..c98fccce 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -40,12 +40,14 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, m_project(project), m_map(map), m_layout(layout), - m_mode(mode) + m_mode(mode), + m_originalMode(mode) { setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); m_scene = new QGraphicsScene(this); + m_preview = m_scene->addPixmap(QPixmap()); ui->graphicsView_Preview->setScene(m_scene); setModeSpecificUi(); @@ -62,11 +64,18 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, } MapImageExporter::~MapImageExporter() { - delete m_timelapseImage; + delete m_timelapseGifImage; delete ui; } void MapImageExporter::setModeSpecificUi() { + m_mode = m_originalMode; + if (!m_map && m_mode == ImageExporterMode::Stitch) { + // Stitch mode is not valid with only a layout open. + // This could happen if a user opens the stitch exporter with a map, then opens a layout. + m_mode = ImageExporterMode::Normal; + } + setWindowTitle(getTitle(m_mode)); ui->label_Description->setText(getDescription(m_mode)); ui->groupBox_Connections->setVisible(m_map && m_mode != ImageExporterMode::Stitch); @@ -92,6 +101,9 @@ void MapImageExporter::setModeSpecificUi() { // Until this is resolved, the selected map and the editor's map must remain the same. ui->comboBox_MapSelection->setEnabled(false); ui->label_MapSelection->setEnabled(false); + + // Timelapse gif has artifacts with transparency, make sure it's disabled. + m_settings.fillColor.setAlpha(255); } } @@ -132,7 +144,7 @@ void MapImageExporter::updateMapSelection() { m_map = newMap; m_layout = newMap->layout(); } - } else if (m_project->layoutIds.contains(text) && m_mode != ImageExporterMode::Stitch) { + } else if (m_project->layoutIds.contains(text)) { auto newLayout = m_project->loadLayout(text); if (newLayout) { m_map = nullptr; @@ -154,6 +166,18 @@ void MapImageExporter::updateMapSelection() { } void MapImageExporter::saveImage() { + // If the preview is empty it's because progress was canceled. + // Try again to create it, and if it's canceled again we'll stop the export. + if (m_preview->pixmap().isNull()) { + updatePreview(); + if (m_preview->pixmap().isNull()) + return; + } + if (m_mode == ImageExporterMode::Timelapse && !m_timelapseGifImage) { + // Shouldn't happen. We have a preview for the timelapse, but no timelapse image. + return; + } + const QString itemName = m_map ? m_map->name() : m_layout->name; QString defaultFilename; switch (m_mode) @@ -180,28 +204,19 @@ void MapImageExporter::saveImage() { case ImageExporterMode::Normal: case ImageExporterMode::Stitch: // Normal and Stitch modes already have the image ready to go in the preview. - if (m_preview->pixmap().isNull()) { - updatePreview(); - if (m_preview->pixmap().isNull()) - return; // Canceled - } m_preview->pixmap().save(filepath); break; case ImageExporterMode::Timelapse: - if (!m_timelapseImage || m_timelapseImage->frameCount() == 0) { - m_timelapseImage = createTimelapseImage(); - if (!m_timelapseImage || m_timelapseImage->frameCount() == 0) - return; // Canceled - } - m_timelapseImage->save(filepath); + m_timelapseGifImage->save(filepath); break; } close(); } } -bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { - if (command->isObsolete()) +bool MapImageExporter::currentHistoryAppliesToFrame(QUndoStack *historyStack) { + const QUndoCommand *command = historyStack->command(historyStack->index()); + if (!command || command->isObsolete()) return false; switch (command->id() & 0xFF) { @@ -227,6 +242,7 @@ bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { case CommandId::ID_EventMove: case CommandId::ID_EventShift: case CommandId::ID_EventCreate: + case CommandId::ID_EventPaste: case CommandId::ID_EventDelete: case CommandId::ID_EventDuplicate: { if (command->id() & IDMask_EventType_Object) return m_settings.showEvents.contains(Event::Group::Object); @@ -241,6 +257,122 @@ bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { } } +QPixmap MapImageExporter::getExpandedPixmap(const QPixmap &pixmap, const QSize &minSize, const QColor &fillColor) { + if (pixmap.width() >= minSize.width() && pixmap.height() >= minSize.height()) + return pixmap; + + QPixmap resizedPixmap = QPixmap(minSize); + QPainter painter(&resizedPixmap); + resizedPixmap.fill(fillColor); + painter.drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap); + painter.end(); + return resizedPixmap; +} + +struct TimelapseStep { + QUndoStack* historyStack; + int initialStackIndex; + QString name; +}; + +QGifImage* MapImageExporter::createTimelapseGifImage(QProgressDialog *progress) { + // TODO: Timelapse will play in order of layout changes then map changes (events, connections). Potentially update in the future? + QList steps; + if (m_layout) { + steps.append({ + .historyStack = &m_layout->editHistory, + .initialStackIndex = m_layout->editHistory.index(), + .name = "layout", + }); + } + if (m_map) { + steps.append({ + .historyStack = m_map->editHistory(), + .initialStackIndex = m_map->editHistory()->index(), + .name = "map", + }); + } + + // Rewind the edit histories and get the maximum map size for the gif's canvas. + QSize canvasSize = QSize(0,0); + for (const auto &step : steps) { + progress->setLabelText(QString("Rewinding %1 edit history...").arg(step.name)); + progress->setMinimum(0); + progress->setMaximum(step.initialStackIndex); + progress->setValue(progress->minimum()); + do { + if (currentHistoryAppliesToFrame(step.historyStack)) { + // This command is relevant, record the size of the map at this point. + QMargins margins = getMargins(m_map); + canvasSize = canvasSize.expandedTo(QSize(m_layout->getWidth() * 16 + margins.left() + margins.right(), + m_layout->getHeight() * 16 + margins.top() + margins.bottom())); + } + if (step.historyStack->canUndo()){ + step.historyStack->undo(); + } else break; + progress->setValue(step.initialStackIndex - step.historyStack->index()); + } while (!progress->wasCanceled()); + } + + auto timelapseImg = new QGifImage(canvasSize); + timelapseImg->setDefaultDelay(m_settings.timelapseDelayMs); + timelapseImg->setDefaultTransparentColor(m_settings.fillColor); + + // Create the timelapse image frames + for (const auto &step : steps) { + if (step.historyStack->index() >= step.initialStackIndex) + continue; + + // Progress is represented by the number of commands we need to redo to finish the timelapse, + // which can be different than the number of image frames we need to create. + progress->setLabelText(QString("Building %1 timelapse...").arg(step.name)); + progress->setMinimum(step.historyStack->index()); + progress->setMaximum(step.initialStackIndex - step.historyStack->index()); + progress->setValue(progress->minimum()); + + int framesToSkip = m_settings.timelapseSkipAmount - 1; + while (step.historyStack->canRedo() && step.historyStack->index() < step.initialStackIndex && !progress->wasCanceled()) { + if (currentHistoryAppliesToFrame(step.historyStack) && --framesToSkip <= 0) { + // Render frame, increasing its size if necessary to match the canvas. + QPixmap pixmap = getExpandedPixmap(getFormattedMapPixmap(), canvasSize, m_settings.fillColor); + timelapseImg->addFrame(pixmap.toImage()); + framesToSkip = m_settings.timelapseSkipAmount - 1; + } + step.historyStack->redo(); + progress->setValue(step.historyStack->index() - progress->minimum()); + } + } + + // Ensure all edit histories are restored to their original states. + // We already make sure above that we don't overshoot the initial state, + // so this should only need to happen if progress was canceled. + // Restoring the edit history is required, so we will disable canceling from here on. + if (progress->wasCanceled()) { + delete timelapseImg; + timelapseImg = nullptr; + } + progress->setCancelButton(nullptr); + for (const auto &step : steps) { + if (step.historyStack->index() >= step.initialStackIndex) + continue; + progress->setLabelText(QString("Restoring %1 edit history...").arg(step.name)); + progress->setMinimum(0); + progress->setMaximum(step.initialStackIndex); + progress->setValue(step.historyStack->index()); + while (step.historyStack->canRedo() && step.historyStack->index() < step.initialStackIndex) { + step.historyStack->redo(); + progress->setValue(step.historyStack->index()); + } + } + + // Final frame should always be the current state of the map. + if (timelapseImg) { + QPixmap finalFrame = getExpandedPixmap(getFormattedMapPixmap(), canvasSize, m_settings.fillColor); + timelapseImg->addFrame(finalFrame.toImage()); + } + return timelapseImg; +} + struct StitchedMap { int x; int y; @@ -284,25 +416,24 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { int numDrawn = 0; // Determine the overall dimensions of the stitched maps. - QRect dimensions(0,0, m_map->getWidth(), m_map->getHeight()); + QRect dimensions = QRect(0, 0, m_map->getWidth(), m_map->getHeight()) + getMargins(m_map); for (const StitchedMap &map : stitchedMaps) { - dimensions.setLeft(qMin(dimensions.left(), map.x)); - dimensions.setTop(qMin(dimensions.top(), map.y)); - dimensions.setRight(qMax(dimensions.right(), map.x + (map.map->getWidth()) * 16)); - dimensions.setBottom(qMax(dimensions.bottom(), map.y + (map.map->getHeight()) * 16)); + dimensions |= (QRect(map.x, map.y, map.map->getWidth() * 16, map.map->getHeight() * 16) + getMargins(map.map)); } - // Adjust overall dimensions to account for elements at the edge (the border, grid, etc.) - QMargins margins = getMargins(); - dimensions += margins; - QPixmap stitchedPixmap(dimensions.width(), dimensions.height()); - stitchedPixmap.fill(Qt::black); + stitchedPixmap.fill(m_settings.fillColor); QPainter painter(&stitchedPixmap); painter.translate(-dimensions.left(), -dimensions.top()); // Borders can occlude neighboring maps, so we draw all the borders before drawing any maps. + // Note: Borders can also overlap the borders of neighboring maps. It's not technically wrong to do this, + // but it might suggest to users that something is visible in-game that actually isn't. + // (e.g. in FRLG, Route 18's water border can overlap Fuchsia's tree border. It suggests you could + // see a jarring transition in-game from one of these maps, but because of the collision map the + // player isn't actually able to get close enough to this transition to see it). + // Perhaps some future export setting could limit the border rendering to the visibility range from walkable areas. if (m_settings.showBorder) { progress->setLabelText("Drawing borders..."); progress->setValue(0); @@ -319,8 +450,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { } } - // Draw the layouts and collision images. - // There's no chance these will overlap with neighbors, so we can do both in a single pass. + // Draw the layout and collision images. progress->setLabelText("Drawing maps..."); progress->setValue(0); numDrawn = 0; @@ -338,8 +468,9 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { // Events can be occluded by neighboring maps if they are positioned // near or outside the map's edge, so we draw them after all the maps. - if (eventsEnabled()) { - progress->setLabelText("Drawing map events..."); + // Nothing should be on top of the grid, so it's drawn last. + if (m_settings.showGrid || eventsEnabled()) { + progress->setLabelText("Drawing map decorations..."); progress->setValue(0); numDrawn = 0; for (const StitchedMap &map : stitchedMaps) { @@ -348,22 +479,6 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { } painter.translate(map.x, map.y); paintEvents(&painter, map.map); - painter.translate(-map.x, -map.y); - - progress->setValue(numDrawn++); - } - } - - // Nothing should be on top of the grid, so it's drawn last. - if (m_settings.showGrid) { - progress->setLabelText("Drawing map grids..."); - progress->setValue(0); - numDrawn = 0; - for (const StitchedMap &map : stitchedMaps) { - if (progress->wasCanceled()) { - return QPixmap(); - } - painter.translate(map.x, map.y); paintGrid(&painter, map.map->layout()); painter.translate(-map.x, -map.y); @@ -374,137 +489,47 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { return stitchedPixmap; } -QGifImage* MapImageExporter::createTimelapseImage() { - // Timelapse will play in order of layout changes then map changes (events) - // TODO: potentially update in the future? - auto timelapseImg = new QGifImage(); - timelapseImg->setDefaultDelay(m_settings.timelapseDelayMs); - timelapseImg->setDefaultTransparentColor(QColor(0, 0, 0)); - - // lambda to avoid redundancy - auto generateTimelapseFromHistory = [this, timelapseImg](QString progressText, QUndoStack *historyStack){ - QProgressDialog progress(progressText, "Cancel", 0, 1, this); - progress.setAutoClose(true); - progress.setWindowModality(Qt::WindowModal); - progress.setModal(true); - progress.setMaximum(1); - progress.setValue(0); - - QMargins margins = getMargins(m_map); // m_map may be nullptr here, that's ok. - int maxWidth = m_layout->getWidth() * 16 + margins.left() + margins.right(); - int maxHeight = m_layout->getHeight() * 16 + margins.top() + margins.bottom(); - - // Rewind to the specified start of the map edit history. - int i = 0; - while (historyStack->canUndo()) { - historyStack->undo(); - margins = getMargins(m_map); - maxWidth = qMax(maxWidth, m_layout->getWidth() * 16 + margins.left() + margins.right()); - maxHeight = qMax(maxHeight, m_layout->getHeight() * 16 + margins.top() + margins.bottom()); - i++; - } - - // Draw each frame, skipping the specified number of map edits in - // the undo history. - progress.setMaximum(i); - while (i > 0) { - if (progress.wasCanceled()) { - progress.close(); - while (i > 0 && historyStack->canRedo()) { - i--; - historyStack->redo(); - } - return false; - } - while (historyStack->canRedo() && - !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { - i--; - historyStack->redo(); - } - progress.setValue(progress.maximum() - i); - QPixmap pixmap = getFormattedMapPixmap(); - if (pixmap.width() < maxWidth || pixmap.height() < maxHeight) { - QPixmap resizedPixmap = QPixmap(maxWidth, maxHeight); - QPainter painter(&resizedPixmap); - resizedPixmap.fill(QColor(0, 0, 0)); - painter.drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap); - painter.end(); - pixmap = resizedPixmap; - } - timelapseImg->addFrame(pixmap.toImage()); - for (int j = 0; j < m_settings.timelapseSkipAmount; j++) { - if (i > 0) { - i--; - historyStack->redo(); - while (historyStack->canRedo() && - !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { - i--; - historyStack->redo(); - } - } - } - } - // The latest map state is the last animated frame. - QPixmap pixmap = getFormattedMapPixmap(); - timelapseImg->addFrame(pixmap.toImage()); - progress.close(); - return true; - }; - - if (m_layout && !generateTimelapseFromHistory("Building layout timelapse...", &m_layout->editHistory)) { - delete timelapseImg; - return nullptr; - } - - if (m_map && !generateTimelapseFromHistory("Building map timelapse...", m_map->editHistory())) { - delete timelapseImg; - return nullptr; - } - - return timelapseImg; -} - void MapImageExporter::updatePreview() { + QProgressDialog progress("", "Cancel", 0, 1, this); + progress.setAutoClose(true); + progress.setWindowModality(Qt::WindowModal); + progress.setModal(true); + progress.setMinimumDuration(1000); + QPixmap previewPixmap; - if (m_mode == ImageExporterMode::Stitch) { - QProgressDialog progress("Building map stitch...", "Cancel", 0, 1, this); - progress.setAutoClose(true); - progress.setWindowModality(Qt::WindowModal); - progress.setModal(true); - progress.setMinimumDuration(1000); - previewPixmap = getStitchedImage(&progress); - progress.close(); - } else if (m_mode == ImageExporterMode::Timelapse) { - delete m_timelapseImage; - m_timelapseImage = createTimelapseImage(); - - // We want to convert the QGifImage data into a QMovie for the preview display. - // Both support input/output with a QIODevice, so we use a QBuffer to translate the data. - delete m_timelapseBuffer; - m_timelapseBuffer = new QBuffer(this); - m_timelapseBuffer->open(QBuffer::ReadWrite); - m_timelapseImage->save(m_timelapseBuffer); - m_timelapseBuffer->close(); - - delete m_timelapseMovie; - m_timelapseMovie = new QMovie(m_timelapseBuffer, "gif", this); - m_timelapseMovie->setCacheMode(QMovie::CacheAll); - connect(m_timelapseMovie, &QMovie::frameChanged, [this](int) { - if (m_preview) m_preview->setPixmap(m_timelapseMovie->currentPixmap()); - }); - m_timelapseMovie->start(); - previewPixmap = m_timelapseMovie->currentPixmap(); - - } else if (m_mode == ImageExporterMode::Normal) { + if (m_mode == ImageExporterMode::Normal) { previewPixmap = getFormattedMapPixmap(); - } + } else if (m_mode == ImageExporterMode::Stitch) { + previewPixmap = getStitchedImage(&progress); + } else if (m_mode == ImageExporterMode::Timelapse) { + if (m_timelapseMovie) + m_timelapseMovie->stop(); - if (m_preview) { - if (m_preview->scene()) - m_preview->scene()->removeItem(m_preview); - delete m_preview; + m_timelapseGifImage = createTimelapseGifImage(&progress); + if (!m_timelapseGifImage) { + previewPixmap = QPixmap(); + } else { + // We want to convert the QGifImage data into a QMovie for the preview display. + // Both support input/output with a QIODevice, so we use a QBuffer to translate the data. + delete m_timelapseBuffer; + m_timelapseBuffer = new QBuffer(this); + m_timelapseBuffer->open(QBuffer::ReadWrite); + m_timelapseGifImage->save(m_timelapseBuffer); + m_timelapseBuffer->close(); + + delete m_timelapseMovie; + m_timelapseMovie = new QMovie(m_timelapseBuffer, "gif", this); + m_timelapseMovie->setCacheMode(QMovie::CacheAll); + connect(m_timelapseMovie, &QMovie::frameChanged, [this](int) { + m_preview->setPixmap(m_timelapseMovie->currentPixmap()); + }); + m_timelapseMovie->start(); + previewPixmap = m_timelapseMovie->currentPixmap(); + } } - m_preview = m_scene->addPixmap(previewPixmap); + progress.close(); + + m_preview->setPixmap(previewPixmap); m_scene->setSceneRect(m_scene->itemsBoundingRect()); scalePreview(); } @@ -516,110 +541,94 @@ void MapImageExporter::scalePreview() { } QPixmap MapImageExporter::getFormattedMapPixmap() { - return m_map ? getFormattedMapPixmap(m_map) : getFormattedLayoutPixmap(m_layout); -} - -QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout) { - if (!layout) + if (!m_layout) return QPixmap(); - QMargins margins = getMargins(); - QPixmap pixmap = getResizedPixmap(layout->render(true), margins); + m_layout->render(true); + + // Create pixmap large enough to contain the map and the marginal elements (the border, grid, etc.) + QMargins margins = getMargins(m_map); + QPixmap pixmap = QPixmap(m_layout->pixmap.width() + margins.left() + margins.right(), + m_layout->pixmap.height() + margins.top() + margins.bottom()); + pixmap.fill(m_settings.fillColor); QPainter painter(&pixmap); painter.translate(margins.left(), margins.top()); - paintCollision(&painter, layout); - paintBorder(&painter, layout); - paintGrid(&painter, layout); - - return pixmap; -} - -QPixmap MapImageExporter::getFormattedMapPixmap(Map *map) { - if (!map) - return QPixmap(); - - QMargins margins = getMargins(map); - QPixmap pixmap = getResizedPixmap(map->layout()->render(true), margins); - - QPainter painter(&pixmap); - painter.translate(margins.left(), margins.top()); - - paintCollision(&painter, map->layout()); - paintBorder(&painter, map->layout()); - paintConnections(&painter, map); - paintEvents(&painter, map); - paintGrid(&painter, map->layout()); + paintBorder(&painter, m_layout); + painter.drawPixmap(0, 0, m_layout->pixmap); + paintCollision(&painter, m_layout); + if (m_map) { + paintConnections(&painter, m_map); + paintEvents(&painter, m_map); + } + paintGrid(&painter, m_layout); return pixmap; } QMargins MapImageExporter::getMargins(const Map *map) { QMargins margins; - - const int borderPixelWidth = BORDER_DISTANCE * 16; - const int borderPixelHeight = BORDER_DISTANCE * 16; if (m_settings.showBorder) { - margins = QMargins(borderPixelWidth, borderPixelHeight, borderPixelWidth, borderPixelHeight); + // The border may technically extend beyond BORDER_DISTANCE, but when the border is painted + // we will be limiting it to the visible sight range. + margins = QMargins(BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE) * 16; } else if (map && connectionsEnabled()) { for (const auto &connection : map->getConnections()) { const QString dir = connection->direction(); if (!m_settings.showConnections.contains(dir)) continue; - if (dir == "up") margins.setTop(borderPixelHeight); - else if (dir == "down") margins.setBottom(borderPixelHeight); - else if (dir == "left") margins.setLeft(borderPixelWidth); - else if (dir == "right") margins.setRight(borderPixelWidth); + auto targetMap = connection->targetMap(); + if (!targetMap) continue; + + QRect rect = targetMap->getConnectionRect(dir); + if (dir == "up") margins.setTop(rect.height() * 16); + else if (dir == "down") margins.setBottom(rect.height() * 16); + else if (dir == "left") margins.setLeft(rect.width() * 16); + else if (dir == "right") margins.setRight(rect.width() * 16); } } - if (m_settings.showGrid) { - margins += QMargins(0, 0, 1, 1); + // Account for outer grid line + if (margins.right() == 0) margins.setRight(1); + if (margins.bottom() == 0) margins.setBottom(1); } return margins; } -QPixmap MapImageExporter::getResizedPixmap(const QPixmap &pixmap, const QMargins &margins) { - if (margins.isNull()) - return pixmap; - - QPixmap resizedPixmap = QPixmap(pixmap.width() + margins.left() + margins.right(), - pixmap.height() + margins.top() + margins.bottom()); - resizedPixmap.fill(Qt::black); - - QPainter painter(&resizedPixmap); - painter.drawPixmap(margins.left(), margins.top(), pixmap); - painter.end(); - return resizedPixmap; -} - void MapImageExporter::paintCollision(QPainter *painter, Layout *layout) { if (!m_settings.showCollision) return; - layout->renderCollision(true); - auto savedOpacity = painter->opacity(); painter->setOpacity(static_cast(porymapConfig.collisionOpacity) / 100); - painter->drawPixmap(0, 0, layout->collision_pixmap); + painter->drawPixmap(0, 0, layout->renderCollision(true)); painter->setOpacity(savedOpacity); } -// TODO: Route109 map border has an empty row? void MapImageExporter::paintBorder(QPainter *painter, Layout *layout) { if (!m_settings.showBorder) return; - layout->renderBorder(); + layout->renderBorder(true); + + // Clip parts of the border that would be beyond player visibility. + QRect visibleArea(0, 0, layout->getWidth() * 16, layout->getHeight() * 16); + visibleArea += (QMargins(BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE) * 16); + painter->save(); + painter->setClipRect(visibleArea); int borderHorzDist = layout->getBorderDrawWidth(); int borderVertDist = layout->getBorderDrawHeight(); for (int y = -borderVertDist; y < layout->getHeight() + borderVertDist; y += layout->getBorderHeight()) for (int x = -borderHorzDist; x < layout->getWidth() + borderHorzDist; x += layout->getBorderWidth()) { - if (layout->isWithinBounds(x, y)) continue; // Skip border painting if it would be covered by the rest of the map. + // Skip border painting if it would be fully covered by the rest of the map + if (layout->isWithinBounds(QRect(x, y, layout->getBorderWidth(), layout->getBorderHeight()))) + continue; painter->drawPixmap(x * 16, y * 16, layout->border_pixmap); } + + painter->restore(); } void MapImageExporter::paintConnections(QPainter *painter, const Map *map) { diff --git a/src/vendor/QtGifImage/gifimage/qgifimage.cpp b/src/vendor/QtGifImage/gifimage/qgifimage.cpp index b607c7e0..d15d67a3 100644 --- a/src/vendor/QtGifImage/gifimage/qgifimage.cpp +++ b/src/vendor/QtGifImage/gifimage/qgifimage.cpp @@ -356,6 +356,15 @@ QVector QGifImage::globalColorTable() const return d->globalColorTable; } +/*! + Return canvas size. + */ +QSize QGifImage::getCanvasSize() const +{ + Q_D(const QGifImage); + return d->getCanvasSize(); +} + /*! Return background color of the gif canvas. It only makes sense when global color table is not empty. diff --git a/src/vendor/QtGifImage/gifimage/qgifimage.h b/src/vendor/QtGifImage/gifimage/qgifimage.h index f81a28ae..b1a21271 100644 --- a/src/vendor/QtGifImage/gifimage/qgifimage.h +++ b/src/vendor/QtGifImage/gifimage/qgifimage.h @@ -52,6 +52,8 @@ public: int loopCount() const; void setLoopCount(int loop); + QSize getCanvasSize() const; + int frameCount() const; QImage frame(int index) const; From cf51ca1fc41b7b9804568e3dfbf229bbb19dbf78 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 28 Mar 2025 16:25:18 -0400 Subject: [PATCH 10/12] Fix some issues with settings, canvas size, connections rendering --- forms/mapimageexporter.ui | 46 +++++++++++-- include/core/editcommands.h | 17 +++-- include/ui/mapimageexporter.h | 20 +++--- src/core/editcommands.cpp | 42 +++++++++++- src/ui/mapimageexporter.cpp | 125 +++++++++++++++++++++++----------- 5 files changed, 190 insertions(+), 60 deletions(-) diff --git a/forms/mapimageexporter.ui b/forms/mapimageexporter.ui index 805ca6f2..f2f4c09a 100644 --- a/forms/mapimageexporter.ui +++ b/forms/mapimageexporter.ui @@ -25,6 +25,9 @@ 0 + + Qt::FocusPolicy::ClickFocus + @@ -166,7 +169,7 @@ - + Collision @@ -191,7 +194,10 @@ - + + + Qt::FocusPolicy::StrongFocus + @@ -217,7 +223,10 @@ - + + + Qt::FocusPolicy::StrongFocus + @@ -253,9 +262,22 @@ - + + + <html><head/><body><p>If checked, the image in the preview window will not be recreated when the settings are changed.</p></body></html> + - Preview actual size + Disable preview updates + + + + + + + <html><head/><body><p>If checked, the image shown in the preview window will not scale to fit into the available space. The aspect ratio of the image will never change.</p></body></html> + + + Disable preview scaling @@ -266,6 +288,9 @@ Reset + + false + @@ -286,6 +311,9 @@ Cancel + + false + @@ -293,6 +321,9 @@ Save + + false + @@ -407,6 +438,11 @@ QComboBox
noscrollcombobox.h
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
diff --git a/include/core/editcommands.h b/include/core/editcommands.h index eabfacc0..9a54063c 100644 --- a/include/core/editcommands.h +++ b/include/core/editcommands.h @@ -47,6 +47,13 @@ enum CommandId { #define IDMask_EventType_Trigger (1 << 11) #define IDMask_EventType_Heal (1 << 12) +#define IDMask_ConnectionDirection_Up (1 << 8) +#define IDMask_ConnectionDirection_Down (1 << 9) +#define IDMask_ConnectionDirection_Left (1 << 10) +#define IDMask_ConnectionDirection_Right (1 << 11) +#define IDMask_ConnectionDirection_Dive (1 << 12) +#define IDMask_ConnectionDirection_Emerge (1 << 13) + /// Implements a command to commit metatile paint actions /// onto the map using the pencil tool. class PaintMetatile : public QUndoCommand { @@ -400,7 +407,7 @@ public: void redo() override; bool mergeWith(const QUndoCommand *command) override; - int id() const override { return CommandId::ID_MapConnectionMove; } + int id() const override; private: MapConnection *connection; @@ -421,7 +428,7 @@ public: void undo() override; void redo() override; - int id() const override { return CommandId::ID_MapConnectionChangeDirection; } + int id() const override; private: QPointer connection; @@ -443,7 +450,7 @@ public: void undo() override; void redo() override; - int id() const override { return CommandId::ID_MapConnectionChangeMap; } + int id() const override; private: QPointer connection; @@ -465,7 +472,7 @@ public: void undo() override; void redo() override; - int id() const override { return CommandId::ID_MapConnectionAdd; } + int id() const override; private: Map *map = nullptr; @@ -485,7 +492,7 @@ public: void undo() override; void redo() override; - int id() const override { return CommandId::ID_MapConnectionRemove; } + int id() const override; private: Map *map = nullptr; diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index ffd893e5..ffc4d272 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -21,10 +21,12 @@ struct ImageExporterSettings { bool showGrid = false; bool showBorder = false; bool showCollision = false; - bool previewActualSize = false; + bool disablePreviewScaling = false; + bool disablePreviewUpdates = false; int timelapseSkipAmount = 1; int timelapseDelayMs = 200; - QColor fillColor = Qt::transparent; // Not exposed as a setting in the UI atm. + // Not exposed as a setting in the UI atm (our color input widget has no alpha channel). + QColor fillColor = Qt::transparent; }; class MapImageExporter : public QDialog @@ -63,9 +65,10 @@ private: void setModeSpecificUi(); void setSelectionText(const QString &text); void updateMapSelection(); + void resetSettings(); QString getTitle(ImageExporterMode mode); QString getDescription(ImageExporterMode mode); - void updatePreview(); + void updatePreview(bool forceUpdate = false); void scalePreview(); bool eventsEnabled(); void setEventGroupEnabled(Event::Group group, bool enable); @@ -81,7 +84,7 @@ private: void paintEvents(QPainter *painter, const Map *map); void paintGrid(QPainter *painter, const Layout *layout = nullptr); QMargins getMargins(const Map *map); - QPixmap getExpandedPixmap(const QPixmap &pixmap, const QSize &minSize, const QColor &fillColor); + QPixmap getExpandedPixmap(const QPixmap &pixmap, const QSize &targetSize, const QColor &fillColor); bool currentHistoryAppliesToFrame(QUndoStack *historyStack); protected: @@ -102,15 +105,16 @@ private slots: void on_checkBox_ConnectionRight_stateChanged(int state); void on_checkBox_AllConnections_stateChanged(int state); - void on_checkBox_Elevation_stateChanged(int state); + void on_checkBox_Collision_stateChanged(int state); void on_checkBox_Grid_stateChanged(int state); void on_checkBox_Border_stateChanged(int state); void on_pushButton_Reset_pressed(); - void on_spinBox_TimelapseDelay_valueChanged(int delayMs); - void on_spinBox_FrameSkip_valueChanged(int skip); + void on_spinBox_TimelapseDelay_editingFinished(); + void on_spinBox_FrameSkip_editingFinished(); - void on_checkBox_ActualSize_stateChanged(int state); + void on_checkBox_DisablePreviewScaling_stateChanged(int state); + void on_checkBox_DisablePreviewUpdates_stateChanged(int state); }; #endif // MAPIMAGEEXPORTER_H diff --git a/src/core/editcommands.cpp b/src/core/editcommands.cpp index 0cc378fc..801bdd43 100644 --- a/src/core/editcommands.cpp +++ b/src/core/editcommands.cpp @@ -5,7 +5,7 @@ #include -int getEventTypeMask(QList events) { +int getEventTypeMask(const QList &events) { int eventTypeMask = 0; for (auto event : events) { Event::Group groupType = event->getEventGroup(); @@ -24,6 +24,26 @@ int getEventTypeMask(QList events) { return eventTypeMask; } +int getConnectionDirectionMask(const QList &directions) { + int mask = 0; + for (auto direction : directions) { + if (direction == "up") { + mask |= IDMask_ConnectionDirection_Up; + } else if (direction == "down") { + mask |= IDMask_ConnectionDirection_Down; + } else if (direction == "left") { + mask |= IDMask_ConnectionDirection_Left; + } else if (direction == "right") { + mask |= IDMask_ConnectionDirection_Right; + } else if (direction == "dive") { + mask |= IDMask_ConnectionDirection_Dive; + } else if (direction == "emerge") { + mask |= IDMask_ConnectionDirection_Emerge; + } + } + return mask; +} + void renderBlocks(Layout *layout, bool ignoreCache = false) { layout->layoutItem->draw(ignoreCache); layout->collisionItem->draw(ignoreCache); @@ -587,6 +607,10 @@ bool MapConnectionMove::mergeWith(const QUndoCommand *command) { return true; } +int MapConnectionMove::id() const { + return CommandId::ID_MapConnectionMove | getConnectionDirectionMask({this->connection->direction()}); +} + /****************************************************************************** ************************************************************************ ******************************************************************************/ @@ -629,6 +653,10 @@ void MapConnectionChangeDirection::undo() { QUndoCommand::undo(); } +int MapConnectionChangeDirection::id() const { + return CommandId::ID_MapConnectionChangeDirection | getConnectionDirectionMask({this->oldDirection, this->newDirection}); +} + /****************************************************************************** ************************************************************************ ******************************************************************************/ @@ -664,6 +692,10 @@ void MapConnectionChangeMap::undo() { QUndoCommand::undo(); } +int MapConnectionChangeMap::id() const { + return CommandId::ID_MapConnectionChangeMap | getConnectionDirectionMask({this->connection->direction()}); +} + /****************************************************************************** ************************************************************************ ******************************************************************************/ @@ -708,6 +740,10 @@ void MapConnectionAdd::undo() { QUndoCommand::undo(); } +int MapConnectionAdd::id() const { + return CommandId::ID_MapConnectionAdd | getConnectionDirectionMask({this->connection->direction()}); +} + /****************************************************************************** ************************************************************************ ******************************************************************************/ @@ -745,3 +781,7 @@ void MapConnectionRemove::undo() { QUndoCommand::undo(); } + +int MapConnectionRemove::id() const { + return CommandId::ID_MapConnectionRemove | getConnectionDirectionMask({this->connection->direction()}); +} diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index c98fccce..23b1b8a0 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -96,22 +96,26 @@ void MapImageExporter::setModeSpecificUi() { } if (m_mode == ImageExporterMode::Timelapse) { - // At the moment edit history for events (and the DraggablePixmapItem class) - // depend on the editor and assume their map is the current map. - // Until this is resolved, the selected map and the editor's map must remain the same. + // TODO: At the moment edit history for events (and the DraggablePixmapItem class) + // explicitly depend on the editor and assume their map is currently open. + // Other edit commands rely on this more subtly, like triggering API callbacks or + // spending time rendering their layout (which can make creating timelapses very slow). + // Until this is resolved, the selected map/layout must remain the same as in the editor. + // We enforce this here by disabling the selector, and in MainWindow by programmatically + // changing the exporter's map/layout selection if the user opens a new one in the editor. ui->comboBox_MapSelection->setEnabled(false); ui->label_MapSelection->setEnabled(false); - - // Timelapse gif has artifacts with transparency, make sure it's disabled. - m_settings.fillColor.setAlpha(255); } + + // Update for any mode-specific default settings + resetSettings(); } // Allow the window to open before displaying the preview. void MapImageExporter::showEvent(QShowEvent *event) { QWidget::showEvent(event); if (!event->spontaneous()) - QTimer::singleShot(0, this, &MapImageExporter::updatePreview); + QTimer::singleShot(0, this, [this](){ updatePreview(); }); } void MapImageExporter::resizeEvent(QResizeEvent *event) { @@ -166,12 +170,12 @@ void MapImageExporter::updateMapSelection() { } void MapImageExporter::saveImage() { - // If the preview is empty it's because progress was canceled. - // Try again to create it, and if it's canceled again we'll stop the export. - if (m_preview->pixmap().isNull()) { - updatePreview(); + // If the preview is empty (because progress was canceled) or if updates were disabled + // then we should ensure the image in the preview is up-to-date before exporting. + if (m_preview->pixmap().isNull() || m_settings.disablePreviewUpdates) { + updatePreview(true); if (m_preview->pixmap().isNull()) - return; + return; // Canceled } if (m_mode == ImageExporterMode::Timelapse && !m_timelapseGifImage) { // Shouldn't happen. We have a preview for the timelapse, but no timelapse image. @@ -237,8 +241,15 @@ bool MapImageExporter::currentHistoryAppliesToFrame(QUndoStack *historyStack) { case CommandId::ID_MapConnectionChangeDirection: case CommandId::ID_MapConnectionChangeMap: case CommandId::ID_MapConnectionAdd: - case CommandId::ID_MapConnectionRemove: - return connectionsEnabled(); + case CommandId::ID_MapConnectionRemove: { + if (!connectionsEnabled()) + return false; + if (command->id() & IDMask_ConnectionDirection_Up) return m_settings.showConnections.contains("up"); + if (command->id() & IDMask_ConnectionDirection_Down) return m_settings.showConnections.contains("down"); + if (command->id() & IDMask_ConnectionDirection_Left) return m_settings.showConnections.contains("left"); + if (command->id() & IDMask_ConnectionDirection_Right) return m_settings.showConnections.contains("right"); + return false; + } case CommandId::ID_EventMove: case CommandId::ID_EventShift: case CommandId::ID_EventCreate: @@ -257,15 +268,18 @@ bool MapImageExporter::currentHistoryAppliesToFrame(QUndoStack *historyStack) { } } -QPixmap MapImageExporter::getExpandedPixmap(const QPixmap &pixmap, const QSize &minSize, const QColor &fillColor) { - if (pixmap.width() >= minSize.width() && pixmap.height() >= minSize.height()) +QPixmap MapImageExporter::getExpandedPixmap(const QPixmap &pixmap, const QSize &targetSize, const QColor &fillColor) { + if (pixmap.width() >= targetSize.width() && pixmap.height() >= targetSize.height()) return pixmap; - QPixmap resizedPixmap = QPixmap(minSize); + QPixmap resizedPixmap = QPixmap(targetSize); QPainter painter(&resizedPixmap); resizedPixmap.fill(fillColor); - painter.drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap); - painter.end(); + + // Center the old pixmap in the new resized one. + int x = (targetSize.width() - pixmap.width()) / 2; + int y = (targetSize.height() - pixmap.height()) / 2; + painter.drawPixmap(x, y, pixmap.width(), pixmap.height(), pixmap); return resizedPixmap; } @@ -278,13 +292,11 @@ struct TimelapseStep { QGifImage* MapImageExporter::createTimelapseGifImage(QProgressDialog *progress) { // TODO: Timelapse will play in order of layout changes then map changes (events, connections). Potentially update in the future? QList steps; - if (m_layout) { - steps.append({ - .historyStack = &m_layout->editHistory, - .initialStackIndex = m_layout->editHistory.index(), - .name = "layout", - }); - } + steps.append({ + .historyStack = &m_layout->editHistory, + .initialStackIndex = m_layout->editHistory.index(), + .name = "layout", + }); if (m_map) { steps.append({ .historyStack = m_map->editHistory(), @@ -301,8 +313,8 @@ QGifImage* MapImageExporter::createTimelapseGifImage(QProgressDialog *progress) progress->setMaximum(step.initialStackIndex); progress->setValue(progress->minimum()); do { - if (currentHistoryAppliesToFrame(step.historyStack)) { - // This command is relevant, record the size of the map at this point. + if (currentHistoryAppliesToFrame(step.historyStack) || step.historyStack->index() == step.initialStackIndex) { + // Either this is relevant edit history, or it's the final frame (which is always rendered). Record the size of the map at this point. QMargins margins = getMargins(m_map); canvasSize = canvasSize.expandedTo(QSize(m_layout->getWidth() * 16 + margins.left() + margins.right(), m_layout->getHeight() * 16 + margins.top() + margins.bottom())); @@ -489,7 +501,10 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { return stitchedPixmap; } -void MapImageExporter::updatePreview() { +void MapImageExporter::updatePreview(bool forceUpdate) { + if (m_settings.disablePreviewUpdates && !forceUpdate) + return; + QProgressDialog progress("", "Cancel", 0, 1, this); progress.setAutoClose(true); progress.setWindowModality(Qt::WindowModal); @@ -535,7 +550,7 @@ void MapImageExporter::updatePreview() { } void MapImageExporter::scalePreview() { - if (!m_preview || m_settings.previewActualSize) + if (!m_preview || m_settings.disablePreviewScaling) return; ui->graphicsView_Preview->fitInView(m_preview, Qt::KeepAspectRatioByExpanding); } @@ -689,7 +704,7 @@ void MapImageExporter::setEventGroupEnabled(Event::Group group, bool enable) { } bool MapImageExporter::connectionsEnabled() { - return !m_settings.showConnections.isEmpty(); + return !m_settings.showConnections.isEmpty() && m_mode != ImageExporterMode::Stitch; } void MapImageExporter::setConnectionDirectionEnabled(const QString &dir, bool enable) { @@ -700,7 +715,7 @@ void MapImageExporter::setConnectionDirectionEnabled(const QString &dir, bool en } } -void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) { +void MapImageExporter::on_checkBox_Collision_stateChanged(int state) { m_settings.showCollision = (state == Qt::Checked); updatePreview(); } @@ -819,21 +834,37 @@ void MapImageExporter::on_checkBox_AllConnections_stateChanged(int state) { updatePreview(); } -void MapImageExporter::on_checkBox_ActualSize_stateChanged(int state) { - m_settings.previewActualSize = (state == Qt::Checked); - if (m_settings.previewActualSize) { +void MapImageExporter::on_checkBox_DisablePreviewScaling_stateChanged(int state) { + m_settings.disablePreviewScaling = (state == Qt::Checked); + if (m_settings.disablePreviewScaling) { ui->graphicsView_Preview->resetTransform(); } else { scalePreview(); } } +void MapImageExporter::on_checkBox_DisablePreviewUpdates_stateChanged(int state) { + m_settings.disablePreviewUpdates = (state == Qt::Checked); + if (m_settings.disablePreviewUpdates) { + if (m_timelapseMovie) { + m_timelapseMovie->stop(); + } + } else { + updatePreview(); + } +} + void MapImageExporter::on_pushButton_Reset_pressed() { - m_settings = {}; + resetSettings(); + updatePreview(); +} + +void MapImageExporter::resetSettings() { + m_settings = {}; for (auto widget : this->findChildren()) { const QSignalBlocker b(widget); // Prevent calls to updatePreview - widget->setChecked(false); + widget->setChecked(false); // This assumes the default state of all checkboxes settings is false. } const QSignalBlocker b_TimelapseDelay(ui->spinBox_TimelapseDelay); @@ -842,15 +873,27 @@ void MapImageExporter::on_pushButton_Reset_pressed() { const QSignalBlocker b_FrameSkip(ui->spinBox_FrameSkip); ui->spinBox_FrameSkip->setValue(m_settings.timelapseSkipAmount); - updatePreview(); + if (m_mode == ImageExporterMode::Timelapse) { + // Timelapse gif has artifacts with transparency, make sure it's disabled. + m_settings.fillColor.setAlpha(255); + } } -void MapImageExporter::on_spinBox_TimelapseDelay_valueChanged(int delayMs) { +// These spin boxes can be changed rapidly, so we wait for editing to finish before updating the preview. +void MapImageExporter::on_spinBox_TimelapseDelay_editingFinished() { + int delayMs = ui->spinBox_TimelapseDelay->value(); + if (delayMs == m_settings.timelapseDelayMs) + return; + m_settings.timelapseDelayMs = delayMs; updatePreview(); } -void MapImageExporter::on_spinBox_FrameSkip_valueChanged(int skip) { - m_settings.timelapseSkipAmount = skip; +void MapImageExporter::on_spinBox_FrameSkip_editingFinished() { + int skipAmount = ui->spinBox_FrameSkip->value(); + if (skipAmount == m_settings.timelapseSkipAmount) + return; + + m_settings.timelapseSkipAmount = skipAmount; updatePreview(); } From 44f3b27f2ded0e8e11102487a00b36b11214a51c Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sat, 29 Mar 2025 01:47:54 -0400 Subject: [PATCH 11/12] Fix older Qt builds --- include/core/events.h | 4 ++++ src/ui/mapimageexporter.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/include/core/events.h b/include/core/events.h index 322f09e8..17a510b0 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -615,6 +615,10 @@ private: }; +inline uint qHash(const Event::Group &key, uint seed = 0) { + return qHash(static_cast(key), seed); +} + /// /// Keeps track of scripts diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 23b1b8a0..36c1f7ff 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -57,7 +57,7 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, // Update the map selector when the text changes. // We don't use QComboBox::currentTextChanged to avoid unnecessary re-rendering. - connect(ui->comboBox_MapSelection, &QComboBox::currentIndexChanged, this, &MapImageExporter::updateMapSelection); + connect(ui->comboBox_MapSelection, QOverload::of(&QComboBox::currentIndexChanged), this, &MapImageExporter::updateMapSelection); connect(ui->comboBox_MapSelection->lineEdit(), &QLineEdit::editingFinished, this, &MapImageExporter::updateMapSelection); ui->graphicsView_Preview->setFocus(); From 7c107f3470e99a325528e913599133c3a3f9af93 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 30 Mar 2025 17:38:05 -0400 Subject: [PATCH 12/12] Fix timelapse not considering multiple command ID flags --- src/ui/mapimageexporter.cpp | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 36c1f7ff..39607bca 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -244,11 +244,12 @@ bool MapImageExporter::currentHistoryAppliesToFrame(QUndoStack *historyStack) { case CommandId::ID_MapConnectionRemove: { if (!connectionsEnabled()) return false; - if (command->id() & IDMask_ConnectionDirection_Up) return m_settings.showConnections.contains("up"); - if (command->id() & IDMask_ConnectionDirection_Down) return m_settings.showConnections.contains("down"); - if (command->id() & IDMask_ConnectionDirection_Left) return m_settings.showConnections.contains("left"); - if (command->id() & IDMask_ConnectionDirection_Right) return m_settings.showConnections.contains("right"); - return false; + uint32_t flags = 0; + if (m_settings.showConnections.contains("up")) flags |= IDMask_ConnectionDirection_Up; + if (m_settings.showConnections.contains("down")) flags |= IDMask_ConnectionDirection_Down; + if (m_settings.showConnections.contains("left")) flags |= IDMask_ConnectionDirection_Left; + if (m_settings.showConnections.contains("right")) flags |= IDMask_ConnectionDirection_Right; + return (command->id() & flags) != 0; } case CommandId::ID_EventMove: case CommandId::ID_EventShift: @@ -256,12 +257,15 @@ bool MapImageExporter::currentHistoryAppliesToFrame(QUndoStack *historyStack) { case CommandId::ID_EventPaste: case CommandId::ID_EventDelete: case CommandId::ID_EventDuplicate: { - if (command->id() & IDMask_EventType_Object) return m_settings.showEvents.contains(Event::Group::Object); - if (command->id() & IDMask_EventType_Warp) return m_settings.showEvents.contains(Event::Group::Warp); - if (command->id() & IDMask_EventType_BG) return m_settings.showEvents.contains(Event::Group::Bg); - if (command->id() & IDMask_EventType_Trigger) return m_settings.showEvents.contains(Event::Group::Coord); - if (command->id() & IDMask_EventType_Heal) return m_settings.showEvents.contains(Event::Group::Heal); - return false; + if (!eventsEnabled()) + return false; + uint32_t flags = 0; + if (m_settings.showEvents.contains(Event::Group::Object)) flags |= IDMask_EventType_Object; + if (m_settings.showEvents.contains(Event::Group::Warp)) flags |= IDMask_EventType_Warp; + if (m_settings.showEvents.contains(Event::Group::Bg)) flags |= IDMask_EventType_BG; + if (m_settings.showEvents.contains(Event::Group::Coord)) flags |= IDMask_EventType_Trigger; + if (m_settings.showEvents.contains(Event::Group::Heal)) flags |= IDMask_EventType_Heal; + return (command->id() & flags) != 0; } default: return false;