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;