#include "mapimageexporter.h" #include "ui_mapimageexporter.h" #include "qgifimage.h" #include "editcommands.h" #include #include #include #include #define STITCH_MODE_BORDER_DISTANCE 2 QString getTitle(ImageExporterMode mode) { switch (mode) { case ImageExporterMode::Normal: return "Export Map Image"; case ImageExporterMode::Stitch: return "Export Map Stitch Image"; case ImageExporterMode::Timelapse: return "Export Map Timelapse Image"; } return ""; } MapImageExporter::MapImageExporter(QWidget *parent_, Editor *editor_, ImageExporterMode mode) : QDialog(parent_), ui(new Ui::MapImageExporter) { ui->setupUi(this); this->map = editor_->map; this->editor = editor_; this->mode = mode; this->setWindowTitle(getTitle(this->mode)); this->ui->groupBox_Connections->setVisible(this->mode == ImageExporterMode::Normal); this->ui->groupBox_Timelapse->setVisible(this->mode == ImageExporterMode::Timelapse); this->ui->comboBox_MapSelection->addItems(editor->project->mapNames); this->ui->comboBox_MapSelection->setCurrentText(map->name); this->ui->comboBox_MapSelection->setEnabled(false);// TODO: allow selecting map from drop-down updatePreview(); } MapImageExporter::~MapImageExporter() { delete scene; delete ui; } void MapImageExporter::saveImage() { QString title = getTitle(this->mode); QString defaultFilename; switch (this->mode) { case ImageExporterMode::Normal: defaultFilename = map->name; break; case ImageExporterMode::Stitch: defaultFilename = QString("Stitch_From_%1").arg(map->name); break; case ImageExporterMode::Timelapse: defaultFilename = QString("Timelapse_%1").arg(map->name); break; } QString defaultFilepath = QString("%1/%2.%3") .arg(editor->project->root) .arg(defaultFilename) .arg(this->mode == ImageExporterMode::Timelapse ? "gif" : "png"); QString filter = this->mode == ImageExporterMode::Timelapse ? "Image Files (*.gif)" : "Image Files (*.png *.jpg *.bmp)"; QString filepath = QFileDialog::getSaveFileName(this, title, defaultFilepath, filter); if (!filepath.isEmpty()) { switch (this->mode) { case ImageExporterMode::Normal: this->preview.save(filepath); break; case ImageExporterMode::Stitch: { QProgressDialog progress("Building map stitch...", "Cancel", 0, 1, this); progress.setAutoClose(true); progress.setWindowModality(Qt::WindowModal); progress.setModal(true); QPixmap pixmap = this->getStitchedImage(&progress, this->showBorder); if (progress.wasCanceled()) { progress.close(); return; } pixmap.save(filepath); progress.close(); break; } case ImageExporterMode::Timelapse: QProgressDialog progress("Building map timelapse...", "Cancel", 0, 1, this); progress.setAutoClose(true); progress.setWindowModality(Qt::WindowModal); progress.setModal(true); progress.setMaximum(1); progress.setValue(0); int maxWidth = this->map->getWidth() * 16; int maxHeight = this->map->getHeight() * 16; if (showBorder) { maxWidth += 2 * STITCH_MODE_BORDER_DISTANCE * 16; maxHeight += 2 * STITCH_MODE_BORDER_DISTANCE * 16; } // Rewind to the specified start of the map edit history. int i = 0; while (this->map->editHistory.canUndo()) { progress.setValue(i); this->map->editHistory.undo(); int width = this->map->getWidth() * 16; int height = this->map->getHeight() * 16; if (showBorder) { width += 2 * STITCH_MODE_BORDER_DISTANCE * 16; height += 2 * STITCH_MODE_BORDER_DISTANCE * 16; } if (width > maxWidth) { maxWidth = width; } if (height > maxHeight) { maxHeight = height; } i++; } QGifImage timelapseImg(QSize(maxWidth, maxHeight)); timelapseImg.setDefaultDelay(timelapseDelayMs); timelapseImg.setDefaultTransparentColor(QColor(0, 0, 0)); // 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 && this->map->editHistory.canRedo()) { i--; this->map->editHistory.redo(); } return; } while (this->map->editHistory.canRedo() && !historyItemAppliesToFrame(this->map->editHistory.command(this->map->editHistory.index()))) { i--; this->map->editHistory.redo(); } progress.setValue(progress.maximum() - i); QPixmap pixmap = this->getFormattedMapPixmap(this->map, !this->showBorder); 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 < timelapseSkipAmount; j++) { if (i > 0) { i--; this->map->editHistory.redo(); while (this->map->editHistory.canRedo() && !historyItemAppliesToFrame(this->map->editHistory.command(this->map->editHistory.index()))) { i--; this->map->editHistory.redo(); } } } } // The latest map state is the last animated frame. QPixmap pixmap = this->getFormattedMapPixmap(this->map, !this->showBorder); timelapseImg.addFrame(pixmap.toImage()); timelapseImg.save(filepath); progress.close(); break; } this->close(); } } bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { switch (command->id() & 0xFF) { case CommandId::ID_PaintMetatile: case CommandId::ID_BucketFillMetatile: case CommandId::ID_MagicFillMetatile: case CommandId::ID_ShiftMetatiles: case CommandId::ID_ResizeMap: case CommandId::ID_ScriptEditMap: return true; case CommandId::ID_PaintCollision: case CommandId::ID_BucketFillCollision: case CommandId::ID_MagicFillCollision: return this->showCollision; case CommandId::ID_PaintBorder: return this->showBorder; case CommandId::ID_EventMove: case CommandId::ID_EventShift: case CommandId::ID_EventCreate: case CommandId::ID_EventDelete: case CommandId::ID_EventDuplicate: { bool eventTypeIsApplicable = (this->showObjects && (command->id() & IDMask_EventType_Object) != 0) || (this->showWarps && (command->id() & IDMask_EventType_Warp) != 0) || (this->showBGs && (command->id() & IDMask_EventType_BG) != 0) || (this->showTriggers && (command->id() & IDMask_EventType_Trigger) != 0) || (this->showHealSpots && (command->id() & IDMask_EventType_Heal) != 0); return eventTypeIsApplicable; } default: return false; } } struct StitchedMap { int x; int y; Map* map; }; QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool includeBorder) { // Do a breadth-first search to gather a collection of // all reachable maps with their relative offsets. QSet visited; QList stitchedMaps; QList unvisited; unvisited.append(StitchedMap{0, 0, this->editor->map}); progress->setLabelText("Gathering stitched maps..."); while (!unvisited.isEmpty()) { if (progress->wasCanceled()) { return QPixmap(); } progress->setMaximum(visited.size() + unvisited.size()); progress->setValue(visited.size()); StitchedMap cur = unvisited.takeFirst(); if (visited.contains(cur.map->name)) continue; visited.insert(cur.map->name); stitchedMaps.append(cur); for (MapConnection *connection : cur.map->connections) { if (connection->direction == "dive" || connection->direction == "emerge") continue; int x = cur.x; int y = cur.y; int offset = connection->offset.toInt(nullptr, 0); Map *connectionMap = this->editor->project->loadMap(connection->map_name); if (connection->direction == "up") { x += offset; y -= connectionMap->getHeight(); } else if (connection->direction == "down") { x += offset; y += cur.map->getHeight(); } else if (connection->direction == "left") { x -= connectionMap->getWidth(); y += offset; } else if (connection->direction == "right") { x += cur.map->getWidth(); y += offset; } unvisited.append(StitchedMap{x, y, connectionMap}); } } // Determine the overall dimensions of the stitched maps. int maxX = INT_MIN; int minX = INT_MAX; int maxY = INT_MIN; int minY = INT_MAX; for (StitchedMap map : stitchedMaps) { int left = map.x; int right = map.x + map.map->getWidth(); int top = map.y; int bottom = map.y + map.map->getHeight(); if (left < minX) minX = left; if (right > maxX) maxX = right; if (top < minY) minY = top; if (bottom > maxY) maxY = bottom; } if (includeBorder) { minX -= STITCH_MODE_BORDER_DISTANCE; maxX += STITCH_MODE_BORDER_DISTANCE; minY -= STITCH_MODE_BORDER_DISTANCE; maxY += STITCH_MODE_BORDER_DISTANCE; } // Draw the maps on the full canvas, while taking // their respective offsets into account. progress->setLabelText("Drawing stitched maps..."); progress->setValue(0); progress->setMaximum(stitchedMaps.size()); int numDrawn = 0; QPixmap stitchedPixmap((maxX - minX) * 16, (maxY - minY) * 16); QPainter painter(&stitchedPixmap); for (StitchedMap map : stitchedMaps) { if (progress->wasCanceled()) { return QPixmap(); } progress->setValue(numDrawn); numDrawn++; int pixelX = (map.x - minX) * 16; int pixelY = (map.y - minY) * 16; if (includeBorder) { pixelX -= STITCH_MODE_BORDER_DISTANCE * 16; pixelY -= STITCH_MODE_BORDER_DISTANCE * 16; } QPixmap pixmap = this->getFormattedMapPixmap(map.map, false); painter.drawPixmap(pixelX, pixelY, pixmap); } // When including the borders, we simply draw all the maps again // without their borders, since the first pass results in maps // being occluded by other map borders. if (includeBorder) { progress->setLabelText("Drawing stitched maps without borders..."); progress->setValue(0); progress->setMaximum(stitchedMaps.size()); numDrawn = 0; for (StitchedMap map : stitchedMaps) { if (progress->wasCanceled()) { return QPixmap(); } progress->setValue(numDrawn); numDrawn++; int pixelX = (map.x - minX) * 16; int pixelY = (map.y - minY) * 16; QPixmap pixmapWithoutBorders = this->getFormattedMapPixmap(map.map, true); painter.drawPixmap(pixelX, pixelY, pixmapWithoutBorders); } } return stitchedPixmap; } void MapImageExporter::updatePreview() { if (scene) { delete scene; scene = nullptr; } preview = getFormattedMapPixmap(this->map, false); scene = new QGraphicsScene; scene->addPixmap(preview); this->scene->setSceneRect(this->scene->itemsBoundingRect()); this->ui->graphicsView_Preview->setScene(scene); this->ui->graphicsView_Preview->setFixedSize(scene->itemsBoundingRect().width() + 2, scene->itemsBoundingRect().height() + 2); } QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { QPixmap pixmap; // draw background layer / base image map->render(true); if (showCollision) { map->renderCollision(editor->collisionOpacity, true); pixmap = map->collision_pixmap; } else { pixmap = map->pixmap; } // draw events QPainter eventPainter(&pixmap); QList events = map->getAllEvents(); for (Event *event : events) { editor->project->setEventPixmap(event); QString group = event->get("event_group_type"); if ((showObjects && group == EventGroup::Object) || (showWarps && group == EventGroup::Warp) || (showBGs && group == EventGroup::Bg) || (showTriggers && group == EventGroup::Coord) || (showHealSpots && group == EventGroup::Heal)) eventPainter.drawImage(QPoint(event->getPixelX(), event->getPixelY()), event->pixmap.toImage()); } eventPainter.end(); // draw map border // note: this will break when allowing map to be selected from drop down maybe int borderHeight = 0, borderWidth = 0; bool forceDrawBorder = showUpConnections || showDownConnections || showLeftConnections || showRightConnections; if (!ignoreBorder && (showBorder || forceDrawBorder)) { int borderDistance = this->mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; map->renderBorder(); int borderHorzDist = editor->getBorderDrawDistance(map->getBorderWidth()); int borderVertDist = editor->getBorderDrawDistance(map->getBorderHeight()); borderWidth = borderDistance * 16; borderHeight = borderDistance * 16; QPixmap newPixmap = QPixmap(map->pixmap.width() + borderWidth * 2, map->pixmap.height() + borderHeight * 2); QPainter borderPainter(&newPixmap); for (int y = borderDistance - borderVertDist; y < map->getHeight() + borderVertDist * 2; y += map->getBorderHeight()) { for (int x = borderDistance - borderHorzDist; x < map->getWidth() + borderHorzDist * 2; x += map->getBorderWidth()) { borderPainter.drawPixmap(x * 16, y * 16, map->layout->border_pixmap); } } borderPainter.drawImage(borderWidth, borderHeight, pixmap.toImage()); borderPainter.end(); pixmap = newPixmap; } if (!this->mode) { // if showing connections, draw on outside of image QPainter connectionPainter(&pixmap); for (auto connectionItem : editor->connection_edit_items) { QString direction = connectionItem->connection->direction; if ((showUpConnections && direction == "up") || (showDownConnections && direction == "down") || (showLeftConnections && direction == "left") || (showRightConnections && direction == "right")) connectionPainter.drawImage(connectionItem->initialX + borderWidth, connectionItem->initialY + borderHeight, connectionItem->basePixmap.toImage()); } connectionPainter.end(); } // 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 (showGrid) { int addX = 1, addY = 1; if (borderHeight) addY = 0; if (borderWidth) addX = 0; 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; } return pixmap; } void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) { showCollision = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Grid_stateChanged(int state) { showGrid = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Border_stateChanged(int state) { showBorder = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Objects_stateChanged(int state) { showObjects = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Warps_stateChanged(int state) { showWarps = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_BGs_stateChanged(int state) { showBGs = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Triggers_stateChanged(int state) { showTriggers = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_HealSpots_stateChanged(int state) { showHealSpots = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionUp_stateChanged(int state) { showUpConnections = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionDown_stateChanged(int state) { showDownConnections = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionLeft_stateChanged(int state) { showLeftConnections = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionRight_stateChanged(int state) { showRightConnections = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_pushButton_Save_pressed() { saveImage(); } void MapImageExporter::on_pushButton_Reset_pressed() { for (auto widget : this->findChildren()) widget->setChecked(false); } void MapImageExporter::on_pushButton_Cancel_pressed() { this->close(); } void MapImageExporter::on_spinBox_TimelapseDelay_valueChanged(int delayMs) { timelapseDelayMs = delayMs; } void MapImageExporter::on_spinBox_FrameSkip_valueChanged(int skip) { timelapseSkipAmount = skip; }